feat: support NewAPI as a generic provider type (#10696)

* feat: add support for New API providerType

* feat: support New API as a generic painting provider

* refactor: update styling in painting pages to use Tailwind classes

- Replaced inline styles with Tailwind CSS classes for margin adjustments in AihubmixPage, DmxapiPage, SiliconPage, TokenFluxPage, and ZhipuPage.
- Enhanced consistency and maintainability of the codebase by standardizing styling approach across components.
- Minor refactor in ProviderSelect component to support className prop for better styling flexibility.
This commit is contained in:
Calcium-Ion 2025-10-16 13:07:28 +08:00 committed by kangfenmao
parent 726b2570e2
commit 0502ff48f1
13 changed files with 204 additions and 253 deletions

View File

@ -62,13 +62,14 @@ function handleSpecialProviders(model: Model, provider: Provider): Provider {
// return createVertexProvider(provider)
// }
if (isNewApiProvider(provider)) {
return newApiResolverCreator(model, provider)
}
if (isSystemProvider(provider)) {
if (provider.id === 'aihubmix') {
return aihubmixProviderCreator(model, provider)
}
if (isNewApiProvider(provider)) {
return newApiResolverCreator(model, provider)
}
if (provider.id === 'vertexai') {
return vertexAnthropicProviderCreator(model, provider)
}

View File

@ -283,7 +283,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
'new-api': {
id: 'new-api',
name: 'New API',
type: 'openai',
type: 'new-api',
apiKey: '',
apiHost: 'http://localhost:3000',
models: SYSTEM_MODELS['new-api'],
@ -1422,5 +1422,5 @@ export const isGeminiWebSearchProvider = (provider: Provider) => {
}
export const isNewApiProvider = (provider: Provider) => {
return ['new-api', 'cherryin'].includes(provider.id)
return ['new-api', 'cherryin'].includes(provider.id) || provider.type === 'new-api'
}

View File

@ -14,7 +14,6 @@ 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 { getProviderLabel } from '@renderer/i18n/label'
import FileManager from '@renderer/services/FileManager'
import { translateText } from '@renderer/services/TranslateService'
import { useAppDispatch } from '@renderer/store'
@ -35,6 +34,7 @@ import SendMessageButton from '../home/Inputbar/SendMessageButton'
import { SettingHelpLink, SettingTitle } from '../settings'
import Artboard from './components/Artboard'
import PaintingsList from './components/PaintingsList'
import ProviderSelect from './components/ProviderSelect'
import { type ConfigItem, createModeConfigs, DEFAULT_PAINTING } from './config/aihubmixConfig'
import { checkProviderEnabled } from './utils'
@ -76,20 +76,6 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
const { t } = useTranslation()
const { theme } = useTheme()
const providers = useAllProviders()
const providerOptions = Options.map((option) => {
const provider = providers.find((p) => p.id === option)
if (provider) {
return {
label: getProviderLabel(provider.id),
value: provider.id
}
} else {
return {
label: 'Unknown Provider',
value: undefined
}
}
})
const dispatch = useAppDispatch()
const { generating } = useRuntime()
const navigate = useNavigate()
@ -849,17 +835,12 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
/>
</SettingHelpLink>
</ProviderTitleContainer>
<Select value={providerOptions[1].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>
<ProviderSelect
provider={aihubmixProvider}
options={Options}
onChange={handleProviderChange}
className={'mb-4'}
/>
{/* 使用JSON配置渲染设置项 */}
{modeConfigs[mode].filter((item) => (item.condition ? item.condition(painting) : true)).map(renderConfigItem)}
@ -1034,12 +1015,6 @@ const ModeSegmentedContainer = styled.div`
padding-top: 24px;
`
const SelectOptionContainer = styled.div`
display: flex;
align-items: center;
gap: 8px;
`
// 添加新的样式组件
const ProviderTitleContainer = styled.div`
display: flex;

View File

@ -8,7 +8,6 @@ import { getProviderLogo } from '@renderer/config/providers'
import { usePaintings } from '@renderer/hooks/usePaintings'
import { useAllProviders } from '@renderer/hooks/useProvider'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { getProviderLabel } from '@renderer/i18n/label'
import FileManager from '@renderer/services/FileManager'
import { useAppDispatch } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
@ -29,6 +28,7 @@ import { SettingHelpLink, SettingTitle } from '../settings'
import Artboard from './components/Artboard'
import ImageUploader from './components/ImageUploader'
import PaintingsList from './components/PaintingsList'
import ProviderSelect from './components/ProviderSelect'
import {
COURSE_URL,
DEFAULT_PAINTING,
@ -46,20 +46,6 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
const [painting, setPainting] = useState<DmxapiPainting>(dmxapi_paintings?.[0] || DEFAULT_PAINTING)
const { t } = useTranslation()
const providers = useAllProviders()
const providerOptions = Options.map((option) => {
const provider = providers.find((p) => p.id === option)
if (provider) {
return {
label: getProviderLabel(provider.id),
value: provider.id
}
} else {
return {
label: 'Unknown Provider',
value: undefined
}
}
})
const dmxapiProvider = providers.find((p) => p.id === 'dmxapi')!
@ -785,9 +771,9 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
return (
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
<NavbarCenter className="border-r-0">{t('paintings.title')}</NavbarCenter>
{isMac && (
<NavbarRight style={{ justifyContent: 'flex-end' }}>
<NavbarRight className="justify-end">
<Button size="small" className="nodrag" icon={<PlusOutlined />} onClick={createNewPainting}>
{t('paintings.button.new.image')}
</Button>
@ -797,7 +783,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
<ContentContainer id="content-container">
<LeftContainer>
<ProviderTitleContainer>
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
<SettingTitle className="mb-1">{t('common.provider')}</SettingTitle>
<div>
<SettingHelpLink target="_blank" href={COURSE_URL}>
{t('paintings.paint_course')}
@ -805,28 +791,19 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
<SettingHelpLink target="_blank" href={TOP_UP_URL}>
{t('paintings.top_up')}
</SettingHelpLink>
<ProviderLogo
shape="square"
src={getProviderLogo(dmxapiProvider.id)}
size={16}
style={{ marginLeft: 5 }}
/>
<ProviderLogo shape="square" src={getProviderLogo(dmxapiProvider.id)} size={16} className="ml-1" />
</div>
</ProviderTitleContainer>
<Select value={providerOptions[3].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>
<ProviderSelect
provider={dmxapiProvider}
options={Options}
onChange={handleProviderChange}
className="mb-4"
/>
{painting.generationMode &&
[generationModeType.EDIT, generationModeType.MERGE].includes(painting.generationMode) && (
<>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}></SettingTitle>
<SettingTitle className="mt-4 mb-1"></SettingTitle>
<ImageUploader
fileMap={fileMap}
maxImages={painting.generationMode === generationModeType.EDIT ? 1 : 3}
@ -838,13 +815,13 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
</>
)}
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
<SettingTitle className="mt-4 mb-1">
{t('common.model')} <SettingPrice>{painting.priceModel !== '0' ? painting.priceModel : ''}</SettingPrice>
</SettingTitle>
<Select
value={painting.model}
onChange={onSelectModel}
style={{ width: '100%' }}
className="w-full"
loading={isLoadingModels}
placeholder={isLoadingModels ? t('common.loading') : t('paintings.select_model')}>
{Object.entries(modelOptions).map(([provider, models]) => {
@ -861,11 +838,11 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
})}
</Select>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.image.size')}</SettingTitle>
<SettingTitle className="mt-4 mb-1">{t('paintings.image.size')}</SettingTitle>
<Select
value={isCustomSize ? 'custom' : painting.image_size}
onChange={(value) => onSelectImageSize(value)}
style={{ width: '100%' }}>
className="w-full">
{(() => {
const currentModel = allModels.find((m) => m.id === painting.model)
const modelImageSizes = currentModel?.image_sizes || []
@ -874,7 +851,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
return modelImageSizes.map((size) => {
return (
<Select.Option key={size.value} value={size.value}>
<HStack style={{ alignItems: 'center', gap: 8 }}>
<HStack className="items-center gap-2">
<span>{size.label}</span>
</HStack>
</Select.Option>
@ -884,7 +861,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
{/* 检查当前模型是否支持自定义尺寸 */}
{allModels.find((m) => m.id === painting.model)?.is_custom_size && (
<Select.Option value="custom" key="custom">
<HStack style={{ alignItems: 'center', gap: 8 }}>
<HStack className="items-center gap-2">
<span>{t('paintings.custom_size')}</span>
</HStack>
</Select.Option>
@ -893,7 +870,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
{/* 自定义尺寸输入框 */}
{isCustomSize && allModels.find((m) => m.id === painting.model)?.is_custom_size && (
<div style={{ marginTop: 10 }}>
<div className="mt-2.5">
<HStack style={{ gap: 8, alignItems: 'center' }}>
<InputNumber
placeholder="W"
@ -921,7 +898,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
{painting.generationMode === generationModeType.GENERATION && (
<>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
<SettingTitle className="mt-4 mb-1">
{t('paintings.seed')}
<Tooltip title={t('paintings.seed_desc_tip')}>
<InfoIcon />
@ -941,7 +918,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
</>
)}
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.style_type')}</SettingTitle>
<SettingTitle className="mt-4 mb-1">{t('paintings.style_type')}</SettingTitle>
<SliderContainer>
<RadioTextBox>
{STYLE_TYPE_OPTIONS.map((ele) => (
@ -955,7 +932,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
</RadioTextBox>
</SliderContainer>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
<SettingTitle className="mt-4 mb-1">
{t('paintings.auto_create_paint')}
<Tooltip title={t('paintings.auto_create_paint_tip')}>
<InfoIcon />
@ -1032,11 +1009,6 @@ const ProviderTitleContainer = styled.div`
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;

View File

@ -6,7 +6,7 @@ import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navb
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 { getProviderLogo, isNewApiProvider, PROVIDER_URLS } from '@renderer/config/providers'
import { LanguagesEnum } from '@renderer/config/translate'
import { useTheme } from '@renderer/context/ThemeProvider'
import { usePaintings } from '@renderer/hooks/usePaintings'
@ -17,8 +17,7 @@ import {
getPaintingsBackgroundOptionsLabel,
getPaintingsImageSizeOptionsLabel,
getPaintingsModerationOptionsLabel,
getPaintingsQualityOptionsLabel,
getProviderLabel
getPaintingsQualityOptionsLabel
} from '@renderer/i18n/label'
import PaintingsList from '@renderer/pages/paintings/components/PaintingsList'
import { DEFAULT_PAINTING, MODELS, SUPPORTED_MODELS } from '@renderer/pages/paintings/config/NewApiConfig'
@ -40,6 +39,7 @@ import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
import { SettingHelpLink, SettingTitle } from '../settings'
import Artboard from './components/Artboard'
import ProviderSelect from './components/ProviderSelect'
import { checkProviderEnabled } from './utils'
const logger = loggerService.withContext('NewApiPage')
@ -55,8 +55,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
}
}, [openai_image_generate, openai_image_edit])
const filteredPaintings = useMemo(() => newApiPaintings[mode] || [], [newApiPaintings, mode])
const [painting, setPainting] = useState<PaintingAction>(filteredPaintings[0] || DEFAULT_PAINTING)
// moved below after newApiProvider is defined
const [currentImageIndex, setCurrentImageIndex] = useState(0)
const [isLoading, setIsLoading] = useState(false)
const [abortController, setAbortController] = useState<AbortController | null>(null)
@ -67,27 +66,22 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
const { t } = useTranslation()
const { theme } = useTheme()
const providers = useAllProviders()
const providerOptions = Options.map((option) => {
const provider = providers.find((p) => p.id === option)
if (provider) {
return {
label: getProviderLabel(provider.id),
value: provider.id
}
} else {
return {
label: 'Unknown Provider',
value: undefined
}
}
})
const location = useLocation()
const routeName = location.pathname.split('/').pop() || 'new-api'
const newApiProviders = providers.filter((p) => isNewApiProvider(p))
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 newApiProvider = newApiProviders.find((p) => p.id === routeName) || newApiProviders[0]
const filteredPaintings = useMemo(
() => (newApiPaintings[mode] || []).filter((p) => p.providerId === newApiProvider.id),
[newApiPaintings, mode, newApiProvider.id]
)
const [painting, setPainting] = useState<PaintingAction>({ ...DEFAULT_PAINTING, providerId: newApiProvider.id })
const modeOptions = [
{ label: t('paintings.mode.generate'), value: 'openai_image_generate' },
@ -102,7 +96,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
}, [editImageFiles])
const updatePaintingState = (updates: Partial<PaintingAction>) => {
const updatedPainting = { ...painting, ...updates }
const updatedPainting = { ...painting, providerId: newApiProvider.id, ...updates }
setPainting(updatedPainting)
updatePainting(mode, updatedPainting)
}
@ -138,9 +132,10 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
return {
...DEFAULT_PAINTING,
model: painting.model || modelOptions[0]?.value || '',
id: uuid()
id: uuid(),
providerId: newApiProvider.id
}
}, [modelOptions, painting.model])
}, [modelOptions, painting.model, newApiProvider.id])
const selectedModelConfig = useMemo(
() => MODELS.find((m) => m.name === painting.model) || MODELS[0],
@ -444,11 +439,10 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
// 处理模式切换
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 list = (newApiPaintings[value as keyof PaintingsState] || []).filter(
(p) => p.providerId === newApiProvider.id
)
setPainting(list[0] || { ...DEFAULT_PAINTING, providerId: newApiProvider.id })
}
// 渲染配置项的函数
@ -473,8 +467,10 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
const newPainting = getNewPainting()
addPainting(mode, newPainting)
setPainting(newPainting)
} else {
setPainting(filteredPaintings[0])
}
}, [filteredPaintings, mode, addPainting, painting, getNewPainting])
}, [filteredPaintings, mode, addPainting, getNewPainting])
useEffect(() => {
const timer = spaceClickTimer.current
@ -501,7 +497,9 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
<LeftContainer>
<ProviderTitleContainer>
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
<SettingHelpLink target="_blank" href={'https://docs.newapi.pro/apps/cherry-studio/'}>
<SettingHelpLink
target="_blank"
href={PROVIDER_URLS[newApiProvider.id]?.websites?.docs || 'https://docs.newapi.pro/apps/cherry-studio/'}>
{t('paintings.learn_more')}
<ProviderLogo
shape="square"
@ -512,19 +510,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
</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>
<ProviderSelect provider={newApiProvider} options={Options} onChange={handleProviderChange} />
{/* 当没有可用的 Image Generation 模型时,提示用户先去新增 */}
{modelOptions.length === 0 && (
@ -792,20 +778,12 @@ 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;

View File

@ -1,8 +1,10 @@
import { loggerService } from '@logger'
import { isNewApiProvider } from '@renderer/config/providers'
import { useAllProviders } from '@renderer/hooks/useProvider'
import { useAppDispatch } from '@renderer/store'
import { setDefaultPaintingProvider } from '@renderer/store/settings'
import { PaintingProvider } from '@renderer/types'
import { FC, useEffect } from 'react'
import { PaintingProvider, SystemProviderId } from '@renderer/types'
import { FC, useEffect, useMemo } from 'react'
import { Route, Routes, useParams } from 'react-router-dom'
import AihubmixPage from './AihubmixPage'
@ -14,19 +16,23 @@ import ZhipuPage from './ZhipuPage'
const logger = loggerService.withContext('PaintingsRoutePage')
const Options = ['zhipu', 'aihubmix', 'silicon', 'dmxapi', 'tokenflux', 'new-api']
const BASE_OPTIONS: SystemProviderId[] = ['zhipu', 'aihubmix', 'silicon', 'dmxapi', 'tokenflux']
const PaintingsRoutePage: FC = () => {
const params = useParams()
const provider = params['*']
const dispatch = useAppDispatch()
const providers = useAllProviders()
const Options = useMemo(() => {
return [...BASE_OPTIONS, ...providers.filter((p) => isNewApiProvider(p)).map((p) => p.id)]
}, [providers])
useEffect(() => {
logger.debug(`defaultPaintingProvider: ${provider}`)
if (provider && Options.includes(provider)) {
dispatch(setDefaultPaintingProvider(provider as PaintingProvider))
}
}, [provider, dispatch])
}, [provider, dispatch, Options])
return (
<Routes>
@ -36,7 +42,12 @@ 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} />} />
{/* new-api family providers are mounted dynamically below */}
{providers
.filter((p) => isNewApiProvider(p))
.map((p) => (
<Route key={p.id} path={`/${p.id}`} element={<NewApiPage Options={Options} />} />
))}
</Routes>
)
}

