mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 03:31:24 +08:00
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:
parent
a6822d4037
commit
efad8f9ad0
@ -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 />} />
|
||||
|
||||
8
src/renderer/src/assets/images/paintings/ic_ImageUp.svg
Normal file
8
src/renderer/src/assets/images/paintings/ic_ImageUp.svg
Normal 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 |
BIN
src/renderer/src/assets/images/providers/aihubmix.png
Normal file
BIN
src/renderer/src/assets/images/providers/aihubmix.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@ -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 }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
767
src/renderer/src/pages/paintings/AihubmixPage.tsx
Normal file
767
src/renderer/src/pages/paintings/AihubmixPage.tsx
Normal 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
|
||||
@ -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) => (
|
||||
|
||||
@ -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>
|
||||
|
||||
19
src/renderer/src/pages/paintings/PaintingsRoutePage.tsx
Normal file
19
src/renderer/src/pages/paintings/PaintingsRoutePage.tsx
Normal 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
|
||||
298
src/renderer/src/pages/paintings/config/aihubmixConfig.tsx
Normal file
298
src/renderer/src/pages/paintings/config/aihubmixConfig.tsx
Normal 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'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
112
src/renderer/src/pages/paintings/config/constants.ts
Normal file
112
src/renderer/src/pages/paintings/config/constants.ts
Normal 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'
|
||||
}
|
||||
]
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user