feat: add painting aihubmix provider (#4503)

* add painting aihubmix provider

* fix: Cannot read properties of undefined (reading 'unshift')

* fix: painting redux data

---------

Co-authored-by: zhaochenxue <zhaochenxue@bixin.cn>
Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
This commit is contained in:
chenxue 2025-04-29 14:38:59 +08:00 committed by GitHub
parent a6822d4037
commit efad8f9ad0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 1652 additions and 91 deletions

View File

@ -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 {
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/agents" element={<AgentsPage />} />
<Route path="/paintings" element={<PaintingsPage />} />
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />

View File

@ -0,0 +1,8 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="ic_ImageUp">
<path id="Vector" d="M10.8 21.5H5.5C4.96957 21.5 4.46086 21.2893 4.08579 20.9142C3.71071 20.5391 3.5 20.0304 3.5 19.5V5.5C3.5 4.96957 3.71071 4.46086 4.08579 4.08579C4.46086 3.71071 4.96957 3.5 5.5 3.5H19.5C20.0304 3.5 20.5391 3.71071 20.9142 4.08579C21.2893 4.46086 21.5 4.96957 21.5 5.5V15.5L18.4 12.4C18.0237 12.0312 17.517 11.8258 16.9901 11.8284C16.4632 11.831 15.9586 12.0415 15.586 12.414L6.5 21.5" stroke="#353941" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M14.5 20L17.5 17L20.5 20" stroke="#353941" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_3" d="M17.5 22.5V17" stroke="#353941" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_4" d="M9.5 11.5C10.6046 11.5 11.5 10.6046 11.5 9.5C11.5 8.39543 10.6046 7.5 9.5 7.5C8.39543 7.5 7.5 8.39543 7.5 9.5C7.5 10.6046 8.39543 11.5 9.5 11.5Z" stroke="#353941" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -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 }))
}
}
}

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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<keyof PaintingsState>('generate')
const { addPainting, removePainting, updatePainting, persistentData } = usePaintings()
const filteredPaintings = useMemo(() => persistentData[mode] || [], [persistentData, mode])
const [painting, setPainting] = useState<PaintingAction>(filteredPaintings[0] || DEFAULT_PAINTING)
const [currentImageIndex, setCurrentImageIndex] = useState(0)
const [isLoading, setIsLoading] = useState(false)
const [abortController, setAbortController] = useState<AbortController | null>(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<NodeJS.Timeout>(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<any>(null)
const updatePaintingState = (updates: Partial<PaintingAction>) => {
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<string, string> = {
'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<string, any> = {
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<HTMLTextAreaElement>) => {
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 (
<SettingTitle key={index} style={{ marginBottom: 5, marginTop: 15 }}>
{t(item.title!)}
{item.tooltip && (
<Tooltip title={t(item.tooltip)}>
<InfoIcon />
</Tooltip>
)}
</SettingTitle>
)
case 'select':
return (
<Select
key={index}
disabled={item.disabled}
value={painting[item.key!] || item.initialValue}
options={item.options}
onChange={(v) => updatePaintingState({ [item.key!]: v })}
/>
)
case 'radio':
return (
<Radio.Group
key={index}
value={painting[item.key!]}
onChange={(e) => updatePaintingState({ [item.key!]: e.target.value })}>
{item.options!.map((option) => (
<Radio.Button key={option.value} value={option.value}>
{option.label}
</Radio.Button>
))}
</Radio.Group>
)
case 'slider':
return (
<SliderContainer key={index}>
<Slider
min={item.min}
max={item.max}
step={item.step}
value={painting[item.key!] as number}
onChange={(v) => updatePaintingState({ [item.key!]: v })}
/>
<StyledInputNumber
min={item.min}
max={item.max}
step={item.step}
value={painting[item.key!] as number}
onChange={(v) => updatePaintingState({ [item.key!]: v })}
/>
</SliderContainer>
)
case 'input':
// 处理随机种子按钮的特殊情况
if (item.key === 'seed') {
return (
<Input
key={index}
value={painting[item.key] as string}
onChange={(e) => updatePaintingState({ [item.key!]: e.target.value })}
suffix={
<RedoOutlined onClick={handleRandomSeed} style={{ cursor: 'pointer', color: 'var(--color-text-2)' }} />
}
/>
)
}
return (
<Input
key={index}
value={painting[item.key!] as string}
onChange={(e) => updatePaintingState({ [item.key!]: e.target.value })}
suffix={item.suffix}
/>
)
case 'inputNumber':
return (
<InputNumber
key={index}
min={item.min}
max={item.max}
style={{ width: '100%' }}
value={painting[item.key!] as number}
onChange={(v) => updatePaintingState({ [item.key!]: v })}
/>
)
case 'textarea':
return (
<TextArea
key={index}
value={painting[item.key!] as string}
onChange={(e) => updatePaintingState({ [item.key!]: e.target.value })}
spellCheck={false}
rows={4}
/>
)
case 'switch':
return (
<HStack key={index}>
<Switch
checked={painting[item.key!] as boolean}
onChange={(checked) => updatePaintingState({ [item.key!]: checked })}
/>
</HStack>
)
case 'image':
return (
<ImageUploadButton
key={index}
accept="image/png, image/jpeg, image/gif"
maxCount={1}
showUploadList={false}
listType="picture-card"
onChange={async ({ file }) => {
const path = file.originFileObj?.path || ''
setFileMap({ ...fileMap, [path]: file.originFileObj as unknown as FileType })
updatePaintingState({ [item.key!]: path })
}}>
{painting[item.key!] ? (
<ImagePreview>
<img src={'file://' + painting[item.key!]} alt="预览图" />
</ImagePreview>
) : (
<ImageSizeImage src={IcImageUp} theme={theme} />
)}
</ImageUploadButton>
)
default:
return null
}
}
const onSelectPainting = (newPainting: PaintingAction) => {
if (generating) return
setPainting(newPainting)
setCurrentImageIndex(0)
}
useEffect(() => {
if (filteredPaintings.length === 0) {
addPainting(mode, getNewPainting())
setPainting(DEFAULT_PAINTING)
}
}, [filteredPaintings, mode, addPainting, painting])
useEffect(() => {
return () => {
if (spaceClickTimer.current) {
clearTimeout(spaceClickTimer.current)
}
}
}, [])
return (
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
{isMac && (
<NavbarRight style={{ justifyContent: 'flex-end' }}>
<Button size="small" className="nodrag" icon={<PlusOutlined />} onClick={handleAddPainting}>
{t('paintings.button.new.image')}
</Button>
</NavbarRight>
)}
</Navbar>
<ContentContainer id="content-container">
<LeftContainer>
<ProviderTitleContainer>
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
<SettingHelpLink target="_blank" href={aihubmixProvider.apiHost}>
{t('paintings.learn_more')}
<ProviderLogo
shape="square"
src={getProviderLogo(aihubmixProvider.id)}
size={16}
style={{ marginLeft: 5 }}
/>
</SettingHelpLink>
</ProviderTitleContainer>
<Select value={providerOptions[0].value} onChange={handleProviderChange} style={{ marginBottom: 15 }}>
{providerOptions.map((provider) => (
<Select.Option value={provider.value} key={provider.value}>
<SelectOptionContainer>
<ProviderLogo shape="square" src={getProviderLogo(provider.value || '')} size={16} />
{provider.label}
</SelectOptionContainer>
</Select.Option>
))}
</Select>
{/* 使用JSON配置渲染设置项 */}
{modeConfigs[mode].map(renderConfigItem)}
</LeftContainer>
<MainContainer>
{/* 添加功能切换分段控制器 */}
<ModeSegmentedContainer>
<Segmented shape="round" value={mode} onChange={handleModeChange} options={modeOptions} />
</ModeSegmentedContainer>
<Artboard
painting={painting}
isLoading={isLoading}
currentImageIndex={currentImageIndex}
onPrevImage={prevImage}
onNextImage={nextImage}
onCancel={onCancel}
/>
<InputContainer>
<Textarea
ref={textareaRef}
variant="borderless"
disabled={isLoading}
value={painting.prompt}
spellCheck={false}
onChange={(e) => updatePaintingState({ prompt: e.target.value })}
placeholder={isTranslating ? t('paintings.translating') : t('paintings.prompt_placeholder_edit')}
onKeyDown={handleKeyDown}
/>
<Toolbar>
<ToolbarMenu>
<TranslateButton
text={textareaRef.current?.resizableTextArea?.textArea?.value}
onTranslated={(translatedText) => updatePaintingState({ prompt: translatedText })}
disabled={isLoading || isTranslating}
isLoading={isTranslating}
style={{ marginRight: 6, borderRadius: '50%' }}
/>
<SendMessageButton sendMessage={onGenerate} disabled={isLoading} />
</ToolbarMenu>
</Toolbar>
</InputContainer>
</MainContainer>
<PaintingsList
namespace={mode}
paintings={filteredPaintings}
selectedPainting={painting}
onSelectPainting={onSelectPainting}
onDeletePainting={onDeletePainting}
onNewPainting={handleAddPainting}
/>
</ContentContainer>
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
`
const ContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
height: 100%;
background-color: var(--color-background);
overflow: hidden;
`
const LeftContainer = styled(Scrollbar)`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
padding: 20px;
background-color: var(--color-background);
max-width: var(--assistants-width);
border-right: 0.5px solid var(--color-border);
`
const MainContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
background-color: var(--color-background);
`
const InputContainer = styled.div`
display: flex;
flex-direction: column;
min-height: 95px;
max-height: 95px;
position: relative;
border: 1px solid var(--color-border-soft);
transition: all 0.3s ease;
margin: 0 20px 15px 20px;
border-radius: 10px;
`
const Textarea = styled(TextArea)`
padding: 10px;
border-radius: 0;
display: flex;
flex: 1;
font-family: Ubuntu;
resize: none !important;
overflow: auto;
width: auto;
`
const Toolbar = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
justify-content: flex-end;
padding: 0 8px;
padding-bottom: 0;
height: 40px;
`
const ToolbarMenu = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
`
const InfoIcon = styled(InfoCircleFilled)`
margin-left: 5px;
cursor: help;
color: #8d94a6;
font-size: 12px;
`
const SliderContainer = styled.div`
display: flex;
align-items: center;
gap: 16px;
.ant-slider {
flex: 1;
}
`
const StyledInputNumber = styled(InputNumber)`
width: 70px;
`
const ProviderLogo = styled(Avatar)`
border: 0.5px solid var(--color-border);
`
// 添加新的样式组件
const ModeSegmentedContainer = styled.div`
display: flex;
justify-content: center;
padding-top: 24px;
`
const SelectOptionContainer = styled.div`
display: flex;
align-items: center;
gap: 8px;
`
// 添加新的样式组件
const ProviderTitleContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
`
const ImageSizeImage = styled.img<{ theme: string }>`
filter: ${({ theme }) => (theme === 'dark' ? 'invert(100%)' : 'none')};
margin-top: 8px;
`
const ImageUploadButton = styled(Upload)`
& .ant-upload.ant-upload-select,
.ant-upload-list-item-container {
width: 100% !important;
height: 100% !important;
aspect-ratio: 1 !important;
}
`
// 修改 ImagePreview 组件,添加悬停效果
const ImagePreview = styled.div`
width: 100%;
height: 100%;
position: relative;
border-radius: 6px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
&:hover::after {
content: '点击替换';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
`
export default AihubmixPage

View File

@ -3,7 +3,7 @@ import DragableList from '@renderer/components/DragableList'
import Scrollbar from '@renderer/components/Scrollbar'
import { usePaintings } from '@renderer/hooks/usePaintings'
import FileManager from '@renderer/services/FileManager'
import { Painting } from '@renderer/types'
import { Painting, PaintingsState } from '@renderer/types'
import { classNames } from '@renderer/utils'
import { Popconfirm } from 'antd'
import { FC, useState } from 'react'
@ -16,6 +16,7 @@ interface PaintingsListProps {
onSelectPainting: (painting: Painting) => void
onDeletePainting: (painting: Painting) => void
onNewPainting: () => void
namespace: keyof PaintingsState
}
const PaintingsList: FC<PaintingsListProps> = ({
@ -23,7 +24,8 @@ const PaintingsList: FC<PaintingsListProps> = ({
selectedPainting,
onSelectPainting,
onDeletePainting,
onNewPainting
onNewPainting,
namespace
}) => {
const { t } = useTranslation()
const [dragging, setDragging] = useState(false)
@ -38,7 +40,7 @@ const PaintingsList: FC<PaintingsListProps> = ({
)}
<DragableList
list={paintings}
onUpdate={updatePaintings}
onUpdate={(value) => updatePaintings(namespace, value)}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(item: Painting) => (

View File

@ -21,15 +21,15 @@ import { getProviderByModel } from '@renderer/services/AssistantService'
import FileManager from '@renderer/services/FileManager'
import { translateText } from '@renderer/services/TranslateService'
import { useAppDispatch } from '@renderer/store'
import { DEFAULT_PAINTING } from '@renderer/store/paintings'
import { setGenerating } from '@renderer/store/runtime'
import type { FileType, Painting } from '@renderer/types'
import { getErrorMessage } from '@renderer/utils'
import { getErrorMessage, uuid } from '@renderer/utils'
import { Button, Input, InputNumber, Radio, Select, Slider, Switch, Tooltip } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import type { FC } from 'react'
import { useEffect, 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'
@ -69,22 +69,53 @@ const IMAGE_SIZES = [
icon: ImageSize9_16
}
]
const generateRandomSeed = () => Math.floor(Math.random() * 1000000).toString()
let _painting: Painting
const DEFAULT_PAINTING: Painting = {
id: uuid(),
urls: [],
files: [],
prompt: '',
negativePrompt: '',
imageSize: '1024x1024',
numImages: 1,
seed: '',
steps: 25,
guidanceScale: 4.5,
model: TEXT_TO_IMAGES_MODELS[0].id
}
const PaintingsPage: FC = () => {
// let _painting: Painting
const PaintingsPage: FC<{ Options: string[] }> = ({ Options }) => {
const { t } = useTranslation()
const { paintings, addPainting, removePainting, updatePainting } = usePaintings()
const [painting, setPainting] = useState<Painting>(_painting || paintings[0])
const [painting, setPainting] = useState<Painting>(DEFAULT_PAINTING)
const { theme } = useTheme()
const providers = useAllProviders()
const siliconProvider = providers.find((p) => p.id === 'silicon')!
const providerOptions = Options.map((option) => {
const provider = providers.find((p) => p.id === option)
return {
label: t(`provider.${provider?.id}`),
value: provider?.id
}
})
const [currentImageIndex, setCurrentImageIndex] = useState(0)
const [isLoading, setIsLoading] = useState(false)
const [abortController, setAbortController] = useState<AbortController | null>(null)
const dispatch = useAppDispatch()
const { generating } = useRuntime()
const navigate = useNavigate()
const location = useLocation()
const getNewPainting = () => {
return {
...DEFAULT_PAINTING,
id: uuid(),
seed: generateRandomSeed()
}
}
const modelOptions = TEXT_TO_IMAGES_MODELS.map((model) => ({
label: model.name,
@ -92,12 +123,12 @@ const PaintingsPage: FC = () => {
}))
const textareaRef = useRef<any>(null)
_painting = painting
// _painting = painting
const updatePaintingState = (updates: Partial<Painting>) => {
const updatedPainting = { ...painting, ...updates }
setPainting(updatedPainting)
updatePainting(updatedPainting)
updatePainting('paintings', updatedPainting)
}
const onSelectModel = (modelId: string) => {
@ -228,10 +259,10 @@ const PaintingsPage: FC = () => {
}
}
removePainting(paintingToDelete)
removePainting('paintings', paintingToDelete)
if (paintings.length === 1) {
setPainting(DEFAULT_PAINTING)
setPainting(getNewPainting())
}
}
@ -286,13 +317,23 @@ const PaintingsPage: FC = () => {
}
}
const handleProviderChange = (providerId: string) => {
const routeName = location.pathname.split('/').pop()
if (providerId !== routeName) {
navigate('../' + providerId, { replace: true })
}
}
useEffect(() => {
if (paintings.length === 0) {
addPainting('paintings', getNewPainting())
}
return () => {
if (spaceClickTimer.current) {
clearTimeout(spaceClickTimer.current)
}
}
}, [])
}, [paintings.length, addPainting])
return (
<Container>
@ -300,7 +341,11 @@ const PaintingsPage: FC = () => {
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
{isMac && (
<NavbarRight style={{ justifyContent: 'flex-end' }}>
<Button size="small" className="nodrag" icon={<PlusOutlined />} onClick={() => setPainting(addPainting())}>
<Button
size="small"
className="nodrag"
icon={<PlusOutlined />}
onClick={() => setPainting(addPainting('paintings', getNewPainting()))}>
{t('paintings.button.new.image')}
</Button>
</NavbarRight>
@ -309,11 +354,7 @@ const PaintingsPage: FC = () => {
<ContentContainer id="content-container">
<LeftContainer>
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
<Select
value={siliconProvider.id}
disabled={true}
options={[{ label: t(`provider.${siliconProvider.id}`), value: siliconProvider.id }]}
/>
<Select value={providerOptions[1].value} onChange={handleProviderChange} options={providerOptions} />
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('common.model')}</SettingTitle>
<Select value={painting.model} options={modelOptions} onChange={onSelectModel} />
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.image.size')}</SettingTitle>
@ -459,11 +500,12 @@ const PaintingsPage: FC = () => {
</InputContainer>
</MainContainer>
<PaintingsList
namespace="paintings"
paintings={paintings}
selectedPainting={painting}
onSelectPainting={onSelectPainting}
onDeletePainting={onDeletePainting}
onNewPainting={() => setPainting(addPainting())}
onNewPainting={() => setPainting(addPainting('paintings', getNewPainting()))}
/>
</ContentContainer>
</Container>

View File

@ -0,0 +1,19 @@
import { FC } from 'react'
import { Route, Routes } from 'react-router-dom'
import AihubmixPage from './AihubmixPage'
import SiliconPage from './PaintingsPage'
const Options = ['aihubmix', 'silicon']
const PaintingsRoutePage: FC = () => {
return (
<Routes>
<Route path="/" element={<AihubmixPage Options={Options} />} />
<Route path="/aihubmix" element={<AihubmixPage Options={Options} />} />
<Route path="/silicon" element={<SiliconPage Options={Options} />} />
</Routes>
)
}
export default PaintingsRoutePage

View File

@ -0,0 +1,298 @@
import type { PaintingAction, PaintingsState } from '@renderer/types'
import { ASPECT_RATIOS, STYLE_TYPES } from './constants'
// 配置项类型定义
export type ConfigItem = {
type:
| 'select'
| 'radio'
| 'slider'
| 'input'
| 'switch'
| 'inputNumber'
| 'textarea'
| 'title'
| 'description'
| 'image'
key?: keyof PaintingAction | 'commonModel'
title?: string
tooltip?: string
options?: Array<{ label: string; value: string | number; icon?: string }>
min?: number
max?: number
step?: number
suffix?: React.ReactNode
content?: string
disabled?: boolean
initialValue?: string | number
required?: boolean
}
export type AihubmixMode = keyof PaintingsState
// 创建配置项函数
export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
return {
paintings: [],
generate: [
{ type: 'title', title: 'paintings.model', tooltip: 'paintings.generate.model_tip' },
{
type: 'select',
key: 'model',
options: [
{ label: 'V_1', value: 'V_1' },
{ label: 'V_1_TURBO', value: 'V_1_TURBO' },
{ label: 'V_2', value: 'V_2' },
{ label: 'V_2_TURBO', value: 'V_2_TURBO' },
{ label: 'V_2A', value: 'V_2A' },
{ label: 'V_2A_TURBO', value: 'V_2A_TURBO' }
]
},
{ type: 'title', title: 'paintings.aspect_ratio' },
{
type: 'select',
key: 'aspectRatio',
options: ASPECT_RATIOS.map((size) => ({
label: size.label,
value: size.value,
icon: size.icon
}))
},
{
type: 'title',
title: 'paintings.number_images',
tooltip: 'paintings.generate.number_images_tip'
},
{
type: 'slider',
key: 'numImages',
min: 1,
max: 8
},
{
type: 'title',
title: 'paintings.style_type',
tooltip: 'paintings.generate.style_type_tip'
},
{
type: 'select',
key: 'styleType',
options: STYLE_TYPES
},
{
type: 'title',
title: 'paintings.seed',
tooltip: 'paintings.generate.seed_tip'
},
{
type: 'input',
key: 'seed'
},
{
type: 'title',
title: 'paintings.negative_prompt',
tooltip: 'paintings.generate.negative_prompt_tip'
},
{
type: 'textarea',
key: 'negativePrompt'
},
{
type: 'title',
title: 'paintings.magic_prompt_option',
tooltip: 'paintings.generate.magic_prompt_option_tip'
},
{
type: 'switch',
key: 'magicPromptOption'
}
],
edit: [
{ type: 'title', title: 'paintings.edit.image_file' },
{
type: 'image',
key: 'imageFile'
},
{ type: 'title', title: 'paintings.model', tooltip: 'paintings.edit.model_tip' },
{
type: 'select',
key: 'model',
options: [
{ label: 'V_2', value: 'V_2' },
{ label: 'V_2_TURBO', value: 'V_2_TURBO' }
]
},
{
type: 'title',
title: 'paintings.number_images',
tooltip: 'paintings.edit.number_images_tip'
},
{
type: 'slider',
key: 'numImages',
min: 1,
max: 8
},
{
type: 'title',
title: 'paintings.style_type',
tooltip: 'paintings.edit.style_type_tip'
},
{
type: 'select',
key: 'styleType',
options: STYLE_TYPES
},
{
type: 'title',
title: 'paintings.seed',
tooltip: 'paintings.edit.seed_tip'
},
{
type: 'input',
key: 'seed'
},
{
type: 'title',
title: 'paintings.magic_prompt_option',
tooltip: 'paintings.edit.magic_prompt_option_tip'
},
{
type: 'switch',
key: 'magicPromptOption'
}
],
remix: [
{ type: 'title', title: 'paintings.remix.image_file' },
{
type: 'image',
key: 'imageFile'
},
{ type: 'title', title: 'paintings.model', tooltip: 'paintings.remix.model_tip' },
{
type: 'select',
key: 'model',
options: [
{ label: 'V_1', value: 'V_1' },
{ label: 'V_1_TURBO', value: 'V_1_TURBO' },
{ label: 'V_2', value: 'V_2' },
{ label: 'V_2_TURBO', value: 'V_2_TURBO' },
{ label: 'V_2A', value: 'V_2A' },
{ label: 'V_2A_TURBO', value: 'V_2A_TURBO' }
]
},
{ type: 'title', title: 'paintings.aspect_ratio' },
{
type: 'select',
key: 'aspectRatio',
options: ASPECT_RATIOS.map((size) => ({
label: size.label,
value: size.value,
icon: size.icon
}))
},
{ type: 'title', title: 'paintings.remix.image_weight' },
{
type: 'slider',
key: 'imageWeight',
min: 1,
max: 100
},
{
type: 'title',
title: 'paintings.number_images',
tooltip: 'paintings.remix.number_images_tip'
},
{
type: 'slider',
key: 'numImages',
min: 1,
max: 8
},
{
type: 'title',
title: 'paintings.style_type',
tooltip: 'paintings.remix.style_type_tip'
},
{
type: 'select',
key: 'styleType',
options: STYLE_TYPES
},
{
type: 'title',
title: 'paintings.seed',
tooltip: 'paintings.remix.seed_tip'
},
{
type: 'input',
key: 'seed'
},
{
type: 'title',
title: 'paintings.negative_prompt',
tooltip: 'paintings.remix.negative_prompt_tip'
},
{
type: 'textarea',
key: 'negativePrompt'
},
{
type: 'title',
title: 'paintings.magic_prompt_option',
tooltip: 'paintings.remix.magic_prompt_option_tip'
},
{
type: 'switch',
key: 'magicPromptOption'
}
],
upscale: [
{ type: 'title', title: 'paintings.upscale.image_file' },
{
type: 'image',
key: 'imageFile',
required: true
},
{ type: 'title', title: 'paintings.upscale.resemblance', tooltip: 'paintings.upscale.resemblance_tip' },
{ type: 'slider', key: 'resemblance', min: 1, max: 100 },
{ type: 'title', title: 'paintings.upscale.detail', tooltip: 'paintings.upscale.detail_tip' },
{
type: 'slider',
key: 'detail',
min: 1,
max: 100
},
{
type: 'title',
title: 'paintings.number_images',
tooltip: 'paintings.upscale.number_images_tip'
},
{
type: 'slider',
key: 'numImages',
min: 1,
max: 8
},
{
type: 'title',
title: 'paintings.seed',
tooltip: 'paintings.upscale.seed_tip'
},
{
type: 'input',
key: 'seed'
},
{
type: 'title',
title: 'paintings.magic_prompt_option',
tooltip: 'paintings.upscale.magic_prompt_option_tip'
},
{
type: 'switch',
key: 'magicPromptOption'
}
]
}
}

View File

@ -0,0 +1,112 @@
import ImageSize1_1 from '@renderer/assets/images/paintings/image-size-1-1.svg'
import ImageSize1_2 from '@renderer/assets/images/paintings/image-size-1-2.svg'
import ImageSize3_2 from '@renderer/assets/images/paintings/image-size-3-2.svg'
import ImageSize3_4 from '@renderer/assets/images/paintings/image-size-3-4.svg'
import ImageSize9_16 from '@renderer/assets/images/paintings/image-size-9-16.svg'
import ImageSize16_9 from '@renderer/assets/images/paintings/image-size-16-9.svg'
import type { PaintingAction } from '@renderer/types'
// 几种默认的绘画配置
export const DEFAULT_PAINTING: PaintingAction = {
id: 'aihubmix_1',
model: 'V_2',
aspectRatio: 'ASPECT_1_1',
numImages: 1,
styleType: 'AUTO',
prompt: '',
negativePrompt: '',
magicPromptOption: true,
seed: '',
imageWeight: 50,
resemblance: 50,
detail: 50,
imageFile: undefined,
mask: undefined,
files: [],
urls: []
}
export const ASPECT_RATIOS = [
{
label: '1:1',
value: 'ASPECT_1_1',
icon: ImageSize1_1
},
{
label: '3:1',
value: 'ASPECT_3_1',
icon: ImageSize3_2
},
{
label: '1:3',
value: 'ASPECT_1_3',
icon: ImageSize1_2
},
{
label: '3:2',
value: 'ASPECT_3_2',
icon: ImageSize3_2
},
{
label: '2:3',
value: 'ASPECT_2_3',
icon: ImageSize1_2
},
{
label: '4:3',
value: 'ASPECT_4_3',
icon: ImageSize3_4
},
{
label: '3:4',
value: 'ASPECT_3_4',
icon: ImageSize3_4
},
{
label: '16:9',
value: 'ASPECT_16_9',
icon: ImageSize16_9
},
{
label: '9:16',
value: 'ASPECT_9_16',
icon: ImageSize9_16
},
{
label: '16:10',
value: 'SPECT_16_10',
icon: ImageSize16_9
},
{
label: '10:16',
value: 'ASPECT_10_16',
icon: ImageSize9_16
}
]
export const STYLE_TYPES = [
{
label: '自动',
value: 'AUTO'
},
{
label: '通用',
value: 'GENERAL'
},
{
label: '写实',
value: 'REALISTIC'
},
{
label: '设计',
value: 'DESIGN'
},
{
label: '3D',
value: 'RENDER_3D'
},
{
label: '动漫',
value: 'ANIME'
}
]

View File

@ -1,49 +1,51 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { TEXT_TO_IMAGES_MODELS } from '@renderer/config/models'
import { Painting } from '@renderer/types'
import { uuid } from '@renderer/utils'
export interface PaintingsState {
paintings: Painting[]
}
export const DEFAULT_PAINTING: Painting = {
id: uuid(),
urls: [],
files: [],
prompt: '',
negativePrompt: '',
imageSize: '1024x1024',
numImages: 1,
seed: '',
steps: 25,
guidanceScale: 4.5,
model: TEXT_TO_IMAGES_MODELS[0].id
}
import { PaintingAction, PaintingsState } from '@renderer/types'
const initialState: PaintingsState = {
paintings: [DEFAULT_PAINTING]
paintings: [],
generate: [],
remix: [],
edit: [],
upscale: []
}
const paintingsSlice = createSlice({
name: 'paintings',
initialState,
reducers: {
updatePaintings: (state, action: PayloadAction<Painting[]>) => {
state.paintings = action.payload
},
addPainting: (state, action: PayloadAction<Painting>) => {
state.paintings.unshift(action.payload)
},
removePainting: (state, action: PayloadAction<Painting>) => {
if (state.paintings.length === 1) {
state.paintings = [DEFAULT_PAINTING]
addPainting: (
state: PaintingsState,
action: PayloadAction<{ namespace?: keyof PaintingsState; painting: PaintingAction }>
) => {
const { namespace = 'paintings', painting } = action.payload
if (state[namespace]) {
state[namespace].unshift(painting)
} else {
state.paintings = state.paintings.filter((c) => c.id !== action.payload.id)
state[namespace] = [painting]
}
},
updatePainting: (state, action: PayloadAction<Painting>) => {
state.paintings = state.paintings.map((c) => (c.id === action.payload.id ? action.payload : c))
removePainting: (
state: PaintingsState,
action: PayloadAction<{ namespace?: keyof PaintingsState; painting: PaintingAction }>
) => {
const { namespace = 'paintings', painting } = action.payload
// @ts-ignore - TypeScript 无法正确推断数组元素类型与过滤条件的兼容性
state[namespace] = state[namespace].filter((c) => c.id !== painting.id)
},
updatePainting: (
state: PaintingsState,
action: PayloadAction<{ namespace?: keyof PaintingsState; painting: PaintingAction }>
) => {
const { namespace = 'paintings', painting } = action.payload
state[namespace] = state[namespace].map((c) => (c.id === painting.id ? painting : c))
},
updatePaintings: (
state: PaintingsState,
action: PayloadAction<{ namespace?: keyof PaintingsState; paintings: PaintingAction[] }>
) => {
const { namespace = 'paintings', paintings } = action.payload
// @ts-ignore - TypeScript 无法正确推断数组元素类型与过滤条件的兼容性
state[namespace] = paintings
}
}
})

View File

@ -163,11 +163,14 @@ export type Suggestion = {
content: string
}
export interface Painting {
export type PaintingParams = {
id: string
model?: string
urls: string[]
files: FileType[]
}
export interface Painting extends PaintingParams {
model?: string
prompt?: string
negativePrompt?: string
imageSize?: string
@ -178,6 +181,61 @@ export interface Painting {
promptEnhancement?: boolean
}
export interface GeneratePainting extends PaintingParams {
model: string
prompt: string
aspectRatio?: string
numImages?: number
styleType?: string
seed?: string
negativePrompt?: string
magicPromptOption?: boolean
}
export interface EditPainting extends PaintingParams {
imageFile: string
mask: FileType
model: string
prompt: string
numImages?: number
styleType?: string
seed?: string
magicPromptOption?: boolean
}
export interface RemixPainting extends PaintingParams {
imageFile: string
model: string
prompt: string
aspectRatio?: string
imageWeight: number
numImages?: number
styleType?: string
seed?: string
negativePrompt?: string
magicPromptOption?: boolean
}
export interface ScalePainting extends PaintingParams {
imageFile: string
prompt: string
resemblance?: number
detail?: number
numImages?: number
seed?: string
magicPromptOption?: boolean
}
export type PaintingAction = Partial<GeneratePainting & RemixPainting & EditPainting & ScalePainting> & PaintingParams
export interface PaintingsState {
paintings: Painting[]
generate: Partial<GeneratePainting> & PaintingParams[]
remix: Partial<RemixPainting> & PaintingParams[]
edit: Partial<EditPainting> & PaintingParams[]
upscale: Partial<ScalePainting> & PaintingParams[]
}
export type MinAppType = {
id: string
name: string