View File

@ -12,14 +12,12 @@ import { HStack, VStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar'
import TranslateButton from '@renderer/components/TranslateButton'
import { isMac } from '@renderer/config/constant'
import { getProviderLogo } from '@renderer/config/providers'
import { 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 { getProviderLabel } from '@renderer/i18n/label'
import { getProviderByModel } from '@renderer/services/AssistantService'
import FileManager from '@renderer/services/FileManager'
import { translateText } from '@renderer/services/TranslateService'
@ -27,7 +25,7 @@ import { useAppDispatch } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import type { FileMetadata, Painting } from '@renderer/types'
import { getErrorMessage, uuid } from '@renderer/utils'
import { Avatar, Button, Input, InputNumber, Radio, Select, Slider, Switch, Tooltip } from 'antd'
import { Button, Input, InputNumber, Radio, Select, Slider, Switch, Tooltip } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { Info } from 'lucide-react'
import type { FC } from 'react'
@ -40,6 +38,7 @@ import SendMessageButton from '../home/Inputbar/SendMessageButton'
import { SettingTitle } from '../settings'
import Artboard from './components/Artboard'
import PaintingsList from './components/PaintingsList'
import ProviderSelect from './components/ProviderSelect'
import { checkProviderEnabled } from './utils'
export const TEXT_TO_IMAGES_MODELS = [
@ -115,22 +114,8 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
const [painting, setPainting] = useState<Painting>(siliconflow_paintings[0] || DEFAULT_PAINTING)
const { theme } = useTheme()
const providers = useAllProviders()
const providerOptions = Options.map((option) => {
const provider = providers.find((p) => p.id === option)
if (provider) {
return {
label: getProviderLabel(provider.id),
value: provider.id
}
} else {
return {
label: 'Unknown Provider',
value: undefined
}
}
})
const siliconflowProvider = providers.find((p) => p.id === 'silicon')
const siliconFlowProvider = providers.find((p) => p.id === 'silicon')!
const [currentImageIndex, setCurrentImageIndex] = useState(0)
const [isLoading, setIsLoading] = useState(false)
@ -170,7 +155,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
}
const onGenerate = async () => {
await checkProviderEnabled(siliconflowProvider!, t)
await checkProviderEnabled(siliconFlowProvider!, t)
if (painting.files.length > 0) {
const confirmed = await window.modal.confirm({
@ -389,17 +374,8 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
<ContentContainer id="content-container">
<LeftContainer>
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
<Select value={providerOptions[2].value} onChange={handleProviderChange}>
{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>
<ProviderSelect provider={siliconFlowProvider} options={Options} onChange={handleProviderChange} />
<SettingTitle className="mt-4 mb-1">{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
@ -672,14 +648,4 @@ const StyledInputNumber = styled(InputNumber)`
width: 70px;
`
const SelectOptionContainer = styled.div`
display: flex;
align-items: center;
gap: 8px;
`
const ProviderLogo = styled(Avatar)`
flex-shrink: 0;
`
export default SiliconPage

View File

@ -10,7 +10,6 @@ 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 { getProviderLabel } from '@renderer/i18n/label'
import FileManager from '@renderer/services/FileManager'
import { translateText } from '@renderer/services/TranslateService'
import { useAppDispatch } from '@renderer/store'
@ -31,6 +30,7 @@ import { SettingHelpLink, SettingTitle } from '../settings'
import Artboard from './components/Artboard'
import { DynamicFormRender } from './components/DynamicFormRender'
import PaintingsList from './components/PaintingsList'
import ProviderSelect from './components/ProviderSelect'
import { DEFAULT_TOKENFLUX_PAINTING, type TokenFluxModel } from './config/tokenFluxConfig'
import { checkProviderEnabled } from './utils'
import TokenFluxService from './utils/TokenFluxService'
@ -55,21 +55,6 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
tokenFluxPaintings[0] || { ...DEFAULT_TOKENFLUX_PAINTING, id: uuid() }
)
const providerOptions = Options.map((option) => {
const provider = providers.find((p) => p.id === option)
if (provider) {
return {
label: getProviderLabel(provider.id),
value: provider.id
}
} else {
return {
label: 'Unknown Provider',
value: undefined
}
}
})
const dispatch = useAppDispatch()
const { generating } = useRuntime()
const navigate = useNavigate()
@ -387,19 +372,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
</SettingHelpLink>
</ProviderTitleContainer>
<Select
value={providerOptions.find((p) => p.value === 'tokenflux')?.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>
<ProviderSelect provider={tokenfluxProvider} options={Options} onChange={handleProviderChange} />
{/* Model & Pricing Section */}
<SectionTitle
@ -775,11 +748,4 @@ const ProviderTitleContainer = styled.div`
align-items: center;
margin-bottom: 5px;
`
const SelectOptionContainer = styled.div`
display: flex;
align-items: center;
gap: 8px;
`
export default TokenFluxPage

View File

@ -8,7 +8,6 @@ import { getProviderLogo } from '@renderer/config/providers'
import { usePaintings } from '@renderer/hooks/usePaintings'
import { useAllProviders } from '@renderer/hooks/useProvider'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { getProviderLabel } from '@renderer/i18n/label'
import FileManager from '@renderer/services/FileManager'
import { useAppDispatch } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
@ -24,6 +23,7 @@ import SendMessageButton from '../home/Inputbar/SendMessageButton'
import { SettingHelpLink, SettingTitle } from '../settings'
import Artboard from './components/Artboard'
import PaintingsList from './components/PaintingsList'
import ProviderSelect from './components/ProviderSelect'
import {
COURSE_URL,
DEFAULT_PAINTING,
@ -50,21 +50,6 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [painting?.id]) // 只在painting的id改变时执行避免无限循环
const providerOptions = Options.map((option) => {
const provider = providers.find((p) => p.id === option)
if (provider) {
return {
label: getProviderLabel(provider.id),
value: provider.id
}
} else {
return {
label: 'Unknown Provider',
value: undefined
}
}
})
const zhipuProvider = providers.find((p) => p.id === 'zhipu')!
const [currentImageIndex, setCurrentImageIndex] = useState(0)
@ -370,16 +355,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
/>
</div>
</ProviderTitleContainer>
<Select value={providerOptions[0].value} onChange={handleProviderChange} style={{ marginBottom: 15 }}>
{providerOptions.map((provider) => (
<Select.Option value={provider.value} key={provider.value}>
<SelectOptionContainer>
<ProviderLogo shape="square" src={getProviderLogo(provider.value || '')} size={16} />
{provider.label}
</SelectOptionContainer>
</Select.Option>
))}
</Select>
<ProviderSelect provider={zhipuProvider} options={Options} onChange={handleProviderChange} className="mb-4" />
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('common.model')}</SettingTitle>
<Select
@ -563,12 +539,6 @@ const ProviderTitleContainer = styled.div`
margin-bottom: 10px;
`
const SelectOptionContainer = styled.div`
display: flex;
align-items: center;
gap: 8px;
`
const ProviderLogo = styled(Avatar)`
border-radius: 4px;
`

View File

@ -0,0 +1,99 @@
import { Select, SelectItem } from '@heroui/react'
import { ProviderAvatarPrimitive } from '@renderer/components/ProviderAvatar'
import { getProviderLogo } from '@renderer/config/providers'
import ImageStorage from '@renderer/services/ImageStorage'
import { getProviderNameById } from '@renderer/services/ProviderService'
import { Provider } from '@types'
import React, { FC, useEffect, useState } from 'react'
type ProviderSelectProps = {
provider: Provider
options: string[]
onChange: (value: string) => void
style?: React.CSSProperties
className?: string
}
const ProviderSelect: FC<ProviderSelectProps> = ({ provider, options, onChange, style, className }) => {
const [customLogos, setCustomLogos] = useState<Record<string, string>>({})
useEffect(() => {
const loadLogos = async () => {
const logos: Record<string, string> = {}
for (const providerId of options) {
try {
const logoData = await ImageStorage.get(`provider-${providerId}`)
if (logoData) {
logos[providerId] = logoData
}
} catch (error) {
// Ignore errors for providers without custom logos
}
}
setCustomLogos(logos)
}
loadLogos()
}, [options])
const getProviderLogoSrc = (providerId: string) => {
const systemLogo = getProviderLogo(providerId)
if (systemLogo) {
return systemLogo
}
return customLogos[providerId]
}
const providerOptions = options.map((option) => {
return {
label: getProviderNameById(option),
value: option
}
})
return (
<Select
selectedKeys={[provider.id]}
onSelectionChange={(keys) => {
const selectedKey = Array.from(keys)[0] as string
onChange(selectedKey)
}}
style={style}
className={`w-full ${className || ''}`}
renderValue={(items) => {
return items.map((item) => (
<div key={item.key} className="flex items-center gap-2">
<div className="flex h-4 w-4 items-center justify-center">
<ProviderAvatarPrimitive
providerId={item.key as string}
providerName={item.textValue || ''}
logoSrc={getProviderLogoSrc(item.key as string)}
size={16}
/>
</div>
<span>{item.textValue}</span>
</div>
))
}}>
{providerOptions.map((providerOption) => (
<SelectItem
key={providerOption.value}
textValue={providerOption.label}
startContent={
<div className="flex h-4 w-4 items-center justify-center">
<ProviderAvatarPrimitive
providerId={providerOption.value}
providerName={providerOption.label}
logoSrc={getProviderLogoSrc(providerOption.value)}
size={16}
/>
</div>
}>
{providerOption.label}
</SelectItem>
))}
</Select>
)
}
export default ProviderSelect

View File

@ -252,7 +252,8 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
{ label: 'OpenAI-Response', value: 'openai-response' },
{ label: 'Gemini', value: 'gemini' },
{ label: 'Anthropic', value: 'anthropic' },
{ label: 'Azure OpenAI', value: 'azure-openai' }
{ label: 'Azure OpenAI', value: 'azure-openai' },
{ label: 'New API', value: 'new-api' }
]}
/>
</Form.Item>

View File

@ -12,6 +12,15 @@ export function getProviderName(model?: Model) {
return getFancyProviderName(provider)
}
export function getProviderNameById(pid: string) {
const provider = getStoreProviders().find((p) => p.id === pid)
if (provider) {
return getFancyProviderName(provider)
} else {
return 'Unknown Provider'
}
}
export function getProviderByModel(model?: Model) {
const id = model?.provider
const provider = getStoreProviders().find((p) => p.id === id)

View File

@ -370,6 +370,7 @@ export type ProviderType =
| 'mistral'
| 'aws-bedrock'
| 'vertex-anthropic'
| 'new-api'
export type ModelType = 'text' | 'vision' | 'embedding' | 'reasoning' | 'function_calling' | 'web_search' | 'rerank'
@ -418,6 +419,8 @@ export type PaintingParams = {
id: string
urls: string[]
files: FileMetadata[]
// provider that this painting belongs to (for new-api family separation)
providerId?: string
}
export type PaintingProvider = 'zhipu' | 'aihubmix' | 'silicon' | 'dmxapi' | 'new-api'