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 0000000000..1fa0b5e513 Binary files /dev/null and b/src/renderer/src/assets/images/providers/aihubmix.png differ 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 ( +