mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +08:00
parent
d029bb76c5
commit
387bc064cd
@ -1703,24 +1703,6 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
name: 'ERNIE-Speed-128K',
|
||||
group: '免费模型'
|
||||
},
|
||||
{
|
||||
id: 'THUDM/glm-4-9b-chat',
|
||||
provider: 'dmxapi',
|
||||
name: 'THUDM/glm-4-9b-chat',
|
||||
group: '免费模型'
|
||||
},
|
||||
{
|
||||
id: 'glm-4-flash',
|
||||
provider: 'dmxapi',
|
||||
name: 'glm-4-flash',
|
||||
group: '免费模型'
|
||||
},
|
||||
{
|
||||
id: 'hunyuan-lite',
|
||||
provider: 'dmxapi',
|
||||
name: 'hunyuan-lite',
|
||||
group: '免费模型'
|
||||
},
|
||||
{
|
||||
id: 'gpt-4o',
|
||||
provider: 'dmxapi',
|
||||
|
||||
@ -940,7 +940,10 @@
|
||||
"magic_prompt_option_tip": "Intelligently enhances upscaling prompts"
|
||||
},
|
||||
"text_desc_required": "Please enter image description first",
|
||||
"image_handle_required": "Please upload an image first.",
|
||||
"req_error_text": "Operation failed. Please try again. Avoid using 'copyrighted' or 'sensitive' words in your prompt.",
|
||||
"req_error_token": "Please check the validity of the token",
|
||||
"req_error_no_balance": "Please check the validity of the token",
|
||||
"auto_create_paint": "Auto-create image",
|
||||
"auto_create_paint_tip": "After the image is generated, a new image will be created automatically.",
|
||||
"select_model": "Select Model",
|
||||
|
||||
@ -940,7 +940,10 @@
|
||||
"rendering_speed": "レンダリング速度",
|
||||
"translating": "翻訳中...",
|
||||
"text_desc_required": "画像の説明を先に入力してください",
|
||||
"image_handle_required": "最初に画像をアップロードしてください。",
|
||||
"req_error_text": "実行に失敗しました。もう一度お試しください。プロンプトに「著作権用語」や「センシティブな用語」を含めないでください。",
|
||||
"req_error_token": "トークンの有効性を確認してください",
|
||||
"req_error_no_balance": "トークンの有効性を確認してください",
|
||||
"auto_create_paint": "画像を自動作成",
|
||||
"auto_create_paint_tip": "画像が生成された後、自動的に新しい画像が作成されます。",
|
||||
"select_model": "モデルを選択",
|
||||
|
||||
@ -940,7 +940,10 @@
|
||||
"magic_prompt_option_tip": "Улучшает увеличение изображений с помощью интеллектуального оптимизирования промптов"
|
||||
},
|
||||
"text_desc_required": "Пожалуйста, сначала введите описание изображения",
|
||||
"image_handle_required": "Пожалуйста, сначала загрузите изображение.",
|
||||
"req_error_text": "Операция не удалась, повторите попытку. Пожалуйста, избегайте защищенных авторским правом терминов и конфиденциальных слов в запросах.",
|
||||
"req_error_token": "Пожалуйста, проверьте действительность токена",
|
||||
"req_error_no_balance": "Пожалуйста, проверьте действительность токена",
|
||||
"auto_create_paint": "Автоматическое создание изображения",
|
||||
"auto_create_paint_tip": "После генерации изображения будет автоматически создано новое.",
|
||||
"select_model": "Выбрать модель",
|
||||
|
||||
@ -941,6 +941,9 @@
|
||||
},
|
||||
"text_desc_required": "请先输入图片描述",
|
||||
"req_error_text": "运行失败,请重试。提示词避免“版权词”和”敏感词”哦。",
|
||||
"req_error_token": "请检查令牌有效性",
|
||||
"req_error_no_balance": "请检查令牌有效性",
|
||||
"image_handle_required": "请先上传图片",
|
||||
"auto_create_paint": "自动新建图片",
|
||||
"auto_create_paint_tip": "在图片生成后,会自动新建图片",
|
||||
"select_model": "选择模型",
|
||||
|
||||
@ -940,7 +940,10 @@
|
||||
},
|
||||
"rendering_speed": "渲染速度",
|
||||
"text_desc_required": "請先輸入圖片描述",
|
||||
"image_handle_required": "請先上傳圖片。",
|
||||
"req_error_text": "运行失败,请重试。提示词避免“版权词”和”敏感词”哦。",
|
||||
"req_error_token": "請檢查令牌的有效性",
|
||||
"req_error_no_balance": "請檢查令牌的有效性",
|
||||
"auto_create_paint": "自動新增圖片",
|
||||
"auto_create_paint_tip": "圖片生成後,會自動新增圖片",
|
||||
"select_model": "選擇模型",
|
||||
|
||||
@ -15,8 +15,8 @@ import { useAppDispatch } from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import type { FileType, PaintingsState } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { DmxapiPainting, PaintingAction } from '@types'
|
||||
import { Avatar, Button, Input, Radio, Select, Switch, Tooltip } from 'antd'
|
||||
import { DmxapiPainting } from '@types'
|
||||
import { Avatar, Button, Input, Radio, Segmented, Select, Switch, Tooltip } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { Info } from 'lucide-react'
|
||||
import React, { FC } from 'react'
|
||||
@ -25,14 +25,19 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { generationModeType } from '../../types'
|
||||
import SendMessageButton from '../home/Inputbar/SendMessageButton'
|
||||
import { SettingHelpLink, SettingTitle } from '../settings'
|
||||
import Artboard from './components/Artboard'
|
||||
import ImageUploader from './components/ImageUploader'
|
||||
import PaintingsList from './components/PaintingsList'
|
||||
import {
|
||||
COURSE_URL,
|
||||
DEFAULT_PAINTING,
|
||||
IMAGE_EDIT_MODELS,
|
||||
IMAGE_MERGE_MODELS,
|
||||
IMAGE_SIZES,
|
||||
MODEOPTIONS,
|
||||
STYLE_TYPE_OPTIONS,
|
||||
TEXT_TO_IMAGES_MODELS
|
||||
} from './config/DmxapiConfig'
|
||||
@ -64,11 +69,71 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
const getNewPainting = () => {
|
||||
interface FileMapType {
|
||||
imageFiles?: FileType[]
|
||||
paths?: string[]
|
||||
}
|
||||
|
||||
const [fileMap, setFileMap] = useState<FileMapType>({
|
||||
imageFiles: [],
|
||||
paths: []
|
||||
})
|
||||
|
||||
const modeOptions = MODEOPTIONS.map((ele) => {
|
||||
return {
|
||||
label: t(ele.label),
|
||||
value: ele.value
|
||||
}
|
||||
})
|
||||
|
||||
const getModelOptions = (mode: generationModeType) => {
|
||||
if (mode === generationModeType.EDIT) {
|
||||
return IMAGE_EDIT_MODELS.map((model) => ({
|
||||
label: model.name,
|
||||
value: model.id
|
||||
}))
|
||||
}
|
||||
|
||||
if (mode === generationModeType.MERGE) {
|
||||
return IMAGE_MERGE_MODELS.map((model) => ({
|
||||
label: model.name,
|
||||
value: model.id
|
||||
}))
|
||||
}
|
||||
|
||||
// 默认情况或其它模式下的选项
|
||||
return TEXT_TO_IMAGES_MODELS.map((model) => ({
|
||||
label: model.name,
|
||||
value: model.id
|
||||
}))
|
||||
}
|
||||
|
||||
const [modelOptions, setModelOptions] = useState(() => {
|
||||
// 根据当前painting的generationMode初始化modelOptions
|
||||
const currentMode = painting?.generationMode || (MODEOPTIONS[0].value as generationModeType)
|
||||
return getModelOptions(currentMode)
|
||||
})
|
||||
|
||||
const textareaRef = useRef<any>(null)
|
||||
|
||||
// 更新painting状态的辅助函数
|
||||
const updatePaintingState = (updates: Partial<DmxapiPainting>) => {
|
||||
const updatedPainting = { ...painting, ...updates }
|
||||
setPainting(updatedPainting)
|
||||
updatePainting('DMXAPIPaintings', updatedPainting)
|
||||
}
|
||||
|
||||
const getNewPainting = (params?: Partial<DmxapiPainting>) => {
|
||||
clearImages()
|
||||
const generationMode = params?.generationMode || painting?.generationMode || MODEOPTIONS[0].value
|
||||
const modelOptionsList = getModelOptions(generationMode as generationModeType)
|
||||
return {
|
||||
...DEFAULT_PAINTING,
|
||||
id: uuid(),
|
||||
seed: generateRandomSeed()
|
||||
seed: generateRandomSeed(),
|
||||
generationMode,
|
||||
model: modelOptionsList[0]?.value,
|
||||
...params
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,19 +147,6 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
setPainting(addPainting('DMXAPIPaintings', copyPainting))
|
||||
}
|
||||
|
||||
const modelOptions = TEXT_TO_IMAGES_MODELS.map((model) => ({
|
||||
label: model.name,
|
||||
value: model.id
|
||||
}))
|
||||
|
||||
const textareaRef = useRef<any>(null)
|
||||
|
||||
const updatePaintingState = (updates: Partial<DmxapiPainting>) => {
|
||||
const updatedPainting = { ...painting, ...updates }
|
||||
setPainting(updatedPainting)
|
||||
updatePainting('DMXAPIPaintings', updatedPainting)
|
||||
}
|
||||
|
||||
const onSelectModel = (modelId: string) => {
|
||||
const model = TEXT_TO_IMAGES_MODELS.find((m) => m.id === modelId)
|
||||
if (model) {
|
||||
@ -135,6 +187,62 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const onbeforeunload = (file, index?: number) => {
|
||||
const path = URL.createObjectURL(file)
|
||||
|
||||
// 更新 fileMap
|
||||
setFileMap((prevFileMap) => {
|
||||
const currentFiles = prevFileMap.imageFiles || []
|
||||
const currentPaths = prevFileMap.paths || []
|
||||
|
||||
let newFiles: FileType[]
|
||||
let newPaths: string[]
|
||||
|
||||
if (index !== undefined) {
|
||||
// 替换指定索引的图片
|
||||
newFiles = [...currentFiles]
|
||||
newFiles[index] = file as FileType
|
||||
|
||||
newPaths = [...currentPaths]
|
||||
newPaths[index] = path
|
||||
} else {
|
||||
// 添加新图片到最后
|
||||
newFiles = [...currentFiles, file as FileType]
|
||||
newPaths = [...currentPaths, path]
|
||||
}
|
||||
|
||||
return {
|
||||
imageFiles: newFiles,
|
||||
paths: newPaths
|
||||
}
|
||||
})
|
||||
|
||||
return false // 阻止默认上传行为
|
||||
}
|
||||
|
||||
const onGenerationModeChange = (v: generationModeType) => {
|
||||
clearImages()
|
||||
const newModelOptions = getModelOptions(v)
|
||||
setModelOptions(newModelOptions)
|
||||
const firstModel = newModelOptions[0]?.value
|
||||
|
||||
// 如果有urls,创建新的painting
|
||||
if (Array.isArray(painting.urls) && painting.urls.length > 0) {
|
||||
const newPainting = getNewPainting({
|
||||
generationMode: v,
|
||||
model: firstModel // 使用新模式下的第一个模型
|
||||
})
|
||||
const addedPainting = addPainting('DMXAPIPaintings', newPainting)
|
||||
setPainting(addedPainting)
|
||||
} else {
|
||||
// 否则更新当前painting
|
||||
updatePaintingState({
|
||||
generationMode: v,
|
||||
model: firstModel // 使用新模式下的第一个模型
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 检查提供者状态函数
|
||||
const checkProviderStatus = () => {
|
||||
if (!dmxapiProvider.enabled) {
|
||||
@ -152,6 +260,14 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
if (!painting.prompt) {
|
||||
throw new Error('paintings.text_desc_required')
|
||||
}
|
||||
|
||||
if (
|
||||
painting.generationMode &&
|
||||
[generationModeType.EDIT, generationModeType.MERGE].includes(painting.generationMode) &&
|
||||
(!fileMap.imageFiles || fileMap.imageFiles.length === 0)
|
||||
) {
|
||||
throw new Error('paintings.image_handle_required')
|
||||
}
|
||||
}
|
||||
|
||||
// 准备V1生成请求函数
|
||||
@ -162,6 +278,10 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
n: painting.n
|
||||
}
|
||||
|
||||
const headerExpand = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (painting.aspect_ratio) {
|
||||
params['aspect_ratio'] = painting.aspect_ratio
|
||||
}
|
||||
@ -184,21 +304,57 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
|
||||
return {
|
||||
body: JSON.stringify(params),
|
||||
headerExpand: headerExpand,
|
||||
endpoint: `${dmxapiProvider.apiHost}/v1/images/generations`
|
||||
}
|
||||
}
|
||||
|
||||
// API请求函数
|
||||
const callApi = async (requestConfig: { endpoint: string; body: any }, controller: AbortController) => {
|
||||
const { endpoint, body } = requestConfig
|
||||
const headers = {}
|
||||
// 准备V2生成请求函数
|
||||
const prepareV2GenerateRequest = (prompt: string, painting: DmxapiPainting) => {
|
||||
const params = {
|
||||
prompt,
|
||||
n: painting.n,
|
||||
model: painting.model
|
||||
}
|
||||
|
||||
// 如果是JSON数据,添加Content-Type头
|
||||
if (typeof body === 'string') {
|
||||
headers['Content-Type'] = 'application/json'
|
||||
headers['Authorization'] = `Bearer ${dmxapiProvider.apiKey}`
|
||||
headers['User-Agent'] = 'DMXAPI/1.0.0 (https://www.dmxapi.com)'
|
||||
headers['Accept'] = 'application/json'
|
||||
if (painting.image_size) {
|
||||
params['size'] = '1024x1024'
|
||||
}
|
||||
|
||||
if (painting.style_type) {
|
||||
params.prompt = prompt + ',风格:' + painting.style_type
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
for (const key in params) {
|
||||
formData.append(key, params[key])
|
||||
}
|
||||
|
||||
if (Array.isArray(fileMap.imageFiles)) {
|
||||
fileMap.imageFiles.forEach((file) => {
|
||||
formData.append(`image`, file as unknown as Blob)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
body: formData,
|
||||
endpoint: `${dmxapiProvider.apiHost}/v1/images/edits`
|
||||
}
|
||||
}
|
||||
|
||||
// API请求函数
|
||||
const callApi = async (
|
||||
requestConfig: { endpoint: string; body: any; headerExpand?: any },
|
||||
controller: AbortController
|
||||
) => {
|
||||
const { endpoint, body, headerExpand } = requestConfig
|
||||
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${dmxapiProvider.apiKey}`,
|
||||
'User-Agent': 'DMXAPI/1.0.0 (https://www.dmxapi.com)',
|
||||
...headerExpand
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
@ -209,10 +365,23 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
throw new Error('paintings.req_error_token')
|
||||
} else if (response.status === 403) {
|
||||
throw new Error('paintings.req_error_no_balance')
|
||||
}
|
||||
|
||||
throw new Error('操作失败,请稍后重试')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (
|
||||
painting.generationMode &&
|
||||
[generationModeType.EDIT, generationModeType.MERGE].includes(painting.generationMode)
|
||||
) {
|
||||
return data.data.map((item: { b64_json: string }) => 'data:image/png;base64,' + item.b64_json)
|
||||
}
|
||||
return data.data.map((item: { url: string }) => item.url)
|
||||
}
|
||||
|
||||
@ -246,9 +415,16 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
|
||||
// 准备请求配置函数
|
||||
const prepareRequestConfig = (prompt: string, painting: PaintingAction) => {
|
||||
const prepareRequestConfig = (prompt: string, painting: DmxapiPainting) => {
|
||||
// 根据模式和模型版本返回不同的请求配置
|
||||
return prepareV1GenerateRequest(prompt, painting)
|
||||
if (
|
||||
painting.generationMode !== undefined &&
|
||||
[generationModeType.MERGE, generationModeType.EDIT].includes(painting.generationMode)
|
||||
) {
|
||||
return prepareV2GenerateRequest(prompt, painting)
|
||||
} else {
|
||||
return prepareV1GenerateRequest(prompt, painting)
|
||||
}
|
||||
}
|
||||
|
||||
const onGenerate = async () => {
|
||||
@ -338,7 +514,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
setCurrentImageIndex((prev) => (prev - 1 + painting.files.length) % painting.files.length)
|
||||
}
|
||||
|
||||
const onDeletePainting = (paintingToDelete: DmxapiPainting) => {
|
||||
const onDeletePainting = async (paintingToDelete: DmxapiPainting) => {
|
||||
if (paintingToDelete.id === painting.id) {
|
||||
const currentIndex = DMXAPIPaintings.findIndex((p) => p.id === paintingToDelete.id)
|
||||
|
||||
@ -349,17 +525,25 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
}
|
||||
|
||||
removePainting(mode, paintingToDelete).then(() => {})
|
||||
// 删除绘画
|
||||
await removePainting(mode, paintingToDelete)
|
||||
|
||||
// 检查是否删除空了
|
||||
if (!DMXAPIPaintings || DMXAPIPaintings.length === 1) {
|
||||
// 如果删除后没有绘画了,创建一个新的
|
||||
const newPainting = getNewPainting()
|
||||
const addedPainting = addPainting('DMXAPIPaintings', newPainting)
|
||||
setPainting(addedPainting)
|
||||
}
|
||||
}
|
||||
|
||||
const onSelectPainting = (newPainting: DmxapiPainting) => {
|
||||
if (generating) return
|
||||
clearImages()
|
||||
setPainting(newPainting)
|
||||
setCurrentImageIndex(0)
|
||||
}
|
||||
|
||||
const spaceClickTimer = useRef<NodeJS.Timeout>(null)
|
||||
|
||||
const handleProviderChange = (providerId: string) => {
|
||||
const routeName = location.pathname.split('/').pop()
|
||||
if (providerId !== routeName) {
|
||||
@ -367,20 +551,97 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 清除图片函数
|
||||
const clearImages = () => {
|
||||
setFileMap(() => ({ paths: [], imageFiles: [] }))
|
||||
}
|
||||
|
||||
const handleDeleteImage = (index: number) => {
|
||||
setFileMap((prevFileMap) => {
|
||||
const newPaths = [...(prevFileMap.paths || [])]
|
||||
const newImageFiles = [...(prevFileMap.imageFiles || [])]
|
||||
|
||||
// 删除指定索引的图片
|
||||
newPaths.splice(index, 1)
|
||||
newImageFiles.splice(index, 1)
|
||||
|
||||
return {
|
||||
paths: newPaths,
|
||||
imageFiles: newImageFiles
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 定义大图的默认图片
|
||||
const defaultCoverImage = () => {
|
||||
if (painting.generationMode === generationModeType.EDIT) {
|
||||
if (painting?.urls.length === 0 && fileMap.paths && fileMap.paths?.length > 0 && fileMap.paths[0]) {
|
||||
return (
|
||||
<EmptyImgBox>
|
||||
<EmptyImg bgUrl={fileMap.paths[0]}></EmptyImg>
|
||||
</EmptyImgBox>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (painting?.urls?.length > 0 || DMXAPIPaintings?.length > 1) {
|
||||
return null
|
||||
} else {
|
||||
return (
|
||||
<EmptyImgBox>
|
||||
<EmptyImg></EmptyImg>
|
||||
</EmptyImgBox>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const defaultLoadText = () => {
|
||||
if (
|
||||
painting.generationMode &&
|
||||
[generationModeType.EDIT, generationModeType.MERGE].includes(painting.generationMode)
|
||||
) {
|
||||
return (
|
||||
<LoadTextWrap>
|
||||
<div>
|
||||
正在用 OpenAI 官方 gpt-image-1 模型生产,
|
||||
<br />
|
||||
预计等待2~5分钟效果最好,
|
||||
<br />
|
||||
本次消耗金额请到DMIAPI后台日志查看
|
||||
</div>
|
||||
</LoadTextWrap>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!DMXAPIPaintings || DMXAPIPaintings.length === 0) {
|
||||
const newPainting = getNewPainting()
|
||||
addPainting('DMXAPIPaintings', newPainting)
|
||||
setPainting(newPainting)
|
||||
} else if (painting && !painting.generationMode) {
|
||||
// 如果当前painting没有generationMode,添加默认值
|
||||
const updatedPainting = { ...painting, generationMode: MODEOPTIONS[0].value }
|
||||
setPainting(updatedPainting)
|
||||
updatePainting('DMXAPIPaintings', updatedPainting)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (spaceClickTimer.current) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
clearTimeout(spaceClickTimer.current)
|
||||
// 确保所有paintings都有generationMode属性
|
||||
DMXAPIPaintings.forEach((p) => {
|
||||
if (!p.generationMode) {
|
||||
const updatedPainting = { ...p, generationMode: MODEOPTIONS[0].value }
|
||||
updatePainting('DMXAPIPaintings', updatedPainting)
|
||||
}
|
||||
})
|
||||
|
||||
// 确保modelOptions与当前painting的generationMode保持一致
|
||||
if (painting?.generationMode) {
|
||||
setModelOptions(getModelOptions(painting.generationMode as generationModeType))
|
||||
}
|
||||
}, [DMXAPIPaintings, DMXAPIPaintings.length, addPainting, mode])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []) // 空依赖数组,只在组件挂载时执行一次
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@ -422,40 +683,60 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
{painting.generationMode &&
|
||||
[generationModeType.EDIT, generationModeType.MERGE].includes(painting.generationMode) && (
|
||||
<>
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>参考图</SettingTitle>
|
||||
<ImageUploader
|
||||
fileMap={fileMap}
|
||||
maxImages={painting.generationMode === generationModeType.EDIT ? 1 : 3}
|
||||
onClearImages={clearImages}
|
||||
onDeleteImage={handleDeleteImage}
|
||||
onAddImage={onbeforeunload}
|
||||
mode={painting.generationMode}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<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>
|
||||
<Radio.Group
|
||||
value={painting.image_size}
|
||||
onChange={(e) => onSelectImageSize(e.target.value)}
|
||||
style={{ display: 'flex' }}>
|
||||
{IMAGE_SIZES.map((size) => (
|
||||
<RadioButton value={size.value} key={size.value}>
|
||||
<VStack alignItems="center">
|
||||
<ImageSizeImage src={size.icon} theme={theme} />
|
||||
<span>{size.label}</span>
|
||||
</VStack>
|
||||
</RadioButton>
|
||||
))}
|
||||
</Radio.Group>
|
||||
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
|
||||
{t('paintings.seed')}
|
||||
<Tooltip title={t('paintings.seed_desc_tip')}>
|
||||
<InfoIcon />
|
||||
</Tooltip>
|
||||
</SettingTitle>
|
||||
<Input
|
||||
value={painting.seed}
|
||||
pattern="[0-9]*"
|
||||
onChange={(e) => onInputSeed(e)}
|
||||
suffix={
|
||||
<RedoOutlined
|
||||
onClick={() => updatePaintingState({ seed: Math.floor(Math.random() * 1000000).toString() })}
|
||||
style={{ cursor: 'pointer', color: 'var(--color-text-2)' }}
|
||||
{painting.generationMode === generationModeType.GENERATION && (
|
||||
<>
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.image.size')}</SettingTitle>
|
||||
<Radio.Group
|
||||
value={painting.image_size}
|
||||
onChange={(e) => onSelectImageSize(e.target.value)}
|
||||
style={{ display: 'flex' }}>
|
||||
{IMAGE_SIZES.map((size) => (
|
||||
<RadioButton value={size.value} key={size.value}>
|
||||
<VStack alignItems="center">
|
||||
<ImageSizeImage src={size.icon} theme={theme} />
|
||||
<span>{size.label}</span>
|
||||
</VStack>
|
||||
</RadioButton>
|
||||
))}
|
||||
</Radio.Group>
|
||||
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
|
||||
{t('paintings.seed')}
|
||||
<Tooltip title={t('paintings.seed_desc_tip')}>
|
||||
<InfoIcon />
|
||||
</Tooltip>
|
||||
</SettingTitle>
|
||||
<Input
|
||||
value={painting.seed}
|
||||
pattern="[0-9]*"
|
||||
onChange={(e) => onInputSeed(e)}
|
||||
suffix={
|
||||
<RedoOutlined
|
||||
onClick={() => updatePaintingState({ seed: Math.floor(Math.random() * 1000000).toString() })}
|
||||
style={{ cursor: 'pointer', color: 'var(--color-text-2)' }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.style_type')}</SettingTitle>
|
||||
<SliderContainer>
|
||||
@ -482,6 +763,14 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
</HStack>
|
||||
</LeftContainer>
|
||||
<MainContainer>
|
||||
<ModeSegmentedContainer>
|
||||
<Segmented
|
||||
shape="round"
|
||||
value={painting.generationMode}
|
||||
onChange={onGenerationModeChange}
|
||||
options={modeOptions}
|
||||
/>
|
||||
</ModeSegmentedContainer>
|
||||
<Artboard
|
||||
painting={painting}
|
||||
isLoading={isLoading}
|
||||
@ -489,13 +778,8 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
onPrevImage={prevImage}
|
||||
onNextImage={nextImage}
|
||||
onCancel={onCancel}
|
||||
imageCover={
|
||||
painting?.urls?.length > 0 || DMXAPIPaintings?.length > 1 ? null : (
|
||||
<EmptyImgBox>
|
||||
<EmptyImg></EmptyImg>
|
||||
</EmptyImgBox>
|
||||
)
|
||||
}
|
||||
imageCover={defaultCoverImage()}
|
||||
loadText={defaultLoadText()}
|
||||
/>
|
||||
<InputContainer>
|
||||
<Textarea
|
||||
@ -684,6 +968,13 @@ const RadioTextItem = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
// 添加新的样式组件
|
||||
const ModeSegmentedContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 24px;
|
||||
`
|
||||
|
||||
const EmptyImgBox = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@ -692,11 +983,25 @@ const EmptyImgBox = styled.div`
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const EmptyImg = styled.div`
|
||||
const EmptyImg = styled.div<{ bgUrl?: string }>`
|
||||
width: 70vh;
|
||||
height: 70vh;
|
||||
background-size: 100% 100%;
|
||||
background-image: url(${DMXAPIToImg});
|
||||
background-size: cover;
|
||||
background-image: ${(props) => (props.bgUrl ? `url(${props.bgUrl})` : `url(${DMXAPIToImg})`)};
|
||||
`
|
||||
|
||||
const LoadTextWrap = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: black;
|
||||
text-shadow:
|
||||
-1px -1px 0 #ffffff,
|
||||
1px -1px 0 #ffffff,
|
||||
-1px 1px 0 #ffffff,
|
||||
1px 1px 0 #ffffff;
|
||||
`
|
||||
|
||||
export default DmxapiPage
|
||||
|
||||
@ -15,6 +15,7 @@ interface ArtboardProps {
|
||||
onCancel: () => void
|
||||
retry?: (painting: Painting) => void
|
||||
imageCover?: React.ReactNode
|
||||
loadText?: React.ReactNode
|
||||
}
|
||||
|
||||
const Artboard: FC<ArtboardProps> = ({
|
||||
@ -25,7 +26,8 @@ const Artboard: FC<ArtboardProps> = ({
|
||||
onNextImage,
|
||||
onCancel,
|
||||
retry,
|
||||
imageCover
|
||||
imageCover,
|
||||
loadText
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@ -82,6 +84,8 @@ const Artboard: FC<ArtboardProps> = ({
|
||||
</div>
|
||||
) : imageCover ? (
|
||||
imageCover
|
||||
) : loadText && isLoading ? (
|
||||
''
|
||||
) : (
|
||||
<div>{t('paintings.image_placeholder')}</div>
|
||||
)}
|
||||
@ -90,6 +94,7 @@ const Artboard: FC<ArtboardProps> = ({
|
||||
{isLoading && (
|
||||
<LoadingOverlay>
|
||||
<Spin size="large" />
|
||||
{loadText ? loadText : ''}
|
||||
<CancelButton onClick={onCancel}>{t('common.cancel')}</CancelButton>
|
||||
</LoadingOverlay>
|
||||
)}
|
||||
|
||||
201
src/renderer/src/pages/paintings/components/ImageUploader.tsx
Normal file
201
src/renderer/src/pages/paintings/components/ImageUploader.tsx
Normal file
@ -0,0 +1,201 @@
|
||||
import { DeleteOutlined } from '@ant-design/icons'
|
||||
import IcImageUp from '@renderer/assets/images/paintings/ic_ImageUp.svg'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import type { FileType } from '@renderer/types'
|
||||
import { Popconfirm, Upload } from 'antd'
|
||||
import { Button } from 'antd'
|
||||
import type { RcFile, UploadProps } from 'antd/es/upload'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface ImageUploaderProps {
|
||||
fileMap: {
|
||||
imageFiles?: FileType[]
|
||||
paths?: string[]
|
||||
}
|
||||
maxImages: number
|
||||
onClearImages: () => void
|
||||
onDeleteImage: (index: number) => void
|
||||
onAddImage: (file: File, index?: number) => void
|
||||
mode: string
|
||||
}
|
||||
|
||||
const ImageUploader: React.FC<ImageUploaderProps> = ({
|
||||
fileMap,
|
||||
maxImages,
|
||||
onClearImages,
|
||||
onDeleteImage,
|
||||
onAddImage
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
const handleBeforeUpload = (file: RcFile, index?: number) => {
|
||||
onAddImage(file, index)
|
||||
return false // 阻止默认上传行为
|
||||
}
|
||||
|
||||
// 自定义上传请求,不执行任何网络请求
|
||||
const customRequest: UploadProps['customRequest'] = ({ onSuccess }) => {
|
||||
if (onSuccess) {
|
||||
onSuccess('ok' as any)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderContainer>
|
||||
{fileMap.imageFiles && fileMap.imageFiles.length > 0 && (
|
||||
<Button size="small" onClick={onClearImages}>
|
||||
清除全部
|
||||
</Button>
|
||||
)}
|
||||
</HeaderContainer>
|
||||
|
||||
<UploadImageList>
|
||||
{fileMap.paths && fileMap.paths.length > 0 ? (
|
||||
<>
|
||||
{fileMap.paths.map((src, index) => (
|
||||
<UploadImageItem key={index}>
|
||||
<ImageUploadButton
|
||||
accept="image/png, image/jpeg"
|
||||
maxCount={1}
|
||||
multiple={false}
|
||||
showUploadList={false}
|
||||
listType="picture-card"
|
||||
action=""
|
||||
customRequest={customRequest}
|
||||
beforeUpload={(file) => {
|
||||
handleBeforeUpload(file, index)
|
||||
}}>
|
||||
<ImagePreview>
|
||||
<img src={src} alt={`预览图${index + 1}`} />
|
||||
</ImagePreview>
|
||||
</ImageUploadButton>
|
||||
<Popconfirm
|
||||
title="确定要删除这张图片吗?"
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onConfirm={() => onDeleteImage(index)}>
|
||||
<DeleteButton>
|
||||
<DeleteOutlined />
|
||||
</DeleteButton>
|
||||
</Popconfirm>
|
||||
</UploadImageItem>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
|
||||
{fileMap.imageFiles && fileMap.imageFiles.length < maxImages ? (
|
||||
<UploadImageItem>
|
||||
<ImageUploadButton
|
||||
multiple={false}
|
||||
accept="image/png, image/jpeg"
|
||||
maxCount={1}
|
||||
showUploadList={false}
|
||||
listType="picture-card"
|
||||
action=""
|
||||
customRequest={customRequest}
|
||||
beforeUpload={(file) => {
|
||||
handleBeforeUpload(file)
|
||||
}}>
|
||||
<ImageSizeImage src={IcImageUp} theme={theme} />
|
||||
</ImageUploadButton>
|
||||
</UploadImageItem>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</UploadImageList>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// 样式组件
|
||||
const HeaderContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
|
||||
const ImageUploadButton = styled(Upload)`
|
||||
& .ant-upload.ant-upload-select,
|
||||
.ant-upload-list-item-container {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
aspect-ratio: 1 !important;
|
||||
}
|
||||
margin-bottom: 5px;
|
||||
`
|
||||
|
||||
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;
|
||||
}
|
||||
`
|
||||
|
||||
const ImageSizeImage = styled.img<{ theme: string }>`
|
||||
filter: ${({ theme }) => (theme === 'dark' ? 'invert(100%)' : 'none')};
|
||||
margin-top: 8px;
|
||||
`
|
||||
|
||||
const UploadImageList = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
|
||||
const UploadImageItem = styled.div`
|
||||
width: 45%;
|
||||
height: 45%;
|
||||
margin-bottom: 5px;
|
||||
margin-right: 5px;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const DeleteButton = styled.button`
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 10;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
export default ImageUploader
|
||||
@ -7,6 +7,8 @@ import ImageSize16_9 from '@renderer/assets/images/paintings/image-size-16-9.svg
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { DmxapiPainting } from '@types'
|
||||
|
||||
import { generationModeType } from '../../../types'
|
||||
|
||||
export const STYLE_TYPE_OPTIONS = [
|
||||
{ label: '吉卜力', value: '吉卜力' },
|
||||
{ label: '皮克斯', value: '皮克斯' },
|
||||
@ -45,6 +47,22 @@ export const TEXT_TO_IMAGES_MODELS = [
|
||||
}
|
||||
]
|
||||
|
||||
export const IMAGE_EDIT_MODELS = [
|
||||
{
|
||||
id: 'gpt-image-1',
|
||||
provider: 'DMXAPI',
|
||||
name: 'OpenAI:gpt-image-1'
|
||||
}
|
||||
]
|
||||
|
||||
export const IMAGE_MERGE_MODELS = [
|
||||
{
|
||||
id: 'gpt-image-1',
|
||||
provider: 'DMXAPI',
|
||||
name: 'OpenAI:gpt-image-1'
|
||||
}
|
||||
]
|
||||
|
||||
export const IMAGE_SIZES = [
|
||||
{
|
||||
label: '1:1',
|
||||
@ -91,5 +109,12 @@ export const DEFAULT_PAINTING: DmxapiPainting = {
|
||||
seed: '',
|
||||
style_type: '',
|
||||
model: TEXT_TO_IMAGES_MODELS[0].id,
|
||||
autoCreate: false
|
||||
autoCreate: false,
|
||||
generationMode: generationModeType.GENERATION
|
||||
}
|
||||
|
||||
export const MODEOPTIONS = [
|
||||
{ label: 'paintings.mode.generate', value: generationModeType.GENERATION },
|
||||
{ label: '改图', value: generationModeType.EDIT },
|
||||
{ label: '合并图', value: generationModeType.MERGE }
|
||||
]
|
||||
|
||||
@ -256,6 +256,12 @@ export interface ScalePainting extends PaintingParams {
|
||||
renderingSpeed?: string
|
||||
}
|
||||
|
||||
export enum generationModeType {
|
||||
GENERATION = 'generation',
|
||||
EDIT = 'edit',
|
||||
MERGE = 'merge'
|
||||
}
|
||||
|
||||
export interface DmxapiPainting extends PaintingParams {
|
||||
model?: string
|
||||
prompt?: string
|
||||
@ -265,6 +271,7 @@ export interface DmxapiPainting extends PaintingParams {
|
||||
seed?: string
|
||||
style_type?: string
|
||||
autoCreate?: boolean
|
||||
generationMode?: generationModeType
|
||||
}
|
||||
|
||||
export interface TokenFluxPainting extends PaintingParams {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user