mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-25 19:30:17 +08:00
feat: add painting support for NewAPI provider (#7905)
* feat: add NewAPI painting support * fix(NewApiPage): update help link to point to the correct documentation * feat(NewApiPage): support image generation in API client * fix: resolve the issue of messy drawing data from aihubmix provider * feat: group model options in dropdown by category * fix: update translation to use LanguagesEnum
This commit is contained in:
parent
de75992e7b
commit
a343377a43
@ -106,7 +106,7 @@ export class NewAPIClient extends BaseApiClient {
|
||||
return client
|
||||
}
|
||||
|
||||
if (model.endpoint_type === 'openai') {
|
||||
if (model.endpoint_type === 'openai' || model.endpoint_type === 'image-generation') {
|
||||
const client = this.clients.get('openai')
|
||||
if (!client || !this.isValidClient(client)) {
|
||||
throw new Error('Failed to get openai client')
|
||||
|
||||
10
src/renderer/src/config/endpointTypes.ts
Normal file
10
src/renderer/src/config/endpointTypes.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { EndpointType } from '@renderer/types'
|
||||
|
||||
export const endpointTypeOptions: { label: string; value: EndpointType }[] = [
|
||||
{ value: 'openai', label: 'endpoint_type.openai' },
|
||||
{ value: 'openai-response', label: 'endpoint_type.openai-response' },
|
||||
{ value: 'anthropic', label: 'endpoint_type.anthropic' },
|
||||
{ value: 'gemini', label: 'endpoint_type.gemini' },
|
||||
{ value: 'image-generation', label: 'endpoint_type.image-generation' },
|
||||
{ value: 'jina-rerank', label: 'endpoint_type.jina-rerank' }
|
||||
]
|
||||
@ -11,6 +11,8 @@ export function usePaintings() {
|
||||
const upscale = useAppSelector((state) => state.paintings.upscale)
|
||||
const DMXAPIPaintings = useAppSelector((state) => state.paintings.DMXAPIPaintings)
|
||||
const tokenFluxPaintings = useAppSelector((state) => state.paintings.tokenFluxPaintings)
|
||||
const openai_image_generate = useAppSelector((state) => state.paintings.openai_image_generate)
|
||||
const openai_image_edit = useAppSelector((state) => state.paintings.openai_image_edit)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
@ -24,6 +26,10 @@ export function usePaintings() {
|
||||
upscale,
|
||||
tokenFluxPaintings
|
||||
},
|
||||
newApiPaintings: {
|
||||
openai_image_generate,
|
||||
openai_image_edit
|
||||
},
|
||||
addPainting: (namespace: keyof PaintingsState, painting: PaintingAction) => {
|
||||
dispatch(addPainting({ namespace, painting }))
|
||||
return painting
|
||||
|
||||
@ -471,6 +471,14 @@
|
||||
"messages": "Messages",
|
||||
"user": "User"
|
||||
},
|
||||
"endpoint_type": {
|
||||
"openai": "OpenAI",
|
||||
"openai-response": "OpenAI-Response",
|
||||
"anthropic": "Anthropic",
|
||||
"gemini": "Gemini",
|
||||
"image-generation": "Image Generation",
|
||||
"jina-rerank": "Jina Rerank"
|
||||
},
|
||||
"files": {
|
||||
"actions": "Actions",
|
||||
"all": "All Files",
|
||||
@ -868,6 +876,8 @@
|
||||
"title": "Ollama"
|
||||
},
|
||||
"paintings": {
|
||||
"no_image_generation_model": "No available image generation model, please add a model and set the endpoint type to {{endpoint_type}}",
|
||||
"go_to_settings": "Go to Settings",
|
||||
"button.delete.image": "Delete Image",
|
||||
"button.delete.image.confirm": "Are you sure you want to delete this image?",
|
||||
"button.new.image": "New Image",
|
||||
@ -941,6 +951,9 @@
|
||||
"allow_adult": "Allow adult",
|
||||
"allow_none": "Not allowed"
|
||||
},
|
||||
"image_size_options": {
|
||||
"auto": "Auto"
|
||||
},
|
||||
"quality": "Quality",
|
||||
"moderation": "Moderation",
|
||||
"background": "Background",
|
||||
|
||||
@ -471,6 +471,14 @@
|
||||
"messages": "メッセージ",
|
||||
"user": "ユーザー"
|
||||
},
|
||||
"endpoint_type": {
|
||||
"openai": "OpenAI",
|
||||
"openai-response": "OpenAI-Response",
|
||||
"anthropic": "Anthropic",
|
||||
"gemini": "Gemini",
|
||||
"image-generation": "画像生成",
|
||||
"jina-rerank": "Jina Rerank"
|
||||
},
|
||||
"files": {
|
||||
"actions": "操作",
|
||||
"all": "すべてのファイル",
|
||||
@ -868,6 +876,8 @@
|
||||
"title": "Ollama"
|
||||
},
|
||||
"paintings": {
|
||||
"no_image_generation_model": "利用可能な画像生成モデルがありません。モデルを追加し、エンドポイントタイプを {{endpoint_type}} に設定してください",
|
||||
"go_to_settings": "設定に移動",
|
||||
"button.delete.image": "画像を削除",
|
||||
"button.delete.image.confirm": "この画像を削除してもよろしいですか?",
|
||||
"button.new.image": "新しい画像",
|
||||
@ -939,6 +949,9 @@
|
||||
"allow_adult": "許可する",
|
||||
"allow_none": "許可しない"
|
||||
},
|
||||
"image_size_options": {
|
||||
"auto": "自動"
|
||||
},
|
||||
"quality": "品質",
|
||||
"moderation": "敏感度",
|
||||
"background": "背景",
|
||||
|
||||
@ -471,6 +471,14 @@
|
||||
"messages": "Сообщения",
|
||||
"user": "Пользователь"
|
||||
},
|
||||
"endpoint_type": {
|
||||
"openai": "OpenAI",
|
||||
"openai-response": "OpenAI-Response",
|
||||
"anthropic": "Anthropic",
|
||||
"gemini": "Gemini",
|
||||
"image-generation": "Изображение",
|
||||
"jina-rerank": "Jina Rerank"
|
||||
},
|
||||
"files": {
|
||||
"actions": "Действия",
|
||||
"all": "Все файлы",
|
||||
@ -868,6 +876,8 @@
|
||||
"title": "Ollama"
|
||||
},
|
||||
"paintings": {
|
||||
"no_image_generation_model": "Нет доступных моделей изображения, пожалуйста, добавьте модель и установите тип конечной точки на {{endpoint_type}}",
|
||||
"go_to_settings": "Перейти в настройки",
|
||||
"button.delete.image": "Удалить изображение",
|
||||
"button.delete.image.confirm": "Вы уверены, что хотите удалить это изображение?",
|
||||
"button.new.image": "Новое изображение",
|
||||
@ -940,6 +950,9 @@
|
||||
"allow_adult": "Разрешено взрослые",
|
||||
"allow_none": "Не разрешено"
|
||||
},
|
||||
"image_size_options": {
|
||||
"auto": "Авто"
|
||||
},
|
||||
"quality": "Качество",
|
||||
"moderation": "Сенсорность",
|
||||
"background": "Фон",
|
||||
|
||||
@ -471,6 +471,14 @@
|
||||
"messages": "消息数",
|
||||
"user": "用户"
|
||||
},
|
||||
"endpoint_type": {
|
||||
"openai": "OpenAI",
|
||||
"openai-response": "OpenAI-Response",
|
||||
"anthropic": "Anthropic",
|
||||
"gemini": "Gemini",
|
||||
"image-generation": "图片生成",
|
||||
"jina-rerank": "Jina 重排序"
|
||||
},
|
||||
"files": {
|
||||
"actions": "操作",
|
||||
"all": "所有文件",
|
||||
@ -868,6 +876,8 @@
|
||||
"title": "Ollama"
|
||||
},
|
||||
"paintings": {
|
||||
"no_image_generation_model": "暂无可用的图片生成模型,请先新增模型并设置端点类型为 {{endpoint_type}}",
|
||||
"go_to_settings": "去设置",
|
||||
"button.delete.image": "删除图片",
|
||||
"button.delete.image.confirm": "确定要删除此图片吗?",
|
||||
"button.new.image": "新建图片",
|
||||
@ -936,6 +946,9 @@
|
||||
"allow_adult": "允许成人",
|
||||
"allow_none": "不允许"
|
||||
},
|
||||
"image_size_options": {
|
||||
"auto": "自动"
|
||||
},
|
||||
"aspect_ratios": {
|
||||
"square": "方形",
|
||||
"portrait": "竖图",
|
||||
|
||||
@ -471,6 +471,14 @@
|
||||
"messages": "訊息數",
|
||||
"user": "使用者"
|
||||
},
|
||||
"endpoint_type": {
|
||||
"openai": "OpenAI",
|
||||
"openai-response": "OpenAI-Response",
|
||||
"anthropic": "Anthropic",
|
||||
"gemini": "Gemini",
|
||||
"image-generation": "圖片生成",
|
||||
"jina-rerank": "Jina Rerank"
|
||||
},
|
||||
"files": {
|
||||
"actions": "操作",
|
||||
"all": "所有檔案",
|
||||
@ -868,6 +876,8 @@
|
||||
"title": "Ollama"
|
||||
},
|
||||
"paintings": {
|
||||
"no_image_generation_model": "暫無可用的圖片生成模型,請先新增模型並設置端點類型為 {{endpoint_type}}",
|
||||
"go_to_settings": "去設置",
|
||||
"button.delete.image": "刪除繪圖",
|
||||
"button.delete.image.confirm": "確定要刪除此繪圖嗎?",
|
||||
"button.new.image": "新繪圖",
|
||||
@ -940,6 +950,9 @@
|
||||
"allow_adult": "允許成人",
|
||||
"allow_none": "不允許"
|
||||
},
|
||||
"image_size_options": {
|
||||
"auto": "自動"
|
||||
},
|
||||
"quality": "品質",
|
||||
"moderation": "敏感度",
|
||||
"background": "背景",
|
||||
|
||||
830
src/renderer/src/pages/paintings/NewApiPage.tsx
Normal file
830
src/renderer/src/pages/paintings/NewApiPage.tsx
Normal file
@ -0,0 +1,830 @@
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import AiProvider from '@renderer/aiCore'
|
||||
import IcImageUp from '@renderer/assets/images/paintings/ic_ImageUp.svg'
|
||||
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import TranslateButton from '@renderer/components/TranslateButton'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { getProviderLogo } from '@renderer/config/providers'
|
||||
import { LanguagesEnum } from '@renderer/config/translate'
|
||||
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 PaintingsList from '@renderer/pages/paintings/components/PaintingsList'
|
||||
import {
|
||||
DEFAULT_PAINTING,
|
||||
getModelGroup,
|
||||
MODELS,
|
||||
SUPPORTED_MODELS
|
||||
} from '@renderer/pages/paintings/config/NewApiConfig'
|
||||
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 { PaintingAction, PaintingsState } from '@renderer/types'
|
||||
import { FileMetadata } from '@renderer/types'
|
||||
import { getErrorMessage, uuid } from '@renderer/utils'
|
||||
import { Avatar, Button, Empty, InputNumber, Segmented, Select, Upload } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import React, { FC } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import SendMessageButton from '../home/Inputbar/SendMessageButton'
|
||||
import { SettingHelpLink, SettingTitle } from '../settings'
|
||||
import Artboard from './components/Artboard'
|
||||
|
||||
const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const [mode, setMode] = useState<keyof PaintingsState>('openai_image_generate')
|
||||
const { addPainting, removePainting, updatePainting, newApiPaintings } = usePaintings()
|
||||
const filteredPaintings = useMemo(() => newApiPaintings[mode] || [], [newApiPaintings, mode])
|
||||
const [painting, setPainting] = useState<PaintingAction>(filteredPaintings[0] || DEFAULT_PAINTING)
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [abortController, setAbortController] = useState<AbortController | null>(null)
|
||||
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
const [editImageFiles, setEditImageFiles] = useState<File[]>([])
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const providers = useAllProviders()
|
||||
const providerOptions = Options.map((option) => {
|
||||
const provider = providers.find((p) => p.id === option)
|
||||
return {
|
||||
label: t(`provider.${provider?.id}`),
|
||||
value: provider?.id
|
||||
}
|
||||
})
|
||||
const dispatch = useAppDispatch()
|
||||
const { generating } = useRuntime()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const { autoTranslateWithSpace } = useSettings()
|
||||
const spaceClickTimer = useRef<NodeJS.Timeout>(null)
|
||||
const newApiProvider = providers.find((p) => p.id === 'new-api')!
|
||||
|
||||
const modeOptions = [
|
||||
{ label: t('paintings.mode.generate'), value: 'openai_image_generate' },
|
||||
{ label: t('paintings.mode.edit'), value: 'openai_image_edit' }
|
||||
]
|
||||
|
||||
const textareaRef = useRef<any>(null)
|
||||
|
||||
// 获取编辑模式的图片文件
|
||||
const editImages = useMemo(() => {
|
||||
return editImageFiles
|
||||
}, [editImageFiles])
|
||||
|
||||
const updatePaintingState = (updates: Partial<PaintingAction>) => {
|
||||
const updatedPainting = { ...painting, ...updates }
|
||||
setPainting(updatedPainting)
|
||||
updatePainting(mode, updatedPainting)
|
||||
}
|
||||
|
||||
// ---------------- Model Related Configurations ----------------
|
||||
// const modelOptions = MODELS.map((m) => ({ label: m.name, value: m.name }))
|
||||
|
||||
const modelOptions = useMemo(() => {
|
||||
const customModels = newApiProvider.models
|
||||
.filter((m) => m.endpoint_type && m.endpoint_type === 'image-generation')
|
||||
.map((m) => ({
|
||||
label: m.name,
|
||||
value: m.id,
|
||||
custom: !SUPPORTED_MODELS.includes(m.id),
|
||||
group: getModelGroup(m.id)
|
||||
}))
|
||||
return [...customModels]
|
||||
}, [newApiProvider.models])
|
||||
|
||||
// 根据 group 将模型进行分组,便于在下拉列表中分组渲染
|
||||
const groupedModelOptions = useMemo(() => {
|
||||
return modelOptions.reduce<Record<string, typeof modelOptions>>((acc, option) => {
|
||||
const groupName = option.group
|
||||
if (!acc[groupName]) {
|
||||
acc[groupName] = []
|
||||
}
|
||||
acc[groupName].push(option)
|
||||
return acc
|
||||
}, {})
|
||||
}, [modelOptions])
|
||||
|
||||
const getNewPainting = useCallback(() => {
|
||||
return {
|
||||
...DEFAULT_PAINTING,
|
||||
model: painting.model || modelOptions[0]?.value || '',
|
||||
id: uuid()
|
||||
}
|
||||
}, [modelOptions, painting.model])
|
||||
|
||||
const selectedModelConfig = useMemo(
|
||||
() => MODELS.find((m) => m.name === painting.model) || MODELS[0],
|
||||
[painting.model]
|
||||
)
|
||||
|
||||
const handleModelChange = (value: string) => {
|
||||
const modelConfig = MODELS.find((m) => m.name === value)
|
||||
const updates: Partial<PaintingAction> = { model: value }
|
||||
|
||||
// 设置默认值
|
||||
if (modelConfig?.imageSizes?.length) {
|
||||
updates.size = modelConfig.imageSizes[0].value
|
||||
}
|
||||
if (modelConfig?.quality?.length) {
|
||||
updates.quality = modelConfig.quality[0].value
|
||||
}
|
||||
if (modelConfig?.moderation?.length) {
|
||||
updates.moderation = modelConfig.moderation[0].value
|
||||
}
|
||||
updates.n = 1
|
||||
updatePaintingState(updates)
|
||||
}
|
||||
|
||||
const handleSizeChange = (value: string) => {
|
||||
updatePaintingState({ size: value })
|
||||
}
|
||||
|
||||
const handleQualityChange = (value: string) => {
|
||||
updatePaintingState({ quality: value })
|
||||
}
|
||||
|
||||
const handleModerationChange = (value: string) => {
|
||||
updatePaintingState({ moderation: value })
|
||||
}
|
||||
|
||||
const handleNChange = (value: number | string | null) => {
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
updatePaintingState({ n: Number(value) })
|
||||
}
|
||||
}
|
||||
|
||||
const handleError = (error: unknown) => {
|
||||
if (error instanceof Error && error.name !== 'AbortError') {
|
||||
window.modal.error({
|
||||
content: getErrorMessage(error),
|
||||
centered: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const downloadImages = async (urls: string[]) => {
|
||||
const downloadedFiles = await Promise.all(
|
||||
urls.map(async (url) => {
|
||||
try {
|
||||
if (!url?.trim()) {
|
||||
console.error('图像URL为空')
|
||||
window.message.warning({
|
||||
content: t('message.empty_url'),
|
||||
key: 'empty-url-warning'
|
||||
})
|
||||
return null
|
||||
}
|
||||
return await window.api.file.download(url)
|
||||
} catch (error) {
|
||||
console.error('下载图像失败:', 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
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return downloadedFiles.filter((file): file is FileMetadata => file !== null)
|
||||
}
|
||||
|
||||
const onGenerate = async () => {
|
||||
if (painting.files.length > 0) {
|
||||
const confirmed = await window.modal.confirm({
|
||||
content: t('paintings.regenerate.confirm'),
|
||||
centered: true
|
||||
})
|
||||
|
||||
if (!confirmed) return
|
||||
await FileManager.deleteFiles(painting.files)
|
||||
}
|
||||
|
||||
const prompt = textareaRef.current?.resizableTextArea?.textArea?.value || ''
|
||||
updatePaintingState({ prompt })
|
||||
|
||||
if (!newApiProvider.enabled) {
|
||||
window.modal.error({
|
||||
content: t('error.provider_disabled'),
|
||||
centered: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const AI = new AiProvider(newApiProvider)
|
||||
|
||||
if (!AI.getApiKey()) {
|
||||
window.modal.error({
|
||||
content: t('error.no_api_key'),
|
||||
centered: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!painting.model || !painting.prompt) {
|
||||
return
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
setAbortController(controller)
|
||||
setIsLoading(true)
|
||||
dispatch(setGenerating(true))
|
||||
|
||||
let body: string | FormData = ''
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${AI.getApiKey()}`
|
||||
}
|
||||
const url = newApiProvider.apiHost + `/v1/images/generations`
|
||||
const editUrl = newApiProvider.apiHost + `/v1/images/edits`
|
||||
|
||||
try {
|
||||
if (mode === 'openai_image_generate') {
|
||||
const requestData = {
|
||||
prompt,
|
||||
model: painting.model,
|
||||
size: painting.size === 'auto' ? undefined : painting.size,
|
||||
background: painting.background === 'auto' ? undefined : painting.background,
|
||||
n: painting.n,
|
||||
quality: painting.quality === 'auto' ? undefined : painting.quality,
|
||||
moderation: painting.moderation === 'auto' ? undefined : painting.moderation
|
||||
}
|
||||
|
||||
body = JSON.stringify(requestData)
|
||||
headers['Content-Type'] = 'application/json'
|
||||
} else if (mode === 'openai_image_edit') {
|
||||
// -------- Edit Mode --------
|
||||
if (editImages.length === 0) {
|
||||
window.message.warning({ content: t('paintings.image_file_required') })
|
||||
return
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('prompt', prompt)
|
||||
if (painting.background && painting.background !== 'auto') {
|
||||
formData.append('background', painting.background)
|
||||
}
|
||||
|
||||
if (painting.size && painting.size !== 'auto') {
|
||||
formData.append('size', painting.size)
|
||||
}
|
||||
|
||||
if (painting.quality && painting.quality !== 'auto') {
|
||||
formData.append('quality', painting.quality)
|
||||
}
|
||||
|
||||
if (painting.moderation && painting.moderation !== 'auto') {
|
||||
formData.append('moderation', painting.moderation)
|
||||
}
|
||||
|
||||
// append images
|
||||
editImages.forEach((file) => {
|
||||
formData.append('image', file)
|
||||
})
|
||||
|
||||
// TODO: mask support later
|
||||
|
||||
body = formData
|
||||
|
||||
// For edit mode we do not set content-type; browser will set multipart boundary
|
||||
}
|
||||
|
||||
const requestUrl = mode === 'openai_image_edit' ? editUrl : url
|
||||
const response = await fetch(requestUrl, { method: 'POST', headers, body })
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error?.message || '生成图像失败')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const urls = data.data.filter((item) => item.url).map((item) => item.url)
|
||||
const base64s = data.data.filter((item) => item.b64_json).map((item) => item.b64_json)
|
||||
|
||||
if (urls.length > 0) {
|
||||
const validFiles = await downloadImages(urls)
|
||||
await FileManager.addFiles(validFiles)
|
||||
updatePaintingState({ files: validFiles, urls })
|
||||
}
|
||||
|
||||
if (base64s?.length > 0) {
|
||||
const validFiles = await Promise.all(
|
||||
base64s.map(async (base64) => {
|
||||
return await window.api.file.saveBase64Image(base64)
|
||||
})
|
||||
)
|
||||
await FileManager.addFiles(validFiles)
|
||||
updatePaintingState({ files: validFiles, urls: validFiles.map((file) => file.name) })
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
dispatch(setGenerating(false))
|
||||
setAbortController(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRetry = async (painting: PaintingAction) => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const validFiles = await downloadImages(painting.urls)
|
||||
await FileManager.addFiles(validFiles)
|
||||
updatePaintingState({ files: validFiles, urls: painting.urls })
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
abortController?.abort()
|
||||
}
|
||||
|
||||
const nextImage = () => {
|
||||
setCurrentImageIndex((prev) => (prev + 1) % painting.files.length)
|
||||
}
|
||||
|
||||
const prevImage = () => {
|
||||
setCurrentImageIndex((prev) => (prev - 1 + painting.files.length) % painting.files.length)
|
||||
}
|
||||
|
||||
const handleAddPainting = () => {
|
||||
const newPainting = addPainting(mode, getNewPainting())
|
||||
updatePainting(mode, newPainting)
|
||||
setPainting(newPainting)
|
||||
return newPainting
|
||||
}
|
||||
|
||||
const onDeletePainting = (paintingToDelete: PaintingAction) => {
|
||||
if (paintingToDelete.id === painting.id) {
|
||||
const currentIndex = filteredPaintings.findIndex((p) => p.id === paintingToDelete.id)
|
||||
|
||||
if (currentIndex > 0) {
|
||||
setPainting(filteredPaintings[currentIndex - 1])
|
||||
} else if (filteredPaintings.length > 1) {
|
||||
setPainting(filteredPaintings[1])
|
||||
}
|
||||
}
|
||||
|
||||
removePainting(mode, paintingToDelete)
|
||||
}
|
||||
|
||||
const translate = async () => {
|
||||
if (isTranslating) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!painting.prompt) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsTranslating(true)
|
||||
const translatedText = await translateText(painting.prompt, LanguagesEnum.enUS)
|
||||
updatePaintingState({ prompt: translatedText })
|
||||
} catch (error) {
|
||||
console.error('Translation failed:', error)
|
||||
} finally {
|
||||
setIsTranslating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (autoTranslateWithSpace && event.key === ' ') {
|
||||
setSpaceClickCount((prev) => prev + 1)
|
||||
|
||||
if (spaceClickTimer.current) {
|
||||
clearTimeout(spaceClickTimer.current)
|
||||
}
|
||||
|
||||
spaceClickTimer.current = setTimeout(() => {
|
||||
setSpaceClickCount(0)
|
||||
}, 200)
|
||||
|
||||
if (spaceClickCount === 2) {
|
||||
setSpaceClickCount(0)
|
||||
setIsTranslating(true)
|
||||
translate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleProviderChange = (providerId: string) => {
|
||||
const routeName = location.pathname.split('/').pop()
|
||||
if (providerId !== routeName) {
|
||||
navigate('../' + providerId, { replace: true })
|
||||
}
|
||||
}
|
||||
|
||||
// 处理模式切换
|
||||
const handleModeChange = (value: string) => {
|
||||
setMode(value as keyof PaintingsState)
|
||||
if (newApiPaintings[value as keyof PaintingsState] && newApiPaintings[value as keyof PaintingsState].length > 0) {
|
||||
setPainting(newApiPaintings[value as keyof PaintingsState][0])
|
||||
} else {
|
||||
setPainting(DEFAULT_PAINTING)
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染配置项的函数
|
||||
const onSelectPainting = (newPainting: PaintingAction) => {
|
||||
if (generating) return
|
||||
setPainting(newPainting)
|
||||
setCurrentImageIndex(0)
|
||||
}
|
||||
|
||||
const handleImageUpload = (file: File) => {
|
||||
setEditImageFiles((prev) => [...prev, file])
|
||||
return false // 阻止默认上传行为
|
||||
}
|
||||
|
||||
// 当 modelOptions 为空时,引导用户跳转到 Provider 设置页面,新增 image-generation 端点模型
|
||||
const handleShowAddModelPopup = () => {
|
||||
navigate(`/settings/provider?id=${newApiProvider.id}`)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (filteredPaintings.length === 0) {
|
||||
const newPainting = getNewPainting()
|
||||
addPainting(mode, newPainting)
|
||||
setPainting(newPainting)
|
||||
}
|
||||
}, [filteredPaintings, mode, addPainting, painting, getNewPainting])
|
||||
|
||||
useEffect(() => {
|
||||
const timer = spaceClickTimer.current
|
||||
return () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
|
||||
{isMac && (
|
||||
<NavbarRight style={{ justifyContent: 'flex-end' }}>
|
||||
<Button size="small" className="nodrag" icon={<PlusOutlined />} onClick={handleAddPainting}>
|
||||
{t('paintings.button.new.image')}
|
||||
</Button>
|
||||
</NavbarRight>
|
||||
)}
|
||||
</Navbar>
|
||||
<ContentContainer id="content-container">
|
||||
<LeftContainer>
|
||||
<ProviderTitleContainer>
|
||||
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
|
||||
<SettingHelpLink target="_blank" href={'https://docs.newapi.pro/apps/cherry-studio/'}>
|
||||
{t('paintings.learn_more')}
|
||||
<ProviderLogo
|
||||
shape="square"
|
||||
src={getProviderLogo(newApiProvider.id)}
|
||||
size={16}
|
||||
style={{ marginLeft: 5 }}
|
||||
/>
|
||||
</SettingHelpLink>
|
||||
</ProviderTitleContainer>
|
||||
|
||||
<Select
|
||||
value={providerOptions.find((p) => p.value === 'new-api')?.value}
|
||||
onChange={handleProviderChange}
|
||||
style={{ width: '100%' }}>
|
||||
{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>
|
||||
|
||||
{/* 当没有可用的 Image Generation 模型时,提示用户先去新增 */}
|
||||
{modelOptions.length === 0 && (
|
||||
<Empty
|
||||
style={{ marginTop: 24 }}
|
||||
description={t('paintings.no_image_generation_model', {
|
||||
endpoint_type: t('endpoint_type.image-generation')
|
||||
})}>
|
||||
<Button type="primary" onClick={handleShowAddModelPopup}>
|
||||
{t('paintings.go_to_settings')}
|
||||
</Button>
|
||||
</Empty>
|
||||
)}
|
||||
|
||||
{modelOptions.length > 0 && (
|
||||
<>
|
||||
{mode === 'openai_image_edit' && (
|
||||
<>
|
||||
<SettingTitle style={{ marginTop: 20 }}>{t('paintings.input_image')}</SettingTitle>
|
||||
<ImageUploadButton
|
||||
accept="image/png, image/jpeg, image/gif"
|
||||
maxCount={16}
|
||||
showUploadList={true}
|
||||
listType="picture"
|
||||
beforeUpload={handleImageUpload}>
|
||||
<ImagePlaceholder>
|
||||
<ImageSizeImage src={IcImageUp} theme={theme} />
|
||||
</ImagePlaceholder>
|
||||
</ImageUploadButton>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Model Selector */}
|
||||
<SettingTitle style={{ marginTop: 20 }}>{t('paintings.model')}</SettingTitle>
|
||||
<Select value={painting.model} onChange={handleModelChange} style={{ width: '100%', marginBottom: 15 }}>
|
||||
{Object.entries(groupedModelOptions).map(([groupName, options]) => (
|
||||
<Select.OptGroup label={groupName} key={groupName}>
|
||||
{options.map((m) => (
|
||||
<Select.Option value={m.value} key={m.value}>
|
||||
{m.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select.OptGroup>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{/* Image Size */}
|
||||
{selectedModelConfig?.imageSizes && selectedModelConfig.imageSizes.length > 0 && (
|
||||
<>
|
||||
<SettingTitle>{t('paintings.image.size')}</SettingTitle>
|
||||
<Select value={painting.size} onChange={handleSizeChange} style={{ width: '100%', marginBottom: 15 }}>
|
||||
{selectedModelConfig.imageSizes.map((s) => (
|
||||
<Select.Option value={s.value} key={s.value}>
|
||||
{t(`paintings.image_size_options.${s.value}`, { defaultValue: s.value })}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Quality */}
|
||||
{selectedModelConfig?.quality && selectedModelConfig.quality.length > 0 && (
|
||||
<>
|
||||
<SettingTitle>{t('paintings.quality')}</SettingTitle>
|
||||
<Select
|
||||
value={painting.quality}
|
||||
onChange={handleQualityChange}
|
||||
style={{ width: '100%', marginBottom: 15 }}>
|
||||
{selectedModelConfig.quality.map((q) => (
|
||||
<Select.Option value={q.value} key={q.value}>
|
||||
{t(`paintings.quality_options.${q.value}`, { defaultValue: q.value })}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Moderation */}
|
||||
{mode !== 'openai_image_edit' &&
|
||||
selectedModelConfig?.moderation &&
|
||||
selectedModelConfig.moderation.length > 0 && (
|
||||
<>
|
||||
<SettingTitle>{t('paintings.moderation')}</SettingTitle>
|
||||
<Select
|
||||
value={painting.moderation}
|
||||
onChange={handleModerationChange}
|
||||
style={{ width: '100%', marginBottom: 15 }}>
|
||||
{selectedModelConfig.moderation.map((m) => (
|
||||
<Select.Option value={m.value} key={m.value}>
|
||||
{t(`paintings.moderation_options.${m.value}`, { defaultValue: m.value })}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Background */}
|
||||
{mode === 'openai_image_edit' &&
|
||||
selectedModelConfig?.background &&
|
||||
selectedModelConfig.background.length > 0 && (
|
||||
<>
|
||||
<SettingTitle>{t('paintings.background')}</SettingTitle>
|
||||
<Select
|
||||
value={painting.background}
|
||||
onChange={(value) => updatePaintingState({ background: value })}
|
||||
style={{ width: '100%', marginBottom: 15 }}>
|
||||
{selectedModelConfig.background.map((b) => (
|
||||
<Select.Option value={b.value} key={b.value}>
|
||||
{t(`paintings.background_options.${b.value}`, { defaultValue: b.value })}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Number of Images (n) */}
|
||||
{selectedModelConfig?.max_images && (
|
||||
<>
|
||||
<SettingTitle>{t('paintings.number_images')}</SettingTitle>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={selectedModelConfig.max_images}
|
||||
value={painting.n || 1}
|
||||
onChange={handleNChange}
|
||||
style={{ width: '100%', marginBottom: 15 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</LeftContainer>
|
||||
<MainContainer>
|
||||
{/* 添加功能切换分段控制器 */}
|
||||
<ModeSegmentedContainer>
|
||||
<Segmented shape="round" value={mode} onChange={handleModeChange} options={modeOptions} />
|
||||
</ModeSegmentedContainer>
|
||||
<Artboard
|
||||
painting={painting}
|
||||
isLoading={isLoading}
|
||||
currentImageIndex={currentImageIndex}
|
||||
onPrevImage={prevImage}
|
||||
onNextImage={nextImage}
|
||||
onCancel={onCancel}
|
||||
retry={handleRetry}
|
||||
/>
|
||||
<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')
|
||||
: painting.model?.startsWith('imagen-')
|
||||
? t('paintings.prompt_placeholder_en')
|
||||
: t('paintings.prompt_placeholder_edit')
|
||||
}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<Toolbar>
|
||||
<ToolbarMenu>
|
||||
<TranslateButton
|
||||
text={textareaRef.current?.resizableTextArea?.textArea?.value}
|
||||
onTranslated={(translatedText) => updatePaintingState({ prompt: translatedText })}
|
||||
disabled={isLoading || isTranslating}
|
||||
isLoading={isTranslating}
|
||||
style={{ marginRight: 6, borderRadius: '50%' }}
|
||||
/>
|
||||
<SendMessageButton sendMessage={onGenerate} disabled={isLoading} />
|
||||
</ToolbarMenu>
|
||||
</Toolbar>
|
||||
</InputContainer>
|
||||
</MainContainer>
|
||||
<PaintingsList
|
||||
namespace={mode}
|
||||
paintings={filteredPaintings}
|
||||
selectedPainting={painting}
|
||||
onSelectPainting={onSelectPainting}
|
||||
onDeletePainting={onDeletePainting}
|
||||
onNewPainting={handleAddPainting}
|
||||
/>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
background-color: var(--color-background);
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const LeftContainer = styled(Scrollbar)`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
background-color: var(--color-background);
|
||||
max-width: var(--assistants-width);
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
const MainContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
|
||||
const InputContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 95px;
|
||||
max-height: 95px;
|
||||
position: relative;
|
||||
border: 1px solid var(--color-border-soft);
|
||||
transition: all 0.3s ease;
|
||||
margin: 0 20px 15px 20px;
|
||||
border-radius: 10px;
|
||||
`
|
||||
|
||||
const Textarea = styled(TextArea)`
|
||||
padding: 10px;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
resize: none !important;
|
||||
overflow: auto;
|
||||
width: auto;
|
||||
`
|
||||
|
||||
const Toolbar = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
padding: 0 8px;
|
||||
padding-bottom: 0;
|
||||
height: 40px;
|
||||
`
|
||||
|
||||
const ToolbarMenu = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
`
|
||||
|
||||
const ProviderLogo = styled(Avatar)`
|
||||
border: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
// 添加新的样式组件
|
||||
const ModeSegmentedContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 24px;
|
||||
`
|
||||
|
||||
const SelectOptionContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
// 添加新的样式组件
|
||||
const ProviderTitleContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
`
|
||||
|
||||
const ImageUploadButton = styled(Upload)`
|
||||
& .ant-upload.ant-upload-select {
|
||||
width: 100% !important;
|
||||
height: 60px !important;
|
||||
border: 1px dashed var(--color-border);
|
||||
}
|
||||
`
|
||||
|
||||
const ImagePlaceholder = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const ImageSizeImage = styled.img<{ theme: string }>`
|
||||
filter: ${({ theme }) => (theme === 'dark' ? 'invert(100%)' : 'none')};
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
`
|
||||
|
||||
export default NewApiPage
|
||||
@ -6,10 +6,11 @@ import { Route, Routes, useParams } from 'react-router-dom'
|
||||
|
||||
import AihubmixPage from './AihubmixPage'
|
||||
import DmxapiPage from './DmxapiPage'
|
||||
import NewApiPage from './NewApiPage'
|
||||
import SiliconPage from './SiliconPage'
|
||||
import TokenFluxPage from './TokenFluxPage'
|
||||
|
||||
const Options = ['aihubmix', 'silicon', 'dmxapi', 'tokenflux']
|
||||
const Options = ['aihubmix', 'silicon', 'dmxapi', 'tokenflux', 'new-api']
|
||||
|
||||
const PaintingsRoutePage: FC = () => {
|
||||
const params = useParams()
|
||||
@ -30,6 +31,7 @@ const PaintingsRoutePage: FC = () => {
|
||||
<Route path="/silicon" element={<SiliconPage Options={Options} />} />
|
||||
<Route path="/dmxapi" element={<DmxapiPage Options={Options} />} />
|
||||
<Route path="/tokenflux" element={<TokenFluxPage Options={Options} />} />
|
||||
<Route path="/new-api" element={<NewApiPage Options={Options} />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
47
src/renderer/src/pages/paintings/config/NewApiConfig.ts
Normal file
47
src/renderer/src/pages/paintings/config/NewApiConfig.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { GeneratePainting } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
|
||||
export const SUPPORTED_MODELS = ['gpt-image-1']
|
||||
|
||||
export const MODELS = [
|
||||
{
|
||||
name: 'gpt-image-1',
|
||||
group: 'OpenAI',
|
||||
imageSizes: [{ value: 'auto' }, { value: '1024x1024' }, { value: '1536x1024' }, { value: '1024x1536' }],
|
||||
max_images: 10,
|
||||
quality: [{ value: 'auto' }, { value: 'high' }, { value: 'medium' }, { value: 'low' }],
|
||||
response_format: [{ value: 'b64_json' }],
|
||||
moderation: [{ value: 'auto' }, { value: 'low' }],
|
||||
output_compression_format: [{ value: 'jpeg' }, { value: 'webp' }],
|
||||
output_format: [{ value: 'image/png' }, { value: 'image/jpeg' }, { value: 'image/webp' }],
|
||||
background: [{ value: 'auto' }, { value: 'transparent' }, { value: 'opaque' }]
|
||||
}
|
||||
]
|
||||
|
||||
export const DEFAULT_PAINTING: GeneratePainting = {
|
||||
id: uuid(),
|
||||
urls: [],
|
||||
files: [],
|
||||
model: '',
|
||||
prompt: '',
|
||||
quality: 'auto',
|
||||
n: 1,
|
||||
background: 'auto',
|
||||
moderation: 'auto',
|
||||
size: 'auto'
|
||||
}
|
||||
|
||||
export const getModelGroup = (model: string): string => {
|
||||
const modelConfig = MODELS.find((m) => m.name === model)
|
||||
if (modelConfig) {
|
||||
return modelConfig.group
|
||||
}
|
||||
if (model.includes('flux')) {
|
||||
return 'Black Forest Lab'
|
||||
} else if (model.includes('imagen')) {
|
||||
return 'Gemini'
|
||||
} else if (model.includes('dall-e')) {
|
||||
return 'OpenAI'
|
||||
}
|
||||
return 'Custom'
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||
import { endpointTypeOptions } from '@renderer/config/endpointTypes'
|
||||
import {
|
||||
isEmbeddingModel,
|
||||
isFunctionCallingModel,
|
||||
@ -147,11 +148,11 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ provider, model, onUpdate
|
||||
tooltip={t('settings.models.add.endpoint_type.tooltip')}
|
||||
rules={[{ required: true, message: t('settings.models.add.endpoint_type.required') }]}>
|
||||
<Select placeholder={t('settings.models.add.endpoint_type.placeholder')}>
|
||||
<Select.Option value="openai">OpenAI</Select.Option>
|
||||
<Select.Option value="openai-response">OpenAI-Response</Select.Option>
|
||||
<Select.Option value="anthropic">Anthropic</Select.Option>
|
||||
<Select.Option value="gemini">Gemini</Select.Option>
|
||||
<Select.Option value="jina-rerank">Jina-Rerank</Select.Option>
|
||||
{endpointTypeOptions.map((opt) => (
|
||||
<Select.Option key={opt.value} value={opt.value}>
|
||||
{t(opt.label)}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { endpointTypeOptions } from '@renderer/config/endpointTypes'
|
||||
import { useDynamicLabelWidth } from '@renderer/hooks/useDynamicLabelWidth'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { EndpointType, Model, Provider } from '@renderer/types'
|
||||
@ -12,6 +13,7 @@ interface ShowParams {
|
||||
title: string
|
||||
provider: Provider
|
||||
model?: Model
|
||||
endpointType?: EndpointType
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
@ -26,7 +28,7 @@ type FieldType = {
|
||||
endpointType?: EndpointType
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ title, provider, resolve, model }) => {
|
||||
const PopupContainer: React.FC<Props> = ({ title, provider, resolve, model, endpointType }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const [form] = Form.useForm()
|
||||
const { addModel, models } = useProvider(provider.id)
|
||||
@ -104,10 +106,10 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve, model }) =>
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
group: model.group,
|
||||
endpointType: 'openai'
|
||||
endpointType: endpointType ?? 'openai'
|
||||
}
|
||||
: {
|
||||
endpointType: 'openai'
|
||||
endpointType: endpointType ?? 'openai'
|
||||
}
|
||||
}>
|
||||
<Form.Item
|
||||
@ -143,11 +145,11 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve, model }) =>
|
||||
tooltip={t('settings.models.add.endpoint_type.tooltip')}
|
||||
rules={[{ required: true, message: t('settings.models.add.endpoint_type.required') }]}>
|
||||
<Select placeholder={t('settings.models.add.endpoint_type.placeholder')}>
|
||||
<Select.Option value="openai">OpenAI</Select.Option>
|
||||
<Select.Option value="openai-response">OpenAI-Response</Select.Option>
|
||||
<Select.Option value="anthropic">Anthropic</Select.Option>
|
||||
<Select.Option value="gemini">Gemini</Select.Option>
|
||||
<Select.Option value="jina-rerank">Jina-Rerank</Select.Option>
|
||||
{endpointTypeOptions.map((opt) => (
|
||||
<Select.Option key={opt.value} value={opt.value}>
|
||||
{t(opt.label)}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item style={{ marginBottom: 8, textAlign: 'center' }}>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { endpointTypeOptions } from '@renderer/config/endpointTypes'
|
||||
import { useDynamicLabelWidth } from '@renderer/hooks/useDynamicLabelWidth'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { EndpointType, Model, Provider } from '@renderer/types'
|
||||
@ -83,11 +84,11 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve, batchModels
|
||||
tooltip={t('settings.models.add.endpoint_type.tooltip')}
|
||||
rules={[{ required: true, message: t('settings.models.add.endpoint_type.required') }]}>
|
||||
<Select placeholder={t('settings.models.add.endpoint_type.placeholder')}>
|
||||
<Select.Option value="openai">OpenAI</Select.Option>
|
||||
<Select.Option value="openai-response">OpenAI-Response</Select.Option>
|
||||
<Select.Option value="anthropic">Anthropic</Select.Option>
|
||||
<Select.Option value="gemini">Gemini</Select.Option>
|
||||
<Select.Option value="jina-rerank">Jina-Rerank</Select.Option>
|
||||
{endpointTypeOptions.map((opt) => (
|
||||
<Select.Option key={opt.value} value={opt.value}>
|
||||
{t(opt.label)}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item style={{ marginBottom: 8, textAlign: 'center' }}>
|
||||
|
||||
@ -8,7 +8,9 @@ const initialState: PaintingsState = {
|
||||
edit: [],
|
||||
upscale: [],
|
||||
DMXAPIPaintings: [],
|
||||
tokenFluxPaintings: []
|
||||
tokenFluxPaintings: [],
|
||||
openai_image_generate: [],
|
||||
openai_image_edit: []
|
||||
}
|
||||
|
||||
const paintingsSlice = createSlice({
|
||||
|
||||
@ -178,7 +178,7 @@ export type ProviderType =
|
||||
|
||||
export type ModelType = 'text' | 'vision' | 'embedding' | 'reasoning' | 'function_calling' | 'web_search'
|
||||
|
||||
export type EndpointType = 'openai' | 'openai-response' | 'anthropic' | 'gemini' | 'jina-rerank'
|
||||
export type EndpointType = 'openai' | 'openai-response' | 'anthropic' | 'gemini' | 'image-generation' | 'jina-rerank'
|
||||
|
||||
export type ModelPricing = {
|
||||
input_per_million_tokens: number
|
||||
@ -209,7 +209,7 @@ export type PaintingParams = {
|
||||
files: FileMetadata[]
|
||||
}
|
||||
|
||||
export type PaintingProvider = 'aihubmix' | 'silicon' | 'dmxapi'
|
||||
export type PaintingProvider = 'aihubmix' | 'silicon' | 'dmxapi' | 'new-api'
|
||||
|
||||
export interface Painting extends PaintingParams {
|
||||
model?: string
|
||||
@ -318,6 +318,8 @@ export interface PaintingsState {
|
||||
upscale: Partial<ScalePainting> & PaintingParams[]
|
||||
DMXAPIPaintings: DmxapiPainting[]
|
||||
tokenFluxPaintings: TokenFluxPainting[]
|
||||
openai_image_generate: Partial<GeneratePainting> & PaintingParams[]
|
||||
openai_image_edit: Partial<EditPainting> & PaintingParams[]
|
||||
}
|
||||
|
||||
export type MinAppType = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user