diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index 88290a37a6..3a98d6a854 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -1703,24 +1703,6 @@ export const SYSTEM_MODELS: Record = { 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', diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 12f96d748d..7dd6b9636f 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 9b96b9f706..2ea6b1034e 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -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": "モデルを選択", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 21e3adad95..3bd1b4a83c 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -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": "Выбрать модель", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index fb81a89108..f7d7ca3c1a 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -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": "选择模型", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index b7c1ad01dd..b9b1571f6f 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -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": "選擇模型", diff --git a/src/renderer/src/pages/paintings/DmxapiPage.tsx b/src/renderer/src/pages/paintings/DmxapiPage.tsx index 8b131b6599..64fe53904f 100644 --- a/src/renderer/src/pages/paintings/DmxapiPage.tsx +++ b/src/renderer/src/pages/paintings/DmxapiPage.tsx @@ -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({ + 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(null) + + // 更新painting状态的辅助函数 + const updatePaintingState = (updates: Partial) => { + const updatedPainting = { ...painting, ...updates } + setPainting(updatedPainting) + updatePainting('DMXAPIPaintings', updatedPainting) + } + + const getNewPainting = (params?: Partial) => { + 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(null) - - const updatePaintingState = (updates: Partial) => { - 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(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 ( + + + + ) + } + } + + if (painting?.urls?.length > 0 || DMXAPIPaintings?.length > 1) { + return null + } else { + return ( + + + + ) + } + } + + const defaultLoadText = () => { + if ( + painting.generationMode && + [generationModeType.EDIT, generationModeType.MERGE].includes(painting.generationMode) + ) { + return ( + +
+ 正在用 OpenAI 官方 gpt-image-1 模型生产, +
+ 预计等待2~5分钟效果最好, +
+ 本次消耗金额请到DMIAPI后台日志查看 +
+
+ ) + } + + 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 ( @@ -422,40 +683,60 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { ))} + {painting.generationMode && + [generationModeType.EDIT, generationModeType.MERGE].includes(painting.generationMode) && ( + <> + 参考图 + + + )} + {t('common.model')} onInputSeed(e)} - suffix={ - updatePaintingState({ seed: Math.floor(Math.random() * 1000000).toString() })} - style={{ cursor: 'pointer', color: 'var(--color-text-2)' }} + {painting.generationMode === generationModeType.GENERATION && ( + <> + {t('paintings.image.size')} + onSelectImageSize(e.target.value)} + style={{ display: 'flex' }}> + {IMAGE_SIZES.map((size) => ( + + + + {size.label} + + + ))} + + + + {t('paintings.seed')} + + + + + onInputSeed(e)} + suffix={ + updatePaintingState({ seed: Math.floor(Math.random() * 1000000).toString() })} + style={{ cursor: 'pointer', color: 'var(--color-text-2)' }} + /> + } /> - } - /> + + )} {t('paintings.style_type')} @@ -482,6 +763,14 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { + + + = ({ Options }) => { onPrevImage={prevImage} onNextImage={nextImage} onCancel={onCancel} - imageCover={ - painting?.urls?.length > 0 || DMXAPIPaintings?.length > 1 ? null : ( - - - - ) - } + imageCover={defaultCoverImage()} + loadText={defaultLoadText()} />