From efad8f9ad0a5cc8f16bdb95e2f3a2863b94c623d Mon Sep 17 00:00:00 2001 From: chenxue Date: Tue, 29 Apr 2025 14:38:59 +0800 Subject: [PATCH] feat: add painting aihubmix provider (#4503) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add painting aihubmix provider * fix: Cannot read properties of undefined (reading 'unshift') * fix: painting redux data --------- Co-authored-by: zhaochenxue Co-authored-by: 亢奋猫 --- src/renderer/src/App.tsx | 4 +- .../assets/images/paintings/ic_ImageUp.svg | 8 + .../src/assets/images/providers/aihubmix.png | Bin 0 -> 30257 bytes src/renderer/src/hooks/usePaintings.ts | 47 +- src/renderer/src/i18n/locales/en-us.json | 54 +- src/renderer/src/i18n/locales/ja-jp.json | 54 +- src/renderer/src/i18n/locales/ru-ru.json | 54 +- src/renderer/src/i18n/locales/zh-cn.json | 54 +- src/renderer/src/i18n/locales/zh-tw.json | 54 +- .../src/pages/paintings/AihubmixPage.tsx | 767 ++++++++++++++++++ .../src/pages/paintings/PaintingsList.tsx | 8 +- .../src/pages/paintings/PaintingsPage.tsx | 78 +- .../pages/paintings/PaintingsRoutePage.tsx | 19 + .../pages/paintings/config/aihubmixConfig.tsx | 298 +++++++ .../src/pages/paintings/config/constants.ts | 112 +++ src/renderer/src/store/paintings.ts | 70 +- src/renderer/src/types/index.ts | 62 +- 17 files changed, 1652 insertions(+), 91 deletions(-) create mode 100644 src/renderer/src/assets/images/paintings/ic_ImageUp.svg create mode 100644 src/renderer/src/assets/images/providers/aihubmix.png create mode 100644 src/renderer/src/pages/paintings/AihubmixPage.tsx create mode 100644 src/renderer/src/pages/paintings/PaintingsRoutePage.tsx create mode 100644 src/renderer/src/pages/paintings/config/aihubmixConfig.tsx create mode 100644 src/renderer/src/pages/paintings/config/constants.ts diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index c5e70a920e..52b098c957 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -17,7 +17,7 @@ import AppsPage from './pages/apps/AppsPage' import FilesPage from './pages/files/FilesPage' import HomePage from './pages/home/HomePage' import KnowledgePage from './pages/knowledge/KnowledgePage' -import PaintingsPage from './pages/paintings/PaintingsPage' +import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage' import SettingsPage from './pages/settings/SettingsPage' import TranslatePage from './pages/translate/TranslatePage' @@ -36,7 +36,7 @@ function App(): React.ReactElement { } /> } /> - } /> + } /> } /> } /> } /> diff --git a/src/renderer/src/assets/images/paintings/ic_ImageUp.svg b/src/renderer/src/assets/images/paintings/ic_ImageUp.svg new file mode 100644 index 0000000000..cbd8974dd0 --- /dev/null +++ b/src/renderer/src/assets/images/paintings/ic_ImageUp.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/renderer/src/assets/images/providers/aihubmix.png b/src/renderer/src/assets/images/providers/aihubmix.png new file mode 100644 index 0000000000000000000000000000000000000000..1fa0b5e51320a74b401605cea4648deca922fe81 GIT binary patch literal 30257 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 IM6N<$g0}O#Q2+n{ literal 0 HcmV?d00001 diff --git a/src/renderer/src/hooks/usePaintings.ts b/src/renderer/src/hooks/usePaintings.ts index 8a8ed550ef..ed41ce87c2 100644 --- a/src/renderer/src/hooks/usePaintings.ts +++ b/src/renderer/src/hooks/usePaintings.ts @@ -1,44 +1,37 @@ -import { TEXT_TO_IMAGES_MODELS } from '@renderer/config/models' import FileManager from '@renderer/services/FileManager' import { useAppDispatch, useAppSelector } from '@renderer/store' import { addPainting, removePainting, updatePainting, updatePaintings } from '@renderer/store/paintings' -import { Painting } from '@renderer/types' -import { uuid } from '@renderer/utils' +import { PaintingAction, PaintingsState } from '@renderer/types' export function usePaintings() { const paintings = useAppSelector((state) => state.paintings.paintings) + const generate = useAppSelector((state) => state.paintings.generate) + const remix = useAppSelector((state) => state.paintings.remix) + const edit = useAppSelector((state) => state.paintings.edit) + const upscale = useAppSelector((state) => state.paintings.upscale) const dispatch = useAppDispatch() - const generateRandomSeed = () => Math.floor(Math.random() * 1000000).toString() return { paintings, - addPainting: () => { - const newPainting: Painting = { - model: TEXT_TO_IMAGES_MODELS[0].id, - id: uuid(), - urls: [], - files: [], - prompt: '', - negativePrompt: '', - imageSize: '1024x1024', - numImages: 1, - seed: generateRandomSeed(), - steps: 25, - guidanceScale: 4.5, - promptEnhancement: true - } - dispatch(addPainting(newPainting)) - return newPainting + persistentData: { + generate, + remix, + edit, + upscale }, - removePainting: async (painting: Painting) => { + addPainting: (namespace: keyof PaintingsState, painting: PaintingAction) => { + dispatch(addPainting({ namespace, painting })) + return painting + }, + removePainting: async (namespace: keyof PaintingsState, painting: PaintingAction) => { FileManager.deleteFiles(painting.files) - dispatch(removePainting(painting)) + dispatch(removePainting({ namespace, painting })) }, - updatePainting: (painting: Painting) => { - dispatch(updatePainting(painting)) + updatePainting: (namespace: keyof PaintingsState, painting: PaintingAction) => { + dispatch(updatePainting({ namespace, painting })) }, - updatePaintings: (paintings: Painting[]) => { - dispatch(updatePaintings(paintings)) + updatePaintings: (namespace: keyof PaintingsState, paintings: PaintingAction[]) => { + dispatch(updatePaintings({ namespace, paintings })) } } } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 82963d1642..296b4ab1d0 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -690,7 +690,59 @@ "regenerate.confirm": "This will replace your existing generated images. Do you want to continue?", "seed": "Seed", "seed_tip": "The same seed and prompt can produce similar images", - "title": "Images" + "title": "Images", + "magic_prompt_option": "Magic Prompt", + "model": "Model Version", + "aspect_ratio": "Aspect Ratio", + "style_type": "Style", + "learn_more": "Learn More", + "prompt_placeholder_edit": "Enter your image description, text drawing uses “double quotes” to wrap", + "proxy_required": "Currently, you need to open a proxy to view the generated images, it will be supported in the future", + "image_file_required": "Please upload an image first", + "image_file_retry": "Please re-upload an image first", + "mode": { + "generate": "Draw", + "edit": "Edit", + "remix": "Remix", + "upscale": "Upscale" + }, + "generate": { + "model_tip": "Model version: V2 is the latest model of the interface, V2A is the fast model, V_1 is the first-generation model, _TURBO is the acceleration version", + "number_images_tip": "Number of images to generate", + "seed_tip": "Controls image generation randomness for reproducible results", + "negative_prompt_tip": "Describe unwanted elements, only for V_1, V_1_TURBO, V_2, and V_2_TURBO", + "magic_prompt_option_tip": "Intelligently enhances prompts for better results", + "style_type_tip": "Image generation style for V_2 and above" + }, + "edit": { + "image_file": "Edited Image", + "model_tip": "Only supports V_2 and V_2_TURBO versions", + "number_images_tip": "Number of edited results to generate", + "style_type_tip": "Style for edited image, only for V_2 and above", + "seed_tip": "Controls editing randomness", + "magic_prompt_option_tip": "Intelligently enhances editing prompts" + }, + "remix": { + "model_tip": "Select AI model version for remixing", + "image_file": "Reference Image", + "image_weight": "Reference Image Weight", + "image_weight_tip": "Adjust reference image influence", + "number_images_tip": "Number of remix results to generate", + "seed_tip": "Control the randomness of the mixed result", + "style_type_tip": "Style for remixed image, only for V_2 and above", + "negative_prompt_tip": "Describe unwanted elements in remix results", + "magic_prompt_option_tip": "Intelligently enhances remix prompts" + }, + "upscale": { + "image_file": "Image to upscale", + "resemblance": "Similarity", + "resemblance_tip": "Controls similarity to original image", + "detail": "Detail", + "detail_tip": "Controls detail enhancement level", + "number_images_tip": "Number of upscaled results to generate", + "seed_tip": "Controls upscaling randomness", + "magic_prompt_option_tip": "Intelligently enhances upscaling prompts" + } }, "plantuml": { "download": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 7d4b5b28af..b15688cf79 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -690,7 +690,59 @@ "regenerate.confirm": "これにより、既存の生成画像が置き換えられます。続行しますか?", "seed": "シード", "seed_tip": "同じシードとプロンプトで似た画像を生成できます", - "title": "画像" + "title": "画像", + "magic_prompt_option": "プロンプト強化", + "model": "モデルバージョン", + "aspect_ratio": "画幅比例", + "style_type": "スタイル", + "learn_more": "詳しくはこちら", + "prompt_placeholder_edit": "画像の説明を入力します。テキスト描画には '二重引用符' を使用します", + "proxy_required": "現在、プロキシを開く必要があります。これは、将来サポートされる予定です", + "image_file_required": "画像を先にアップロードしてください", + "image_file_retry": "画像を先にアップロードしてください", + "mode": { + "generate": "画像生成", + "edit": "部分編集", + "remix": "混合", + "upscale": "拡大" + }, + "generate": { + "model_tip": "モデルバージョン:V2 は最新 API モデル、V2A は高速モデル、V_1 は初代モデル、_TURBO は高速処理版です", + "number_images_tip": "一度に生成する画像の枚数", + "seed_tip": "画像生成のランダム性を制御して、同じ生成結果を再現します", + "negative_prompt_tip": "画像に含めたくない内容を説明します", + "magic_prompt_option_tip": "生成効果を向上させるための提示詞を最適化します", + "style_type_tip": "画像生成スタイル、V_2 以上のバージョンでのみ適用" + }, + "edit": { + "image_file": "編集画像", + "model_tip": "部分編集は V_2 と V_2_TURBO のバージョンのみサポートします", + "number_images_tip": "生成される編集結果の数", + "style_type_tip": "編集後の画像スタイル、V_2 以上のバージョンでのみ適用", + "seed_tip": "編集結果のランダム性を制御します", + "magic_prompt_option_tip": "編集効果を向上させるための提示詞を最適化します" + }, + "remix": { + "model_tip": "リミックスに使用する AI モデルのバージョンを選択します", + "image_file": "参照画像", + "image_weight": "参照画像の重み", + "image_weight_tip": "参照画像の影響度を調整します", + "number_images_tip": "生成されるリミックス結果の数", + "seed_tip": "リミックス結果のランダム性を制御します", + "style_type_tip": "リミックス後の画像スタイル、V_2 以上のバージョンでのみ適用", + "negative_prompt_tip": "リミックス結果に含めたくない内容を説明します", + "magic_prompt_option_tip": "リミックス効果を向上させるための提示詞を最適化します" + }, + "upscale": { + "image_file": "拡大する画像", + "resemblance": "類似度", + "resemblance_tip": "拡大結果と原画像の類似度を制御します", + "detail": "詳細度", + "detail_tip": "拡大画像の詳細度を制御します", + "number_images_tip": "生成される拡大結果の数", + "seed_tip": "拡大結果のランダム性を制御します", + "magic_prompt_option_tip": "拡大効果を向上させるための提示詞を最適化します" + } }, "plantuml": { "download": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 210a76d559..058486286b 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -690,7 +690,59 @@ "regenerate.confirm": "Это заменит ваши существующие сгенерированные изображения. Хотите продолжить?", "seed": "Ключ генерации", "seed_tip": "Одинаковый ключ генерации и промпт могут производить похожие изображения", - "title": "Изображения" + "title": "Изображения", + "magic_prompt_option": "Улучшение промпта", + "model": "Версия", + "aspect_ratio": "Пропорции изображения", + "style_type": "Стиль", + "learn_more": "Узнать больше", + "prompt_placeholder_edit": "Введите ваше описание изображения, текстовая отрисовка использует двойные кавычки для обертки", + "proxy_required": "Сейчас необходимо открыть прокси для просмотра сгенерированных изображений, в будущем будет поддерживаться прямое соединение", + "image_file_required": "Пожалуйста, сначала загрузите изображение", + "image_file_retry": "Пожалуйста, сначала загрузите изображение", + "mode": { + "generate": "Рисование", + "edit": "Редактирование", + "remix": "Смешивание", + "upscale": "Увеличение" + }, + "generate": { + "model_tip": "Версия модели: V2 — последняя модель интерфейса, V2A — быстрая модель, V_1 — первая модель, _TURBO — ускоренная версия", + "number_images_tip": "Количество изображений для генерации", + "seed_tip": "Контролирует случайный характер генерации изображений для воспроизводимых результатов", + "negative_prompt_tip": "Опишите элементы, которые вы не хотите включать в изображение, поддерживаются только версии V_1, V_1_TURBO, V_2 и V_2_TURBO", + "magic_prompt_option_tip": "Улучшает генерацию изображений с помощью интеллектуального оптимизирования промптов", + "style_type_tip": "Стиль генерации изображений, поддерживается только для версий V_2 и выше" + }, + "edit": { + "image_file": "Редактируемое изображение", + "model_tip": "Частичное редактирование поддерживается только версиями V_2 и V_2_TURBO", + "number_images_tip": "Количество редактированных результатов для генерации", + "style_type_tip": "Стиль редактированного изображения, поддерживается только для версий V_2 и выше", + "seed_tip": "Контролирует случайный характер редактирования изображений для воспроизводимых результатов", + "magic_prompt_option_tip": "Улучшает редактирование изображений с помощью интеллектуального оптимизирования промптов" + }, + "remix": { + "model_tip": "Выберите версию AI-модели для перемешивания", + "image_file": "Ссылка на изображение", + "image_weight": "Вес изображения", + "image_weight_tip": "Насколько сильно влияние изображения на результат", + "number_images_tip": "Количество перемешанных результатов для генерации", + "seed_tip": "Контролирует случайный характер перемешивания изображений для воспроизводимых результатов", + "style_type_tip": "Стиль перемешанного изображения, поддерживается только для версий V_2 и выше", + "negative_prompt_tip": "Опишите элементы, которые вы не хотите включать в изображение", + "magic_prompt_option_tip": "Улучшает перемешивание изображений с помощью интеллектуального оптимизирования промптов" + }, + "upscale": { + "image_file": "Изображение для увеличения", + "resemblance": "Сходство", + "resemblance_tip": "Насколько близко результат увеличения к исходному изображению", + "detail": "Детали", + "detail_tip": "Насколько детально увеличенное изображение", + "number_images_tip": "Количество увеличенных результатов для генерации", + "seed_tip": "Контролирует случайный характер увеличения изображений для воспроизводимых результатов", + "magic_prompt_option_tip": "Улучшает увеличение изображений с помощью интеллектуального оптимизирования промптов" + } }, "plantuml": { "download": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index cc092161f7..b2f81fa149 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -690,7 +690,59 @@ "regenerate.confirm": "这将覆盖已生成的图片,是否继续?", "seed": "随机种子", "seed_tip": "相同的种子和提示词可以生成相似的图片", - "title": "图片" + "title": "图片", + "magic_prompt_option": "提示词增强", + "model": "版本", + "aspect_ratio": "画幅比例", + "style_type": "风格", + "learn_more": "了解更多", + "prompt_placeholder_edit": "输入你的图片描述,文本绘制用 “双引号” 包裹", + "proxy_required": "目前需要打开代理才能查看生成图片,后续会支持国内直连", + "image_file_required": "请先上传图片", + "image_file_retry": "请重新上传图片", + "mode": { + "generate": "绘图", + "edit": "编辑", + "remix": "混合", + "upscale": "放大" + }, + "generate": { + "model_tip": "模型版本:V2 为接口最新模型,V2A 为快速模型、V_1 为初代模型,_TURBO 为加速版本", + "number_images_tip": "单次出图数量", + "seed_tip": "控制图像生成的随机性,用于复现相同的生成结果", + "negative_prompt_tip": "描述不想在图像中出现的元素,仅支持 V_1、V_1_TURBO、V_2 和 V_2_TURBO 版本", + "magic_prompt_option_tip": "智能优化提示词以提升生成效果", + "style_type_tip": "图像生成风格,仅适用于 V_2 及以上版本" + }, + "edit": { + "image_file": "编辑的图像", + "model_tip": "局部编辑仅支持 V_2 和 V_2_TURBO 版本", + "number_images_tip": "生成的编辑结果数量", + "style_type_tip": "编辑后的图像风格,仅适用于 V_2 及以上版本", + "seed_tip": "控制编辑结果的随机性", + "magic_prompt_option_tip": "智能优化编辑提示词" + }, + "remix": { + "model_tip": "选择重混使用的 AI 模型版本", + "image_file": "参考图", + "image_weight": "参考图权重", + "image_weight_tip": "调整参考图像的影响程度", + "number_images_tip": "生成的重混结果数量", + "seed_tip": "控制重混结果的随机性", + "style_type_tip": "重混后的图像风格,仅适用于 V_2 及以上版本", + "negative_prompt_tip": "描述不想在重混结果中出现的元素", + "magic_prompt_option_tip": "智能优化重混提示词" + }, + "upscale": { + "image_file": "需要放大的图片", + "resemblance": "相似度", + "resemblance_tip": "控制放大结果与原图的相似程度", + "detail": "细节", + "detail_tip": "控制放大图像的细节增强程度", + "number_images_tip": "生成的放大结果数量", + "seed_tip": "控制放大结果的随机性", + "magic_prompt_option_tip": "智能优化放大提示词" + } }, "plantuml": { "download": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 3afbe14b59..89144e93cf 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -690,7 +690,59 @@ "regenerate.confirm": "這將覆蓋已生成的圖片,是否繼續?", "seed": "隨機種子", "seed_tip": "相同的種子和提示詞可以生成相似的圖片", - "title": "繪圖" + "title": "繪圖", + "magic_prompt_option": "提示詞增強", + "model": "版本", + "aspect_ratio": "畫幅比例", + "style_type": "風格", + "learn_more": "了解更多", + "prompt_placeholder_edit": "輸入你的圖片描述,文本繪製用 '雙引號' 包裹", + "proxy_required": "目前需要打開代理才能查看生成圖片,後續會支持國內直連", + "image_file_required": "請先上傳圖片", + "image_file_retry": "請重新上傳圖片", + "mode": { + "generate": "繪圖", + "edit": "編輯", + "remix": "混合", + "upscale": "放大" + }, + "generate": { + "model_tip": "模型版本:V2 為接口最新模型,V2A 為快速模型、V_1 為初代模型,_TURBO 為加速版本", + "number_images_tip": "單次出圖數量", + "seed_tip": "控制圖像生成的隨機性,用於重現相同的生成結果", + "negative_prompt_tip": "描述不想在圖像中出現的元素,僅支援 V_1、V_1_TURBO、V_2 和 V_2_TURBO 版本", + "magic_prompt_option_tip": "智能優化提示詞以提升生成效果", + "style_type_tip": "圖像生成風格,僅適用於 V_2 及以上版本" + }, + "edit": { + "image_file": "編輯的圖像", + "model_tip": "局部編輯僅支援 V_2 和 V_2_TURBO 版本", + "number_images_tip": "生成的編輯結果數量", + "style_type_tip": "編輯後的圖像風格,僅適用於 V_2 及以上版本", + "seed_tip": "控制編輯結果的隨機性", + "magic_prompt_option_tip": "智能優化編輯提示詞" + }, + "remix": { + "model_tip": "選擇重混使用的 AI 模型版本", + "image_file": "參考圖", + "image_weight": "參考圖權重", + "image_weight_tip": "調整參考圖像的影響程度", + "number_images_tip": "生成的重混結果數量", + "seed_tip": "控制重混結果的隨機性", + "style_type_tip": "重混後的圖像風格,僅適用於 V_2 及以上版本", + "negative_prompt_tip": "描述不想在重混結果中出現的元素", + "magic_prompt_option_tip": "智能優化重混提示詞" + }, + "upscale": { + "image_file": "需要放大的圖片", + "resemblance": "相似度", + "resemblance_tip": "控制放大結果與原圖的相似程度", + "detail": "細節", + "detail_tip": "控制放大圖像的細節增強程度", + "number_images_tip": "生成的放大結果數量", + "seed_tip": "控制放大結果的隨機性", + "magic_prompt_option_tip": "智能優化放大提示詞" + } }, "plantuml": { "download": { diff --git a/src/renderer/src/pages/paintings/AihubmixPage.tsx b/src/renderer/src/pages/paintings/AihubmixPage.tsx new file mode 100644 index 0000000000..d9a6c2b485 --- /dev/null +++ b/src/renderer/src/pages/paintings/AihubmixPage.tsx @@ -0,0 +1,767 @@ +import { InfoCircleFilled, PlusOutlined, RedoOutlined } from '@ant-design/icons' +import IcImageUp from '@renderer/assets/images/paintings/ic_ImageUp.svg' +import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar' +import { HStack } from '@renderer/components/Layout' +import Scrollbar from '@renderer/components/Scrollbar' +import TranslateButton from '@renderer/components/TranslateButton' +import { isMac } from '@renderer/config/constant' +import { getProviderLogo } from '@renderer/config/providers' +import { useTheme } from '@renderer/context/ThemeProvider' +import { usePaintings } from '@renderer/hooks/usePaintings' +import { useAllProviders } from '@renderer/hooks/useProvider' +import { useRuntime } from '@renderer/hooks/useRuntime' +import { useSettings } from '@renderer/hooks/useSettings' +import FileManager from '@renderer/services/FileManager' +import { translateText } from '@renderer/services/TranslateService' +import { useAppDispatch } from '@renderer/store' +import { setGenerating } from '@renderer/store/runtime' +import type { FileType } from '@renderer/types' +import type { PaintingAction, PaintingsState } from '@renderer/types' +import { getErrorMessage, uuid } from '@renderer/utils' +import { Avatar, Button, Input, InputNumber, Radio, Segmented, Select, Slider, Switch, Tooltip, Upload } from 'antd' +import TextArea from 'antd/es/input/TextArea' +import type { FC } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useLocation, useNavigate } from 'react-router-dom' +import styled from 'styled-components' + +import SendMessageButton from '../home/Inputbar/SendMessageButton' +import { SettingHelpLink, SettingTitle } from '../settings' +import Artboard from './Artboard' +import { type ConfigItem, createModeConfigs } from './config/aihubmixConfig' +import { DEFAULT_PAINTING } from './config/constants' +import PaintingsList from './PaintingsList' + +// 使用函数创建配置项 +const modeConfigs = createModeConfigs() + +const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { + const [mode, setMode] = useState('generate') + const { addPainting, removePainting, updatePainting, persistentData } = usePaintings() + const filteredPaintings = useMemo(() => persistentData[mode] || [], [persistentData, mode]) + const [painting, setPainting] = useState(filteredPaintings[0] || DEFAULT_PAINTING) + const [currentImageIndex, setCurrentImageIndex] = useState(0) + const [isLoading, setIsLoading] = useState(false) + const [abortController, setAbortController] = useState(null) + const [spaceClickCount, setSpaceClickCount] = useState(0) + const [isTranslating, setIsTranslating] = useState(false) + const [fileMap, setFileMap] = useState<{ [key: string]: FileType }>({}) + + const { t } = useTranslation() + const { theme } = useTheme() + const providers = useAllProviders() + const providerOptions = Options.map((option) => { + const provider = providers.find((p) => p.id === option) + return { + label: t(`provider.${provider?.id}`), + value: provider?.id + } + }) + const dispatch = useAppDispatch() + const { generating } = useRuntime() + const navigate = useNavigate() + const location = useLocation() + const { autoTranslateWithSpace } = useSettings() + const spaceClickTimer = useRef(null) + const aihubmixProvider = providers.find((p) => p.id === 'aihubmix')! + + const modeOptions = [ + { label: t('paintings.mode.generate'), value: 'generate' }, + // { label: t('paintings.mode.edit'), value: 'edit' }, + { label: t('paintings.mode.remix'), value: 'remix' }, + { label: t('paintings.mode.upscale'), value: 'upscale' } + ] + const getNewPainting = () => { + return { + ...DEFAULT_PAINTING, + id: uuid() + } + } + + const textareaRef = useRef(null) + + const updatePaintingState = (updates: Partial) => { + const updatedPainting = { ...painting, ...updates } + setPainting(updatedPainting) + updatePainting(mode, updatedPainting) + } + + const onGenerate = async () => { + if (painting.files.length > 0) { + const confirmed = await window.modal.confirm({ + content: t('paintings.regenerate.confirm'), + centered: true + }) + + if (!confirmed) return + await FileManager.deleteFiles(painting.files) + } + + const prompt = textareaRef.current?.resizableTextArea?.textArea?.value || '' + updatePaintingState({ prompt }) + + if (!aihubmixProvider.enabled) { + window.modal.error({ + content: t('error.provider_disabled'), + centered: true + }) + return + } + + if (!aihubmixProvider.apiKey) { + window.modal.error({ + content: t('error.no_api_key'), + centered: true + }) + return + } + + if (!painting.model || !painting.prompt) { + return + } + + const controller = new AbortController() + setAbortController(controller) + setIsLoading(true) + dispatch(setGenerating(true)) + + let body: string | FormData = '' + const headers: Record = { + 'Api-Key': aihubmixProvider.apiKey + } + + // 不使用 AiProvider 的通用规则,而是直接调用自定义接口 + try { + if (mode === 'generate') { + const requestData = { + image_request: { + prompt, + model: painting.model, + aspect_ratio: painting.aspectRatio, + num_images: painting.numImages, + style_type: painting.styleType, + seed: painting.seed ? +painting.seed : undefined, + negative_prompt: painting.negativePrompt || undefined, + magic_prompt_option: painting.magicPromptOption ? 'ON' : 'OFF' + } + } + body = JSON.stringify(requestData) + headers['Content-Type'] = 'application/json' + } else { + if (!painting.imageFile) { + window.modal.error({ + content: t('paintings.image_file_required'), + centered: true + }) + return + } + if (!fileMap[painting.imageFile]) { + window.modal.error({ + content: t('paintings.image_file_retry'), + centered: true + }) + return + } + const form = new FormData() + let imageRequest: Record = { + prompt, + num_images: painting.numImages, + seed: painting.seed ? +painting.seed : undefined, + magic_prompt_option: painting.magicPromptOption ? 'ON' : 'OFF' + } + if (mode === 'remix') { + imageRequest = { + ...imageRequest, + model: painting.model, + aspect_ratio: painting.aspectRatio, + image_weight: painting.imageWeight, + style_type: painting.styleType + } + } else if (mode === 'upscale') { + imageRequest = { + ...imageRequest, + resemblance: painting.resemblance, + detail: painting.detail + } + } else if (mode === 'edit') { + imageRequest = { + ...imageRequest, + model: painting.model, + style_type: painting.styleType + } + } + form.append('image_request', JSON.stringify(imageRequest)) + form.append('image_file', fileMap[painting.imageFile] as unknown as Blob) + body = form + } + + // 直接调用自定义接口 + const response = await fetch(aihubmixProvider.apiHost + `/ideogram/` + mode, { method: 'POST', headers, body }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error?.message || '生成图像失败') + } + + const data = await response.json() + const urls = data.data.map((item: any) => item.url) + + if (urls.length > 0) { + const downloadedFiles = await Promise.all( + urls.map(async (url) => { + try { + return await window.api.file.download(url) + } catch (error) { + console.error('下载图像失败:', error) + return null + } + }) + ) + + const validFiles = downloadedFiles.filter((file): file is FileType => file !== null) + + // 如果没有成功下载任何文件但有URLs,显示代理提示 + if (validFiles.length === 0 && urls.length > 0) { + window.modal.error({ + content: t('paintings.proxy_required'), + centered: true + }) + } + + await FileManager.addFiles(validFiles) + + updatePaintingState({ files: validFiles, urls }) + } + } catch (error: unknown) { + if (error instanceof Error && error.name !== 'AbortError') { + window.modal.error({ + content: getErrorMessage(error), + centered: true + }) + } + } finally { + setIsLoading(false) + dispatch(setGenerating(false)) + setAbortController(null) + } + } + + const onCancel = () => { + abortController?.abort() + } + + const nextImage = () => { + setCurrentImageIndex((prev) => (prev + 1) % painting.files.length) + } + + const prevImage = () => { + setCurrentImageIndex((prev) => (prev - 1 + painting.files.length) % painting.files.length) + } + + const handleAddPainting = () => { + const newPainting = addPainting(mode, getNewPainting()) + updatePainting(mode, newPainting) + setPainting(newPainting) + return newPainting + } + + const onDeletePainting = (paintingToDelete: PaintingAction) => { + if (paintingToDelete.id === painting.id) { + const currentIndex = filteredPaintings.findIndex((p) => p.id === paintingToDelete.id) + + if (currentIndex > 0) { + setPainting(filteredPaintings[currentIndex - 1]) + } else if (filteredPaintings.length > 1) { + setPainting(filteredPaintings[1]) + } + } + + removePainting(mode, paintingToDelete) + + if (filteredPaintings.length === 1) { + const defaultPainting = { + ...DEFAULT_PAINTING, + id: uuid() + } + setPainting(defaultPainting) + } + } + + const translate = async () => { + if (isTranslating) { + return + } + + if (!painting.prompt) { + return + } + + try { + setIsTranslating(true) + const translatedText = await translateText(painting.prompt, 'english') + updatePaintingState({ prompt: translatedText }) + } catch (error) { + console.error('Translation failed:', error) + } finally { + setIsTranslating(false) + } + } + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (autoTranslateWithSpace && event.key === ' ') { + setSpaceClickCount((prev) => prev + 1) + + if (spaceClickTimer.current) { + clearTimeout(spaceClickTimer.current) + } + + spaceClickTimer.current = setTimeout(() => { + setSpaceClickCount(0) + }, 200) + + if (spaceClickCount === 2) { + setSpaceClickCount(0) + setIsTranslating(true) + translate() + } + } + } + + const handleProviderChange = (providerId: string) => { + const routeName = location.pathname.split('/').pop() + if (providerId !== routeName) { + navigate('../' + providerId, { replace: true }) + } + } + // 处理模式切换 + const handleModeChange = (value: string) => { + setMode(value as keyof PaintingsState) + if (persistentData[value as keyof PaintingsState] && persistentData[value as keyof PaintingsState].length > 0) { + setPainting(persistentData[value as keyof PaintingsState][0]) + } else { + setPainting(DEFAULT_PAINTING) + } + } + + // 处理随机种子的点击事件 >=0<=2147483647 + const handleRandomSeed = () => { + const randomSeed = Math.floor(Math.random() * 2147483647).toString() + updatePaintingState({ seed: randomSeed }) + return randomSeed + } + + // 渲染配置项的函数 + const renderConfigItem = (item: ConfigItem, index: number) => { + switch (item.type) { + case 'title': + return ( + + {t(item.title!)} + {item.tooltip && ( + + + + )} + + ) + case 'select': + return ( + updatePaintingState({ [item.key!]: e.target.value })} + suffix={ + + } + /> + ) + } + return ( + updatePaintingState({ [item.key!]: e.target.value })} + suffix={item.suffix} + /> + ) + case 'inputNumber': + return ( + updatePaintingState({ [item.key!]: v })} + /> + ) + case 'textarea': + return ( +