mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-25 03:10:08 +08:00
feat: 文字生成图新增提供商DMXAPI (#6352)
dmxapi文字生成图 Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
This commit is contained in:
parent
f27e66a399
commit
d7847f1efa
@ -386,7 +386,11 @@ class FileStorage {
|
||||
}
|
||||
}
|
||||
|
||||
public downloadFile = async (_: Electron.IpcMainInvokeEvent, url: string): Promise<FileType> => {
|
||||
public downloadFile = async (
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
url: string,
|
||||
isUseContentType?: boolean
|
||||
): Promise<FileType> => {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
@ -411,7 +415,7 @@ class FileStorage {
|
||||
}
|
||||
|
||||
// 如果文件名没有后缀,根据Content-Type添加后缀
|
||||
if (!filename.includes('.')) {
|
||||
if (isUseContentType || !filename.includes('.')) {
|
||||
const contentType = response.headers.get('Content-Type')
|
||||
const ext = this.getExtensionFromMimeType(contentType)
|
||||
filename += ext
|
||||
|
||||
@ -74,7 +74,7 @@ const api = {
|
||||
selectFolder: () => ipcRenderer.invoke(IpcChannel.File_SelectFolder),
|
||||
saveImage: (name: string, data: string) => ipcRenderer.invoke(IpcChannel.File_SaveImage, name, data),
|
||||
base64Image: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64Image, fileId),
|
||||
download: (url: string) => ipcRenderer.invoke(IpcChannel.File_Download, url),
|
||||
download: (url: string, isUseContentType?: boolean) => ipcRenderer.invoke(IpcChannel.File_Download, url, isUseContentType),
|
||||
copy: (fileId: string, destPath: string) => ipcRenderer.invoke(IpcChannel.File_Copy, fileId, destPath),
|
||||
binaryImage: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_BinaryImage, fileId),
|
||||
base64File: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64File, fileId),
|
||||
|
||||
BIN
src/renderer/src/assets/images/providers/DMXAPI-to-img.webp
Normal file
BIN
src/renderer/src/assets/images/providers/DMXAPI-to-img.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
BIN
src/renderer/src/assets/images/providers/dmxapi-logo.webp
Normal file
BIN
src/renderer/src/assets/images/providers/dmxapi-logo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
@ -9,10 +9,12 @@ export function usePaintings() {
|
||||
const remix = useAppSelector((state) => state.paintings.remix)
|
||||
const edit = useAppSelector((state) => state.paintings.edit)
|
||||
const upscale = useAppSelector((state) => state.paintings.upscale)
|
||||
const DMXAPIPaintings = useAppSelector((state) => state.paintings.DMXAPIPaintings)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
paintings,
|
||||
DMXAPIPaintings,
|
||||
persistentData: {
|
||||
generate,
|
||||
remix,
|
||||
|
||||
@ -813,6 +813,7 @@
|
||||
"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",
|
||||
"seed_desc_tip": "The same seed and prompt can generate similar images, setting -1 will generate different results each time",
|
||||
"title": "Images",
|
||||
"magic_prompt_option": "Magic Prompt",
|
||||
"model": "Model Version",
|
||||
@ -820,6 +821,7 @@
|
||||
"style_type": "Style",
|
||||
"rendering_speed": "Rendering Speed",
|
||||
"learn_more": "Learn More",
|
||||
"paint_course":"tutorial",
|
||||
"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",
|
||||
@ -885,7 +887,8 @@
|
||||
"number_images_tip": "Number of upscaled results to generate",
|
||||
"seed_tip": "Controls upscaling randomness",
|
||||
"magic_prompt_option_tip": "Intelligently enhances upscaling prompts"
|
||||
}
|
||||
},
|
||||
"text_desc_required": "Please enter image description first"
|
||||
},
|
||||
"prompts": {
|
||||
"explanation": "Explain this concept to me",
|
||||
@ -1567,6 +1570,9 @@
|
||||
"rate_limit": "Rate limiting",
|
||||
"tooltip": "You need to log in to Github before using Github Copilot"
|
||||
},
|
||||
"dmxapi": {
|
||||
"select_platform": "Select the platform"
|
||||
},
|
||||
"delete.content": "Are you sure you want to delete this provider?",
|
||||
"delete.title": "Delete Provider",
|
||||
"docs_check": "Check",
|
||||
|
||||
@ -813,6 +813,7 @@
|
||||
"regenerate.confirm": "これにより、既存の生成画像が置き換えられます。続行しますか?",
|
||||
"seed": "シード",
|
||||
"seed_tip": "同じシードとプロンプトで似た画像を生成できます",
|
||||
"seed_desc_tip": "同じシードとプロンプトで類似した画像を生成できますが、-1 に設定すると毎回異なる結果が生成されます",
|
||||
"title": "画像",
|
||||
"magic_prompt_option": "プロンプト強化",
|
||||
"model": "モデルバージョン",
|
||||
@ -820,6 +821,7 @@
|
||||
"style_type": "スタイル",
|
||||
"learn_more": "詳しくはこちら",
|
||||
"prompt_placeholder_edit": "画像の説明を入力します。テキスト描画には '二重引用符' を使用します",
|
||||
"paint_course":"チュートリアル",
|
||||
"proxy_required": "現在、プロキシを開く必要があります。これは、将来サポートされる予定です",
|
||||
"image_file_required": "画像を先にアップロードしてください",
|
||||
"image_file_retry": "画像を先にアップロードしてください",
|
||||
@ -885,7 +887,8 @@
|
||||
"magic_prompt_option_tip": "拡大効果を向上させるための提示詞を最適化します"
|
||||
},
|
||||
"rendering_speed": "レンダリング速度",
|
||||
"translating": "翻訳中..."
|
||||
"translating": "翻訳中...",
|
||||
"text_desc_required": "画像の説明を先に入力してください"
|
||||
},
|
||||
"prompts": {
|
||||
"explanation": "この概念を説明してください",
|
||||
@ -1554,6 +1557,9 @@
|
||||
"rate_limit": "レート制限",
|
||||
"tooltip": "Github Copilot を使用するには、まず Github にログインする必要があります。"
|
||||
},
|
||||
"dmxapi": {
|
||||
"select_platform": "プラットフォームを選択"
|
||||
},
|
||||
"delete.content": "このプロバイダーを削除してもよろしいですか?",
|
||||
"delete.title": "プロバイダーを削除",
|
||||
"docs_check": "チェック",
|
||||
|
||||
@ -813,6 +813,7 @@
|
||||
"regenerate.confirm": "Это заменит ваши существующие сгенерированные изображения. Хотите продолжить?",
|
||||
"seed": "Ключ генерации",
|
||||
"seed_tip": "Одинаковый ключ генерации и промпт могут производить похожие изображения",
|
||||
"seed_desc_tip": "Одинаковые сиды и промпты могут генерировать похожие изображения, установка -1 будет создавать разные результаты каждый раз",
|
||||
"title": "Изображения",
|
||||
"magic_prompt_option": "Улучшение промпта",
|
||||
"model": "Версия",
|
||||
@ -821,6 +822,7 @@
|
||||
"rendering_speed": "Скорость рендеринга",
|
||||
"learn_more": "Узнать больше",
|
||||
"prompt_placeholder_edit": "Введите ваше описание изображения, текстовая отрисовка использует двойные кавычки для обертки",
|
||||
"paint_course":"Руководство / Учебник",
|
||||
"proxy_required": "Сейчас необходимо открыть прокси для просмотра сгенерированных изображений, в будущем будет поддерживаться прямое соединение",
|
||||
"image_file_required": "Пожалуйста, сначала загрузите изображение",
|
||||
"image_file_retry": "Пожалуйста, сначала загрузите изображение",
|
||||
@ -885,7 +887,9 @@
|
||||
"number_images_tip": "Количество увеличенных результатов для генерации",
|
||||
"seed_tip": "Контролирует случайный характер увеличения изображений для воспроизводимых результатов",
|
||||
"magic_prompt_option_tip": "Улучшает увеличение изображений с помощью интеллектуального оптимизирования промптов"
|
||||
}
|
||||
},
|
||||
"rendering_speed": "Скорость рендеринга",
|
||||
"text_desc_required": "Пожалуйста, сначала введите описание изображения"
|
||||
},
|
||||
"prompts": {
|
||||
"explanation": "Объясните мне этот концепт",
|
||||
@ -1554,6 +1558,9 @@
|
||||
"rate_limit": "Ограничение скорости",
|
||||
"tooltip": "Для использования Github Copilot необходимо сначала войти в Github."
|
||||
},
|
||||
"dmxapi": {
|
||||
"select_platform": "Выберите платформу"
|
||||
},
|
||||
"delete.content": "Вы уверены, что хотите удалить этот провайдер?",
|
||||
"delete.title": "Удалить провайдер",
|
||||
"docs_check": "Проверить",
|
||||
|
||||
@ -813,6 +813,7 @@
|
||||
"regenerate.confirm": "这将覆盖已生成的图片,是否继续?",
|
||||
"seed": "随机种子",
|
||||
"seed_tip": "相同的种子和提示词可以生成相似的图片",
|
||||
"seed_desc_tip": "相同的种子和提示词可以生成相似的图片,设置 -1 每次生成都不一样",
|
||||
"title": "图片",
|
||||
"magic_prompt_option": "提示词增强",
|
||||
"model": "版本",
|
||||
@ -820,6 +821,7 @@
|
||||
"style_type": "风格",
|
||||
"rendering_speed": "渲染速度",
|
||||
"learn_more": "了解更多",
|
||||
"paint_course":"教程",
|
||||
"prompt_placeholder_edit": "输入你的图片描述,文本绘制用 \"双引号\" 包裹",
|
||||
"proxy_required": "目前需要打开代理才能查看生成图片,后续会支持国内直连",
|
||||
"image_file_required": "请先上传图片",
|
||||
@ -885,7 +887,8 @@
|
||||
"number_images_tip": "生成的放大结果数量",
|
||||
"seed_tip": "控制放大结果的随机性",
|
||||
"magic_prompt_option_tip": "智能优化放大提示词"
|
||||
}
|
||||
},
|
||||
"text_desc_required": "请先输入图片描述"
|
||||
},
|
||||
"prompts": {
|
||||
"explanation": "帮我解释一下这个概念",
|
||||
@ -1567,6 +1570,9 @@
|
||||
"rate_limit": "速率限制",
|
||||
"tooltip": "使用 Github Copilot 需要先登录 Github"
|
||||
},
|
||||
"dmxapi": {
|
||||
"select_platform": "选择平台"
|
||||
},
|
||||
"delete.content": "确定要删除此模型提供商吗?",
|
||||
"delete.title": "删除提供商",
|
||||
"docs_check": "查看",
|
||||
|
||||
@ -814,6 +814,7 @@
|
||||
"regenerate.confirm": "這將覆蓋已生成的圖片,是否繼續?",
|
||||
"seed": "隨機種子",
|
||||
"seed_tip": "相同的種子和提示詞可以生成相似的圖片",
|
||||
"seed_desc_tip": "相同的種子和提示詞可以生成相似的圖片,設置 -1 每次生成都不一樣",
|
||||
"title": "繪圖",
|
||||
"magic_prompt_option": "提示詞增強",
|
||||
"model": "版本",
|
||||
@ -821,6 +822,7 @@
|
||||
"style_type": "風格",
|
||||
"learn_more": "了解更多",
|
||||
"prompt_placeholder_edit": "輸入你的圖片描述,文本繪製用 '雙引號' 包裹",
|
||||
"paint_course":"教程",
|
||||
"proxy_required": "目前需要打開代理才能查看生成圖片,後續會支持國內直連",
|
||||
"image_file_required": "請先上傳圖片",
|
||||
"image_file_retry": "請重新上傳圖片",
|
||||
@ -886,7 +888,8 @@
|
||||
"seed_tip": "控制放大結果的隨機性",
|
||||
"magic_prompt_option_tip": "智能優化放大提示詞"
|
||||
},
|
||||
"rendering_speed": "渲染速度"
|
||||
"rendering_speed": "渲染速度",
|
||||
"text_desc_required": "請先輸入圖片描述"
|
||||
},
|
||||
"prompts": {
|
||||
"explanation": "幫我解釋一下這個概念",
|
||||
@ -1558,6 +1561,9 @@
|
||||
"rate_limit": "速率限制",
|
||||
"tooltip": "使用 Github Copilot 需要先登入 Github"
|
||||
},
|
||||
"dmxapi": {
|
||||
"select_platform": "選擇平臺"
|
||||
},
|
||||
"delete.content": "確定要刪除此提供者嗎?",
|
||||
"delete.title": "刪除提供者",
|
||||
"docs_check": "檢查",
|
||||
|
||||
702
src/renderer/src/pages/paintings/DmxapiPage.tsx
Normal file
702
src/renderer/src/pages/paintings/DmxapiPage.tsx
Normal file
@ -0,0 +1,702 @@
|
||||
import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
|
||||
import DMXAPIToImg from '@renderer/assets/images/providers/DMXAPI-to-img.webp'
|
||||
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { VStack } from '@renderer/components/Layout'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
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, PaintingsState } from '@renderer/types'
|
||||
import { getErrorMessage, uuid } from '@renderer/utils'
|
||||
import { DmxapiPainting, PaintingAction } from '@types'
|
||||
import { Avatar, Button, Input, Radio, Select, Tooltip } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { Info } from 'lucide-react'
|
||||
import React, { 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'
|
||||
import { SettingHelpLink, SettingTitle } from '../settings'
|
||||
import Artboard from './Artboard'
|
||||
import {
|
||||
COURSE_URL,
|
||||
DEFAULT_PAINTING,
|
||||
IMAGE_SIZES,
|
||||
STYLE_TYPE_OPTIONS,
|
||||
TEXT_TO_IMAGES_MODELS
|
||||
} from './config/DmxapiConfig'
|
||||
import PaintingsList from './PaintingsList'
|
||||
|
||||
const generateRandomSeed = () => Math.floor(Math.random() * 1000000).toString()
|
||||
|
||||
const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const [mode] = useState<keyof PaintingsState>('DMXAPIPaintings')
|
||||
const { DMXAPIPaintings, addPainting, removePainting, updatePainting } = usePaintings()
|
||||
const [painting, setPainting] = useState<DmxapiPainting>(DMXAPIPaintings?.[0] || DEFAULT_PAINTING)
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation()
|
||||
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 dmxapiProvider = providers.find((p) => p.id === 'dmxapi')!
|
||||
|
||||
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,
|
||||
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) {
|
||||
updatePaintingState({ model: modelId })
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
abortController?.abort()
|
||||
}
|
||||
|
||||
const onSelectImageSize = (v: string) => {
|
||||
const size = IMAGE_SIZES.find((i) => i.value === v)
|
||||
size && updatePaintingState({ image_size: size.value, aspect_ratio: size.label })
|
||||
}
|
||||
|
||||
const onSelectStyleType = (v: string) => {
|
||||
if (v === painting.style_type) {
|
||||
updatePaintingState({ style_type: '' })
|
||||
} else {
|
||||
updatePaintingState({ style_type: v })
|
||||
}
|
||||
}
|
||||
|
||||
const onInputSeed = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value
|
||||
// 允许空值或合法整数,且大于等于 -1
|
||||
if (value === '' || value === '-' || /^-?\d+$/.test(value)) {
|
||||
const numValue = parseInt(value, 10)
|
||||
|
||||
if (numValue >= -1 || value === '' || value === '-') {
|
||||
updatePaintingState({ seed: value })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查提供者状态函数
|
||||
const checkProviderStatus = () => {
|
||||
if (!dmxapiProvider.enabled) {
|
||||
throw new Error('error.provider_disabled')
|
||||
}
|
||||
|
||||
if (!dmxapiProvider.apiKey) {
|
||||
throw new Error('error.no_api_key')
|
||||
}
|
||||
|
||||
if (!painting.model) {
|
||||
throw new Error('error.missing_required_fields')
|
||||
}
|
||||
|
||||
if (!painting.prompt) {
|
||||
throw new Error('paintings.text_desc_required')
|
||||
}
|
||||
}
|
||||
|
||||
// 准备V1生成请求函数
|
||||
const prepareV1GenerateRequest = (prompt: string, painting: DmxapiPainting) => {
|
||||
const params = {
|
||||
prompt,
|
||||
model: painting.model,
|
||||
n: painting.n
|
||||
}
|
||||
|
||||
if (painting.aspect_ratio) {
|
||||
params['aspect_ratio'] = painting.aspect_ratio
|
||||
}
|
||||
|
||||
if (painting.image_size) {
|
||||
params['size'] = painting.image_size
|
||||
}
|
||||
|
||||
if (painting.seed) {
|
||||
if (Number(painting.seed) >= -1) {
|
||||
params['seed'] = Number(painting.seed)
|
||||
} else {
|
||||
params['seed'] = -1
|
||||
}
|
||||
}
|
||||
|
||||
if (painting.style_type) {
|
||||
params.prompt = prompt + ',风格:' + painting.style_type
|
||||
}
|
||||
|
||||
return {
|
||||
body: JSON.stringify(params),
|
||||
endpoint: `${dmxapiProvider.apiHost}/v1/images/generations`
|
||||
}
|
||||
}
|
||||
|
||||
// API请求函数
|
||||
const callApi = async (requestConfig: { endpoint: string; body: any }) => {
|
||||
const { endpoint, body } = requestConfig
|
||||
const headers = {}
|
||||
|
||||
// 如果是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'
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error?.message || '操作失败')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.data.map((item: { url: string }) => item.url)
|
||||
}
|
||||
|
||||
// 下载图像函数
|
||||
const downloadImages = async (urls: string[]) => {
|
||||
return Promise.all(
|
||||
urls.map(async (url) => {
|
||||
try {
|
||||
if (!url || url.trim() === '') {
|
||||
window.message.warning({
|
||||
content: t('message.empty_url'),
|
||||
key: 'empty-url-warning'
|
||||
})
|
||||
return null
|
||||
}
|
||||
return await window.api.file.download(url, true)
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
(error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL'))
|
||||
) {
|
||||
window.message.warning({
|
||||
content: t('message.empty_url'),
|
||||
key: 'empty-url-warning'
|
||||
})
|
||||
}
|
||||
return null
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// 准备请求配置函数
|
||||
const prepareRequestConfig = (prompt: string, painting: PaintingAction) => {
|
||||
// 根据模式和模型版本返回不同的请求配置
|
||||
return prepareV1GenerateRequest(prompt, painting)
|
||||
}
|
||||
|
||||
const onGenerate = async () => {
|
||||
try {
|
||||
// 获取提示词
|
||||
const prompt = textareaRef.current?.resizableTextArea?.textArea?.value || ''
|
||||
updatePaintingState({ prompt })
|
||||
|
||||
// 检查提供者状态
|
||||
checkProviderStatus()
|
||||
|
||||
// 处理已有文件
|
||||
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 controller = new AbortController()
|
||||
setAbortController(controller)
|
||||
setIsLoading(true)
|
||||
dispatch(setGenerating(true))
|
||||
|
||||
// 准备请求配置
|
||||
const requestConfig = prepareRequestConfig(prompt, painting)
|
||||
|
||||
// 发送API请求
|
||||
const urls = await callApi(requestConfig)
|
||||
|
||||
// 下载图像
|
||||
if (urls.length > 0) {
|
||||
const downloadedFiles = await downloadImages(urls)
|
||||
const validFiles = downloadedFiles.filter((file): file is FileType => file !== null)
|
||||
|
||||
// 保存文件并更新状态
|
||||
await FileManager.addFiles(validFiles)
|
||||
updatePaintingState({ files: validFiles, urls })
|
||||
}
|
||||
} catch (error) {
|
||||
// 错误处理
|
||||
if (error instanceof Error && error.name !== 'AbortError') {
|
||||
window.modal.error({
|
||||
content:
|
||||
error.message.startsWith('paintings.') || error.message.startsWith('error.')
|
||||
? t(error.message)
|
||||
: getErrorMessage(error),
|
||||
centered: true
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
// 清理状态
|
||||
setIsLoading(false)
|
||||
dispatch(setGenerating(false))
|
||||
setAbortController(null)
|
||||
}
|
||||
}
|
||||
|
||||
const nextImage = () => {
|
||||
setCurrentImageIndex((prev) => (prev + 1) % painting.files.length)
|
||||
}
|
||||
|
||||
const prevImage = () => {
|
||||
setCurrentImageIndex((prev) => (prev - 1 + painting.files.length) % painting.files.length)
|
||||
}
|
||||
|
||||
const onDeletePainting = (paintingToDelete: DmxapiPainting) => {
|
||||
if (paintingToDelete.id === painting.id) {
|
||||
const currentIndex = DMXAPIPaintings.findIndex((p) => p.id === paintingToDelete.id)
|
||||
|
||||
if (currentIndex > 0) {
|
||||
setPainting(DMXAPIPaintings[currentIndex - 1])
|
||||
} else if (DMXAPIPaintings.length > 1) {
|
||||
setPainting(DMXAPIPaintings[1])
|
||||
}
|
||||
}
|
||||
|
||||
removePainting(mode, paintingToDelete).then(() => {})
|
||||
}
|
||||
|
||||
const onSelectPainting = (newPainting: DmxapiPainting) => {
|
||||
if (generating) return
|
||||
setPainting(newPainting)
|
||||
setCurrentImageIndex(0)
|
||||
}
|
||||
|
||||
const { autoTranslateWithSpace } = useSettings()
|
||||
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
const spaceClickTimer = useRef<NodeJS.Timeout>(null)
|
||||
|
||||
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().then(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleProviderChange = (providerId: string) => {
|
||||
const routeName = location.pathname.split('/').pop()
|
||||
if (providerId !== routeName) {
|
||||
navigate('../' + providerId, { replace: true })
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!DMXAPIPaintings || DMXAPIPaintings.length === 0) {
|
||||
const newPainting = getNewPainting()
|
||||
addPainting('DMXAPIPaintings', newPainting)
|
||||
setPainting(newPainting)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (spaceClickTimer.current) {
|
||||
clearTimeout(spaceClickTimer.current)
|
||||
}
|
||||
}
|
||||
}, [DMXAPIPaintings, DMXAPIPaintings.length, addPainting, mode])
|
||||
|
||||
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={() => setPainting(addPainting('DMXAPIPaintings', getNewPainting()))}>
|
||||
{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={COURSE_URL}>
|
||||
{t('paintings.paint_course')}
|
||||
<ProviderLogo
|
||||
shape="square"
|
||||
src={getProviderLogo(dmxapiProvider.id)}
|
||||
size={16}
|
||||
style={{ marginLeft: 5 }}
|
||||
/>
|
||||
</SettingHelpLink>
|
||||
</ProviderTitleContainer>
|
||||
<Select value={providerOptions[2].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>
|
||||
<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)' }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.style_type')}</SettingTitle>
|
||||
<SliderContainer>
|
||||
<RadioTextBox>
|
||||
{STYLE_TYPE_OPTIONS.map((ele) => (
|
||||
<RadioTextItem
|
||||
key={ele.label}
|
||||
className={painting.style_type === ele.label ? 'selected' : ''}
|
||||
onClick={() => onSelectStyleType(ele.label)}>
|
||||
{ele.label}
|
||||
</RadioTextItem>
|
||||
))}
|
||||
</RadioTextBox>
|
||||
</SliderContainer>
|
||||
</LeftContainer>
|
||||
<MainContainer>
|
||||
{painting?.urls?.length > 0 || DMXAPIPaintings?.length > 1 ? (
|
||||
<Artboard
|
||||
painting={painting}
|
||||
isLoading={isLoading}
|
||||
currentImageIndex={currentImageIndex}
|
||||
onPrevImage={prevImage}
|
||||
onNextImage={nextImage}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
) : (
|
||||
<EmptyImgBox>
|
||||
<EmptyImg></EmptyImg>
|
||||
</EmptyImgBox>
|
||||
)}
|
||||
|
||||
<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')}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<Toolbar>
|
||||
<ToolbarMenu>
|
||||
<SendMessageButton sendMessage={onGenerate} disabled={isLoading} />
|
||||
</ToolbarMenu>
|
||||
</Toolbar>
|
||||
</InputContainer>
|
||||
</MainContainer>
|
||||
<PaintingsList
|
||||
namespace="DMXAPIPaintings"
|
||||
paintings={DMXAPIPaintings}
|
||||
selectedPainting={painting}
|
||||
onSelectPainting={onSelectPainting}
|
||||
onDeletePainting={onDeletePainting}
|
||||
onNewPainting={() => setPainting(addPainting('DMXAPIPaintings', getNewPainting()))}
|
||||
/>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
// 添加新的样式组件
|
||||
const ProviderTitleContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
`
|
||||
|
||||
const ProviderLogo = styled(Avatar)`
|
||||
border: 0.5px solid var(--color-border);
|
||||
`
|
||||
const SelectOptionContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
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;
|
||||
resize: none !important;
|
||||
overflow: auto;
|
||||
width: auto;
|
||||
`
|
||||
|
||||
const Toolbar = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
padding: 0 8px;
|
||||
height: 40px;
|
||||
`
|
||||
|
||||
const ToolbarMenu = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
`
|
||||
|
||||
const ImageSizeImage = styled.img<{ theme: string }>`
|
||||
filter: ${({ theme }) => (theme === 'dark' ? 'invert(100%)' : 'none')};
|
||||
margin-top: 8px;
|
||||
`
|
||||
|
||||
const RadioButton = styled(Radio.Button)`
|
||||
width: 30px;
|
||||
height: 55px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const InfoIcon = styled(Info)`
|
||||
margin-left: 5px;
|
||||
cursor: help;
|
||||
color: var(--color-text-2);
|
||||
opacity: 0.6;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const SliderContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.ant-slider {
|
||||
flex: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const RadioTextBox = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const RadioTextItem = styled.div`
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid var(--color-border);
|
||||
|
||||
/* 默认状态 */
|
||||
background-color: var(--color-background);
|
||||
|
||||
/* 悬浮状态 */
|
||||
&:hover {
|
||||
background-color: var(--color-hover, #f0f0f0);
|
||||
}
|
||||
|
||||
/* 选中状态 - 需要添加selected类名 */
|
||||
&.selected {
|
||||
background-color: var(--color-primary, #1890ff);
|
||||
color: white;
|
||||
border: 1px solid var(--color-primary, #1890ff);
|
||||
}
|
||||
`
|
||||
|
||||
const EmptyImgBox = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const EmptyImg = styled.div`
|
||||
width: 70vh;
|
||||
height: 70vh;
|
||||
background-size: 100% 100%;
|
||||
background-image: url(${DMXAPIToImg});
|
||||
`
|
||||
|
||||
export default DmxapiPage
|
||||
@ -2,9 +2,10 @@ import { FC } from 'react'
|
||||
import { Route, Routes } from 'react-router-dom'
|
||||
|
||||
import AihubmixPage from './AihubmixPage'
|
||||
import DmxapiPage from './DmxapiPage'
|
||||
import SiliconPage from './PaintingsPage'
|
||||
|
||||
const Options = ['aihubmix', 'silicon']
|
||||
const Options = ['aihubmix', 'silicon', 'dmxapi']
|
||||
|
||||
const PaintingsRoutePage: FC = () => {
|
||||
return (
|
||||
@ -12,6 +13,7 @@ const PaintingsRoutePage: FC = () => {
|
||||
<Route path="/" element={<AihubmixPage Options={Options} />} />
|
||||
<Route path="/aihubmix" element={<AihubmixPage Options={Options} />} />
|
||||
<Route path="/silicon" element={<SiliconPage Options={Options} />} />
|
||||
<Route path="/dmxapi" element={<DmxapiPage Options={Options} />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
94
src/renderer/src/pages/paintings/config/DmxapiConfig.ts
Normal file
94
src/renderer/src/pages/paintings/config/DmxapiConfig.ts
Normal file
@ -0,0 +1,94 @@
|
||||
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 { uuid } from '@renderer/utils'
|
||||
import { DmxapiPainting } from '@types'
|
||||
|
||||
export const STYLE_TYPE_OPTIONS = [
|
||||
{ label: '吉卜力', value: '吉卜力' },
|
||||
{ label: '皮克斯', value: '皮克斯' },
|
||||
{ label: '绒线玩偶', value: '绒线玩偶' },
|
||||
{ label: '水彩画', value: '水彩画' },
|
||||
{ label: '卡通插画', value: '卡通插画' },
|
||||
{ label: '3D卡通', value: '3D卡通' },
|
||||
{ label: '日系动漫', value: '日系动漫' },
|
||||
{ label: '木雕', value: '木雕' },
|
||||
{ label: '唯美古风', value: '唯美古风' },
|
||||
{ label: '2.5D动画', value: '2.5D动画' },
|
||||
{ label: '清新日漫', value: '清新日漫' },
|
||||
{ label: '黏土', value: '黏土' },
|
||||
{ label: '小人书插画', value: '小人书插画' },
|
||||
{ label: '浮世绘', value: '浮世绘' },
|
||||
{ label: '毛毡', value: '毛毡' },
|
||||
{ label: '美式复古', value: '美式复古' },
|
||||
{ label: '赛博朋克', value: '赛博朋克' },
|
||||
{ label: '素描', value: '素描' },
|
||||
{ label: '莫奈花园', value: '莫奈花园' },
|
||||
{ label: '厚涂手绘', value: '厚涂手绘' },
|
||||
{ label: '扁平', value: '扁平' },
|
||||
{ label: '肌理', value: '肌理' },
|
||||
{ label: '像素艺术', value: '像素艺术' },
|
||||
{ label: '街头艺术', value: '街头艺术' },
|
||||
{ label: '迷幻', value: '迷幻' },
|
||||
{ label: '国风工笔', value: '国风工笔' },
|
||||
{ label: '巴洛克', value: '巴洛克' }
|
||||
]
|
||||
|
||||
export const TEXT_TO_IMAGES_MODELS = [
|
||||
{
|
||||
id: 'seedream-3.0',
|
||||
provider: 'DMXAPI',
|
||||
name: ' 即梦 seedream-3.0'
|
||||
}
|
||||
]
|
||||
|
||||
export const IMAGE_SIZES = [
|
||||
{
|
||||
label: '1:1',
|
||||
value: '1328x1328',
|
||||
icon: ImageSize1_1
|
||||
},
|
||||
{
|
||||
label: '1:2',
|
||||
value: '800x1600',
|
||||
icon: ImageSize1_2
|
||||
},
|
||||
{
|
||||
label: '3:2',
|
||||
value: '1584x1056',
|
||||
icon: ImageSize3_2
|
||||
},
|
||||
{
|
||||
label: '3:4',
|
||||
value: '1104x1472',
|
||||
icon: ImageSize3_4
|
||||
},
|
||||
{
|
||||
label: '16:9',
|
||||
value: '1664x936',
|
||||
icon: ImageSize16_9
|
||||
},
|
||||
{
|
||||
label: '9:16',
|
||||
value: '936x1664',
|
||||
icon: ImageSize9_16
|
||||
}
|
||||
]
|
||||
|
||||
export const COURSE_URL = 'http://seedream.dmxapi.cn/'
|
||||
|
||||
export const DEFAULT_PAINTING: DmxapiPainting = {
|
||||
id: uuid(),
|
||||
urls: [],
|
||||
files: [],
|
||||
prompt: '',
|
||||
image_size: '1328x1328',
|
||||
aspect_ratio: '1:1',
|
||||
n: 1,
|
||||
seed: '',
|
||||
style_type: '',
|
||||
model: TEXT_TO_IMAGES_MODELS[0].id
|
||||
}
|
||||
@ -40,6 +40,7 @@ export type AihubmixMode = keyof PaintingsState
|
||||
export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
|
||||
return {
|
||||
paintings: [],
|
||||
DMXAPIPaintings: [],
|
||||
generate: [
|
||||
{ type: 'title', title: 'paintings.model', tooltip: 'paintings.generate.model_tip' },
|
||||
{
|
||||
|
||||
@ -0,0 +1,122 @@
|
||||
import DmxapiLogo from '@renderer/assets/images/providers/dmxapi-logo.webp'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { Provider } from '@renderer/types'
|
||||
import { Radio, RadioChangeEvent, Space } from 'antd'
|
||||
import { FC, useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingSubtitle } from '..'
|
||||
|
||||
interface DMXAPISettingsProps {
|
||||
provider: Provider
|
||||
setApiKey: (apiKey: string) => void
|
||||
}
|
||||
|
||||
// DMXAPI平台选项
|
||||
enum PlatformType {
|
||||
OFFICIAL = 'https://www.DMXAPI.cn',
|
||||
INTERNATIONAL = 'https://www.DMXAPI.com',
|
||||
OVERSEA = 'https://ssvip.DMXAPI.com'
|
||||
}
|
||||
|
||||
const PlatformOptions = [
|
||||
{
|
||||
label: 'www.DMXAPI.cn 人民币站',
|
||||
value: PlatformType.OFFICIAL,
|
||||
apiKeyWebsite: 'https://www.dmxapi.cn/register?aff=bwwY'
|
||||
},
|
||||
{
|
||||
label: 'www.DMXAPI.com 国际站',
|
||||
value: PlatformType.INTERNATIONAL,
|
||||
apiKeyWebsite: 'https://www.dmxapi.com/register'
|
||||
},
|
||||
{
|
||||
label: 'ssvip.DMXAPI.com 生产级商用站',
|
||||
value: PlatformType.OVERSEA,
|
||||
apiKeyWebsite: 'https://ssvip.dmxapi.com/register'
|
||||
}
|
||||
]
|
||||
|
||||
const DMXAPISettings: FC<DMXAPISettingsProps> = ({ provider: initialProvider }) => {
|
||||
const { provider, updateProvider } = useProvider(initialProvider.id)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 获取当前选中的平台,如果没有设置则默认为官方平台
|
||||
const getCurrentPlatform = (): PlatformType => {
|
||||
if (!provider.apiHost) return PlatformType.OFFICIAL
|
||||
|
||||
if (provider.apiHost.includes('DMXAPI.com')) {
|
||||
return provider.apiHost.includes('ssvip') ? PlatformType.OVERSEA : PlatformType.INTERNATIONAL
|
||||
}
|
||||
|
||||
return PlatformType.OFFICIAL
|
||||
}
|
||||
|
||||
// 状态管理
|
||||
const [selectedPlatform, setSelectedPlatform] = useState<PlatformType>(getCurrentPlatform())
|
||||
|
||||
// 处理平台选择变更
|
||||
const handlePlatformChange = useCallback(
|
||||
(e: RadioChangeEvent) => {
|
||||
const platform = e.target.value as PlatformType
|
||||
setSelectedPlatform(platform)
|
||||
updateProvider({ ...provider, apiHost: platform })
|
||||
},
|
||||
[provider, updateProvider]
|
||||
)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<LogoContainer>
|
||||
<Logo src={DmxapiLogo}></Logo>
|
||||
</LogoContainer>
|
||||
|
||||
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.dmxapi.select_platform')}</SettingSubtitle>
|
||||
<Radio.Group
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8
|
||||
}}
|
||||
onChange={handlePlatformChange}
|
||||
value={selectedPlatform}
|
||||
options={PlatformOptions.map((option) => ({
|
||||
...option,
|
||||
label: (
|
||||
<span>
|
||||
{option.label}{' '}
|
||||
<a href={option.apiKeyWebsite} target="_blank" rel="noopener noreferrer">
|
||||
(获得 API密钥)
|
||||
</a>
|
||||
</span>
|
||||
)
|
||||
}))}></Radio.Group>
|
||||
</Space>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
// 样式组件
|
||||
const Container = styled.div`
|
||||
margin-top: 16px;
|
||||
margin-bottom: 30px;
|
||||
`
|
||||
|
||||
const LogoContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
`
|
||||
|
||||
const Logo = styled.img`
|
||||
height: 70px;
|
||||
display: block;
|
||||
width: auto;
|
||||
`
|
||||
|
||||
export default DMXAPISettings
|
||||
@ -32,6 +32,7 @@ import {
|
||||
SettingTitle
|
||||
} from '..'
|
||||
import ApiCheckPopup from './ApiCheckPopup'
|
||||
import DMXAPISettings from './DMXAPISettings'
|
||||
import GithubCopilotSettings from './GithubCopilotSettings'
|
||||
import GPUStackSettings from './GPUStackSettings'
|
||||
import HealthCheckPopup from './HealthCheckPopup'
|
||||
@ -64,6 +65,8 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
|
||||
const isAzureOpenAI = provider.id === 'azure-openai' || provider.type === 'azure-openai'
|
||||
|
||||
const isDmxapi = provider.id === 'dmxapi'
|
||||
|
||||
const providerConfig = PROVIDER_CONFIG[provider.id]
|
||||
const officialWebsite = providerConfig?.websites?.official
|
||||
const apiKeyWebsite = providerConfig?.websites?.apiKey
|
||||
@ -328,6 +331,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
/>
|
||||
)}
|
||||
{provider.id === 'openai' && <OpenAIAlert />}
|
||||
{isDmxapi && <DMXAPISettings provider={provider} setApiKey={setApiKey} />}
|
||||
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.api_key')}</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input.Password
|
||||
@ -358,35 +362,43 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
{apiKeyWebsite && (
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<HStack>
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.provider.get_api_key')}
|
||||
</SettingHelpLink>
|
||||
{!isDmxapi && (
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.provider.get_api_key')}
|
||||
</SettingHelpLink>
|
||||
)}
|
||||
</HStack>
|
||||
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
)}
|
||||
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input
|
||||
value={apiHost}
|
||||
placeholder={t('settings.provider.api_host')}
|
||||
onChange={(e) => setApiHost(e.target.value)}
|
||||
onBlur={onUpdateApiHost}
|
||||
/>
|
||||
{!isEmpty(configedApiHost) && apiHost !== configedApiHost && (
|
||||
<Button danger onClick={onReset}>
|
||||
{t('settings.provider.api.url.reset')}
|
||||
</Button>
|
||||
)}
|
||||
</Space.Compact>
|
||||
{isOpenAIProvider(provider) && (
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<SettingHelpText
|
||||
style={{ marginLeft: 6, marginRight: '1em', whiteSpace: 'break-spaces', wordBreak: 'break-all' }}>
|
||||
{hostPreview()}
|
||||
</SettingHelpText>
|
||||
<SettingHelpText style={{ minWidth: 'fit-content' }}>{t('settings.provider.api.url.tip')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
{!isDmxapi && (
|
||||
<>
|
||||
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input
|
||||
value={apiHost}
|
||||
placeholder={t('settings.provider.api_host')}
|
||||
onChange={(e) => setApiHost(e.target.value)}
|
||||
onBlur={onUpdateApiHost}
|
||||
/>
|
||||
{!isEmpty(configedApiHost) && apiHost !== configedApiHost && (
|
||||
<Button danger onClick={onReset}>
|
||||
{t('settings.provider.api.url.reset')}
|
||||
</Button>
|
||||
)}
|
||||
</Space.Compact>
|
||||
{isOpenAIProvider(provider) && (
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<SettingHelpText
|
||||
style={{ marginLeft: 6, marginRight: '1em', whiteSpace: 'break-spaces', wordBreak: 'break-all' }}>
|
||||
{hostPreview()}
|
||||
</SettingHelpText>
|
||||
<SettingHelpText style={{ minWidth: 'fit-content' }}>
|
||||
{t('settings.provider.api.url.tip')}
|
||||
</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isAzureOpenAI && (
|
||||
<>
|
||||
|
||||
@ -46,7 +46,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 106,
|
||||
version: 107,
|
||||
blacklist: ['runtime', 'messages', 'messageBlocks'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@ -1445,6 +1445,16 @@ const migrateConfig = {
|
||||
} catch (error) {
|
||||
return state
|
||||
}
|
||||
},
|
||||
'107': (state: RootState) => {
|
||||
try {
|
||||
if (state.paintings && !state.paintings.DMXAPIPaintings) {
|
||||
state.paintings.DMXAPIPaintings = []
|
||||
}
|
||||
return state
|
||||
} catch (error) {
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,8 @@ const initialState: PaintingsState = {
|
||||
generate: [],
|
||||
remix: [],
|
||||
edit: [],
|
||||
upscale: []
|
||||
upscale: [],
|
||||
DMXAPIPaintings: []
|
||||
}
|
||||
|
||||
const paintingsSlice = createSlice({
|
||||
|
||||
@ -246,6 +246,16 @@ export interface ScalePainting extends PaintingParams {
|
||||
renderingSpeed?: string
|
||||
}
|
||||
|
||||
export interface DmxapiPainting extends PaintingParams {
|
||||
model?: string
|
||||
prompt?: string
|
||||
n?: number
|
||||
aspect_ratio?: string
|
||||
image_size?: string
|
||||
seed?: string
|
||||
style_type?: string
|
||||
}
|
||||
|
||||
export type PaintingAction = Partial<GeneratePainting & RemixPainting & EditPainting & ScalePainting> & PaintingParams
|
||||
|
||||
export interface PaintingsState {
|
||||
@ -254,6 +264,7 @@ export interface PaintingsState {
|
||||
remix: Partial<RemixPainting> & PaintingParams[]
|
||||
edit: Partial<EditPainting> & PaintingParams[]
|
||||
upscale: Partial<ScalePainting> & PaintingParams[]
|
||||
DMXAPIPaintings: DmxapiPainting[]
|
||||
}
|
||||
|
||||
export type MinAppType = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user