From 57469f860e4e5c2eda64dd2895da26fbbd62b24d Mon Sep 17 00:00:00 2001 From: CanisMinor Date: Fri, 20 Mar 2026 14:10:01 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84=20style:=20redesign=20image=20/=20?= =?UTF-8?q?video=20(#13126)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ refactor: Refactor image and video * chore: rabase canary * style: update * style: update * style: update * style: update * style: update * style: update * style: update * chore: update i18n * style: update * fix: fix config * fix: fix proxy * fix: fix type * chore: fix test --- locales/ar/components.json | 6 + locales/ar/image.json | 5 +- locales/ar/video.json | 3 +- locales/bg-BG/components.json | 6 + locales/bg-BG/image.json | 5 +- locales/bg-BG/video.json | 3 +- locales/de-DE/components.json | 6 + locales/de-DE/image.json | 5 +- locales/de-DE/video.json | 3 +- locales/en-US/components.json | 6 + locales/en-US/image.json | 5 +- locales/en-US/video.json | 3 +- locales/es-ES/components.json | 6 + locales/es-ES/image.json | 5 +- locales/es-ES/video.json | 3 +- locales/fa-IR/components.json | 6 + locales/fa-IR/image.json | 5 +- locales/fa-IR/video.json | 3 +- locales/fr-FR/components.json | 6 + locales/fr-FR/image.json | 5 +- locales/fr-FR/video.json | 3 +- locales/it-IT/components.json | 6 + locales/it-IT/image.json | 5 +- locales/it-IT/video.json | 3 +- locales/ja-JP/components.json | 6 + locales/ja-JP/image.json | 5 +- locales/ja-JP/video.json | 3 +- locales/ko-KR/components.json | 6 + locales/ko-KR/image.json | 3 +- locales/ko-KR/video.json | 1 + locales/nl-NL/components.json | 6 + locales/nl-NL/image.json | 5 +- locales/nl-NL/video.json | 3 +- locales/pl-PL/components.json | 6 + locales/pl-PL/image.json | 5 +- locales/pl-PL/video.json | 3 +- locales/pt-BR/components.json | 6 + locales/pt-BR/image.json | 5 +- locales/pt-BR/video.json | 3 +- locales/ru-RU/components.json | 6 + locales/ru-RU/image.json | 5 +- locales/ru-RU/video.json | 3 +- locales/tr-TR/components.json | 6 + locales/tr-TR/image.json | 5 +- locales/tr-TR/video.json | 3 +- locales/vi-VN/components.json | 6 + locales/vi-VN/image.json | 5 +- locales/vi-VN/video.json | 3 +- locales/zh-CN/components.json | 6 + locales/zh-CN/image.json | 5 +- locales/zh-CN/video.json | 3 +- locales/zh-TW/components.json | 6 + locales/zh-TW/image.json | 5 +- locales/zh-TW/video.json | 3 +- packages/model-bank/src/types/aiModel.ts | 8 + src/features/CommandMenu/SearchResults.tsx | 139 ++++- src/features/CommandMenu/types.ts | 5 +- src/features/CommandMenu/utils/context.ts | 5 + .../CommandMenu/utils/contextCommands.ts | 1 + .../ChatList/components/VirtualizedList.tsx | 12 +- .../GenerationTopicList/TopicItem.tsx | 135 ----- .../GenerationTopicList/TopicList.tsx | 73 --- src/features/GenerationTopicList/index.tsx | 7 - src/features/GenerationTopicPanel/index.tsx | 86 --- src/features/ImageSidePanel/index.tsx | 84 --- src/features/ImageTopicPanel/index.tsx | 31 -- .../List/GenerationListItemRenderer.tsx | 229 ++++++++ .../List/GenerationMultipleProvidersItem.tsx | 112 ++++ .../components/List/index.tsx | 54 +- .../components/ModelDetailPanel.tsx | 518 ++++++++++-------- .../components/PanelContent.tsx | 16 +- src/features/ModelSwitchPanel/index.tsx | 6 + src/features/ModelSwitchPanel/types.ts | 15 + src/locales/default/components.ts | 6 + src/locales/default/image.ts | 5 +- src/locales/default/video.ts | 3 +- src/proxy.ts | 1 + .../components/GenerationModelItem.tsx} | 85 ++- .../components/PromptTitle.tsx} | 18 +- .../features/CreateGenerationPage.tsx | 91 +++ .../features/GenerationFeed/index.tsx | 26 +- .../features/GenerationInput/ConfigAction.tsx | 27 + .../GenerationInvalidAPIKey.tsx | 54 ++ .../GenerationInput/GenerationPromptInput.tsx | 112 ++++ .../GenerationInput/ImagePreviewHeader.tsx | 29 + .../GenerationInput/InlineImageReference.tsx | 99 ++++ .../GenerationInput/InlineVideoFrames.tsx | 74 +++ .../features/GenerationInput/ModelAction.tsx | 27 + .../features/GenerationInput/UploadCard.tsx | 283 ++++++++++ .../features/GenerationInput/index.ts | 7 + .../Body/List/Item/GridItem.tsx | 73 +++ .../Body/List/Item/ListItem.tsx | 59 ++ .../GenerationLayout/Body/List/Item/index.tsx | 104 ++++ .../Body/List}/NewTopicButton.tsx | 0 .../Body/List}/SkeletonList.tsx | 0 .../Body/List}/StoreContext.tsx | 0 .../GenerationLayout/Body/List/TopicList.tsx | 56 ++ .../Body/List}/TopicUrlSync.tsx | 0 .../GenerationLayout/Body/List/index.tsx | 38 ++ .../features/GenerationLayout/Body/index.tsx | 76 +++ .../GenerationLayout/Header/AddButton.tsx | 30 + .../GenerationLayout/Header/index.tsx | 44 ++ .../features/GenerationLayout/Sidebar.tsx | 23 + .../features/GenerationLayout/index.tsx | 26 + .../features/GenerationLayout}/style.ts | 4 - .../features/GenerationLayout/types.ts | 10 + .../features/GenerationWorkspace/Content.tsx | 55 ++ .../GenerationWorkspace/EmptyState.tsx | 27 + .../features/GenerationWorkspace/index.tsx | 49 ++ .../{ => (create)}/image/NotSupportClient.tsx | 0 .../image/_layout/RegisterHotkeys.tsx | 0 .../(main)/(create)/image/_layout/index.tsx | 27 + .../{ => (create)}/image/_layout/type.ts | 0 .../ConfigPanel/ImageConfigSkeleton.tsx | 0 .../components/AspectRatioSelect/index.tsx | 0 .../ConfigPanel/components/CfgSliderInput.tsx | 0 .../components/DimensionControlGroup.tsx | 0 .../ConfigPanel/components/ImageNum.tsx | 1 - .../ConfigPanel/components/ImageUpload.tsx | 8 +- .../ConfigPanel/components/ImageUrl.tsx | 2 +- .../components/ImageUrlsUpload.tsx | 2 +- .../components/InputNumber/index.tsx | 0 .../components/ModelSelect/ImageModelItem.tsx | 15 + .../components/ModelSelect/index.tsx | 16 +- .../MultiImagesUpload/ImageManageModal.tsx | 2 +- .../components/MultiImagesUpload/index.tsx | 8 +- .../ConfigPanel/components/QualitySelect.tsx | 0 .../components/ResolutionSelect.tsx | 0 .../components/SeedNumberInput.tsx | 0 .../ConfigPanel/components/Select/index.tsx | 0 .../ConfigPanel/components/SizeSelect.tsx | 0 .../components/StepsSliderInput.tsx | 0 .../image/features}/ConfigPanel/constants.ts | 0 .../ConfigPanel/hooks/useAutoDimensions.ts | 0 .../ConfigPanel/hooks/useDragAndDrop.ts | 0 .../hooks/useUploadFilesValidation.ts | 0 .../image/features/ConfigPanel/index.ts | 12 + .../image/features}/ConfigPanel/style.ts | 0 .../__tests__/dimensionConstraints.test.ts | 0 .../utils/__tests__/imageValidation.test.ts | 0 .../ConfigPanel/utils/dimensionConstraints.ts | 0 .../ConfigPanel/utils/imageValidation.ts | 0 .../features/GenerationFeed/BatchItem.tsx | 119 ++-- .../GenerationItem/ActionButtons.tsx | 0 .../GenerationItem/ElapsedTime.tsx | 0 .../GenerationItem/ErrorState.tsx | 2 +- .../GenerationItem/LoadingState.tsx | 10 +- .../GenerationItem/SuccessState.tsx | 0 .../GenerationFeed/GenerationItem/index.tsx | 0 .../GenerationFeed/GenerationItem/styles.ts | 35 +- .../GenerationFeed/GenerationItem/types.ts | 0 .../GenerationItem/utils.test.ts | 12 +- .../GenerationFeed/GenerationItem/utils.ts | 4 +- .../GenerationFeed/ReferenceImages.tsx | 85 +++ .../image/features/GenerationFeed/index.tsx | 24 + .../features/ImageWorkspace/SkeletonList.tsx | 29 +- .../image/features/ImageWorkspace/index.tsx | 31 ++ .../image/features/PromptInput/Title.tsx | 9 + .../image/features/PromptInput/index.tsx | 314 +++++++++++ src/routes/(main)/(create)/image/index.tsx | 16 + .../(main)/{ => (create)}/image/loading.tsx | 0 .../(main)/(create)/video/_layout/index.tsx | 24 + .../ConfigPanel/VideoConfigSkeleton.tsx | 0 .../ConfigPanel/components/FrameUpload.tsx | 2 +- .../components/ModelSelect/VideoModelItem.tsx | 15 + .../components/ModelSelect/index.tsx | 16 +- .../video/features/ConfigPanel/index.ts | 2 + .../features/GenerationFeed/BatchItem.tsx | 150 ++++- .../GenerationFeed/VideoErrorItem.tsx | 4 +- .../GenerationFeed/VideoLoadingItem.tsx | 2 +- .../GenerationFeed/VideoReferenceFrames.tsx | 79 +++ .../GenerationFeed/VideoSuccessItem.tsx | 4 +- .../video/features/GenerationFeed/index.tsx | 24 + .../video/features/PromptInput/Title.tsx | 9 + .../video/features/PromptInput/index.tsx | 341 ++++++++++++ .../features/VideoWorkspace/SkeletonList.tsx | 28 +- .../video/features/VideoWorkspace/index.tsx | 30 + src/routes/(main)/(create)/video/index.tsx | 16 + .../(main)/{ => (create)}/video/loading.tsx | 0 .../image/_layout/ConfigPanel/index.tsx | 129 ----- src/routes/(main)/image/_layout/Header.tsx | 23 - src/routes/(main)/image/_layout/Sidebar.tsx | 19 - .../(main)/image/_layout/TopicSidebar.tsx | 27 - src/routes/(main)/image/_layout/index.tsx | 25 - src/routes/(main)/image/_layout/style.ts | 16 - .../GenerationFeed/ReferenceImages.tsx | 120 ---- .../image/features/GenerationFeed/index.tsx | 97 ---- .../image/features/ImageWorkspace/Content.tsx | 38 -- .../features/ImageWorkspace/EmptyState.tsx | 14 - .../image/features/ImageWorkspace/index.tsx | 22 - .../image/features/PromptInput/Title.tsx | 36 -- .../image/features/PromptInput/index.tsx | 163 ------ src/routes/(main)/image/index.tsx | 27 - .../components/ModelSelect/VideoModelItem.tsx | 79 --- .../video/_layout/ConfigPanel/index.tsx | 214 -------- src/routes/(main)/video/_layout/Header.tsx | 13 - src/routes/(main)/video/_layout/Sidebar.tsx | 19 - .../(main)/video/_layout/TopicSidebar.tsx | 42 -- src/routes/(main)/video/_layout/index.tsx | 23 - .../GenerationFeed/VideoReferenceFrames.tsx | 119 ---- .../video/features/PromptInput/index.tsx | 136 ----- .../video/features/VideoWorkspace/Content.tsx | 35 -- .../features/VideoWorkspace/EmptyState.tsx | 14 - .../video/features/VideoWorkspace/index.tsx | 20 - src/routes/(main)/video/index.tsx | 27 - .../router/desktopRouter.config.desktop.tsx | 8 +- src/spa/router/desktopRouter.config.tsx | 14 +- src/store/aiInfra/slices/aiProvider/action.ts | 21 +- src/store/global/initialState.ts | 4 + src/store/global/selectors/systemStatus.ts | 4 + .../image/slices/generationTopic/action.ts | 11 +- .../video/slices/generationTopic/action.ts | 8 - 212 files changed, 4016 insertions(+), 2485 deletions(-) delete mode 100644 src/features/GenerationTopicList/TopicItem.tsx delete mode 100644 src/features/GenerationTopicList/TopicList.tsx delete mode 100644 src/features/GenerationTopicList/index.tsx delete mode 100644 src/features/GenerationTopicPanel/index.tsx delete mode 100644 src/features/ImageSidePanel/index.tsx delete mode 100644 src/features/ImageTopicPanel/index.tsx create mode 100644 src/features/ModelSwitchPanel/components/List/GenerationListItemRenderer.tsx create mode 100644 src/features/ModelSwitchPanel/components/List/GenerationMultipleProvidersItem.tsx rename src/routes/(main)/{image/_layout/ConfigPanel/components/ModelSelect/ImageModelItem.tsx => (create)/components/GenerationModelItem.tsx} (57%) rename src/routes/(main)/{video/features/PromptInput/Title.tsx => (create)/components/PromptTitle.tsx} (60%) create mode 100644 src/routes/(main)/(create)/features/CreateGenerationPage.tsx rename src/routes/(main)/{video => (create)}/features/GenerationFeed/index.tsx (73%) create mode 100644 src/routes/(main)/(create)/features/GenerationInput/ConfigAction.tsx create mode 100644 src/routes/(main)/(create)/features/GenerationInput/GenerationInvalidAPIKey.tsx create mode 100644 src/routes/(main)/(create)/features/GenerationInput/GenerationPromptInput.tsx create mode 100644 src/routes/(main)/(create)/features/GenerationInput/ImagePreviewHeader.tsx create mode 100644 src/routes/(main)/(create)/features/GenerationInput/InlineImageReference.tsx create mode 100644 src/routes/(main)/(create)/features/GenerationInput/InlineVideoFrames.tsx create mode 100644 src/routes/(main)/(create)/features/GenerationInput/ModelAction.tsx create mode 100644 src/routes/(main)/(create)/features/GenerationInput/UploadCard.tsx create mode 100644 src/routes/(main)/(create)/features/GenerationInput/index.ts create mode 100644 src/routes/(main)/(create)/features/GenerationLayout/Body/List/Item/GridItem.tsx create mode 100644 src/routes/(main)/(create)/features/GenerationLayout/Body/List/Item/ListItem.tsx create mode 100644 src/routes/(main)/(create)/features/GenerationLayout/Body/List/Item/index.tsx rename src/{features/GenerationTopicList => routes/(main)/(create)/features/GenerationLayout/Body/List}/NewTopicButton.tsx (100%) rename src/{features/GenerationTopicList => routes/(main)/(create)/features/GenerationLayout/Body/List}/SkeletonList.tsx (100%) rename src/{features/GenerationTopicList => routes/(main)/(create)/features/GenerationLayout/Body/List}/StoreContext.tsx (100%) create mode 100644 src/routes/(main)/(create)/features/GenerationLayout/Body/List/TopicList.tsx rename src/{features/GenerationTopicList => routes/(main)/(create)/features/GenerationLayout/Body/List}/TopicUrlSync.tsx (100%) create mode 100644 src/routes/(main)/(create)/features/GenerationLayout/Body/List/index.tsx create mode 100644 src/routes/(main)/(create)/features/GenerationLayout/Body/index.tsx create mode 100644 src/routes/(main)/(create)/features/GenerationLayout/Header/AddButton.tsx create mode 100644 src/routes/(main)/(create)/features/GenerationLayout/Header/index.tsx create mode 100644 src/routes/(main)/(create)/features/GenerationLayout/Sidebar.tsx create mode 100644 src/routes/(main)/(create)/features/GenerationLayout/index.tsx rename src/routes/(main)/{video/_layout => (create)/features/GenerationLayout}/style.ts (75%) create mode 100644 src/routes/(main)/(create)/features/GenerationLayout/types.ts create mode 100644 src/routes/(main)/(create)/features/GenerationWorkspace/Content.tsx create mode 100644 src/routes/(main)/(create)/features/GenerationWorkspace/EmptyState.tsx create mode 100644 src/routes/(main)/(create)/features/GenerationWorkspace/index.tsx rename src/routes/(main)/{ => (create)}/image/NotSupportClient.tsx (100%) rename src/routes/(main)/{ => (create)}/image/_layout/RegisterHotkeys.tsx (100%) create mode 100644 src/routes/(main)/(create)/image/_layout/index.tsx rename src/routes/(main)/{ => (create)}/image/_layout/type.ts (100%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/ImageConfigSkeleton.tsx (100%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/components/AspectRatioSelect/index.tsx (100%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/components/CfgSliderInput.tsx (100%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/components/DimensionControlGroup.tsx (100%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/components/ImageNum.tsx (99%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/components/ImageUpload.tsx (97%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/components/ImageUrl.tsx (88%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/components/ImageUrlsUpload.tsx (94%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/components/InputNumber/index.tsx (100%) create mode 100644 src/routes/(main)/(create)/image/features/ConfigPanel/components/ModelSelect/ImageModelItem.tsx rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/components/ModelSelect/index.tsx (91%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/components/MultiImagesUpload/ImageManageModal.tsx (98%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/components/MultiImagesUpload/index.tsx (98%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/components/QualitySelect.tsx (100%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/components/ResolutionSelect.tsx (100%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/components/SeedNumberInput.tsx (100%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/components/Select/index.tsx (100%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/components/SizeSelect.tsx (100%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/components/StepsSliderInput.tsx (100%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/constants.ts (100%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/hooks/useAutoDimensions.ts (100%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/hooks/useDragAndDrop.ts (100%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/hooks/useUploadFilesValidation.ts (100%) create mode 100644 src/routes/(main)/(create)/image/features/ConfigPanel/index.ts rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/style.ts (100%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/utils/__tests__/dimensionConstraints.test.ts (100%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/utils/__tests__/imageValidation.test.ts (100%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/utils/dimensionConstraints.ts (100%) rename src/routes/(main)/{image/_layout => (create)/image/features}/ConfigPanel/utils/imageValidation.ts (100%) rename src/routes/(main)/{ => (create)}/image/features/GenerationFeed/BatchItem.tsx (61%) rename src/routes/(main)/{ => (create)}/image/features/GenerationFeed/GenerationItem/ActionButtons.tsx (100%) rename src/routes/(main)/{ => (create)}/image/features/GenerationFeed/GenerationItem/ElapsedTime.tsx (100%) rename src/routes/(main)/{ => (create)}/image/features/GenerationFeed/GenerationItem/ErrorState.tsx (98%) rename src/routes/(main)/{ => (create)}/image/features/GenerationFeed/GenerationItem/LoadingState.tsx (81%) rename src/routes/(main)/{ => (create)}/image/features/GenerationFeed/GenerationItem/SuccessState.tsx (100%) rename src/routes/(main)/{ => (create)}/image/features/GenerationFeed/GenerationItem/index.tsx (100%) rename src/routes/(main)/{ => (create)}/image/features/GenerationFeed/GenerationItem/styles.ts (66%) rename src/routes/(main)/{ => (create)}/image/features/GenerationFeed/GenerationItem/types.ts (100%) rename src/routes/(main)/{ => (create)}/image/features/GenerationFeed/GenerationItem/utils.test.ts (98%) rename src/routes/(main)/{ => (create)}/image/features/GenerationFeed/GenerationItem/utils.ts (97%) create mode 100644 src/routes/(main)/(create)/image/features/GenerationFeed/ReferenceImages.tsx create mode 100644 src/routes/(main)/(create)/image/features/GenerationFeed/index.tsx rename src/routes/(main)/{ => (create)}/image/features/ImageWorkspace/SkeletonList.tsx (67%) create mode 100644 src/routes/(main)/(create)/image/features/ImageWorkspace/index.tsx create mode 100644 src/routes/(main)/(create)/image/features/PromptInput/Title.tsx create mode 100644 src/routes/(main)/(create)/image/features/PromptInput/index.tsx create mode 100644 src/routes/(main)/(create)/image/index.tsx rename src/routes/(main)/{ => (create)}/image/loading.tsx (100%) create mode 100644 src/routes/(main)/(create)/video/_layout/index.tsx rename src/routes/(main)/{video/_layout => (create)/video/features}/ConfigPanel/VideoConfigSkeleton.tsx (100%) rename src/routes/(main)/{video/_layout => (create)/video/features}/ConfigPanel/components/FrameUpload.tsx (89%) create mode 100644 src/routes/(main)/(create)/video/features/ConfigPanel/components/ModelSelect/VideoModelItem.tsx rename src/routes/(main)/{video/_layout => (create)/video/features}/ConfigPanel/components/ModelSelect/index.tsx (91%) create mode 100644 src/routes/(main)/(create)/video/features/ConfigPanel/index.ts rename src/routes/(main)/{ => (create)}/video/features/GenerationFeed/BatchItem.tsx (54%) rename src/routes/(main)/{ => (create)}/video/features/GenerationFeed/VideoErrorItem.tsx (93%) rename src/routes/(main)/{ => (create)}/video/features/GenerationFeed/VideoLoadingItem.tsx (95%) create mode 100644 src/routes/(main)/(create)/video/features/GenerationFeed/VideoReferenceFrames.tsx rename src/routes/(main)/{ => (create)}/video/features/GenerationFeed/VideoSuccessItem.tsx (81%) create mode 100644 src/routes/(main)/(create)/video/features/GenerationFeed/index.tsx create mode 100644 src/routes/(main)/(create)/video/features/PromptInput/Title.tsx create mode 100644 src/routes/(main)/(create)/video/features/PromptInput/index.tsx rename src/routes/(main)/{ => (create)}/video/features/VideoWorkspace/SkeletonList.tsx (71%) create mode 100644 src/routes/(main)/(create)/video/features/VideoWorkspace/index.tsx create mode 100644 src/routes/(main)/(create)/video/index.tsx rename src/routes/(main)/{ => (create)}/video/loading.tsx (100%) delete mode 100644 src/routes/(main)/image/_layout/ConfigPanel/index.tsx delete mode 100644 src/routes/(main)/image/_layout/Header.tsx delete mode 100644 src/routes/(main)/image/_layout/Sidebar.tsx delete mode 100644 src/routes/(main)/image/_layout/TopicSidebar.tsx delete mode 100644 src/routes/(main)/image/_layout/index.tsx delete mode 100644 src/routes/(main)/image/_layout/style.ts delete mode 100644 src/routes/(main)/image/features/GenerationFeed/ReferenceImages.tsx delete mode 100644 src/routes/(main)/image/features/GenerationFeed/index.tsx delete mode 100644 src/routes/(main)/image/features/ImageWorkspace/Content.tsx delete mode 100644 src/routes/(main)/image/features/ImageWorkspace/EmptyState.tsx delete mode 100644 src/routes/(main)/image/features/ImageWorkspace/index.tsx delete mode 100644 src/routes/(main)/image/features/PromptInput/Title.tsx delete mode 100644 src/routes/(main)/image/features/PromptInput/index.tsx delete mode 100644 src/routes/(main)/image/index.tsx delete mode 100644 src/routes/(main)/video/_layout/ConfigPanel/components/ModelSelect/VideoModelItem.tsx delete mode 100644 src/routes/(main)/video/_layout/ConfigPanel/index.tsx delete mode 100644 src/routes/(main)/video/_layout/Header.tsx delete mode 100644 src/routes/(main)/video/_layout/Sidebar.tsx delete mode 100644 src/routes/(main)/video/_layout/TopicSidebar.tsx delete mode 100644 src/routes/(main)/video/_layout/index.tsx delete mode 100644 src/routes/(main)/video/features/GenerationFeed/VideoReferenceFrames.tsx delete mode 100644 src/routes/(main)/video/features/PromptInput/index.tsx delete mode 100644 src/routes/(main)/video/features/VideoWorkspace/Content.tsx delete mode 100644 src/routes/(main)/video/features/VideoWorkspace/EmptyState.tsx delete mode 100644 src/routes/(main)/video/features/VideoWorkspace/index.tsx delete mode 100644 src/routes/(main)/video/index.tsx diff --git a/locales/ar/components.json b/locales/ar/components.json index e5645f0d16..af0b266336 100644 --- a/locales/ar/components.json +++ b/locales/ar/components.json @@ -65,6 +65,10 @@ "FileParsingStatus.chunks.status.errorResult": "فشل في التجزئة، يرجى التحقق والمحاولة مرة أخرى. تفاصيل الخطأ:", "FileParsingStatus.chunks.status.processing": "جارٍ التجزئة", "FileParsingStatus.chunks.status.processingTip": "الخادم يقوم بتقسيم أجزاء النص؛ إغلاق الصفحة لن يؤثر على تقدم التجزئة.", + "GenerationModelItem.creditsPerImageApproximate": "تقريباً {{amount}} ائتمانات / صورة", + "GenerationModelItem.creditsPerImageExact": "{{amount}} ائتمانات / صورة", + "GenerationModelItem.creditsPerVideoApproximate": "تقريباً {{amount}} ائتمانات / فيديو", + "GenerationModelItem.creditsPerVideoExact": "{{amount}} ائتمانات / فيديو", "GoBack.back": "رجوع", "HtmlPreview.actions.download": "تنزيل", "HtmlPreview.actions.preview": "معاينة", @@ -116,6 +120,8 @@ "ModelSwitchPanel.detail.pricing.group.text": "النص", "ModelSwitchPanel.detail.pricing.input": "المدخلات ${{amount}}/مليون", "ModelSwitchPanel.detail.pricing.output": "المخرجات ${{amount}}/مليون", + "ModelSwitchPanel.detail.pricing.perImage": "~ {{amount}} / صورة", + "ModelSwitchPanel.detail.pricing.perVideo": "~ {{amount}} / فيديو", "ModelSwitchPanel.detail.pricing.unit.audioInput": "مدخل صوتي", "ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "مدخل صوتي (مخزن)", "ModelSwitchPanel.detail.pricing.unit.audioOutput": "مخرج صوتي", diff --git a/locales/ar/image.json b/locales/ar/image.json index 7718fd6d5c..91a6cb3df9 100644 --- a/locales/ar/image.json +++ b/locales/ar/image.json @@ -11,6 +11,7 @@ "config.imageUrls.label": "صور مرجعية", "config.model.label": "النموذج", "config.prompt.placeholder": "صف ما ترغب في إنشائه", + "config.prompt.placeholderWithRef": "وصف كيف تريد تعديل الصورة", "config.quality.label": "جودة الصورة", "config.quality.options.hd": "عالية الدقة", "config.quality.options.standard": "قياسية", @@ -22,7 +23,7 @@ "config.seed.random": "بذرة عشوائية", "config.size.label": "الحجم", "config.steps.label": "الخطوات", - "config.title": "صورة بالذكاء الاصطناعي", + "config.title": "الإعدادات", "config.width.label": "العرض", "generation.actions.applySeed": "تطبيق البذرة", "generation.actions.copyError": "نسخ رسالة الخطأ", @@ -53,7 +54,7 @@ "notSupportGuide.features.multiProviders.desc": "يدعم عدة مزودي خدمات لإنشاء الصور بالذكاء الاصطناعي، بما في ذلك OpenAI gpt-image-1 وGoogle Imagen وFAL.ai والمزيد، مما يوفر مجموعة واسعة من النماذج.", "notSupportGuide.features.multiProviders.title": "دعم متعدد المزودين", "notSupportGuide.title": "وضع النشر الحالي لا يدعم إنشاء الصور بالذكاء الاصطناعي", - "topic.createNew": "موضوع جديد", + "topic.createNew": "إنشاء موضوع جديد", "topic.deleteConfirm": "حذف موضوع الإنشاء", "topic.deleteConfirmDesc": "أنت على وشك حذف موضوع الإنشاء هذا. لا يمكن التراجع عن هذا الإجراء، يرجى المتابعة بحذر.", "topic.empty": "لا توجد مواضيع إنشاء", diff --git a/locales/ar/video.json b/locales/ar/video.json index 0619741164..880375bcbb 100644 --- a/locales/ar/video.json +++ b/locales/ar/video.json @@ -7,6 +7,7 @@ "config.header.title": "فيديو", "config.imageUrl.label": "الإطار الابتدائي", "config.prompt.placeholder": "صف الفيديو الذي ترغب في إنشائه", + "config.prompt.placeholderWithRef": "وصف المشهد الذي تريد إنشاؤه مع الصورة", "config.referenceImage.label": "صورة مرجعية", "config.resolution.label": "الدقة", "config.seed.label": "البذرة", @@ -20,7 +21,7 @@ "generation.status.failed": "فشل في الإنشاء", "generation.status.generating": "جارٍ الإنشاء...", "generation.validation.endFrameRequiresStartFrame": "لا يمكن استخدام الإطار النهائي بدون إطار ابتدائي. يرجى تعيين إطار ابتدائي أولاً.", - "topic.createNew": "موضوع جديد", + "topic.createNew": "إنشاء موضوع جديد", "topic.deleteConfirm": "حذف موضوع الفيديو", "topic.deleteConfirmDesc": "أنت على وشك حذف موضوع الفيديو هذا. لا يمكن التراجع عن هذا الإجراء.", "topic.title": "مواضيع الفيديو", diff --git a/locales/bg-BG/components.json b/locales/bg-BG/components.json index 5f177e5114..93f32f73cc 100644 --- a/locales/bg-BG/components.json +++ b/locales/bg-BG/components.json @@ -65,6 +65,10 @@ "FileParsingStatus.chunks.status.errorResult": "Разделянето не бе успешно, моля, проверете и опитайте отново. Подробности за грешката:", "FileParsingStatus.chunks.status.processing": "Разделяне", "FileParsingStatus.chunks.status.processingTip": "Сървърът разделя текстовите части; затварянето на страницата няма да прекъсне процеса.", + "GenerationModelItem.creditsPerImageApproximate": "Прибл. {{amount}} Кредита / изображение", + "GenerationModelItem.creditsPerImageExact": "{{amount}} Кредита / изображение", + "GenerationModelItem.creditsPerVideoApproximate": "Прибл. {{amount}} Кредита / видео", + "GenerationModelItem.creditsPerVideoExact": "{{amount}} Кредита / видео", "GoBack.back": "Назад", "HtmlPreview.actions.download": "Изтегли", "HtmlPreview.actions.preview": "Преглед", @@ -116,6 +120,8 @@ "ModelSwitchPanel.detail.pricing.group.text": "Текст", "ModelSwitchPanel.detail.pricing.input": "Вход ${{amount}}/М", "ModelSwitchPanel.detail.pricing.output": "Изход ${{amount}}/М", + "ModelSwitchPanel.detail.pricing.perImage": "~ {{amount}} / изображение", + "ModelSwitchPanel.detail.pricing.perVideo": "~ {{amount}} / видео", "ModelSwitchPanel.detail.pricing.unit.audioInput": "Аудио вход", "ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "Аудио вход (кеширан)", "ModelSwitchPanel.detail.pricing.unit.audioOutput": "Аудио изход", diff --git a/locales/bg-BG/image.json b/locales/bg-BG/image.json index 4adb7b3339..c27e7f8f2c 100644 --- a/locales/bg-BG/image.json +++ b/locales/bg-BG/image.json @@ -11,6 +11,7 @@ "config.imageUrls.label": "Референтни изображения", "config.model.label": "Модел", "config.prompt.placeholder": "Опишете какво искате да генерирате", + "config.prompt.placeholderWithRef": "Опишете как искате да промените изображението", "config.quality.label": "Качество на изображението", "config.quality.options.hd": "Висока резолюция", "config.quality.options.standard": "Стандартно", @@ -22,7 +23,7 @@ "config.seed.random": "Случаен сийд", "config.size.label": "Размер", "config.steps.label": "Стъпки", - "config.title": "AI Изображение", + "config.title": "Конфигурация", "config.width.label": "Ширина", "generation.actions.applySeed": "Приложи сийд", "generation.actions.copyError": "Копирай съобщението за грешка", @@ -53,7 +54,7 @@ "notSupportGuide.features.multiProviders.desc": "Поддържа множество доставчици на AI генериране на изображения, включително OpenAI gpt-image-1, Google Imagen, FAL.ai и други, предлагащи богат избор от модели.", "notSupportGuide.features.multiProviders.title": "Поддръжка на множество доставчици", "notSupportGuide.title": "Текущият режим на внедряване не поддържа AI генериране на изображения", - "topic.createNew": "Нова тема", + "topic.createNew": "Създаване на нова тема", "topic.deleteConfirm": "Изтриване на тема за генериране", "topic.deleteConfirmDesc": "Ще изтриете тази тема за генериране. Това действие не може да бъде отменено, моля, бъдете внимателни.", "topic.empty": "Няма теми за генериране", diff --git a/locales/bg-BG/video.json b/locales/bg-BG/video.json index 04f7f88dfc..cc89cc41a7 100644 --- a/locales/bg-BG/video.json +++ b/locales/bg-BG/video.json @@ -7,6 +7,7 @@ "config.header.title": "Видео", "config.imageUrl.label": "Начална рамка", "config.prompt.placeholder": "Опишете видеото, което искате да генерирате", + "config.prompt.placeholderWithRef": "Опишете сцената, която искате да създадете с изображението", "config.referenceImage.label": "Референтно изображение", "config.resolution.label": "Резолюция", "config.seed.label": "Сийд", @@ -20,7 +21,7 @@ "generation.status.failed": "Генерирането неуспешно", "generation.status.generating": "Генериране...", "generation.validation.endFrameRequiresStartFrame": "Крайната рамка не може да се използва без начална рамка. Моля, задайте първо начална рамка.", - "topic.createNew": "Нова тема", + "topic.createNew": "Създаване на нова тема", "topic.deleteConfirm": "Изтриване на видео тема", "topic.deleteConfirmDesc": "Ще изтриете тази видео тема. Това действие не може да бъде отменено.", "topic.title": "Видео теми", diff --git a/locales/de-DE/components.json b/locales/de-DE/components.json index a092413181..8208c906ec 100644 --- a/locales/de-DE/components.json +++ b/locales/de-DE/components.json @@ -65,6 +65,10 @@ "FileParsingStatus.chunks.status.errorResult": "Segmentierung fehlgeschlagen, bitte überprüfen und erneut versuchen. Fehlerdetails:", "FileParsingStatus.chunks.status.processing": "Segmentierung", "FileParsingStatus.chunks.status.processingTip": "Der Server teilt Textsegmente auf; das Schließen der Seite beeinflusst den Fortschritt nicht.", + "GenerationModelItem.creditsPerImageApproximate": "Ca. {{amount}} Credits / Bild", + "GenerationModelItem.creditsPerImageExact": "{{amount}} Credits / Bild", + "GenerationModelItem.creditsPerVideoApproximate": "Ca. {{amount}} Credits / Video", + "GenerationModelItem.creditsPerVideoExact": "{{amount}} Credits / Video", "GoBack.back": "Zurück", "HtmlPreview.actions.download": "Herunterladen", "HtmlPreview.actions.preview": "Vorschau", @@ -116,6 +120,8 @@ "ModelSwitchPanel.detail.pricing.group.text": "Text", "ModelSwitchPanel.detail.pricing.input": "Input ${{amount}}/M", "ModelSwitchPanel.detail.pricing.output": "Output ${{amount}}/M", + "ModelSwitchPanel.detail.pricing.perImage": "~ {{amount}} / Bild", + "ModelSwitchPanel.detail.pricing.perVideo": "~ {{amount}} / Video", "ModelSwitchPanel.detail.pricing.unit.audioInput": "Audioeingabe", "ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "Audioeingabe (Cache)", "ModelSwitchPanel.detail.pricing.unit.audioOutput": "Audioausgabe", diff --git a/locales/de-DE/image.json b/locales/de-DE/image.json index dedfd1f3da..ca7d145351 100644 --- a/locales/de-DE/image.json +++ b/locales/de-DE/image.json @@ -11,6 +11,7 @@ "config.imageUrls.label": "Referenzbilder", "config.model.label": "Modell", "config.prompt.placeholder": "Beschreiben Sie, was Sie generieren möchten", + "config.prompt.placeholderWithRef": "Beschreiben Sie, wie Sie das Bild anpassen möchten", "config.quality.label": "Bildqualität", "config.quality.options.hd": "Hohe Auflösung", "config.quality.options.standard": "Standard", @@ -22,7 +23,7 @@ "config.seed.random": "Zufälliger Seed", "config.size.label": "Größe", "config.steps.label": "Schritte", - "config.title": "KI-Bild", + "config.title": "Konfiguration", "config.width.label": "Breite", "generation.actions.applySeed": "Seed anwenden", "generation.actions.copyError": "Fehlermeldung kopieren", @@ -53,7 +54,7 @@ "notSupportGuide.features.multiProviders.desc": "Unterstützt mehrere Anbieter für KI-Bilderzeugung, darunter OpenAI gpt-image-1, Google Imagen, FAL.ai und weitere – für eine große Modellauswahl.", "notSupportGuide.features.multiProviders.title": "Unterstützung mehrerer Anbieter", "notSupportGuide.title": "Aktueller Bereitstellungsmodus unterstützt keine KI-Bilderzeugung", - "topic.createNew": "Neues Thema", + "topic.createNew": "Neues Thema erstellen", "topic.deleteConfirm": "Generierungsthema löschen", "topic.deleteConfirmDesc": "Sie sind dabei, dieses Generierungsthema zu löschen. Diese Aktion kann nicht rückgängig gemacht werden. Bitte seien Sie vorsichtig.", "topic.empty": "Keine Generierungsthemen", diff --git a/locales/de-DE/video.json b/locales/de-DE/video.json index 828479f452..4f1d7d5be6 100644 --- a/locales/de-DE/video.json +++ b/locales/de-DE/video.json @@ -7,6 +7,7 @@ "config.header.title": "Video", "config.imageUrl.label": "Startbild", "config.prompt.placeholder": "Beschreiben Sie das Video, das Sie erstellen möchten", + "config.prompt.placeholderWithRef": "Beschreiben Sie die Szene, die Sie mit dem Bild erstellen möchten", "config.referenceImage.label": "Referenzbild", "config.resolution.label": "Auflösung", "config.seed.label": "Seed", @@ -20,7 +21,7 @@ "generation.status.failed": "Erstellung fehlgeschlagen", "generation.status.generating": "Wird erstellt...", "generation.validation.endFrameRequiresStartFrame": "Ein Endbild kann nicht ohne ein Startbild verwendet werden. Bitte zuerst ein Startbild festlegen.", - "topic.createNew": "Neues Thema", + "topic.createNew": "Neues Thema erstellen", "topic.deleteConfirm": "Video-Thema löschen", "topic.deleteConfirmDesc": "Sie sind dabei, dieses Video-Thema zu löschen. Diese Aktion kann nicht rückgängig gemacht werden.", "topic.title": "Video-Themen", diff --git a/locales/en-US/components.json b/locales/en-US/components.json index 3e3127671c..48ef705aee 100644 --- a/locales/en-US/components.json +++ b/locales/en-US/components.json @@ -65,6 +65,10 @@ "FileParsingStatus.chunks.status.errorResult": "Chunking failed, please check and try again. Error detail:", "FileParsingStatus.chunks.status.processing": "Chunking", "FileParsingStatus.chunks.status.processingTip": "The server is splitting text chunks; closing the page will not affect the chunking progress.", + "GenerationModelItem.creditsPerImageApproximate": "Approx. {{amount}} Credits / image", + "GenerationModelItem.creditsPerImageExact": "{{amount}} Credits / image", + "GenerationModelItem.creditsPerVideoApproximate": "Approx. {{amount}} Credits / video", + "GenerationModelItem.creditsPerVideoExact": "{{amount}} Credits / video", "GoBack.back": "Back", "HtmlPreview.actions.download": "Download", "HtmlPreview.actions.preview": "Preview", @@ -116,6 +120,8 @@ "ModelSwitchPanel.detail.pricing.group.text": "Text", "ModelSwitchPanel.detail.pricing.input": "Input ${{amount}}/M", "ModelSwitchPanel.detail.pricing.output": "Output ${{amount}}/M", + "ModelSwitchPanel.detail.pricing.perImage": "~ ${{amount}} / image", + "ModelSwitchPanel.detail.pricing.perVideo": "~ ${{amount}} / video", "ModelSwitchPanel.detail.pricing.unit.audioInput": "Audio Input", "ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "Audio Input (Cached)", "ModelSwitchPanel.detail.pricing.unit.audioOutput": "Audio Output", diff --git a/locales/en-US/image.json b/locales/en-US/image.json index eaf690e128..4bab39f03e 100644 --- a/locales/en-US/image.json +++ b/locales/en-US/image.json @@ -11,6 +11,7 @@ "config.imageUrls.label": "Reference Images", "config.model.label": "Model", "config.prompt.placeholder": "Describe what you want to generate", + "config.prompt.placeholderWithRef": "Describe how you want to adjust the image", "config.quality.label": "Image Quality", "config.quality.options.hd": "High Definition", "config.quality.options.standard": "Standard", @@ -22,7 +23,7 @@ "config.seed.random": "Random Seed", "config.size.label": "Size", "config.steps.label": "Steps", - "config.title": "AI Image", + "config.title": "Configuration", "config.width.label": "Width", "generation.actions.applySeed": "Apply Seed", "generation.actions.copyError": "Copy Error Message", @@ -53,7 +54,7 @@ "notSupportGuide.features.multiProviders.desc": "Supports multiple AI image generation providers, including OpenAI gpt-image-1, Google Imagen, FAL.ai, and more, offering a wide selection of models.", "notSupportGuide.features.multiProviders.title": "Multi-Provider Support", "notSupportGuide.title": "Current Deployment Mode Does Not Support AI Image Generation", - "topic.createNew": "New Topic", + "topic.createNew": "Create New Topic", "topic.deleteConfirm": "Delete Generation Topic", "topic.deleteConfirmDesc": "You are about to delete this generation topic. This action cannot be undone, please proceed with caution.", "topic.empty": "No generation topics", diff --git a/locales/en-US/video.json b/locales/en-US/video.json index dbe93a3ffb..d930374886 100644 --- a/locales/en-US/video.json +++ b/locales/en-US/video.json @@ -7,6 +7,7 @@ "config.header.title": "Video", "config.imageUrl.label": "Start Frame", "config.prompt.placeholder": "Describe the video you want to generate", + "config.prompt.placeholderWithRef": "Describe the scene you want to generate with the image", "config.referenceImage.label": "Reference Image", "config.resolution.label": "Resolution", "config.seed.label": "Seed", @@ -20,7 +21,7 @@ "generation.status.failed": "Generation Failed", "generation.status.generating": "Generating...", "generation.validation.endFrameRequiresStartFrame": "End frame cannot be used without a start frame. Please set a start frame first.", - "topic.createNew": "New Topic", + "topic.createNew": "Create New Topic", "topic.deleteConfirm": "Delete Video Topic", "topic.deleteConfirmDesc": "You are about to delete this video topic. This action cannot be undone.", "topic.title": "Video Topics", diff --git a/locales/es-ES/components.json b/locales/es-ES/components.json index c5f980755f..c409d73445 100644 --- a/locales/es-ES/components.json +++ b/locales/es-ES/components.json @@ -65,6 +65,10 @@ "FileParsingStatus.chunks.status.errorResult": "Error al fragmentar, por favor revisa e inténtalo de nuevo. Detalle del error:", "FileParsingStatus.chunks.status.processing": "Fragmentando", "FileParsingStatus.chunks.status.processingTip": "El servidor está dividiendo los fragmentos de texto; cerrar la página no afectará el progreso.", + "GenerationModelItem.creditsPerImageApproximate": "Aprox. {{amount}} Créditos / imagen", + "GenerationModelItem.creditsPerImageExact": "{{amount}} Créditos / imagen", + "GenerationModelItem.creditsPerVideoApproximate": "Aprox. {{amount}} Créditos / video", + "GenerationModelItem.creditsPerVideoExact": "{{amount}} Créditos / video", "GoBack.back": "Volver", "HtmlPreview.actions.download": "Descargar", "HtmlPreview.actions.preview": "Vista previa", @@ -116,6 +120,8 @@ "ModelSwitchPanel.detail.pricing.group.text": "Texto", "ModelSwitchPanel.detail.pricing.input": "Entrada ${{amount}}/M", "ModelSwitchPanel.detail.pricing.output": "Salida ${{amount}}/M", + "ModelSwitchPanel.detail.pricing.perImage": "~ {{amount}} / imagen", + "ModelSwitchPanel.detail.pricing.perVideo": "~ {{amount}} / video", "ModelSwitchPanel.detail.pricing.unit.audioInput": "Entrada de audio", "ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "Entrada de audio (en caché)", "ModelSwitchPanel.detail.pricing.unit.audioOutput": "Salida de audio", diff --git a/locales/es-ES/image.json b/locales/es-ES/image.json index f9ea89f565..c91c950030 100644 --- a/locales/es-ES/image.json +++ b/locales/es-ES/image.json @@ -11,6 +11,7 @@ "config.imageUrls.label": "Imágenes de referencia", "config.model.label": "Modelo", "config.prompt.placeholder": "Describe lo que deseas generar", + "config.prompt.placeholderWithRef": "Describe cómo quieres ajustar la imagen", "config.quality.label": "Calidad de imagen", "config.quality.options.hd": "Alta definición", "config.quality.options.standard": "Estándar", @@ -22,7 +23,7 @@ "config.seed.random": "Semilla aleatoria", "config.size.label": "Tamaño", "config.steps.label": "Pasos", - "config.title": "Imagen con IA", + "config.title": "Configuración", "config.width.label": "Anchura", "generation.actions.applySeed": "Aplicar semilla", "generation.actions.copyError": "Copiar mensaje de error", @@ -53,7 +54,7 @@ "notSupportGuide.features.multiProviders.desc": "Admite múltiples proveedores de generación de imágenes con IA, incluyendo OpenAI gpt-image-1, Google Imagen, FAL.ai y más, ofreciendo una amplia selección de modelos.", "notSupportGuide.features.multiProviders.title": "Compatibilidad con múltiples proveedores", "notSupportGuide.title": "El modo de implementación actual no admite la generación de imágenes con IA", - "topic.createNew": "Nuevo tema", + "topic.createNew": "Crear nuevo tema", "topic.deleteConfirm": "Eliminar tema de generación", "topic.deleteConfirmDesc": "Estás a punto de eliminar este tema de generación. Esta acción no se puede deshacer, procede con precaución.", "topic.empty": "No hay temas de generación", diff --git a/locales/es-ES/video.json b/locales/es-ES/video.json index 98981cc174..16b0bfef26 100644 --- a/locales/es-ES/video.json +++ b/locales/es-ES/video.json @@ -7,6 +7,7 @@ "config.header.title": "Vídeo", "config.imageUrl.label": "Fotograma inicial", "config.prompt.placeholder": "Describe el vídeo que deseas generar", + "config.prompt.placeholderWithRef": "Describe la escena que deseas generar con la imagen", "config.referenceImage.label": "Imagen de referencia", "config.resolution.label": "Resolución", "config.seed.label": "Semilla", @@ -20,7 +21,7 @@ "generation.status.failed": "Generación fallida", "generation.status.generating": "Generando...", "generation.validation.endFrameRequiresStartFrame": "El fotograma final no puede usarse sin un fotograma inicial. Por favor, establece primero un fotograma inicial.", - "topic.createNew": "Nuevo tema", + "topic.createNew": "Crear nuevo tema", "topic.deleteConfirm": "Eliminar tema de vídeo", "topic.deleteConfirmDesc": "Estás a punto de eliminar este tema de vídeo. Esta acción no se puede deshacer.", "topic.title": "Temas de vídeo", diff --git a/locales/fa-IR/components.json b/locales/fa-IR/components.json index add54ba210..13ddcce9c6 100644 --- a/locales/fa-IR/components.json +++ b/locales/fa-IR/components.json @@ -65,6 +65,10 @@ "FileParsingStatus.chunks.status.errorResult": "تقسیم‌بندی ناموفق بود، لطفاً بررسی کرده و دوباره تلاش کنید. جزئیات خطا:", "FileParsingStatus.chunks.status.processing": "در حال تقسیم‌بندی", "FileParsingStatus.chunks.status.processingTip": "سرور در حال تقسیم متن به بخش‌هاست؛ بستن صفحه تأثیری بر روند تقسیم ندارد.", + "GenerationModelItem.creditsPerImageApproximate": "تقریباً {{amount}} اعتبار / تصویر", + "GenerationModelItem.creditsPerImageExact": "{{amount}} اعتبار / تصویر", + "GenerationModelItem.creditsPerVideoApproximate": "تقریباً {{amount}} اعتبار / ویدیو", + "GenerationModelItem.creditsPerVideoExact": "{{amount}} اعتبار / ویدیو", "GoBack.back": "بازگشت", "HtmlPreview.actions.download": "دانلود", "HtmlPreview.actions.preview": "پیش‌نمایش", @@ -116,6 +120,8 @@ "ModelSwitchPanel.detail.pricing.group.text": "متن", "ModelSwitchPanel.detail.pricing.input": "ورودی ${{amount}}/میلیون", "ModelSwitchPanel.detail.pricing.output": "خروجی ${{amount}}/میلیون", + "ModelSwitchPanel.detail.pricing.perImage": "~ {{amount}} / تصویر", + "ModelSwitchPanel.detail.pricing.perVideo": "~ {{amount}} / ویدیو", "ModelSwitchPanel.detail.pricing.unit.audioInput": "ورودی صوتی", "ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "ورودی صوتی (کش‌شده)", "ModelSwitchPanel.detail.pricing.unit.audioOutput": "خروجی صوتی", diff --git a/locales/fa-IR/image.json b/locales/fa-IR/image.json index d3a470755d..985a8d55cb 100644 --- a/locales/fa-IR/image.json +++ b/locales/fa-IR/image.json @@ -11,6 +11,7 @@ "config.imageUrls.label": "تصاویر مرجع", "config.model.label": "مدل", "config.prompt.placeholder": "توصیف کنید که چه چیزی می‌خواهید تولید شود", + "config.prompt.placeholderWithRef": "توضیح دهید که چگونه می‌خواهید تصویر را تنظیم کنید", "config.quality.label": "کیفیت تصویر", "config.quality.options.hd": "وضوح بالا", "config.quality.options.standard": "استاندارد", @@ -22,7 +23,7 @@ "config.seed.random": "بذر تصادفی", "config.size.label": "اندازه", "config.steps.label": "مراحل", - "config.title": "تصویر هوش مصنوعی", + "config.title": "پیکربندی", "config.width.label": "عرض", "generation.actions.applySeed": "اعمال بذر", "generation.actions.copyError": "کپی پیام خطا", @@ -53,7 +54,7 @@ "notSupportGuide.features.multiProviders.desc": "پشتیبانی از چندین ارائه‌دهنده تولید تصویر هوش مصنوعی، از جمله OpenAI gpt-image-1، Google Imagen، FAL.ai و دیگران، با ارائه انتخاب گسترده‌ای از مدل‌ها.", "notSupportGuide.features.multiProviders.title": "پشتیبانی از چند ارائه‌دهنده", "notSupportGuide.title": "حالت فعلی استقرار از تولید تصویر با هوش مصنوعی پشتیبانی نمی‌کند", - "topic.createNew": "موضوع جدید", + "topic.createNew": "ایجاد موضوع جدید", "topic.deleteConfirm": "حذف موضوع تولید", "topic.deleteConfirmDesc": "در حال حذف این موضوع تولید هستید. این عمل قابل بازگشت نیست، لطفاً با احتیاط ادامه دهید.", "topic.empty": "هیچ موضوع تولیدی وجود ندارد", diff --git a/locales/fa-IR/video.json b/locales/fa-IR/video.json index 944645c92a..b9893c4108 100644 --- a/locales/fa-IR/video.json +++ b/locales/fa-IR/video.json @@ -7,6 +7,7 @@ "config.header.title": "ویدیو", "config.imageUrl.label": "فریم آغازین", "config.prompt.placeholder": "ویدیویی که می‌خواهید تولید شود را توصیف کنید", + "config.prompt.placeholderWithRef": "صحنه‌ای که می‌خواهید با تصویر ایجاد کنید را توصیف کنید", "config.referenceImage.label": "تصویر مرجع", "config.resolution.label": "وضوح تصویر", "config.seed.label": "بذر (Seed)", @@ -20,7 +21,7 @@ "generation.status.failed": "تولید ناموفق بود", "generation.status.generating": "در حال تولید...", "generation.validation.endFrameRequiresStartFrame": "استفاده از فریم پایانی بدون فریم آغازین امکان‌پذیر نیست. لطفاً ابتدا فریم آغازین را تنظیم کنید.", - "topic.createNew": "موضوع جدید", + "topic.createNew": "ایجاد موضوع جدید", "topic.deleteConfirm": "حذف موضوع ویدیو", "topic.deleteConfirmDesc": "در حال حذف این موضوع ویدیو هستید. این عملیات قابل بازگشت نیست.", "topic.title": "موضوعات ویدیو", diff --git a/locales/fr-FR/components.json b/locales/fr-FR/components.json index 47e5d89b95..9c1ca25dd1 100644 --- a/locales/fr-FR/components.json +++ b/locales/fr-FR/components.json @@ -65,6 +65,10 @@ "FileParsingStatus.chunks.status.errorResult": "Échec du découpage, veuillez vérifier et réessayer. Détail de l'erreur :", "FileParsingStatus.chunks.status.processing": "Découpage", "FileParsingStatus.chunks.status.processingTip": "Le serveur est en train de découper le texte ; fermer la page n'interrompra pas le processus.", + "GenerationModelItem.creditsPerImageApproximate": "Env. {{amount}} Crédits / image", + "GenerationModelItem.creditsPerImageExact": "{{amount}} Crédits / image", + "GenerationModelItem.creditsPerVideoApproximate": "Env. {{amount}} Crédits / vidéo", + "GenerationModelItem.creditsPerVideoExact": "{{amount}} Crédits / vidéo", "GoBack.back": "Retour", "HtmlPreview.actions.download": "Télécharger", "HtmlPreview.actions.preview": "Aperçu", @@ -116,6 +120,8 @@ "ModelSwitchPanel.detail.pricing.group.text": "Texte", "ModelSwitchPanel.detail.pricing.input": "Entrée ${{amount}}/M", "ModelSwitchPanel.detail.pricing.output": "Sortie ${{amount}}/M", + "ModelSwitchPanel.detail.pricing.perImage": "~ {{amount}} / image", + "ModelSwitchPanel.detail.pricing.perVideo": "~ {{amount}} / vidéo", "ModelSwitchPanel.detail.pricing.unit.audioInput": "Entrée audio", "ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "Entrée audio (en cache)", "ModelSwitchPanel.detail.pricing.unit.audioOutput": "Sortie audio", diff --git a/locales/fr-FR/image.json b/locales/fr-FR/image.json index 86f43e83ba..c1b8e2ed18 100644 --- a/locales/fr-FR/image.json +++ b/locales/fr-FR/image.json @@ -11,6 +11,7 @@ "config.imageUrls.label": "Images de référence", "config.model.label": "Modèle", "config.prompt.placeholder": "Décrivez ce que vous souhaitez générer", + "config.prompt.placeholderWithRef": "Décrivez comment vous souhaitez ajuster l'image", "config.quality.label": "Qualité de l'image", "config.quality.options.hd": "Haute définition", "config.quality.options.standard": "Standard", @@ -22,7 +23,7 @@ "config.seed.random": "Graine aléatoire", "config.size.label": "Taille", "config.steps.label": "Étapes", - "config.title": "Image IA", + "config.title": "Configuration", "config.width.label": "Largeur", "generation.actions.applySeed": "Appliquer la graine", "generation.actions.copyError": "Copier le message d'erreur", @@ -53,7 +54,7 @@ "notSupportGuide.features.multiProviders.desc": "Prend en charge plusieurs fournisseurs de génération d'images IA, y compris OpenAI gpt-image-1, Google Imagen, FAL.ai, et d'autres, offrant un large choix de modèles.", "notSupportGuide.features.multiProviders.title": "Prise en charge multi-fournisseurs", "notSupportGuide.title": "Le mode de déploiement actuel ne prend pas en charge la génération d'images IA", - "topic.createNew": "Nouveau sujet", + "topic.createNew": "Créer un nouveau sujet", "topic.deleteConfirm": "Supprimer le sujet de génération", "topic.deleteConfirmDesc": "Vous êtes sur le point de supprimer ce sujet de génération. Cette action est irréversible, veuillez procéder avec prudence.", "topic.empty": "Aucun sujet de génération", diff --git a/locales/fr-FR/video.json b/locales/fr-FR/video.json index 903457516c..b217620943 100644 --- a/locales/fr-FR/video.json +++ b/locales/fr-FR/video.json @@ -7,6 +7,7 @@ "config.header.title": "Vidéo", "config.imageUrl.label": "Image de début", "config.prompt.placeholder": "Décrivez la vidéo que vous souhaitez générer", + "config.prompt.placeholderWithRef": "Décrivez la scène que vous souhaitez générer avec l'image", "config.referenceImage.label": "Image de référence", "config.resolution.label": "Résolution", "config.seed.label": "Graine", @@ -20,7 +21,7 @@ "generation.status.failed": "Échec de la génération", "generation.status.generating": "Génération en cours...", "generation.validation.endFrameRequiresStartFrame": "L'image de fin ne peut pas être utilisée sans image de début. Veuillez d'abord définir une image de début.", - "topic.createNew": "Nouveau sujet", + "topic.createNew": "Créer un nouveau sujet", "topic.deleteConfirm": "Supprimer le sujet vidéo", "topic.deleteConfirmDesc": "Vous êtes sur le point de supprimer ce sujet vidéo. Cette action est irréversible.", "topic.title": "Sujets vidéo", diff --git a/locales/it-IT/components.json b/locales/it-IT/components.json index bae3b96143..e7a8e4bd0d 100644 --- a/locales/it-IT/components.json +++ b/locales/it-IT/components.json @@ -65,6 +65,10 @@ "FileParsingStatus.chunks.status.errorResult": "Segmentazione non riuscita, controlla e riprova. Dettagli errore:", "FileParsingStatus.chunks.status.processing": "Segmentazione in corso", "FileParsingStatus.chunks.status.processingTip": "Il server sta dividendo i segmenti di testo; chiudere la pagina non influirà sul progresso.", + "GenerationModelItem.creditsPerImageApproximate": "Circa {{amount}} Crediti / immagine", + "GenerationModelItem.creditsPerImageExact": "{{amount}} Crediti / immagine", + "GenerationModelItem.creditsPerVideoApproximate": "Circa {{amount}} Crediti / video", + "GenerationModelItem.creditsPerVideoExact": "{{amount}} Crediti / video", "GoBack.back": "Indietro", "HtmlPreview.actions.download": "Scarica", "HtmlPreview.actions.preview": "Anteprima", @@ -116,6 +120,8 @@ "ModelSwitchPanel.detail.pricing.group.text": "Testo", "ModelSwitchPanel.detail.pricing.input": "Input ${{amount}}/M", "ModelSwitchPanel.detail.pricing.output": "Output ${{amount}}/M", + "ModelSwitchPanel.detail.pricing.perImage": "~ {{amount}} / immagine", + "ModelSwitchPanel.detail.pricing.perVideo": "~ {{amount}} / video", "ModelSwitchPanel.detail.pricing.unit.audioInput": "Input Audio", "ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "Input Audio (Memorizzato)", "ModelSwitchPanel.detail.pricing.unit.audioOutput": "Output Audio", diff --git a/locales/it-IT/image.json b/locales/it-IT/image.json index 5b19ce6b52..f9e54be670 100644 --- a/locales/it-IT/image.json +++ b/locales/it-IT/image.json @@ -11,6 +11,7 @@ "config.imageUrls.label": "Immagini di riferimento", "config.model.label": "Modello", "config.prompt.placeholder": "Descrivi cosa vuoi generare", + "config.prompt.placeholderWithRef": "Descrivi come desideri modificare l'immagine", "config.quality.label": "Qualità immagine", "config.quality.options.hd": "Alta definizione", "config.quality.options.standard": "Standard", @@ -22,7 +23,7 @@ "config.seed.random": "Seed casuale", "config.size.label": "Dimensione", "config.steps.label": "Passaggi", - "config.title": "Immagine AI", + "config.title": "Configurazione", "config.width.label": "Larghezza", "generation.actions.applySeed": "Applica seed", "generation.actions.copyError": "Copia messaggio di errore", @@ -53,7 +54,7 @@ "notSupportGuide.features.multiProviders.desc": "Supporta più fornitori di generazione immagini AI, inclusi OpenAI gpt-image-1, Google Imagen, FAL.ai e altri, offrendo un'ampia selezione di modelli.", "notSupportGuide.features.multiProviders.title": "Supporto multi-fornitore", "notSupportGuide.title": "La modalità di distribuzione attuale non supporta la generazione di immagini AI", - "topic.createNew": "Nuovo argomento", + "topic.createNew": "Crea Nuovo Argomento", "topic.deleteConfirm": "Elimina argomento di generazione", "topic.deleteConfirmDesc": "Stai per eliminare questo argomento di generazione. Questa azione è irreversibile, procedi con cautela.", "topic.empty": "Nessun argomento di generazione", diff --git a/locales/it-IT/video.json b/locales/it-IT/video.json index 2728fb1bd4..adaefefd7a 100644 --- a/locales/it-IT/video.json +++ b/locales/it-IT/video.json @@ -7,6 +7,7 @@ "config.header.title": "Video", "config.imageUrl.label": "Fotogramma Iniziale", "config.prompt.placeholder": "Descrivi il video che vuoi generare", + "config.prompt.placeholderWithRef": "Descrivi la scena che vuoi generare con l'immagine", "config.referenceImage.label": "Immagine di Riferimento", "config.resolution.label": "Risoluzione", "config.seed.label": "Seed", @@ -20,7 +21,7 @@ "generation.status.failed": "Generazione Fallita", "generation.status.generating": "Generazione in corso...", "generation.validation.endFrameRequiresStartFrame": "Il fotogramma finale non può essere utilizzato senza un fotogramma iniziale. Imposta prima un fotogramma iniziale.", - "topic.createNew": "Nuovo Argomento", + "topic.createNew": "Crea Nuovo Argomento", "topic.deleteConfirm": "Elimina Argomento Video", "topic.deleteConfirmDesc": "Stai per eliminare questo argomento video. Questa azione non può essere annullata.", "topic.title": "Argomenti Video", diff --git a/locales/ja-JP/components.json b/locales/ja-JP/components.json index 16da91b988..28b696d95a 100644 --- a/locales/ja-JP/components.json +++ b/locales/ja-JP/components.json @@ -65,6 +65,10 @@ "FileParsingStatus.chunks.status.errorResult": "分割に失敗しました。再試行する前に確認してください。失敗の理由:", "FileParsingStatus.chunks.status.processing": "分割中", "FileParsingStatus.chunks.status.processingTip": "サーバーがテキストブロックを分割しています。ページを閉じても分割の進行には影響しません", + "GenerationModelItem.creditsPerImageApproximate": "約 {{amount}} クレジット / 画像", + "GenerationModelItem.creditsPerImageExact": "{{amount}} クレジット / 画像", + "GenerationModelItem.creditsPerVideoApproximate": "約 {{amount}} クレジット / 動画", + "GenerationModelItem.creditsPerVideoExact": "{{amount}} クレジット / 動画", "GoBack.back": "戻る", "HtmlPreview.actions.download": "ダウンロード", "HtmlPreview.actions.preview": "プレビュー", @@ -116,6 +120,8 @@ "ModelSwitchPanel.detail.pricing.group.text": "テキスト", "ModelSwitchPanel.detail.pricing.input": "入力 ${{amount}}/M", "ModelSwitchPanel.detail.pricing.output": "出力 ${{amount}}/M", + "ModelSwitchPanel.detail.pricing.perImage": "〜 {{amount}} / 画像", + "ModelSwitchPanel.detail.pricing.perVideo": "〜 {{amount}} / 動画", "ModelSwitchPanel.detail.pricing.unit.audioInput": "音声入力", "ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "音声入力(キャッシュ読み取り)", "ModelSwitchPanel.detail.pricing.unit.audioOutput": "音声出力", diff --git a/locales/ja-JP/image.json b/locales/ja-JP/image.json index 0b387d3130..c8b92a38e7 100644 --- a/locales/ja-JP/image.json +++ b/locales/ja-JP/image.json @@ -11,6 +11,7 @@ "config.imageUrls.label": "参考画像", "config.model.label": "モデル", "config.prompt.placeholder": "生成したい内容を記述してください", + "config.prompt.placeholderWithRef": "画像をどのように調整したいか説明してください", "config.quality.label": "画像品質", "config.quality.options.hd": "高精細", "config.quality.options.standard": "標準", @@ -22,7 +23,7 @@ "config.seed.random": "ランダムシード", "config.size.label": "サイズ", "config.steps.label": "ステップ数", - "config.title": "AI 絵画", + "config.title": "設定", "config.width.label": "幅", "generation.actions.applySeed": "シードを適用する", "generation.actions.copyError": "エラー情報をコピー", @@ -53,7 +54,7 @@ "notSupportGuide.features.multiProviders.desc": "OpenAI gpt-image-1、Google Imagen、FAL.ai など複数のAI絵画サービスプロバイダーをサポートし、多彩なモデル選択を提供します", "notSupportGuide.features.multiProviders.title": "複数プロバイダー対応", "notSupportGuide.title": "現在のデプロイモードはAI絵画をサポートしていません", - "topic.createNew": "新しいテーマを作成", + "topic.createNew": "新しいトピックを作成", "topic.deleteConfirm": "生成テーマを削除", "topic.deleteConfirmDesc": "この生成テーマを削除すると復元できません。慎重に操作してください。", "topic.empty": "生成されたテーマはありません", diff --git a/locales/ja-JP/video.json b/locales/ja-JP/video.json index 0142700733..fb9c8bf1ce 100644 --- a/locales/ja-JP/video.json +++ b/locales/ja-JP/video.json @@ -7,6 +7,7 @@ "config.header.title": "ビデオ", "config.imageUrl.label": "開始フレーム", "config.prompt.placeholder": "生成したいビデオの内容を説明してください", + "config.prompt.placeholderWithRef": "生成したい画像のシーンを説明してください", "config.referenceImage.label": "参照画像", "config.resolution.label": "解像度", "config.seed.label": "シード値", @@ -20,7 +21,7 @@ "generation.status.failed": "生成に失敗しました", "generation.status.generating": "生成中...", "generation.validation.endFrameRequiresStartFrame": "終了フレームを使用するには、開始フレームを設定してください。", - "topic.createNew": "新しいトピック", + "topic.createNew": "新しいトピックを作成", "topic.deleteConfirm": "ビデオトピックを削除", "topic.deleteConfirmDesc": "このビデオトピックを削除しようとしています。この操作は元に戻せません。", "topic.title": "ビデオトピック", diff --git a/locales/ko-KR/components.json b/locales/ko-KR/components.json index 044e29ae45..8228327f5d 100644 --- a/locales/ko-KR/components.json +++ b/locales/ko-KR/components.json @@ -65,6 +65,10 @@ "FileParsingStatus.chunks.status.errorResult": "분할에 실패했습니다. 확인 후 다시 시도하세요. 실패 원인:", "FileParsingStatus.chunks.status.processing": "분할 중", "FileParsingStatus.chunks.status.processingTip": "서버에서 텍스트 블록을 분할 중입니다. 페이지를 닫아도 작업은 계속됩니다.", + "GenerationModelItem.creditsPerImageApproximate": "약 {{amount}} 크레딧 / 이미지", + "GenerationModelItem.creditsPerImageExact": "{{amount}} 크레딧 / 이미지", + "GenerationModelItem.creditsPerVideoApproximate": "약 {{amount}} 크레딧 / 비디오", + "GenerationModelItem.creditsPerVideoExact": "{{amount}} 크레딧 / 비디오", "GoBack.back": "뒤로가기", "HtmlPreview.actions.download": "다운로드", "HtmlPreview.actions.preview": "미리보기", @@ -116,6 +120,8 @@ "ModelSwitchPanel.detail.pricing.group.text": "텍스트", "ModelSwitchPanel.detail.pricing.input": "입력 ${{amount}}/백만자", "ModelSwitchPanel.detail.pricing.output": "출력 ${{amount}}/백만자", + "ModelSwitchPanel.detail.pricing.perImage": "~ {{amount}} / 이미지", + "ModelSwitchPanel.detail.pricing.perVideo": "~ {{amount}} / 비디오", "ModelSwitchPanel.detail.pricing.unit.audioInput": "오디오 입력", "ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "오디오 입력 (캐시 읽기)", "ModelSwitchPanel.detail.pricing.unit.audioOutput": "오디오 출력", diff --git a/locales/ko-KR/image.json b/locales/ko-KR/image.json index 303fc45203..5d98ba3860 100644 --- a/locales/ko-KR/image.json +++ b/locales/ko-KR/image.json @@ -11,6 +11,7 @@ "config.imageUrls.label": "참고 이미지", "config.model.label": "모델", "config.prompt.placeholder": "생성하고 싶은 내용을 설명하세요", + "config.prompt.placeholderWithRef": "이미지를 어떻게 조정하고 싶은지 설명하세요", "config.quality.label": "이미지 품질", "config.quality.options.hd": "고화질", "config.quality.options.standard": "표준", @@ -22,7 +23,7 @@ "config.seed.random": "무작위 시드", "config.size.label": "크기", "config.steps.label": "단계 수", - "config.title": "AI 그림 그리기", + "config.title": "구성", "config.width.label": "너비", "generation.actions.applySeed": "시드 적용", "generation.actions.copyError": "오류 정보 복사", diff --git a/locales/ko-KR/video.json b/locales/ko-KR/video.json index 29ce845d09..fae335af00 100644 --- a/locales/ko-KR/video.json +++ b/locales/ko-KR/video.json @@ -7,6 +7,7 @@ "config.header.title": "비디오", "config.imageUrl.label": "시작 프레임", "config.prompt.placeholder": "생성하고 싶은 비디오를 설명하세요", + "config.prompt.placeholderWithRef": "생성하고자 하는 이미지의 장면을 설명하세요", "config.referenceImage.label": "참조 이미지", "config.resolution.label": "해상도", "config.seed.label": "시드", diff --git a/locales/nl-NL/components.json b/locales/nl-NL/components.json index f189f0e573..2a51fb669f 100644 --- a/locales/nl-NL/components.json +++ b/locales/nl-NL/components.json @@ -65,6 +65,10 @@ "FileParsingStatus.chunks.status.errorResult": "Verdeling mislukt, controleer en probeer opnieuw. Foutdetails:", "FileParsingStatus.chunks.status.processing": "Bezig met verdelen", "FileParsingStatus.chunks.status.processingTip": "De server splitst tekstfragmenten; het sluiten van de pagina heeft geen invloed op de voortgang.", + "GenerationModelItem.creditsPerImageApproximate": "Ongeveer {{amount}} Credits / afbeelding", + "GenerationModelItem.creditsPerImageExact": "{{amount}} Credits / afbeelding", + "GenerationModelItem.creditsPerVideoApproximate": "Ongeveer {{amount}} Credits / video", + "GenerationModelItem.creditsPerVideoExact": "{{amount}} Credits / video", "GoBack.back": "Terug", "HtmlPreview.actions.download": "Downloaden", "HtmlPreview.actions.preview": "Voorbeeld", @@ -116,6 +120,8 @@ "ModelSwitchPanel.detail.pricing.group.text": "Tekst", "ModelSwitchPanel.detail.pricing.input": "Invoer ${{amount}}/M", "ModelSwitchPanel.detail.pricing.output": "Uitvoer ${{amount}}/M", + "ModelSwitchPanel.detail.pricing.perImage": "~ {{amount}} / afbeelding", + "ModelSwitchPanel.detail.pricing.perVideo": "~ {{amount}} / video", "ModelSwitchPanel.detail.pricing.unit.audioInput": "Audio-invoer", "ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "Audio-invoer (cache)", "ModelSwitchPanel.detail.pricing.unit.audioOutput": "Audio-uitvoer", diff --git a/locales/nl-NL/image.json b/locales/nl-NL/image.json index 41e467ab49..8985018416 100644 --- a/locales/nl-NL/image.json +++ b/locales/nl-NL/image.json @@ -11,6 +11,7 @@ "config.imageUrls.label": "Referentieafbeeldingen", "config.model.label": "Model", "config.prompt.placeholder": "Beschrijf wat je wilt genereren", + "config.prompt.placeholderWithRef": "Beschrijf hoe u de afbeelding wilt aanpassen", "config.quality.label": "Beeldkwaliteit", "config.quality.options.hd": "Hoge definitie", "config.quality.options.standard": "Standaard", @@ -22,7 +23,7 @@ "config.seed.random": "Willekeurige seed", "config.size.label": "Formaat", "config.steps.label": "Stappen", - "config.title": "AI-afbeelding", + "config.title": "Configuratie", "config.width.label": "Breedte", "generation.actions.applySeed": "Seed toepassen", "generation.actions.copyError": "Foutmelding kopiëren", @@ -53,7 +54,7 @@ "notSupportGuide.features.multiProviders.desc": "Ondersteunt meerdere AI-afbeeldingsgeneratoren, waaronder OpenAI gpt-image-1, Google Imagen, FAL.ai en meer, met een ruime keuze aan modellen.", "notSupportGuide.features.multiProviders.title": "Ondersteuning voor meerdere providers", "notSupportGuide.title": "Huidige implementatiemodus ondersteunt geen AI-afbeeldingsgeneratie", - "topic.createNew": "Nieuw onderwerp", + "topic.createNew": "Nieuw Onderwerp Maken", "topic.deleteConfirm": "Generatieonderwerp verwijderen", "topic.deleteConfirmDesc": "Je staat op het punt dit generatieonderwerp te verwijderen. Deze actie kan niet ongedaan worden gemaakt, wees voorzichtig.", "topic.empty": "Geen generatieonderwerpen", diff --git a/locales/nl-NL/video.json b/locales/nl-NL/video.json index bcd26560a7..ebe9a5f33e 100644 --- a/locales/nl-NL/video.json +++ b/locales/nl-NL/video.json @@ -7,6 +7,7 @@ "config.header.title": "Video", "config.imageUrl.label": "Startframe", "config.prompt.placeholder": "Beschrijf de video die je wilt genereren", + "config.prompt.placeholderWithRef": "Beschrijf de scène die je wilt genereren met de afbeelding", "config.referenceImage.label": "Referentieafbeelding", "config.resolution.label": "Resolutie", "config.seed.label": "Zaadwaarde", @@ -20,7 +21,7 @@ "generation.status.failed": "Generatie Mislukt", "generation.status.generating": "Bezig met genereren...", "generation.validation.endFrameRequiresStartFrame": "Eindframe kan niet worden gebruikt zonder een startframe. Stel eerst een startframe in.", - "topic.createNew": "Nieuw Onderwerp", + "topic.createNew": "Nieuw Onderwerp Maken", "topic.deleteConfirm": "Videothema Verwijderen", "topic.deleteConfirmDesc": "Je staat op het punt dit videothema te verwijderen. Deze actie kan niet ongedaan worden gemaakt.", "topic.title": "Videothema's", diff --git a/locales/pl-PL/components.json b/locales/pl-PL/components.json index 6902868943..f60eb00cea 100644 --- a/locales/pl-PL/components.json +++ b/locales/pl-PL/components.json @@ -65,6 +65,10 @@ "FileParsingStatus.chunks.status.errorResult": "Dzielenie na fragmenty nie powiodło się, sprawdź i spróbuj ponownie. Szczegóły błędu:", "FileParsingStatus.chunks.status.processing": "Dzielenie na fragmenty", "FileParsingStatus.chunks.status.processingTip": "Serwer dzieli tekst na fragmenty; zamknięcie strony nie wpłynie na postęp procesu.", + "GenerationModelItem.creditsPerImageApproximate": "Około {{amount}} Kredytów / obraz", + "GenerationModelItem.creditsPerImageExact": "{{amount}} Kredytów / obraz", + "GenerationModelItem.creditsPerVideoApproximate": "Około {{amount}} Kredytów / wideo", + "GenerationModelItem.creditsPerVideoExact": "{{amount}} Kredytów / wideo", "GoBack.back": "Wstecz", "HtmlPreview.actions.download": "Pobierz", "HtmlPreview.actions.preview": "Podgląd", @@ -116,6 +120,8 @@ "ModelSwitchPanel.detail.pricing.group.text": "Tekst", "ModelSwitchPanel.detail.pricing.input": "Dane wejściowe ${{amount}}/M", "ModelSwitchPanel.detail.pricing.output": "Dane wyjściowe ${{amount}}/M", + "ModelSwitchPanel.detail.pricing.perImage": "~ {{amount}} / obraz", + "ModelSwitchPanel.detail.pricing.perVideo": "~ {{amount}} / wideo", "ModelSwitchPanel.detail.pricing.unit.audioInput": "Wejście audio", "ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "Wejście audio (z bufora)", "ModelSwitchPanel.detail.pricing.unit.audioOutput": "Wyjście audio", diff --git a/locales/pl-PL/image.json b/locales/pl-PL/image.json index 3c619fac7e..dc702886d0 100644 --- a/locales/pl-PL/image.json +++ b/locales/pl-PL/image.json @@ -11,6 +11,7 @@ "config.imageUrls.label": "Obrazy referencyjne", "config.model.label": "Model", "config.prompt.placeholder": "Opisz, co chcesz wygenerować", + "config.prompt.placeholderWithRef": "Opisz, jak chcesz dostosować obraz", "config.quality.label": "Jakość obrazu", "config.quality.options.hd": "Wysoka rozdzielczość", "config.quality.options.standard": "Standardowa", @@ -22,7 +23,7 @@ "config.seed.random": "Losowe ziarno", "config.size.label": "Rozmiar", "config.steps.label": "Kroki", - "config.title": "Obraz AI", + "config.title": "Konfiguracja", "config.width.label": "Szerokość", "generation.actions.applySeed": "Zastosuj ziarno", "generation.actions.copyError": "Skopiuj komunikat o błędzie", @@ -53,7 +54,7 @@ "notSupportGuide.features.multiProviders.desc": "Obsługuje wielu dostawców generowania obrazów AI, w tym OpenAI gpt-image-1, Google Imagen, FAL.ai i innych, oferując szeroki wybór modeli.", "notSupportGuide.features.multiProviders.title": "Wsparcie wielu dostawców", "notSupportGuide.title": "Obecny tryb wdrożenia nie obsługuje generowania obrazów AI", - "topic.createNew": "Nowy temat", + "topic.createNew": "Utwórz nowy temat", "topic.deleteConfirm": "Usuń temat generowania", "topic.deleteConfirmDesc": "Zamierzasz usunąć ten temat generowania. Tej operacji nie można cofnąć, proszę zachować ostrożność.", "topic.empty": "Brak tematów generowania", diff --git a/locales/pl-PL/video.json b/locales/pl-PL/video.json index 018e708936..8687c2ce18 100644 --- a/locales/pl-PL/video.json +++ b/locales/pl-PL/video.json @@ -7,6 +7,7 @@ "config.header.title": "Wideo", "config.imageUrl.label": "Klatka początkowa", "config.prompt.placeholder": "Opisz wideo, które chcesz wygenerować", + "config.prompt.placeholderWithRef": "Opisz scenę, którą chcesz wygenerować na obrazie", "config.referenceImage.label": "Obraz referencyjny", "config.resolution.label": "Rozdzielczość", "config.seed.label": "Ziarno", @@ -20,7 +21,7 @@ "generation.status.failed": "Generowanie nie powiodło się", "generation.status.generating": "Generowanie...", "generation.validation.endFrameRequiresStartFrame": "Klatka końcowa wymaga ustawienia klatki początkowej. Najpierw ustaw klatkę początkową.", - "topic.createNew": "Nowy temat", + "topic.createNew": "Utwórz nowy temat", "topic.deleteConfirm": "Usuń temat wideo", "topic.deleteConfirmDesc": "Zamierzasz usunąć ten temat wideo. Tej operacji nie można cofnąć.", "topic.title": "Tematy wideo", diff --git a/locales/pt-BR/components.json b/locales/pt-BR/components.json index 452555cee6..0066a0fa8a 100644 --- a/locales/pt-BR/components.json +++ b/locales/pt-BR/components.json @@ -65,6 +65,10 @@ "FileParsingStatus.chunks.status.errorResult": "Falha na divisão, verifique e tente novamente. Detalhes do erro:", "FileParsingStatus.chunks.status.processing": "Dividindo", "FileParsingStatus.chunks.status.processingTip": "O servidor está dividindo os trechos de texto; fechar a página não afetará o progresso.", + "GenerationModelItem.creditsPerImageApproximate": "Aprox. {{amount}} Créditos / imagem", + "GenerationModelItem.creditsPerImageExact": "{{amount}} Créditos / imagem", + "GenerationModelItem.creditsPerVideoApproximate": "Aprox. {{amount}} Créditos / vídeo", + "GenerationModelItem.creditsPerVideoExact": "{{amount}} Créditos / vídeo", "GoBack.back": "Voltar", "HtmlPreview.actions.download": "Baixar", "HtmlPreview.actions.preview": "Visualizar", @@ -116,6 +120,8 @@ "ModelSwitchPanel.detail.pricing.group.text": "Texto", "ModelSwitchPanel.detail.pricing.input": "Entrada ${{amount}}/M", "ModelSwitchPanel.detail.pricing.output": "Saída ${{amount}}/M", + "ModelSwitchPanel.detail.pricing.perImage": "~ {{amount}} / imagem", + "ModelSwitchPanel.detail.pricing.perVideo": "~ {{amount}} / vídeo", "ModelSwitchPanel.detail.pricing.unit.audioInput": "Entrada de Áudio", "ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "Entrada de Áudio (em Cache)", "ModelSwitchPanel.detail.pricing.unit.audioOutput": "Saída de Áudio", diff --git a/locales/pt-BR/image.json b/locales/pt-BR/image.json index f351a1db8c..03dd0cdf24 100644 --- a/locales/pt-BR/image.json +++ b/locales/pt-BR/image.json @@ -11,6 +11,7 @@ "config.imageUrls.label": "Imagens de Referência", "config.model.label": "Modelo", "config.prompt.placeholder": "Descreva o que você deseja gerar", + "config.prompt.placeholderWithRef": "Descreva como você deseja ajustar a imagem", "config.quality.label": "Qualidade da Imagem", "config.quality.options.hd": "Alta Definição", "config.quality.options.standard": "Padrão", @@ -22,7 +23,7 @@ "config.seed.random": "Semente Aleatória", "config.size.label": "Tamanho", "config.steps.label": "Etapas", - "config.title": "Imagem com IA", + "config.title": "Configuração", "config.width.label": "Largura", "generation.actions.applySeed": "Aplicar Semente", "generation.actions.copyError": "Copiar Mensagem de Erro", @@ -53,7 +54,7 @@ "notSupportGuide.features.multiProviders.desc": "Suporta múltiplos provedores de geração de imagens com IA, incluindo OpenAI gpt-image-1, Google Imagen, FAL.ai e outros, oferecendo uma ampla seleção de modelos.", "notSupportGuide.features.multiProviders.title": "Suporte a Múltiplos Provedores", "notSupportGuide.title": "O Modo de Implantação Atual Não Suporta Geração de Imagens com IA", - "topic.createNew": "Novo Tópico", + "topic.createNew": "Criar Novo Tópico", "topic.deleteConfirm": "Excluir Tópico de Geração", "topic.deleteConfirmDesc": "Você está prestes a excluir este tópico de geração. Esta ação não pode ser desfeita, prossiga com cautela.", "topic.empty": "Nenhum tópico de geração", diff --git a/locales/pt-BR/video.json b/locales/pt-BR/video.json index 6fb6b2d64a..0fe38f463b 100644 --- a/locales/pt-BR/video.json +++ b/locales/pt-BR/video.json @@ -7,6 +7,7 @@ "config.header.title": "Vídeo", "config.imageUrl.label": "Quadro Inicial", "config.prompt.placeholder": "Descreva o vídeo que você deseja gerar", + "config.prompt.placeholderWithRef": "Descreva a cena que você deseja gerar com a imagem", "config.referenceImage.label": "Imagem de Referência", "config.resolution.label": "Resolução", "config.seed.label": "Semente", @@ -20,7 +21,7 @@ "generation.status.failed": "Falha na Geração", "generation.status.generating": "Gerando...", "generation.validation.endFrameRequiresStartFrame": "O quadro final não pode ser usado sem um quadro inicial. Por favor, defina primeiro um quadro inicial.", - "topic.createNew": "Novo Tópico", + "topic.createNew": "Criar Novo Tópico", "topic.deleteConfirm": "Excluir Tópico de Vídeo", "topic.deleteConfirmDesc": "Você está prestes a excluir este tópico de vídeo. Esta ação não poderá ser desfeita.", "topic.title": "Tópicos de Vídeo", diff --git a/locales/ru-RU/components.json b/locales/ru-RU/components.json index 2f3e105301..efa464777f 100644 --- a/locales/ru-RU/components.json +++ b/locales/ru-RU/components.json @@ -65,6 +65,10 @@ "FileParsingStatus.chunks.status.errorResult": "Сегментация не удалась, пожалуйста, проверьте и повторите попытку. Подробности ошибки:", "FileParsingStatus.chunks.status.processing": "Сегментация", "FileParsingStatus.chunks.status.processingTip": "Сервер разделяет текст на фрагменты; закрытие страницы не повлияет на процесс.", + "GenerationModelItem.creditsPerImageApproximate": "Примерно {{amount}} кредитов / изображение", + "GenerationModelItem.creditsPerImageExact": "{{amount}} кредитов / изображение", + "GenerationModelItem.creditsPerVideoApproximate": "Примерно {{amount}} кредитов / видео", + "GenerationModelItem.creditsPerVideoExact": "{{amount}} кредитов / видео", "GoBack.back": "Назад", "HtmlPreview.actions.download": "Скачать", "HtmlPreview.actions.preview": "Предпросмотр", @@ -116,6 +120,8 @@ "ModelSwitchPanel.detail.pricing.group.text": "Текст", "ModelSwitchPanel.detail.pricing.input": "Ввод ${{amount}}/М", "ModelSwitchPanel.detail.pricing.output": "Вывод ${{amount}}/М", + "ModelSwitchPanel.detail.pricing.perImage": "~ {{amount}} / изображение", + "ModelSwitchPanel.detail.pricing.perVideo": "~ {{amount}} / видео", "ModelSwitchPanel.detail.pricing.unit.audioInput": "Аудио-ввод", "ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "Аудио-ввод (из кэша)", "ModelSwitchPanel.detail.pricing.unit.audioOutput": "Аудио-вывод", diff --git a/locales/ru-RU/image.json b/locales/ru-RU/image.json index d21511678c..eb483e4345 100644 --- a/locales/ru-RU/image.json +++ b/locales/ru-RU/image.json @@ -11,6 +11,7 @@ "config.imageUrls.label": "Референсные изображения", "config.model.label": "Модель", "config.prompt.placeholder": "Опишите, что вы хотите сгенерировать", + "config.prompt.placeholderWithRef": "Опишите, как вы хотите изменить изображение", "config.quality.label": "Качество изображения", "config.quality.options.hd": "Высокое качество", "config.quality.options.standard": "Стандартное", @@ -22,7 +23,7 @@ "config.seed.random": "Случайный сид", "config.size.label": "Размер", "config.steps.label": "Шаги", - "config.title": "AI Изображение", + "config.title": "Конфигурация", "config.width.label": "Ширина", "generation.actions.applySeed": "Применить сид", "generation.actions.copyError": "Скопировать сообщение об ошибке", @@ -53,7 +54,7 @@ "notSupportGuide.features.multiProviders.desc": "Поддержка нескольких провайдеров генерации AI-изображений, включая OpenAI gpt-image-1, Google Imagen, FAL.ai и других, предлагая широкий выбор моделей.", "notSupportGuide.features.multiProviders.title": "Поддержка нескольких провайдеров", "notSupportGuide.title": "Текущий режим развертывания не поддерживает генерацию AI-изображений", - "topic.createNew": "Новая тема", + "topic.createNew": "Создать новую тему", "topic.deleteConfirm": "Удалить тему генерации", "topic.deleteConfirmDesc": "Вы собираетесь удалить эту тему генерации. Это действие необратимо, пожалуйста, будьте осторожны.", "topic.empty": "Нет тем генерации", diff --git a/locales/ru-RU/video.json b/locales/ru-RU/video.json index 41ca4d1c9d..3ae29558e7 100644 --- a/locales/ru-RU/video.json +++ b/locales/ru-RU/video.json @@ -7,6 +7,7 @@ "config.header.title": "Видео", "config.imageUrl.label": "Начальный кадр", "config.prompt.placeholder": "Опишите видео, которое хотите создать", + "config.prompt.placeholderWithRef": "Опишите сцену, которую вы хотите создать с изображением", "config.referenceImage.label": "Референсное изображение", "config.resolution.label": "Разрешение", "config.seed.label": "Сид", @@ -20,7 +21,7 @@ "generation.status.failed": "Ошибка генерации", "generation.status.generating": "Создание...", "generation.validation.endFrameRequiresStartFrame": "Конечный кадр нельзя использовать без начального. Пожалуйста, сначала задайте начальный кадр.", - "topic.createNew": "Новая тема", + "topic.createNew": "Создать новую тему", "topic.deleteConfirm": "Удалить тему видео", "topic.deleteConfirmDesc": "Вы собираетесь удалить эту тему видео. Это действие необратимо.", "topic.title": "Темы видео", diff --git a/locales/tr-TR/components.json b/locales/tr-TR/components.json index 68ea7dd80a..a31296f47f 100644 --- a/locales/tr-TR/components.json +++ b/locales/tr-TR/components.json @@ -65,6 +65,10 @@ "FileParsingStatus.chunks.status.errorResult": "Parçalama başarısız oldu, lütfen kontrol edip tekrar deneyin. Hata detayı:", "FileParsingStatus.chunks.status.processing": "Parçalanıyor", "FileParsingStatus.chunks.status.processingTip": "Sunucu metin parçalarını bölüyor; sayfayı kapatmak işlemi etkilemez.", + "GenerationModelItem.creditsPerImageApproximate": "Yaklaşık {{amount}} Kredi / resim", + "GenerationModelItem.creditsPerImageExact": "{{amount}} Kredi / resim", + "GenerationModelItem.creditsPerVideoApproximate": "Yaklaşık {{amount}} Kredi / video", + "GenerationModelItem.creditsPerVideoExact": "{{amount}} Kredi / video", "GoBack.back": "Geri", "HtmlPreview.actions.download": "İndir", "HtmlPreview.actions.preview": "Önizleme", @@ -116,6 +120,8 @@ "ModelSwitchPanel.detail.pricing.group.text": "Metin", "ModelSwitchPanel.detail.pricing.input": "Girdi ${{amount}}/M", "ModelSwitchPanel.detail.pricing.output": "Çıktı ${{amount}}/M", + "ModelSwitchPanel.detail.pricing.perImage": "~ {{amount}} / resim", + "ModelSwitchPanel.detail.pricing.perVideo": "~ {{amount}} / video", "ModelSwitchPanel.detail.pricing.unit.audioInput": "Ses Girdisi", "ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "Ses Girdisi (Önbellekten)", "ModelSwitchPanel.detail.pricing.unit.audioOutput": "Ses Çıktısı", diff --git a/locales/tr-TR/image.json b/locales/tr-TR/image.json index 91945d60fd..65b9ed47b1 100644 --- a/locales/tr-TR/image.json +++ b/locales/tr-TR/image.json @@ -11,6 +11,7 @@ "config.imageUrls.label": "Referans Görseller", "config.model.label": "Model", "config.prompt.placeholder": "Oluşturmak istediğinizi tanımlayın", + "config.prompt.placeholderWithRef": "Resmi nasıl ayarlamak istediğinizi açıklayın", "config.quality.label": "Görüntü Kalitesi", "config.quality.options.hd": "Yüksek Çözünürlük", "config.quality.options.standard": "Standart", @@ -22,7 +23,7 @@ "config.seed.random": "Rastgele Tohum", "config.size.label": "Boyut", "config.steps.label": "Adımlar", - "config.title": "Yapay Zeka Görseli", + "config.title": "Yapılandırma", "config.width.label": "Genişlik", "generation.actions.applySeed": "Tohumu Uygula", "generation.actions.copyError": "Hata Mesajını Kopyala", @@ -53,7 +54,7 @@ "notSupportGuide.features.multiProviders.desc": "OpenAI gpt-image-1, Google Imagen, FAL.ai ve daha fazlası dahil olmak üzere birden fazla yapay zeka görsel oluşturma sağlayıcısını destekler, geniş model seçenekleri sunar.", "notSupportGuide.features.multiProviders.title": "Çoklu Sağlayıcı Desteği", "notSupportGuide.title": "Mevcut Dağıtım Modu Yapay Zeka Görsel Oluşturmayı Desteklemiyor", - "topic.createNew": "Yeni Konu", + "topic.createNew": "Yeni Konu Oluştur", "topic.deleteConfirm": "Oluşturma Konusunu Sil", "topic.deleteConfirmDesc": "Bu oluşturma konusunu silmek üzeresiniz. Bu işlem geri alınamaz, lütfen dikkatli olun.", "topic.empty": "Henüz oluşturma konusu yok", diff --git a/locales/tr-TR/video.json b/locales/tr-TR/video.json index 7fb88459dd..d7759c4b3a 100644 --- a/locales/tr-TR/video.json +++ b/locales/tr-TR/video.json @@ -7,6 +7,7 @@ "config.header.title": "Video", "config.imageUrl.label": "Başlangıç Kare Görseli", "config.prompt.placeholder": "Oluşturmak istediğiniz videoyu tanımlayın", + "config.prompt.placeholderWithRef": "Oluşturmak istediğiniz sahneyi görüntü ile tanımlayın", "config.referenceImage.label": "Referans Görsel", "config.resolution.label": "Çözünürlük", "config.seed.label": "Tohum", @@ -20,7 +21,7 @@ "generation.status.failed": "Oluşturma Başarısız", "generation.status.generating": "Oluşturuluyor...", "generation.validation.endFrameRequiresStartFrame": "Bitiş karesi, başlangıç karesi olmadan kullanılamaz. Lütfen önce bir başlangıç karesi ayarlayın.", - "topic.createNew": "Yeni Konu", + "topic.createNew": "Yeni Konu Oluştur", "topic.deleteConfirm": "Video Konusunu Sil", "topic.deleteConfirmDesc": "Bu video konusunu silmek üzeresiniz. Bu işlem geri alınamaz.", "topic.title": "Video Konuları", diff --git a/locales/vi-VN/components.json b/locales/vi-VN/components.json index bcdba77948..0bc01005dd 100644 --- a/locales/vi-VN/components.json +++ b/locales/vi-VN/components.json @@ -65,6 +65,10 @@ "FileParsingStatus.chunks.status.errorResult": "Phân đoạn thất bại, vui lòng kiểm tra và thử lại. Chi tiết lỗi:", "FileParsingStatus.chunks.status.processing": "Đang phân đoạn", "FileParsingStatus.chunks.status.processingTip": "Máy chủ đang chia nhỏ văn bản; việc đóng trang sẽ không ảnh hưởng đến tiến trình phân đoạn.", + "GenerationModelItem.creditsPerImageApproximate": "Khoảng {{amount}} Tín dụng / hình ảnh", + "GenerationModelItem.creditsPerImageExact": "{{amount}} Tín dụng / hình ảnh", + "GenerationModelItem.creditsPerVideoApproximate": "Khoảng {{amount}} Tín dụng / video", + "GenerationModelItem.creditsPerVideoExact": "{{amount}} Tín dụng / video", "GoBack.back": "Quay lại", "HtmlPreview.actions.download": "Tải xuống", "HtmlPreview.actions.preview": "Xem trước", @@ -116,6 +120,8 @@ "ModelSwitchPanel.detail.pricing.group.text": "Văn bản", "ModelSwitchPanel.detail.pricing.input": "Đầu vào ${{amount}}/M", "ModelSwitchPanel.detail.pricing.output": "Đầu ra ${{amount}}/M", + "ModelSwitchPanel.detail.pricing.perImage": "~ {{amount}} / hình ảnh", + "ModelSwitchPanel.detail.pricing.perVideo": "~ {{amount}} / video", "ModelSwitchPanel.detail.pricing.unit.audioInput": "Đầu vào âm thanh", "ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "Đầu vào âm thanh (đã lưu)", "ModelSwitchPanel.detail.pricing.unit.audioOutput": "Đầu ra âm thanh", diff --git a/locales/vi-VN/image.json b/locales/vi-VN/image.json index 5b0729af0a..ab1314f744 100644 --- a/locales/vi-VN/image.json +++ b/locales/vi-VN/image.json @@ -11,6 +11,7 @@ "config.imageUrls.label": "Các ảnh tham khảo", "config.model.label": "Mô hình", "config.prompt.placeholder": "Mô tả những gì bạn muốn tạo", + "config.prompt.placeholderWithRef": "Mô tả cách bạn muốn điều chỉnh hình ảnh", "config.quality.label": "Chất lượng ảnh", "config.quality.options.hd": "Độ nét cao", "config.quality.options.standard": "Tiêu chuẩn", @@ -22,7 +23,7 @@ "config.seed.random": "Hạt giống ngẫu nhiên", "config.size.label": "Kích thước", "config.steps.label": "Số bước", - "config.title": "Ảnh AI", + "config.title": "Cấu hình", "config.width.label": "Chiều rộng", "generation.actions.applySeed": "Áp dụng hạt giống", "generation.actions.copyError": "Sao chép thông báo lỗi", @@ -53,7 +54,7 @@ "notSupportGuide.features.multiProviders.desc": "Hỗ trợ nhiều nhà cung cấp tạo ảnh AI, bao gồm OpenAI gpt-image-1, Google Imagen, FAL.ai và nhiều hơn nữa, mang đến lựa chọn mô hình đa dạng.", "notSupportGuide.features.multiProviders.title": "Hỗ trợ nhiều nhà cung cấp", "notSupportGuide.title": "Chế độ triển khai hiện tại không hỗ trợ tạo ảnh AI", - "topic.createNew": "Chủ đề mới", + "topic.createNew": "Tạo Chủ Đề Mới", "topic.deleteConfirm": "Xóa chủ đề tạo ảnh", "topic.deleteConfirmDesc": "Bạn sắp xóa chủ đề tạo ảnh này. Hành động này không thể hoàn tác, vui lòng cẩn trọng.", "topic.empty": "Chưa có chủ đề tạo ảnh", diff --git a/locales/vi-VN/video.json b/locales/vi-VN/video.json index cfd57202fc..bdcef83376 100644 --- a/locales/vi-VN/video.json +++ b/locales/vi-VN/video.json @@ -7,6 +7,7 @@ "config.header.title": "Video", "config.imageUrl.label": "Khung hình bắt đầu", "config.prompt.placeholder": "Mô tả video bạn muốn tạo", + "config.prompt.placeholderWithRef": "Mô tả cảnh bạn muốn tạo với hình ảnh", "config.referenceImage.label": "Hình ảnh tham chiếu", "config.resolution.label": "Độ phân giải", "config.seed.label": "Hạt giống", @@ -20,7 +21,7 @@ "generation.status.failed": "Tạo thất bại", "generation.status.generating": "Đang tạo...", "generation.validation.endFrameRequiresStartFrame": "Không thể sử dụng khung hình kết thúc nếu chưa có khung hình bắt đầu. Vui lòng thiết lập khung hình bắt đầu trước.", - "topic.createNew": "Chủ đề mới", + "topic.createNew": "Tạo Chủ Đề Mới", "topic.deleteConfirm": "Xóa chủ đề video", "topic.deleteConfirmDesc": "Bạn sắp xóa chủ đề video này. Hành động này không thể hoàn tác.", "topic.title": "Chủ đề video", diff --git a/locales/zh-CN/components.json b/locales/zh-CN/components.json index ca75bb4532..989edc2f00 100644 --- a/locales/zh-CN/components.json +++ b/locales/zh-CN/components.json @@ -65,6 +65,10 @@ "FileParsingStatus.chunks.status.errorResult": "分块失败,请检查后重试。失败原因:", "FileParsingStatus.chunks.status.processing": "分块中…", "FileParsingStatus.chunks.status.processingTip": "服务端正在拆分文本块,关闭页面不会影响进度", + "GenerationModelItem.creditsPerImageApproximate": "约 {{amount}} 算力积分/张", + "GenerationModelItem.creditsPerImageExact": "{{amount}} 算力积分/张", + "GenerationModelItem.creditsPerVideoApproximate": "约 {{amount}} 算力积分/条", + "GenerationModelItem.creditsPerVideoExact": "{{amount}} 算力积分/条", "GoBack.back": "返回", "HtmlPreview.actions.download": "下载", "HtmlPreview.actions.preview": "预览", @@ -116,6 +120,8 @@ "ModelSwitchPanel.detail.pricing.group.text": "文本", "ModelSwitchPanel.detail.pricing.input": "输入 ${{amount}}/百万", "ModelSwitchPanel.detail.pricing.output": "输出 ${{amount}}/百万", + "ModelSwitchPanel.detail.pricing.perImage": "约 ${{amount}}/张", + "ModelSwitchPanel.detail.pricing.perVideo": "约 ${{amount}}/条", "ModelSwitchPanel.detail.pricing.unit.audioInput": "音频输入", "ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "音频输入(缓存读取)", "ModelSwitchPanel.detail.pricing.unit.audioOutput": "音频输出", diff --git a/locales/zh-CN/image.json b/locales/zh-CN/image.json index 3d2ec92057..d410a19fd5 100644 --- a/locales/zh-CN/image.json +++ b/locales/zh-CN/image.json @@ -11,6 +11,7 @@ "config.imageUrls.label": "参考图", "config.model.label": "模型", "config.prompt.placeholder": "描述你想要生成的内容", + "config.prompt.placeholderWithRef": "描述你想如何调整图片", "config.quality.label": "图片质量", "config.quality.options.hd": "高清", "config.quality.options.standard": "标准", @@ -22,7 +23,7 @@ "config.seed.random": "随机种子", "config.size.label": "尺寸", "config.steps.label": "步数", - "config.title": "AI 图像", + "config.title": "配置", "config.width.label": "宽度", "generation.actions.applySeed": "应用种子", "generation.actions.copyError": "复制错误信息", @@ -53,7 +54,7 @@ "notSupportGuide.features.multiProviders.desc": "支持多种 AI 绘画服务商,包括 OpenAI、Google Imagen、FAL.ai 等。提供丰富的模型选择", "notSupportGuide.features.multiProviders.title": "多服务商支持", "notSupportGuide.title": "当前部署模式不支持 AI 绘画", - "topic.createNew": "新建主题", + "topic.createNew": "创作新主题", "topic.deleteConfirm": "删除生成主题", "topic.deleteConfirmDesc": "确认删除该生成主题吗?删除后无法恢复", "topic.empty": "暂无生成主题", diff --git a/locales/zh-CN/video.json b/locales/zh-CN/video.json index f7d0f0210a..1d25890be9 100644 --- a/locales/zh-CN/video.json +++ b/locales/zh-CN/video.json @@ -7,6 +7,7 @@ "config.header.title": "视频", "config.imageUrl.label": "起始画面", "config.prompt.placeholder": "描述你想生成的视频内容", + "config.prompt.placeholderWithRef": "结合图片,描述你想生成的画面", "config.referenceImage.label": "参考图像", "config.resolution.label": "分辨率", "config.seed.label": "种子", @@ -20,7 +21,7 @@ "generation.status.failed": "生成失败", "generation.status.generating": "生成中...", "generation.validation.endFrameRequiresStartFrame": "使用结束画面前需先设置起始画面。请先设置起始画面。", - "topic.createNew": "新建主题", + "topic.createNew": "创作新主题", "topic.deleteConfirm": "删除视频主题", "topic.deleteConfirmDesc": "你即将删除该视频主题,此操作无法撤销。", "topic.title": "视频主题", diff --git a/locales/zh-TW/components.json b/locales/zh-TW/components.json index a5281ac4da..a4b8beb145 100644 --- a/locales/zh-TW/components.json +++ b/locales/zh-TW/components.json @@ -65,6 +65,10 @@ "FileParsingStatus.chunks.status.errorResult": "分塊失敗,請檢查後重試。失敗原因:", "FileParsingStatus.chunks.status.processing": "分塊中", "FileParsingStatus.chunks.status.processingTip": "服務端正在拆分文本塊,關閉頁面不影響分塊進度", + "GenerationModelItem.creditsPerImageApproximate": "約 {{amount}} 點數 / 圖片", + "GenerationModelItem.creditsPerImageExact": "{{amount}} 點數 / 圖片", + "GenerationModelItem.creditsPerVideoApproximate": "約 {{amount}} 點數 / 影片", + "GenerationModelItem.creditsPerVideoExact": "{{amount}} 點數 / 影片", "GoBack.back": "返回", "HtmlPreview.actions.download": "下載", "HtmlPreview.actions.preview": "預覽", @@ -116,6 +120,8 @@ "ModelSwitchPanel.detail.pricing.group.text": "文字", "ModelSwitchPanel.detail.pricing.input": "輸入 ${{amount}}/百萬", "ModelSwitchPanel.detail.pricing.output": "輸出 ${{amount}}/百萬", + "ModelSwitchPanel.detail.pricing.perImage": "約 {{amount}} / 圖片", + "ModelSwitchPanel.detail.pricing.perVideo": "約 {{amount}} / 影片", "ModelSwitchPanel.detail.pricing.unit.audioInput": "音訊輸入", "ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead": "音訊輸入(快取)", "ModelSwitchPanel.detail.pricing.unit.audioOutput": "音訊輸出", diff --git a/locales/zh-TW/image.json b/locales/zh-TW/image.json index 9f9f8d782d..134b6e9fee 100644 --- a/locales/zh-TW/image.json +++ b/locales/zh-TW/image.json @@ -11,6 +11,7 @@ "config.imageUrls.label": "參考圖", "config.model.label": "模型", "config.prompt.placeholder": "描述你想要生成的內容", + "config.prompt.placeholderWithRef": "描述您希望如何調整圖片", "config.quality.label": "圖片品質", "config.quality.options.hd": "高清", "config.quality.options.standard": "標準", @@ -22,7 +23,7 @@ "config.seed.random": "隨機種子", "config.size.label": "尺寸", "config.steps.label": "步數", - "config.title": "AI 繪畫", + "config.title": "設定", "config.width.label": "寬度", "generation.actions.applySeed": "應用種子", "generation.actions.copyError": "複製錯誤訊息", @@ -53,7 +54,7 @@ "notSupportGuide.features.multiProviders.desc": "支援多種 AI 繪畫服務商,包括 OpenAI gpt-image-1、Google Imagen、FAL.ai 等,提供豐富的模型選擇", "notSupportGuide.features.multiProviders.title": "多 Providers 支援", "notSupportGuide.title": "當前部署模式不支援 AI 繪畫", - "topic.createNew": "新建主題", + "topic.createNew": "建立新主題", "topic.deleteConfirm": "刪除生成主題", "topic.deleteConfirmDesc": "即將刪除該生成主題,刪除後將無法復原,請謹慎操作。", "topic.empty": "暫無生成主題", diff --git a/locales/zh-TW/video.json b/locales/zh-TW/video.json index 880e4305df..7c08c58539 100644 --- a/locales/zh-TW/video.json +++ b/locales/zh-TW/video.json @@ -7,6 +7,7 @@ "config.header.title": "影片", "config.imageUrl.label": "起始畫面", "config.prompt.placeholder": "描述您想要產生的影片", + "config.prompt.placeholderWithRef": "描述您想要生成圖像的場景", "config.referenceImage.label": "參考圖片", "config.resolution.label": "解析度", "config.seed.label": "種子", @@ -20,7 +21,7 @@ "generation.status.failed": "產生失敗", "generation.status.generating": "產生中...", "generation.validation.endFrameRequiresStartFrame": "未設定起始畫面時無法使用結束畫面。請先設定起始畫面。", - "topic.createNew": "新增主題", + "topic.createNew": "建立新主題", "topic.deleteConfirm": "刪除影片主題", "topic.deleteConfirmDesc": "您即將刪除此影片主題,此操作無法復原。", "topic.title": "影片主題", diff --git a/packages/model-bank/src/types/aiModel.ts b/packages/model-bank/src/types/aiModel.ts index 418b79033b..4c35e763b3 100644 --- a/packages/model-bank/src/types/aiModel.ts +++ b/packages/model-bank/src/types/aiModel.ts @@ -467,6 +467,10 @@ export interface AiModelForSelect { * Approximate per-image price (USD), used when exact calculation is not possible */ approximatePricePerImage?: number; + /** + * Approximate per-video price (USD), used when exact calculation is not possible + */ + approximatePricePerVideo?: number; contextWindowTokens?: number; description?: string; displayName?: string; @@ -476,6 +480,10 @@ export interface AiModelForSelect { * Exact per-image price (USD) calculated from pricing units */ pricePerImage?: number; + /** + * Exact per-video price (USD) when resolved from pricing units + */ + pricePerVideo?: number; pricing?: Pricing; releasedAt?: string; } diff --git a/src/features/CommandMenu/SearchResults.tsx b/src/features/CommandMenu/SearchResults.tsx index b968fbfad1..d4fdeaf301 100644 --- a/src/features/CommandMenu/SearchResults.tsx +++ b/src/features/CommandMenu/SearchResults.tsx @@ -19,6 +19,11 @@ import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { type SearchResult } from '@/database/repositories/search'; +import { useCommandMenuContext } from '@/features/CommandMenu/CommandMenuContext'; +import { useImageStore } from '@/store/image'; +import { generationTopicSelectors as imageGenerationTopicSelectors } from '@/store/image/slices/generationTopic/selectors'; +import { useVideoStore } from '@/store/video'; +import { generationTopicSelectors as videoGenerationTopicSelectors } from '@/store/video/slices/generationTopic/selectors'; import { markdownToTxt } from '@/utils/markdownToTxt'; import { CommandItem } from './components'; @@ -34,13 +39,27 @@ interface SearchResultsProps { typeFilter: ValidSearchType | undefined; } +interface LocalGenerationTopicResult { + createdAt: Date; + id: string; + title: string; + updatedAt: Date; +} + /** * Search results from unified search index. */ const SearchResults = memo( ({ isLoading, onClose, onSetTypeFilter, results, searchQuery, typeFilter }) => { const { t } = useTranslation('common'); + const { t: tImage } = useTranslation('image'); + const { t: tVideo } = useTranslation('video'); const navigate = useNavigate(); + const { menuContext } = useCommandMenuContext(); + const imageTopics = useImageStore(imageGenerationTopicSelectors.generationTopics); + const activeImageTopicId = useImageStore((s) => s.activeGenerationTopicId); + const videoTopics = useVideoStore(videoGenerationTopicSelectors.generationTopics); + const activeVideoTopicId = useVideoStore((s) => s.activeGenerationTopicId); const handleNavigate = (result: SearchResult) => { switch (result.type) { @@ -242,7 +261,51 @@ const SearchResults = memo( onSetTypeFilter(type); }; + const localImageTopicResults: LocalGenerationTopicResult[] = + menuContext === 'painting' + ? (imageTopics || []) + .filter((topic) => { + const title = topic.title || tImage('topic.untitled'); + return title.toLowerCase().includes(searchQuery.toLowerCase()); + }) + .sort((a, b) => { + if (a.id === activeImageTopicId) return -1; + if (b.id === activeImageTopicId) return 1; + return b.updatedAt.getTime() - a.updatedAt.getTime(); + }) + .slice(0, 8) + .map((topic) => ({ + createdAt: topic.createdAt, + id: topic.id, + title: topic.title || tImage('topic.untitled'), + updatedAt: topic.updatedAt, + })) + : []; + + const localVideoTopicResults: LocalGenerationTopicResult[] = + menuContext === 'video' + ? (videoTopics || []) + .filter((topic) => { + const title = topic.title || tVideo('topic.untitled'); + return title.toLowerCase().includes(searchQuery.toLowerCase()); + }) + .sort((a, b) => { + if (a.id === activeVideoTopicId) return -1; + if (b.id === activeVideoTopicId) return 1; + return b.updatedAt.getTime() - a.updatedAt.getTime(); + }) + .slice(0, 8) + .map((topic) => ({ + createdAt: topic.createdAt, + id: topic.id, + title: topic.title || tVideo('topic.untitled'), + updatedAt: topic.updatedAt, + })) + : []; + const hasResults = results.length > 0; + const hasLocalTopicResults = + localImageTopicResults.length > 0 || localVideoTopicResults.length > 0; // Group results by type const messageResults = results.filter((r) => r.type === 'message'); @@ -259,7 +322,7 @@ const SearchResults = memo( const assistantResults = results.filter((r) => r.type === 'communityAgent'); // Don't render anything if no results and not loading - if (!hasResults && !isLoading) { + if (!hasResults && !hasLocalTopicResults && !isLoading) { return null; } @@ -335,6 +398,80 @@ const SearchResults = memo( return ( <> + {localImageTopicResults.length > 0 && ( + + {localImageTopicResults.map((result) => { + const formattedDate = dayjs(result.updatedAt).format('MMM D, YYYY'); + return ( + } + key={`image-topic-${result.id}`} + value={`local-image-topic ${result.id} ${result.title}`} + variant="detailed" + title={ + <> + {t('tab.aiImage')} + + {result.title} + + } + onSelect={() => { + navigate(`/image?topic=${result.id}`); + onClose(); + }} + /> + ); + })} + + )} + + {localVideoTopicResults.length > 0 && ( + + {localVideoTopicResults.map((result) => { + const formattedDate = dayjs(result.updatedAt).format('MMM D, YYYY'); + return ( + } + key={`video-topic-${result.id}`} + value={`local-video-topic ${result.id} ${result.title}`} + variant="detailed" + title={ + <> + {t('tab.video')} + + {result.title} + + } + onSelect={() => { + navigate(`/video?topic=${result.id}`); + onClose(); + }} + /> + ); + })} + + )} + {/* Render search results grouped by type without headers */} {messageResults.length > 0 && ( diff --git a/src/features/CommandMenu/types.ts b/src/features/CommandMenu/types.ts index 26c04237a9..a63a8a20a3 100644 --- a/src/features/CommandMenu/types.ts +++ b/src/features/CommandMenu/types.ts @@ -29,9 +29,10 @@ export type MenuContext = | 'memory' | 'community' | 'page' - | 'painting'; + | 'painting' + | 'video'; export type ContextType = Extract< MenuContext, - 'agent' | 'group' | 'resource' | 'settings' | 'page' | 'painting' + 'agent' | 'group' | 'resource' | 'settings' | 'page' | 'painting' | 'video' >; diff --git a/src/features/CommandMenu/utils/context.ts b/src/features/CommandMenu/utils/context.ts index 6d2abda7af..6c7b98fbed 100644 --- a/src/features/CommandMenu/utils/context.ts +++ b/src/features/CommandMenu/utils/context.ts @@ -29,6 +29,11 @@ const CONTEXT_CONFIGS: ContextConfig[] = [ name: 'Painting', type: 'painting', }, + { + matcher: /^\/video$/, + name: 'Video', + type: 'video', + }, { captureSubPath: true, matcher: /^\/settings(?:\/([^/]+))?/, diff --git a/src/features/CommandMenu/utils/contextCommands.ts b/src/features/CommandMenu/utils/contextCommands.ts index db1a3badd8..df9df68ec9 100644 --- a/src/features/CommandMenu/utils/contextCommands.ts +++ b/src/features/CommandMenu/utils/contextCommands.ts @@ -39,6 +39,7 @@ export const CONTEXT_COMMANDS: Record = { group: [], page: [], painting: [], + video: [], resource: [], settings: [ { diff --git a/src/features/Conversation/ChatList/components/VirtualizedList.tsx b/src/features/Conversation/ChatList/components/VirtualizedList.tsx index 128ef9d56b..1a30e63b33 100644 --- a/src/features/Conversation/ChatList/components/VirtualizedList.tsx +++ b/src/features/Conversation/ChatList/components/VirtualizedList.tsx @@ -201,11 +201,13 @@ const VirtualizedList = memo(({ dataSource, itemContent }) }} {/* BackBottom is placed outside VList so it remains visible regardless of scroll position */} - scrollToBottom(true)} - /> + + scrollToBottom(true)} + /> + ); }, isEqual); diff --git a/src/features/GenerationTopicList/TopicItem.tsx b/src/features/GenerationTopicList/TopicItem.tsx deleted file mode 100644 index 6ad6023a15..0000000000 --- a/src/features/GenerationTopicList/TopicItem.tsx +++ /dev/null @@ -1,135 +0,0 @@ -'use client'; - -import { ActionIcon, Avatar, Flexbox, Popover, Text } from '@lobehub/ui'; -import { App } from 'antd'; -import { cssVar } from 'antd-style'; -import { Trash } from 'lucide-react'; -import React, { memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { useGlobalStore } from '@/store/global'; -import { globalGeneralSelectors } from '@/store/global/selectors'; -import { type ImageGenerationTopic } from '@/types/generation'; - -import { useGenerationTopicContext } from './StoreContext'; - -const formatTime = (date: Date, locale: string) => { - return new Intl.DateTimeFormat(locale, { - day: 'numeric', - month: 'long', - }).format(new Date(date)); -}; - -interface TopicItemProps { - showMoreInfo?: boolean; - style?: React.CSSProperties; - topic: ImageGenerationTopic; -} - -const TopicItem = memo(({ topic, showMoreInfo, style }) => { - const { useStore, namespace } = useGenerationTopicContext(); - const { t } = useTranslation(namespace); - const { modal } = App.useApp(); - const locale = useGlobalStore(globalGeneralSelectors.currentLanguage); - - const isLoading = useStore((s) => s.loadingGenerationTopicIds.includes(topic.id)); - const removeGenerationTopic = useStore((s) => s.removeGenerationTopic); - const switchGenerationTopic = useStore((s) => s.switchGenerationTopic); - const activeTopicId = useStore((s) => s.activeGenerationTopicId); - - const isActive = activeTopicId === topic.id; - - const handleClick = () => { - switchGenerationTopic(topic.id); - }; - - const handleDelete = (e: React.MouseEvent) => { - e.stopPropagation(); - - modal.confirm({ - cancelText: t('cancel', { ns: 'common' }), - content: t('topic.deleteConfirmDesc'), - okButtonProps: { danger: true }, - okText: t('delete', { ns: 'common' }), - onOk: async () => { - try { - await removeGenerationTopic(topic.id); - } catch (error) { - console.error('Delete topic failed:', error); - } - }, - title: t('topic.deleteConfirm'), - }); - }; - - const tooltipContent = ( - - - - {topic.title || t('topic.untitled')} - - - {formatTime(topic.updatedAt, locale)} - - - - - ); - - return ( - - - - {showMoreInfo && tooltipContent} - - - ); -}); - -TopicItem.displayName = 'TopicItem'; - -export default TopicItem; diff --git a/src/features/GenerationTopicList/TopicList.tsx b/src/features/GenerationTopicList/TopicList.tsx deleted file mode 100644 index d6f0e18d24..0000000000 --- a/src/features/GenerationTopicList/TopicList.tsx +++ /dev/null @@ -1,73 +0,0 @@ -'use client'; - -import { useAutoAnimate } from '@formkit/auto-animate/react'; -import { Flexbox } from '@lobehub/ui'; -import { useSize } from 'ahooks'; -import { memo, useRef } from 'react'; - -import { useUserStore } from '@/store/user'; -import { authSelectors } from '@/store/user/slices/auth/selectors'; - -import NewTopicButton from './NewTopicButton'; -import { useGenerationTopicContext } from './StoreContext'; -import TopicItem from './TopicItem'; - -const TopicsList = memo(() => { - const { useStore } = useGenerationTopicContext(); - const isLogin = useUserStore(authSelectors.isLogin); - const useFetchGenerationTopics = useStore((s) => s.useFetchGenerationTopics); - useFetchGenerationTopics(!!isLogin); - const ref = useRef(null); - const { width = 80 } = useSize(ref) || {}; - const [parent] = useAutoAnimate(); - const generationTopics = useStore((s) => s.generationTopics); - const openNewGenerationTopic = useStore((s) => s.openNewGenerationTopic); - - const showMoreInfo = Boolean(width > 120); - - const isEmpty = !generationTopics || generationTopics.length === 0; - if (isEmpty) { - return null; - } - - return ( - - - - {generationTopics.map((topic, index) => ( - - ))} - - - ); -}); - -TopicsList.displayName = 'TopicsList'; - -export default TopicsList; diff --git a/src/features/GenerationTopicList/index.tsx b/src/features/GenerationTopicList/index.tsx deleted file mode 100644 index b04e52a0c1..0000000000 --- a/src/features/GenerationTopicList/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export { default as NewTopicButton } from './NewTopicButton'; -export { default as SkeletonList } from './SkeletonList'; -export type { GenerationTopicContextValue, GenerationTopicStoreSlice } from './StoreContext'; -export { GenerationTopicStoreProvider, useGenerationTopicContext } from './StoreContext'; -export { default as TopicItem } from './TopicItem'; -export { default as TopicList } from './TopicList'; -export { default as TopicUrlSync } from './TopicUrlSync'; diff --git a/src/features/GenerationTopicPanel/index.tsx b/src/features/GenerationTopicPanel/index.tsx deleted file mode 100644 index 0214aef26f..0000000000 --- a/src/features/GenerationTopicPanel/index.tsx +++ /dev/null @@ -1,86 +0,0 @@ -'use client'; - -import { DraggablePanel, DraggablePanelContainer, type DraggablePanelProps } from '@lobehub/ui'; -import { createStaticStyles, cssVar, useResponsive } from 'antd-style'; -import isEqual from 'fast-deep-equal'; -import { memo, type PropsWithChildren, useEffect, useState } from 'react'; - -export const styles = createStaticStyles(({ css }) => ({ - content: css` - height: 100%; - background: ${cssVar.colorBgContainer}; - `, - handle: css` - background: ${cssVar.colorBgContainer} !important; - `, -})); - -interface GenerationTopicPanelProps extends PropsWithChildren { - onExpandChange: (expand: boolean) => void; - onSizeChange: (width: number) => void; - panelWidth: number; - showPanel: boolean; -} - -const GenerationTopicPanel = memo( - ({ children, panelWidth, showPanel, onExpandChange, onSizeChange }) => { - const { md = true } = useResponsive(); - - const [tmpWidth, setWidth] = useState(panelWidth); - if (tmpWidth !== panelWidth) setWidth(panelWidth); - const [cacheExpand, setCacheExpand] = useState(Boolean(showPanel)); - - const handleExpand = (expand: boolean) => { - if (isEqual(expand, showPanel)) return; - onExpandChange(expand); - setCacheExpand(expand); - }; - - useEffect(() => { - if (md && cacheExpand) onExpandChange(true); - if (!md) onExpandChange(false); - }, [md, cacheExpand]); - - const handleSizeChange: DraggablePanelProps['onSizeChange'] = (_, size) => { - if (!size) return; - const nextWidth = typeof size.width === 'string' ? Number.parseInt(size.width) : size.width; - if (!nextWidth) return; - - if (isEqual(nextWidth, panelWidth)) return; - setWidth(nextWidth); - onSizeChange(nextWidth); - }; - - return ( - - - {children} - - - ); - }, -); - -GenerationTopicPanel.displayName = 'GenerationTopicPanel'; - -export default GenerationTopicPanel; diff --git a/src/features/ImageSidePanel/index.tsx b/src/features/ImageSidePanel/index.tsx deleted file mode 100644 index 95fc001ae2..0000000000 --- a/src/features/ImageSidePanel/index.tsx +++ /dev/null @@ -1,84 +0,0 @@ -'use client'; - -import { type DraggablePanelProps } from '@lobehub/ui'; -import { DraggablePanel, DraggablePanelContainer } from '@lobehub/ui'; -import { createStaticStyles, cssVar, useResponsive } from 'antd-style'; -import isEqual from 'fast-deep-equal'; -import { type PropsWithChildren } from 'react'; -import { memo, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import PanelTitle from '@/components/PanelTitle'; -import { FOLDER_WIDTH } from '@/const/layoutTokens'; -import { useGlobalStore } from '@/store/global'; -import { systemStatusSelectors } from '@/store/global/selectors'; - -export const styles = createStaticStyles(({ css }) => ({ - panel: css` - height: 100%; - background: ${cssVar.colorBgLayout}; - `, -})); - -const ImageSidePanel = memo(({ children }) => { - const { md = true } = useResponsive(); - const { t } = useTranslation('image'); - const [imagePanelWidth, showImagePanel, updateSystemStatus] = useGlobalStore((s) => [ - systemStatusSelectors.imagePanelWidth(s), - systemStatusSelectors.showImagePanel(s), - s.updateSystemStatus, - ]); - - const [tmpWidth, setWidth] = useState(imagePanelWidth); - if (tmpWidth !== imagePanelWidth) setWidth(imagePanelWidth); - const [cacheExpand, setCacheExpand] = useState(Boolean(showImagePanel)); - - const handleExpand = (expand: boolean) => { - if (isEqual(expand, showImagePanel)) return; - updateSystemStatus({ showImagePanel: expand }); - setCacheExpand(expand); - }; - useEffect(() => { - if (md && cacheExpand) updateSystemStatus({ showImagePanel: true }); - if (!md) updateSystemStatus({ showImagePanel: false }); - }, [md, cacheExpand]); - - const handleSizeChange: DraggablePanelProps['onSizeChange'] = (_, size) => { - if (!size) return; - const nextWidth = typeof size.width === 'string' ? Number.parseInt(size.width) : size.width; - if (!nextWidth) return; - - if (isEqual(nextWidth, imagePanelWidth)) return; - setWidth(nextWidth); - updateSystemStatus({ imagePanelWidth: nextWidth }); - }; - - return ( - - - - {children} - - - ); -}); - -export default ImageSidePanel; diff --git a/src/features/ImageTopicPanel/index.tsx b/src/features/ImageTopicPanel/index.tsx deleted file mode 100644 index 36facc9ad7..0000000000 --- a/src/features/ImageTopicPanel/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client'; - -import { type PropsWithChildren } from 'react'; -import { memo } from 'react'; - -import GenerationTopicPanel from '@/features/GenerationTopicPanel'; -import { useGlobalStore } from '@/store/global'; -import { systemStatusSelectors } from '@/store/global/selectors'; - -const ImageTopicPanel = memo(({ children }) => { - const [imageTopicPanelWidth, showImageTopicPanel, updateSystemStatus] = useGlobalStore((s) => [ - systemStatusSelectors.imageTopicPanelWidth(s), - systemStatusSelectors.showImageTopicPanel(s), - s.updateSystemStatus, - ]); - - return ( - updateSystemStatus({ showImageTopicPanel: expand })} - onSizeChange={(width) => updateSystemStatus({ imageTopicPanelWidth: width })} - > - {children} - - ); -}); - -ImageTopicPanel.displayName = 'ImageTopicPanel'; - -export default ImageTopicPanel; diff --git a/src/features/ModelSwitchPanel/components/List/GenerationListItemRenderer.tsx b/src/features/ModelSwitchPanel/components/List/GenerationListItemRenderer.tsx new file mode 100644 index 0000000000..766861f743 --- /dev/null +++ b/src/features/ModelSwitchPanel/components/List/GenerationListItemRenderer.tsx @@ -0,0 +1,229 @@ +'use client'; + +import { + ActionIcon, + DropdownMenuPopup, + DropdownMenuPortal, + DropdownMenuPositioner, + DropdownMenuSubmenuRoot, + DropdownMenuSubmenuTrigger, + Flexbox, + Icon, + menuSharedStyles, +} from '@lobehub/ui'; +import { cssVar, cx } from 'antd-style'; +import { LucideArrowRight, LucideBolt } from 'lucide-react'; +import type { ComponentType } from 'react'; +import { memo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import urlJoin from 'url-join'; + +import { ProviderItemRender } from '@/components/ModelSelect'; +import type { PricingMode } from '@/features/ModelSwitchPanel/components/ModelDetailPanel'; +import ModelDetailPanel from '@/features/ModelSwitchPanel/components/ModelDetailPanel'; +import { styles as modelSwitchPanelStyles } from '@/features/ModelSwitchPanel/styles'; +import type { ListItem } from '@/features/ModelSwitchPanel/types'; +import { menuKey } from '@/features/ModelSwitchPanel/utils'; +import type { EnabledProviderWithModels } from '@/types/index'; + +import GenerationMultipleProvidersItem from './GenerationMultipleProvidersItem'; + +export interface GenerationListItemRendererProps { + activeKey: string; + enabledList: EnabledProviderWithModels[]; + item: ListItem; + ModelItemComponent: ComponentType; + onClose: () => void; + onModelChange: (modelId: string, providerId: string) => void; + pricingMode?: PricingMode; +} + +const GenerationListItemRenderer = memo( + ({ item, activeKey, onClose, onModelChange, enabledList, ModelItemComponent, pricingMode }) => { + const { t } = useTranslation('components'); + const navigate = useNavigate(); + const [detailOpen, setDetailOpen] = useState(false); + + switch (item.type) { + case 'no-provider': { + return ( + { + navigate('/settings/provider/all'); + onClose(); + }} + > + {t('ModelSwitchPanel.emptyProvider')} + + + ); + } + + case 'group-header': { + return ( + + + { + e.preventDefault(); + e.stopPropagation(); + const url = urlJoin('/settings/provider', item.provider.id || 'all'); + if (e.ctrlKey || e.metaKey) { + window.open(url, '_blank'); + } else { + navigate(url); + } + onClose(); + }} + /> + + ); + } + + case 'empty-model': { + return ( + { + navigate(`/settings/provider/${item.provider.id}`); + onClose(); + }} + > + {t('ModelSwitchPanel.emptyModel')} + + + ); + } + + case 'provider-model-item': { + const key = menuKey(item.provider.id, item.model.id); + const isActive = key === activeKey; + return ( + + + { + setDetailOpen(false); + onModelChange(item.model.id, item.provider.id); + onClose(); + }} + > + + + + + + + + + + + + ); + } + + case 'model-item-single': { + const singleProvider = item.data.providers[0]; + const key = menuKey(singleProvider.id, item.data.model.id); + const isActive = key === activeKey; + return ( + + + { + setDetailOpen(false); + onModelChange(item.data.model.id, singleProvider.id); + onClose(); + }} + > + + + + + + + + + + + + ); + } + + case 'model-item-multiple': { + return ( + + ); + } + + default: { + return null; + } + } + }, +); + +GenerationListItemRenderer.displayName = 'GenerationListItemRenderer'; + +export default GenerationListItemRenderer; diff --git a/src/features/ModelSwitchPanel/components/List/GenerationMultipleProvidersItem.tsx b/src/features/ModelSwitchPanel/components/List/GenerationMultipleProvidersItem.tsx new file mode 100644 index 0000000000..414d05ea9a --- /dev/null +++ b/src/features/ModelSwitchPanel/components/List/GenerationMultipleProvidersItem.tsx @@ -0,0 +1,112 @@ +'use client'; + +import { + DropdownMenuPopup, + DropdownMenuPortal, + DropdownMenuPositioner, + DropdownMenuSubmenuRoot, + DropdownMenuSubmenuTrigger, + Flexbox, + menuSharedStyles, +} from '@lobehub/ui'; +import { cssVar, cx } from 'antd-style'; +import { Check } from 'lucide-react'; +import type { ComponentType } from 'react'; +import { memo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { ProviderItemRender } from '@/components/ModelSelect'; +import type { PricingMode } from '@/features/ModelSwitchPanel/components/ModelDetailPanel'; +import ModelDetailPanel from '@/features/ModelSwitchPanel/components/ModelDetailPanel'; +import { styles as modelSwitchPanelStyles } from '@/features/ModelSwitchPanel/styles'; +import type { ListItem } from '@/features/ModelSwitchPanel/types'; +import { menuKey } from '@/features/ModelSwitchPanel/utils'; +import type { EnabledProviderWithModels } from '@/types/index'; + +interface GenerationMultipleProvidersItemProps { + activeKey: string; + enabledList: EnabledProviderWithModels[]; + item: Extract; + ModelItemComponent: ComponentType; + onClose: () => void; + onModelChange: (modelId: string, providerId: string) => void; + pricingMode?: PricingMode; +} + +const GenerationMultipleProvidersItem = memo( + ({ item, activeKey, onClose, onModelChange, enabledList, ModelItemComponent, pricingMode }) => { + const { t } = useTranslation('components'); + const [subOpen, setSubOpen] = useState(false); + const activeProvider = item.data.providers.find( + (p) => menuKey(p.id, item.data.model.id) === activeKey, + ); + const isActive = !!activeProvider; + + return ( + + + + + + + + + + + + {t('ModelSwitchPanel.useModelFrom')} + + {item.data.providers.map((p) => { + const pKey = menuKey(p.id, item.data.model.id); + const isProviderActive = activeKey === pKey; + return ( + { + onModelChange(item.data.model.id, p.id); + onClose(); + }} + > + + {isProviderActive ? : null} + + ); + })} + + + + + + + ); + }, +); + +GenerationMultipleProvidersItem.displayName = 'GenerationMultipleProvidersItem'; + +export default GenerationMultipleProvidersItem; diff --git a/src/features/ModelSwitchPanel/components/List/index.tsx b/src/features/ModelSwitchPanel/components/List/index.tsx index 7446623232..27e1edf538 100644 --- a/src/features/ModelSwitchPanel/components/List/index.tsx +++ b/src/features/ModelSwitchPanel/components/List/index.tsx @@ -1,10 +1,11 @@ import { Flexbox } from '@lobehub/ui'; -import { type FC } from 'react'; +import { type ComponentType, type FC } from 'react'; import { useCallback, useLayoutEffect, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useBusinessModelListGuard } from '@/business/client/hooks/useBusinessModelListGuard'; import { useEnabledChatModels } from '@/hooks/useEnabledChatModels'; +import type { EnabledProviderWithModels } from '@/types/aiProvider'; import { FOOTER_HEIGHT, ITEM_HEIGHT, MAX_PANEL_HEIGHT, TOOLBAR_HEIGHT } from '../../const'; import { useBuildListItems } from '../../hooks/useBuildListItems'; @@ -13,22 +14,30 @@ import { usePanelHandlers } from '../../hooks/usePanelHandlers'; import { styles } from '../../styles'; import { type GroupMode } from '../../types'; import { menuKey } from '../../utils'; +import type { PricingMode } from '../ModelDetailPanel'; +import GenerationListItemRenderer from './GenerationListItemRenderer'; import { ListItemRenderer } from './ListItemRenderer'; interface ListProps { + enabledList?: EnabledProviderWithModels[]; groupMode: GroupMode; model?: string; + ModelItemComponent?: ComponentType; onModelChange?: (params: { model: string; provider: string }) => Promise; onOpenChange?: (open: boolean) => void; + pricingMode?: PricingMode; provider?: string; searchKeyword?: string; } export const List: FC = ({ + ModelItemComponent, + enabledList: enabledListProp, groupMode, model: modelProp, onModelChange: onModelChangeProp, onOpenChange, + pricingMode, provider: providerProp, searchKeyword = '', }) => { @@ -37,7 +46,8 @@ export const List: FC = ({ const { isModelRestricted, onRestrictedModelClick } = useBusinessModelListGuard(); const proLabel = isModelRestricted ? tCommon('pro') : undefined; - const enabledList = useEnabledChatModels(); + const chatEnabledList = useEnabledChatModels(); + const enabledList = enabledListProp ?? chatEnabledList; const { model, provider } = useModelAndProvider(modelProp, providerProp); const { handleModelChange, handleClose } = usePanelHandlers({ onModelChange: onModelChangeProp, @@ -115,20 +125,32 @@ export const List: FC = ({ (item.type === 'model-item-multiple' && item.data.providers.some((p) => menuKey(p.id, item.data.model.id) === activeKey)); - const renderItem = (key?: string) => ( - - ); + const renderItem = (key?: string) => + ModelItemComponent ? ( + + ) : ( + + ); return isActive ? (
diff --git a/src/features/ModelSwitchPanel/components/ModelDetailPanel.tsx b/src/features/ModelSwitchPanel/components/ModelDetailPanel.tsx index 8a96fbd2a0..9547b1b7e7 100644 --- a/src/features/ModelSwitchPanel/components/ModelDetailPanel.tsx +++ b/src/features/ModelSwitchPanel/components/ModelDetailPanel.tsx @@ -28,6 +28,7 @@ import { useTranslation } from 'react-i18next'; import { useEnabledChatModels } from '@/hooks/useEnabledChatModels'; import { aiModelSelectors, useAiInfraStore } from '@/store/aiInfra'; +import type { EnabledProviderWithModels } from '@/types/aiProvider'; import { formatTokenNumber } from '@/utils/format'; import { formatPriceByCurrency, getTextInputUnitRate, getTextOutputUnitRate } from '@/utils/index'; @@ -216,274 +217,313 @@ const ABILITY_CONFIG: AbilityItem[] = [ { color: 'cyan', icon: GlobeIcon, key: 'search' }, ]; +export type PricingMode = 'image' | 'video'; + interface ModelDetailPanelProps { + enabledList?: EnabledProviderWithModels[]; model?: string; + pricingMode?: PricingMode; provider?: string; } -const ModelDetailPanel: FC = memo(({ model: modelId, provider }) => { - const { t } = useTranslation('components'); +const ModelDetailPanel: FC = memo( + ({ model: modelId, provider, enabledList: enabledListProp, pricingMode }) => { + const { t } = useTranslation('components'); - const enabledList = useEnabledChatModels(); - const model = useMemo(() => { - if (!modelId || !provider) return undefined; - const providerData = enabledList.find((p) => p.id === provider); - return providerData?.children.find((m) => m.id === modelId); - }, [enabledList, modelId, provider]); + const enabledListFromHook = useEnabledChatModels(); + const enabledList = enabledListProp ?? enabledListFromHook; + const model = useMemo(() => { + if (!modelId || !provider) return undefined; + const providerData = enabledList.find((p) => p.id === provider); + return providerData?.children.find((m) => m.id === modelId); + }, [enabledList, modelId, provider]); - const hasExtendParams = useAiInfraStore( - aiModelSelectors.isModelHasExtendParams(modelId ?? '', provider ?? ''), - ); + const hasExtendParams = useAiInfraStore( + aiModelSelectors.isModelHasExtendParams(modelId ?? '', provider ?? ''), + ); - const [expandedKeys, setExpandedKeys] = useState(() => { - const keys: string[] = []; - if (hasExtendParams) keys.push('config'); - return keys; - }); + const [expandedKeys, setExpandedKeys] = useState(() => { + const keys: string[] = ['pricing']; + // ControlsForm uses ChatInput store + useAgentId; not available on create/image|video routes. + if (hasExtendParams && !pricingMode) keys.push('config'); + return keys; + }); - const hasPricing = !!model?.pricing; - const formatPrice = hasPricing ? getPrice(model!.pricing!) : null; - const pricingGroups = useMemo( - () => (hasPricing ? groupPricingUnits(model!.pricing!.units) : []), - [hasPricing, model?.pricing], - ); + const hasPricing = !!model?.pricing; + const formatPrice = hasPricing ? getPrice(model!.pricing!) : null; + const pricingGroups = useMemo( + () => (hasPricing ? groupPricingUnits(model!.pricing!.units) : []), + [hasPricing, model?.pricing], + ); - if (!model) return null; + const approximatePriceLabel = useMemo(() => { + if (!hasPricing || !model?.pricing || !pricingMode) return null; + const pricing = model.pricing; + const currency = pricing.currency as ModelPriceCurrency | undefined; + if (pricingMode === 'image' && typeof pricing.approximatePricePerImage === 'number') { + const amount = formatPriceByCurrency(pricing.approximatePricePerImage, currency); + return t('ModelSwitchPanel.detail.pricing.perImage', { + amount, + defaultValue: '~ ${{amount}} / image', + }); + } + if (pricingMode === 'video' && typeof pricing.approximatePricePerVideo === 'number') { + const amount = formatPriceByCurrency(pricing.approximatePricePerVideo, currency); + return t('ModelSwitchPanel.detail.pricing.perVideo', { + amount, + defaultValue: '~ ${{amount}} / video', + }); + } + return null; + }, [hasPricing, model?.pricing, pricingMode, t]); - const hasContext = typeof model.contextWindowTokens === 'number'; - const enabledAbilities = ABILITY_CONFIG.filter( - (a) => model.abilities[a.key as keyof typeof model.abilities], - ); - const hasAbilities = enabledAbilities.length > 0; + if (!model) return null; - return ( - - {/* Sections */} - {(hasPricing || hasContext || hasAbilities || hasExtendParams) && ( - setExpandedKeys(keys as string[])} - > - {/* Context Length */} - {hasContext && ( - - {model.contextWindowTokens === 0 - ? '∞' - : `${formatTokenNumber(model.contextWindowTokens!)} tokens`} - - } - title={ - -
- {t('ModelSwitchPanel.detail.context')} - - } - /> - )} + const hasContext = typeof model.contextWindowTokens === 'number'; + const enabledAbilities = ABILITY_CONFIG.filter( + (a) => model.abilities[a.key as keyof typeof model.abilities], + ); + const hasAbilities = enabledAbilities.length > 0; - {/* Abilities */} - {hasAbilities && ( - - {enabledAbilities.map((ability) => ( - - - - ))} + return ( + + {/* Sections */} + {(hasPricing || hasContext || hasAbilities || (hasExtendParams && !pricingMode)) && ( + setExpandedKeys(keys as string[])} + > + {/* Context Length */} + {hasContext && ( + + {model.contextWindowTokens === 0 + ? '∞' + : `${formatTokenNumber(model.contextWindowTokens!)} tokens`} + + } + title={ + +
+ {t('ModelSwitchPanel.detail.context')} - ) - } - title={ - -
- {t('ModelSwitchPanel.detail.abilities')} - - } - > - - {enabledAbilities.map((ability) => ( - - - - {t(`ModelSwitchPanel.detail.abilities.${ability.key}` as any)} + } + /> + )} + + {/* Abilities */} + {hasAbilities && ( + + {enabledAbilities.map((ability) => ( + + + + ))} - - {t( - `ModelSelect.featureTag.${ability.key === 'files' ? 'file' : ability.key}` as any, - )} + ) + } + title={ + +
+ + {t('ModelSwitchPanel.detail.abilities')} - ))} - - - )} + } + > + + {enabledAbilities.map((ability) => ( + + + + {t(`ModelSwitchPanel.detail.abilities.${ability.key}` as any)} + + + {t( + `ModelSelect.featureTag.${ability.key === 'files' ? 'file' : ability.key}` as any, + )} + + + ))} + + + )} - {/* Pricing */} - {hasPricing && formatPrice && ( - - {getCachedTextInputUnitRate(model.pricing!) && ( + {/* Pricing */} + {hasPricing && (formatPrice || approximatePriceLabel) && ( + {approximatePriceLabel} + ) : ( + + {getCachedTextInputUnitRate(model.pricing!) && ( + + + + {formatPrice!.cachedInput} + + + )} - - {formatPrice.cachedInput} + + {formatPrice!.input} - )} - - - - {formatPrice.input} - - - - - - {formatPrice.output} - - - - ) - } - title={ - -
- {t('ModelSwitchPanel.detail.pricing')} - - } - > - - {pricingGroups.map(({ group, units }) => ( - - {pricingGroups.length > 1 && ( - - {t(`ModelSwitchPanel.detail.pricing.group.${group}` as any)} - - )} - {units.map((unit) => ( - - - {UNIT_ICON_MAP[unit.name] && ( - - )} + + + {formatPrice!.output} + + + + )) + } + title={ + +
+ {t('ModelSwitchPanel.detail.pricing')} + + } + > + + {approximatePriceLabel && ( + + {approximatePriceLabel} + + )} + {pricingGroups.map(({ group, units }) => ( + + {pricingGroups.length > 1 && ( + + {t(`ModelSwitchPanel.detail.pricing.group.${group}` as any)} + + )} + {units.map((unit) => ( + + + {UNIT_ICON_MAP[unit.name] && ( + + )} + + {t(`ModelSwitchPanel.detail.pricing.unit.${unit.name}` as any)} + + - {t(`ModelSwitchPanel.detail.pricing.unit.${unit.name}` as any)} + {formatUnitRate(unit, model.pricing?.currency as ModelPriceCurrency)} - - {formatUnitRate(unit, model.pricing?.currency as ModelPriceCurrency)} - - - ))} - - ))} - - - )} - {/* Model Config */} - {hasExtendParams && provider && ( - -
- {t('ModelSwitchPanel.detail.config')} + ))} + + ))} - } - > -
- -
- - )} - - )} - - ); -}); + + )} + {/* Model Config (agent chat only; requires ChatInput zustand provider) */} + {hasExtendParams && provider && !pricingMode && ( + +
+ {t('ModelSwitchPanel.detail.config')} + + } + > +
+ +
+ + )} + + )} + + ); + }, +); ModelDetailPanel.displayName = 'ModelDetailPanel'; diff --git a/src/features/ModelSwitchPanel/components/PanelContent.tsx b/src/features/ModelSwitchPanel/components/PanelContent.tsx index 2d5c270e35..c25dd1cc07 100644 --- a/src/features/ModelSwitchPanel/components/PanelContent.tsx +++ b/src/features/ModelSwitchPanel/components/PanelContent.tsx @@ -1,32 +1,41 @@ import { Flexbox } from '@lobehub/ui'; -import { type FC } from 'react'; +import { type ComponentType, type FC } from 'react'; import { useState } from 'react'; import { Rnd } from 'react-rnd'; import { useEnabledChatModels } from '@/hooks/useEnabledChatModels'; import { useUserStore } from '@/store/user'; import { userGeneralSettingsSelectors } from '@/store/user/slices/settings/selectors/general'; +import type { EnabledProviderWithModels } from '@/types/aiProvider'; import { DEFAULT_WIDTH, ENABLE_RESIZING, MAX_WIDTH, MIN_WIDTH } from '../const'; import { usePanelSize } from '../hooks/usePanelSize'; import { usePanelState } from '../hooks/usePanelState'; import { List } from './List'; +import type { PricingMode } from './ModelDetailPanel'; import { Toolbar } from './Toolbar'; interface PanelContentProps { + enabledList?: EnabledProviderWithModels[]; model?: string; + ModelItemComponent?: ComponentType; onModelChange?: (params: { model: string; provider: string }) => Promise; onOpenChange?: (open: boolean) => void; + pricingMode?: PricingMode; provider?: string; } export const PanelContent: FC = ({ + ModelItemComponent, + enabledList: enabledListProp, model: modelProp, onModelChange: onModelChangeProp, onOpenChange, + pricingMode, provider: providerProp, }) => { - const enabledList = useEnabledChatModels(); + const chatEnabledList = useEnabledChatModels(); + const enabledList = enabledListProp ?? chatEnabledList; const [searchKeyword, setSearchKeyword] = useState(''); const isDevMode = useUserStore((s) => userGeneralSettingsSelectors.config(s).isDevMode); const { groupMode, handleGroupModeChange } = usePanelState(); @@ -42,8 +51,11 @@ export const PanelContent: FC = ({ onSearchKeywordChange={setSearchKeyword} /> ( ({ + ModelItemComponent, children, + enabledList, model: modelProp, onModelChange, onOpenChange, open, placement = 'topLeft', + pricingMode, provider: providerProp, openOnHover = true, }) => { @@ -45,7 +48,10 @@ const ModelSwitchPanel = memo( ; /** * Callback when model changes. If not provided, uses updateAgentConfig from store. */ @@ -64,6 +75,10 @@ export interface ModelSwitchPanelProps { * Dropdown placement. Defaults to 'topLeft'. */ placement?: DropdownPlacement; + /** + * Pass-through to ModelDetailPanel for image/video approximate pricing. + */ + pricingMode?: PricingMode; /** * Current provider ID. If not provided, uses currentAgentModelProvider from store. */ diff --git a/src/locales/default/components.ts b/src/locales/default/components.ts index e0892ca599..1a921a3418 100644 --- a/src/locales/default/components.ts +++ b/src/locales/default/components.ts @@ -76,6 +76,10 @@ export default { 'FileParsingStatus.chunks.status.processing': 'Chunking', 'FileParsingStatus.chunks.status.processingTip': 'The server is splitting text chunks; closing the page will not affect the chunking progress.', + 'GenerationModelItem.creditsPerImageApproximate': 'Approx. {{amount}} Credits / image', + 'GenerationModelItem.creditsPerImageExact': '{{amount}} Credits / image', + 'GenerationModelItem.creditsPerVideoApproximate': 'Approx. {{amount}} Credits / video', + 'GenerationModelItem.creditsPerVideoExact': '{{amount}} Credits / video', 'GoBack.back': 'Back', 'HtmlPreview.actions.download': 'Download', 'HtmlPreview.actions.preview': 'Preview', @@ -134,6 +138,8 @@ export default { 'ModelSwitchPanel.detail.pricing.group.text': 'Text', 'ModelSwitchPanel.detail.pricing.input': 'Input ${{amount}}/M', 'ModelSwitchPanel.detail.pricing.output': 'Output ${{amount}}/M', + 'ModelSwitchPanel.detail.pricing.perImage': '~ ${{amount}} / image', + 'ModelSwitchPanel.detail.pricing.perVideo': '~ ${{amount}} / video', 'ModelSwitchPanel.detail.pricing.unit.audioInput': 'Audio Input', 'ModelSwitchPanel.detail.pricing.unit.audioInput_cacheRead': 'Audio Input (Cached)', 'ModelSwitchPanel.detail.pricing.unit.audioOutput': 'Audio Output', diff --git a/src/locales/default/image.ts b/src/locales/default/image.ts index 4bfe61af2c..980a479ff0 100644 --- a/src/locales/default/image.ts +++ b/src/locales/default/image.ts @@ -11,6 +11,7 @@ export default { 'config.imageUrls.label': 'Reference Images', 'config.model.label': 'Model', 'config.prompt.placeholder': 'Describe what you want to generate', + 'config.prompt.placeholderWithRef': 'Describe how you want to adjust the image', 'config.quality.label': 'Image Quality', 'config.quality.options.hd': 'High Definition', 'config.quality.options.standard': 'Standard', @@ -22,7 +23,7 @@ export default { 'config.seed.random': 'Random Seed', 'config.size.label': 'Size', 'config.steps.label': 'Steps', - 'config.title': 'AI Image', + 'config.title': 'Configuration', 'config.width.label': 'Width', 'generation.actions.applySeed': 'Apply Seed', 'generation.actions.copyError': 'Copy Error Message', @@ -58,7 +59,7 @@ export default { 'Supports multiple AI image generation providers, including OpenAI gpt-image-1, Google Imagen, FAL.ai, and more, offering a wide selection of models.', 'notSupportGuide.features.multiProviders.title': 'Multi-Provider Support', 'notSupportGuide.title': 'Current Deployment Mode Does Not Support AI Image Generation', - 'topic.createNew': 'New Topic', + 'topic.createNew': 'Create New Topic', 'topic.deleteConfirm': 'Delete Generation Topic', 'topic.deleteConfirmDesc': 'You are about to delete this generation topic. This action cannot be undone, please proceed with caution.', diff --git a/src/locales/default/video.ts b/src/locales/default/video.ts index ea4cf1308b..172c92fe41 100644 --- a/src/locales/default/video.ts +++ b/src/locales/default/video.ts @@ -7,6 +7,7 @@ export default { 'config.header.title': 'Video', 'config.imageUrl.label': 'Start Frame', 'config.prompt.placeholder': 'Describe the video you want to generate', + 'config.prompt.placeholderWithRef': 'Describe the scene you want to generate with the image', 'config.referenceImage.label': 'Reference Image', 'config.resolution.label': 'Resolution', 'config.seed.label': 'Seed', @@ -21,7 +22,7 @@ export default { 'End frame cannot be used without a start frame. Please set a start frame first.', 'generation.status.failed': 'Generation Failed', 'generation.status.generating': 'Generating...', - 'topic.createNew': 'New Topic', + 'topic.createNew': 'Create New Topic', 'topic.deleteConfirm': 'Delete Video Topic', 'topic.deleteConfirmDesc': 'You are about to delete this video topic. This action cannot be undone.', diff --git a/src/proxy.ts b/src/proxy.ts index 31c52a0eb9..cff8665af5 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -21,6 +21,7 @@ export const config = { '/changelog(.*)', '/settings(.*)', '/image', + '/video', '/resource', '/resource(.*)', '/profile(.*)', diff --git a/src/routes/(main)/image/_layout/ConfigPanel/components/ModelSelect/ImageModelItem.tsx b/src/routes/(main)/(create)/components/GenerationModelItem.tsx similarity index 57% rename from src/routes/(main)/image/_layout/ConfigPanel/components/ModelSelect/ImageModelItem.tsx rename to src/routes/(main)/(create)/components/GenerationModelItem.tsx index 45e37ed0b9..8734c53f54 100644 --- a/src/routes/(main)/image/_layout/ConfigPanel/components/ModelSelect/ImageModelItem.tsx +++ b/src/routes/(main)/(create)/components/GenerationModelItem.tsx @@ -1,11 +1,14 @@ +'use client'; + import { BRANDING_PROVIDER } from '@lobechat/business-const'; import { CREDITS_PER_DOLLAR } from '@lobechat/const/currency'; import { ModelIcon } from '@lobehub/icons'; import { Flexbox, Popover, Text } from '@lobehub/ui'; import { createStaticStyles, cx } from 'antd-style'; -import { type AiModelForSelect } from 'model-bank'; +import type { AiModelForSelect } from 'model-bank'; import numeral from 'numeral'; import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import NewModelBadge from '@/components/ModelSelect/NewModelBadge'; import { useIsDark } from '@/hooks/useIsDark'; @@ -34,6 +37,8 @@ const styles = createStaticStyles(({ css, cssVar }) => ({ priceText: css` font-weight: 500; color: ${cssVar.colorTextTertiary}; + word-break: keep-all; + white-space: nowrap; `, priceText_dark: css` font-weight: 500; @@ -41,9 +46,14 @@ const styles = createStaticStyles(({ css, cssVar }) => ({ `, })); -type ImageModelItemProps = AiModelForSelect & { +export interface GenerationModelItemProps extends AiModelForSelect { /** - * Provider ID for determining price display format + * Which USD price fields to use: image uses approximatePricePerImage / pricePerImage; video uses approximatePricePerVideo / pricePerVideo. + * @default 'image' + */ + priceKind?: 'image' | 'video'; + /** + * Provider ID for determining price display format (when showPrice is true) */ providerId?: string; /** @@ -56,46 +66,79 @@ type ImageModelItemProps = AiModelForSelect & { * @default true */ showPopover?: boolean; -}; + /** + * Whether to show price in popover (e.g. true for image, false for video) + * @default false + */ + showPrice?: boolean; +} -const ImageModelItem = memo( +const GenerationModelItem = memo( ({ approximatePricePerImage, + approximatePricePerVideo, description, pricePerImage, + pricePerVideo, providerId, showPopover = true, showBadge = true, + showPrice = false, + priceKind = 'image', ...model }) => { const isDarkMode = useIsDark(); + const { t } = useTranslation('components'); const enableBusinessFeatures = useServerConfigStore( serverConfigSelectors.enableBusinessFeatures, ); const priceLabel = useMemo(() => { - // Show credits only for branding provider with business features enabled + if (!showPrice) return undefined; + + const isVideo = priceKind === 'video'; + const exactUsd = isVideo ? pricePerVideo : pricePerImage; + const approxUsd = isVideo ? approximatePricePerVideo : approximatePricePerImage; + if (enableBusinessFeatures && providerId === BRANDING_PROVIDER) { - if (typeof pricePerImage === 'number') { - const credits = pricePerImage * CREDITS_PER_DOLLAR; - return `${numeral(credits).format('0,0')} credits/张`; + if (typeof exactUsd === 'number') { + const credits = exactUsd * CREDITS_PER_DOLLAR; + return t( + isVideo + ? 'GenerationModelItem.creditsPerVideoExact' + : 'GenerationModelItem.creditsPerImageExact', + { amount: numeral(credits).format('0,0') }, + ); } - if (typeof approximatePricePerImage === 'number') { - const credits = approximatePricePerImage * CREDITS_PER_DOLLAR; - return `~ ${numeral(credits).format('0,0')} credits/张`; + if (typeof approxUsd === 'number') { + const credits = approxUsd * CREDITS_PER_DOLLAR; + return t( + isVideo + ? 'GenerationModelItem.creditsPerVideoApproximate' + : 'GenerationModelItem.creditsPerImageApproximate', + { amount: numeral(credits).format('0,0') }, + ); } } else { - // Show USD price for open source version or non-branding providers - if (typeof pricePerImage === 'number') { - return `${numeral(pricePerImage).format('$0,0.00[000]')} / image`; + if (typeof exactUsd === 'number') { + return `${numeral(exactUsd).format('$0,0.00[000]')} / ${isVideo ? 'video' : 'image'}`; } - if (typeof approximatePricePerImage === 'number') { - return `~ ${numeral(approximatePricePerImage).format('$0,0.00[000]')} / image`; + if (typeof approxUsd === 'number') { + return `~ ${numeral(approxUsd).format('$0,0.00[000]')} / ${isVideo ? 'video' : 'image'}`; } } - return undefined; - }, [approximatePricePerImage, enableBusinessFeatures, pricePerImage, providerId]); + }, [ + showPrice, + approximatePricePerImage, + approximatePricePerVideo, + enableBusinessFeatures, + pricePerImage, + pricePerVideo, + priceKind, + providerId, + t, + ]); const popoverContent = useMemo(() => { if (!description && !priceLabel) return null; @@ -140,6 +183,6 @@ const ImageModelItem = memo( }, ); -ImageModelItem.displayName = 'ImageModelItem'; +GenerationModelItem.displayName = 'GenerationModelItem'; -export default ImageModelItem; +export default GenerationModelItem; diff --git a/src/routes/(main)/video/features/PromptInput/Title.tsx b/src/routes/(main)/(create)/components/PromptTitle.tsx similarity index 60% rename from src/routes/(main)/video/features/PromptInput/Title.tsx rename to src/routes/(main)/(create)/components/PromptTitle.tsx index 45a60831fb..7b9a5b1420 100644 --- a/src/routes/(main)/video/features/PromptInput/Title.tsx +++ b/src/routes/(main)/(create)/components/PromptTitle.tsx @@ -2,11 +2,17 @@ import { Center, Icon, Text } from '@lobehub/ui'; import { cssVar } from 'antd-style'; -import { Video } from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -const PromptTitle = () => { - const { t } = useTranslation('video'); +interface PromptTitleProps { + icon: LucideIcon; + namespace: 'image' | 'video'; +} + +const PromptTitle = memo(({ icon, namespace }) => { + const { t } = useTranslation(namespace); return (
@@ -19,7 +25,7 @@ const PromptTitle = () => { borderRadius: 16, }} > - +
{ ); -}; +}); + +PromptTitle.displayName = 'PromptTitle'; export default PromptTitle; diff --git a/src/routes/(main)/(create)/features/CreateGenerationPage.tsx b/src/routes/(main)/(create)/features/CreateGenerationPage.tsx new file mode 100644 index 0000000000..0df8ed8304 --- /dev/null +++ b/src/routes/(main)/(create)/features/CreateGenerationPage.tsx @@ -0,0 +1,91 @@ +'use client'; + +import { Flexbox } from '@lobehub/ui'; +import { AnimatePresence, m as motion } from 'motion/react'; +import type { ComponentType } from 'react'; +import { memo } from 'react'; +import { useMatch } from 'react-router-dom'; + +import NavHeader from '@/features/NavHeader'; +import WideScreenContainer from '@/features/WideScreenContainer'; +import WideScreenButton from '@/features/WideScreenContainer/WideScreenButton'; +import { useQueryState } from '@/hooks/useQueryParam'; + +interface CreateGenerationPageProps { + path: string; + PromptInput: ComponentType<{ disableAnimation?: boolean; showTitle?: boolean }>; + Workspace: ComponentType<{ embedInput?: boolean }>; +} + +const CreateGenerationPage = memo(({ path, Workspace, PromptInput }) => { + const isCurrent = useMatch({ path, end: true }); + const [topic] = useQueryState('topic'); + const isHome = !topic; + + if (!isCurrent) return null; + + return ( + <> + } /> + + + + + {isHome ? ( + + + + + + ) : ( + + + + )} + + + + + {!isHome && ( + + + + + + )} + + + + ); +}); + +CreateGenerationPage.displayName = 'CreateGenerationPage'; + +export default CreateGenerationPage; diff --git a/src/routes/(main)/video/features/GenerationFeed/index.tsx b/src/routes/(main)/(create)/features/GenerationFeed/index.tsx similarity index 73% rename from src/routes/(main)/video/features/GenerationFeed/index.tsx rename to src/routes/(main)/(create)/features/GenerationFeed/index.tsx index 534e4fd62f..b57d9ec409 100644 --- a/src/routes/(main)/video/features/GenerationFeed/index.tsx +++ b/src/routes/(main)/(create)/features/GenerationFeed/index.tsx @@ -3,21 +3,22 @@ import { useAutoAnimate } from '@formkit/auto-animate/react'; import { Flexbox } from '@lobehub/ui'; import { Divider } from 'antd'; +import { type ReactNode } from 'react'; import { Fragment, memo, useEffect, useRef } from 'react'; -import { useVideoStore } from '@/store/video'; -import { generationBatchSelectors } from '@/store/video/selectors'; +import type { GenerationBatch } from '@/types/generation'; -import { VideoGenerationBatchItem } from './BatchItem'; +interface GenerationFeedProps { + batches: GenerationBatch[]; + renderBatchItem: (batch: GenerationBatch) => ReactNode; +} -const GenerationFeed = memo(() => { +const GenerationFeed = memo(({ batches, renderBatchItem }) => { const [parent, enableAnimations] = useAutoAnimate(); const containerRef = useRef(null); const isInitialLoadRef = useRef(true); const prevBatchesCountRef = useRef(0); - const currentGenerationBatches = useVideoStore(generationBatchSelectors.currentGenerationBatches); - const scrollToBottom = (behavior: 'smooth' | 'auto' = 'smooth') => { if (!containerRef.current) return; @@ -36,8 +37,7 @@ const GenerationFeed = memo(() => { }; useEffect(() => { - const currentBatches = currentGenerationBatches || []; - const currentBatchesCount = currentBatches.length; + const currentBatchesCount = batches.length; const prevBatchesCount = prevBatchesCountRef.current; if (currentBatchesCount === 0) { @@ -59,19 +59,19 @@ const GenerationFeed = memo(() => { } prevBatchesCountRef.current = currentBatchesCount; - }, [currentGenerationBatches, enableAnimations]); + }, [batches, enableAnimations]); - if (!currentGenerationBatches || currentGenerationBatches.length === 0) { + if (!batches || batches.length === 0) { return null; } return ( - + - {currentGenerationBatches.map((batch, index) => ( + {batches.map((batch, index) => ( {Boolean(index !== 0) && } - + {renderBatchItem(batch)} ))} diff --git a/src/routes/(main)/(create)/features/GenerationInput/ConfigAction.tsx b/src/routes/(main)/(create)/features/GenerationInput/ConfigAction.tsx new file mode 100644 index 0000000000..99a2c206e0 --- /dev/null +++ b/src/routes/(main)/(create)/features/GenerationInput/ConfigAction.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { SlidersHorizontal } from 'lucide-react'; +import type { ReactNode } from 'react'; +import { memo } from 'react'; + +import Action from '@/features/ChatInput/ActionBar/components/Action'; + +interface ConfigActionProps { + content: ReactNode; + title: ReactNode; +} + +const ConfigAction = memo(({ title, content }) => { + return ( + + ); +}); + +ConfigAction.displayName = 'ConfigAction'; + +export default ConfigAction; diff --git a/src/routes/(main)/(create)/features/GenerationInput/GenerationInvalidAPIKey.tsx b/src/routes/(main)/(create)/features/GenerationInput/GenerationInvalidAPIKey.tsx new file mode 100644 index 0000000000..03745e5985 --- /dev/null +++ b/src/routes/(main)/(create)/features/GenerationInput/GenerationInvalidAPIKey.tsx @@ -0,0 +1,54 @@ +'use client'; + +import { ProviderIcon } from '@lobehub/icons'; +import { Button } from '@lobehub/ui'; +import { ModelProvider } from 'model-bank'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import urlJoin from 'url-join'; + +import BaseErrorForm from '@/features/Conversation/Error/BaseErrorForm'; +import { useProviderName } from '@/hooks/useProviderName'; +import { type GlobalLLMProviderKey } from '@/types/user/settings/modelProvider'; + +interface GenerationInvalidAPIKeyProps { + onNavigate?: () => void; + provider?: string; +} + +const GenerationInvalidAPIKey = memo(({ provider, onNavigate }) => { + const { t } = useTranslation(['modelProvider', 'error']); + const navigate = useNavigate(); + const providerName = useProviderName(provider as GlobalLLMProviderKey); + + return ( + } + title={t(`unlock.apiKey.title`, { name: providerName, ns: 'error' })} + action={ + + } + desc={ + provider === ModelProvider.Bedrock + ? t('bedrock.unlock.description') + : t(`unlock.apiKey.description`, { + name: providerName, + ns: 'error', + }) + } + /> + ); +}); + +GenerationInvalidAPIKey.displayName = 'GenerationInvalidAPIKey'; + +export default GenerationInvalidAPIKey; diff --git a/src/routes/(main)/(create)/features/GenerationInput/GenerationPromptInput.tsx b/src/routes/(main)/(create)/features/GenerationInput/GenerationPromptInput.tsx new file mode 100644 index 0000000000..7015597a03 --- /dev/null +++ b/src/routes/(main)/(create)/features/GenerationInput/GenerationPromptInput.tsx @@ -0,0 +1,112 @@ +'use client'; + +import { ChatInput, ChatInputActionBar, SendButton } from '@lobehub/editor/react'; +import { Flexbox, TextArea } from '@lobehub/ui'; +import { createStaticStyles } from 'antd-style'; +import type { KeyboardEvent, ReactNode } from 'react'; +import { memo } from 'react'; + +interface GenerationPromptInputProps { + className?: string; + disableGenerate?: boolean; + generateLabel: string; + generatingLabel: string; + header?: ReactNode; + inlineContent?: ReactNode; + isCreating?: boolean; + isDarkMode?: boolean; + leftActions?: ReactNode; + maxRows?: number; + minRows?: number; + onGenerate: () => Promise | void; + onValueChange: (value: string) => void; + placeholder: string; + rightActions?: ReactNode; + value?: string; +} + +const styles = createStaticStyles(({ css, cssVar }) => ({ + textarea: css` + padding: 0; + border-radius: 0; + `, +})); + +const GenerationPromptInput = memo( + ({ + className, + header, + inlineContent, + leftActions, + rightActions, + isDarkMode, + isCreating, + value, + onValueChange, + onGenerate, + placeholder, + generateLabel, + generatingLabel, + disableGenerate, + minRows = 3, + maxRows = 6, + }) => { + const handleKeyDown = async (e: KeyboardEvent) => { + if (e.key !== 'Enter' || e.shiftKey || e.nativeEvent.isComposing) return; + + e.preventDefault(); + if (disableGenerate || isCreating || !value?.trim()) return; + + await onGenerate(); + }; + + const textarea = ( +