mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 14:41:24 +08:00
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:
parent
1a972ac0e0
commit
96ce645064
@ -63,13 +63,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)
|
||||
}
|
||||
|
||||
@ -289,7 +289,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',
|
||||
anthropicApiHost: 'http://localhost:3000',
|
||||
@ -1432,5 +1432,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'
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
`
|
||||
|
||||
@ -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
|
||||
@ -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>
|
||||
|
||||
@ -273,6 +273,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'
|
||||
|
||||
@ -11,7 +11,8 @@ export const ProviderTypeSchema = z.enum([
|
||||
'vertexai',
|
||||
'mistral',
|
||||
'aws-bedrock',
|
||||
'vertex-anthropic'
|
||||
'vertex-anthropic',
|
||||
'new-api'
|
||||
])
|
||||
|
||||
export type ProviderType = z.infer<typeof ProviderTypeSchema>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user