diff --git a/src/renderer/pages/settings/ProviderSettings/ModelList/HealthCheckDrawer.tsx b/src/renderer/pages/settings/ProviderSettings/ModelList/HealthCheckDrawer.tsx index 1bdc03f32c..03fd87f5e3 100644 --- a/src/renderer/pages/settings/ProviderSettings/ModelList/HealthCheckDrawer.tsx +++ b/src/renderer/pages/settings/ProviderSettings/ModelList/HealthCheckDrawer.tsx @@ -169,7 +169,7 @@ export default function HealthCheckDrawer({ {isChecking ? (
- + {t('settings.models.check.pipeline_heading')} @@ -242,7 +242,7 @@ export default function HealthCheckDrawer({ rightCell = ( + {skipReasonText} } @@ -250,7 +250,7 @@ export default function HealthCheckDrawer({ classNames={{ placeholder: 'block min-w-0 w-full max-w-full overflow-hidden' }}> - + {t('settings.models.check.status_skipped')} @@ -258,7 +258,7 @@ export default function HealthCheckDrawer({ } else if (checking) { statusCell = rightCell = ( - + {t('settings.models.check.status_checking')} ) @@ -266,16 +266,16 @@ export default function HealthCheckDrawer({ statusCell = ( ) - rightCell = + rightCell = } else if (status === HealthStatus.SUCCESS) { statusCell = rightCell = latency != null ? ( - + {Math.round(latency)}ms ) : ( - + {t('settings.models.check.passed')} ) @@ -286,7 +286,7 @@ export default function HealthCheckDrawer({ errText !== '' ? ( + {errText} } @@ -294,12 +294,12 @@ export default function HealthCheckDrawer({ classNames={{ placeholder: 'block min-w-0 w-full max-w-full overflow-hidden' }}> - + {errText} ) : ( - + {t('settings.models.check.failed')} ) @@ -316,11 +316,11 @@ export default function HealthCheckDrawer({ {Icon ? ( ) : ( - + {model.name?.[0]?.toUpperCase()} )} - + {model.name}
-
+
{t('settings.models.check.select_api_key')}
- {maskApiKey(key)} + + {maskApiKey(key)} + ))} diff --git a/src/renderer/pages/settings/ProviderSettings/ModelList/ModelDrawer/EditModelDrawer.tsx b/src/renderer/pages/settings/ProviderSettings/ModelList/ModelDrawer/EditModelDrawer.tsx index 819fbed3ab..e15d8c8737 100644 --- a/src/renderer/pages/settings/ProviderSettings/ModelList/ModelDrawer/EditModelDrawer.tsx +++ b/src/renderer/pages/settings/ProviderSettings/ModelList/ModelDrawer/EditModelDrawer.tsx @@ -1,12 +1,13 @@ import { Button, - DescriptionSwitch, Input, Select, SelectContent, SelectItem, SelectTrigger, - SelectValue + SelectValue, + Switch, + Tooltip } from '@cherrystudio/ui' import CopyIcon from '@renderer/components/Icons/CopyIcon' import { useModelMutations } from '@renderer/hooks/useModel' @@ -15,7 +16,7 @@ import { getDefaultGroupName } from '@renderer/utils/naming' import { CURRENCY, type Currency, type EndpointType, type Model } from '@shared/data/types/model' import { parseUniqueModelId } from '@shared/data/types/model' import { isNewApiProvider } from '@shared/utils/provider' -import { ChevronDown, ChevronUp, SaveIcon } from 'lucide-react' +import { ChevronDown, ChevronUp, CircleHelp, SaveIcon } from 'lucide-react' import type { FormEvent } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -320,18 +321,20 @@ export default function EditModelDrawer({ providerId, open, model: modelProp, on endpointTypes }} showEndpointType={mode === 'new-api'} + horizontal modelIdDisabled modelIdAction={ - + } endpointTypeError={endpointTypeTouched ? t('settings.models.add.endpoint_type.required') : undefined} onModelIdChange={(value) => { @@ -362,13 +365,18 @@ export default function EditModelDrawer({ providerId, open, model: modelProp, on {showMoreSettings && (
-
- +
+
+ {t('settings.models.add.section.capabilities')} +
+
+ +
@@ -380,14 +388,21 @@ export default function EditModelDrawer({ providerId, open, model: modelProp, on onMaxInputTokensChange={setMaxInputTokens} onMaxOutputTokensChange={setMaxOutputTokens} /> -
- -
-
- +
+ + + + +
+ { setSupportsStreaming(checked) @@ -397,9 +412,15 @@ export default function EditModelDrawer({ providerId, open, model: modelProp, on
-
- -
+
+
+ {t('settings.models.add.section.pricing')} +
+
+ -
- + - -
- { - setInputPrice(event.target.value) - }} - onBlur={() => autoSave({ inputPrice })} - /> - - {currentCurrency} / {t('models.price.million_tokens')} - -
-
+ +
+ { + setInputPrice(event.target.value) + }} + onBlur={() => autoSave({ inputPrice })} + /> + + {currentCurrency} / {t('models.price.million_tokens')} + +
+
- -
- { - setOutputPrice(event.target.value) - }} - onBlur={() => autoSave({ outputPrice })} - /> - - {currentCurrency} / {t('models.price.million_tokens')} - -
-
+ +
+ { + setOutputPrice(event.target.value) + }} + onBlur={() => autoSave({ outputPrice })} + /> + + {currentCurrency} / {t('models.price.million_tokens')} + +
+
+
diff --git a/src/renderer/pages/settings/ProviderSettings/ModelList/ModelDrawer/ModelBasicFields.tsx b/src/renderer/pages/settings/ProviderSettings/ModelList/ModelDrawer/ModelBasicFields.tsx index b5fc7418e3..22dfbcea6d 100644 --- a/src/renderer/pages/settings/ProviderSettings/ModelList/ModelDrawer/ModelBasicFields.tsx +++ b/src/renderer/pages/settings/ProviderSettings/ModelList/ModelDrawer/ModelBasicFields.tsx @@ -14,6 +14,7 @@ interface ModelBasicFieldsProps { modelIdDisabled?: boolean modelIdAction?: ReactNode endpointTypeError?: string + horizontal?: boolean onModelIdChange: (value: string) => void onNameChange: (value: string) => void onGroupChange: (value: string) => void @@ -26,6 +27,7 @@ export function ModelBasicFields({ modelIdDisabled = false, modelIdAction, endpointTypeError, + horizontal = false, onModelIdChange, onNameChange, onGroupChange, @@ -38,8 +40,10 @@ export function ModelBasicFields({ -
+ className={drawerClasses.field} + horizontal={horizontal} + controlClassName={horizontal ? 'w-60' : undefined}> +
+ className={drawerClasses.field} + horizontal={horizontal} + controlClassName={horizontal ? 'w-60' : undefined}> + className={drawerClasses.field} + horizontal={horizontal} + controlClassName={horizontal ? 'w-60' : undefined}> {endpointTypeError}
: null}>
diff --git a/src/renderer/pages/settings/ProviderSettings/ModelList/ModelDrawer/ModelCapabilityToggles.tsx b/src/renderer/pages/settings/ProviderSettings/ModelList/ModelDrawer/ModelCapabilityToggles.tsx index ac6d01bee9..3a4a746eb3 100644 --- a/src/renderer/pages/settings/ProviderSettings/ModelList/ModelDrawer/ModelCapabilityToggles.tsx +++ b/src/renderer/pages/settings/ProviderSettings/ModelList/ModelDrawer/ModelCapabilityToggles.tsx @@ -34,7 +34,7 @@ export function ModelCapabilityToggles({ return (
-
+
{t('models.type.select')}
diff --git a/src/renderer/pages/settings/ProviderSettings/ModelList/ModelDrawer/ModelContextWindowFields.tsx b/src/renderer/pages/settings/ProviderSettings/ModelList/ModelDrawer/ModelContextWindowFields.tsx index 1d0b6a273e..99097f8504 100644 --- a/src/renderer/pages/settings/ProviderSettings/ModelList/ModelDrawer/ModelContextWindowFields.tsx +++ b/src/renderer/pages/settings/ProviderSettings/ModelList/ModelDrawer/ModelContextWindowFields.tsx @@ -26,8 +26,10 @@ export function ModelContextWindowFields({ <> + controlClassName="w-32"> + controlClassName="w-32"> + controlClassName="w-32"> = ({
-
- - setSearchText(event.target.value)} - className={modelListClasses.searchInput} - /> - {searchText ? ( - - ) : null} -
+ setSearchText(event.target.value)} + onClear={() => setSearchText('')} + clearLabel={t('common.clear')} + /> {!hasNoModels ? ( = ({ ref, model, disabled, onE + ) : null} +
+ - {searchText ? ( - - ) : null}
) : null} ) : null} diff --git a/src/renderer/pages/settings/ProviderSettings/ModelList/ModelSyncPreviewPanel.tsx b/src/renderer/pages/settings/ProviderSettings/ModelList/ModelSyncPreviewPanel.tsx index 336a9e147d..866259476f 100644 --- a/src/renderer/pages/settings/ProviderSettings/ModelList/ModelSyncPreviewPanel.tsx +++ b/src/renderer/pages/settings/ProviderSettings/ModelList/ModelSyncPreviewPanel.tsx @@ -136,7 +136,6 @@ export default function ModelSyncPreviewPanel({
-

{model.name}

{modelIdLine(model.id, model.apiModelId)}

@@ -211,7 +210,6 @@ export default function ModelSyncPreviewPanel({
-

{item.model.name}

{modelIdLine(item.model.id, item.model.apiModelId)}

diff --git a/src/renderer/pages/settings/ProviderSettings/ModelList/NewApiBatchAddModelPopup.tsx b/src/renderer/pages/settings/ProviderSettings/ModelList/NewApiBatchAddModelPopup.tsx index 13af5f747b..da071b7371 100644 --- a/src/renderer/pages/settings/ProviderSettings/ModelList/NewApiBatchAddModelPopup.tsx +++ b/src/renderer/pages/settings/ProviderSettings/ModelList/NewApiBatchAddModelPopup.tsx @@ -113,10 +113,12 @@ const PopupContainer: React.FC = ({ title, provider, resolve, batchModels onCancel() } }}> - + - {title} - + + {title} + + {t('settings.models.add.endpoint_type.tooltip')} diff --git a/src/renderer/pages/settings/ProviderSettings/ModelList/__tests__/ModelListSyncDrawer.test.tsx b/src/renderer/pages/settings/ProviderSettings/ModelList/__tests__/ModelListSyncDrawer.test.tsx index 9f9648be89..d363108d99 100644 --- a/src/renderer/pages/settings/ProviderSettings/ModelList/__tests__/ModelListSyncDrawer.test.tsx +++ b/src/renderer/pages/settings/ProviderSettings/ModelList/__tests__/ModelListSyncDrawer.test.tsx @@ -131,32 +131,32 @@ describe('ModelListSyncDrawer', () => { expect(searchInput).toBeInTheDocument() expect(screen.getByTestId('drawer-content')).toHaveClass('w-[min(calc(100vw-24px),520px)]') expect(screen.getByTestId('drawer-body')).toHaveClass('pt-0') - expect(screen.getByText('GPT 5')).toBeInTheDocument() - expect(screen.getByText('Claude Sonnet')).toBeInTheDocument() - expect(screen.getByText('Mistral Large')).toBeInTheDocument() - expect(screen.getByText('Legacy Model')).toBeInTheDocument() + expect(screen.getByText('gpt-5')).toBeInTheDocument() + expect(screen.getByText('claude-sonnet')).toBeInTheDocument() + expect(screen.getByText('mistral-large')).toBeInTheDocument() + expect(screen.getByText('legacy-model')).toBeInTheDocument() fireEvent.change(searchInput, { target: { value: 'claude' } }) - expect(screen.queryByText('GPT 5')).not.toBeInTheDocument() - expect(screen.getByText('Claude Sonnet')).toBeInTheDocument() - expect(screen.queryByText('Mistral Large')).not.toBeInTheDocument() - expect(screen.queryByText('Legacy Model')).not.toBeInTheDocument() + expect(screen.queryByText('gpt-5')).not.toBeInTheDocument() + expect(screen.getByText('claude-sonnet')).toBeInTheDocument() + expect(screen.queryByText('mistral-large')).not.toBeInTheDocument() + expect(screen.queryByText('legacy-model')).not.toBeInTheDocument() }) it('clears pull-result search and restores rows', () => { render() fireEvent.change(screen.getByPlaceholderText('models.search.placeholder'), { target: { value: 'legacy-model' } }) - expect(screen.getByText('Legacy Model')).toBeInTheDocument() - expect(screen.queryByText('GPT 5')).not.toBeInTheDocument() + expect(screen.getByText('legacy-model')).toBeInTheDocument() + expect(screen.queryByText('gpt-5')).not.toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: 'common.clear' })) expect(screen.getByPlaceholderText('models.search.placeholder')).toHaveValue('') - expect(screen.getByText('GPT 5')).toBeInTheDocument() - expect(screen.getByText('Claude Sonnet')).toBeInTheDocument() - expect(screen.getByText('Mistral Large')).toBeInTheDocument() + expect(screen.getByText('gpt-5')).toBeInTheDocument() + expect(screen.getByText('claude-sonnet')).toBeInTheDocument() + expect(screen.getByText('mistral-large')).toBeInTheDocument() }) it('selects only visible added rows while preserving hidden selections', async () => { @@ -170,7 +170,7 @@ describe('ModelListSyncDrawer', () => { fireEvent.click(screen.getByRole('button', { name: 'settings.models.manage.fetch_deselect_all_add' })) expect(screen.getByText('settings.models.manage.fetch_summary_add:0/3')).toBeInTheDocument() - fireEvent.click(screen.getByText('GPT 5')) + fireEvent.click(screen.getByText('gpt-5')) expect(screen.getByText('settings.models.manage.fetch_summary_add:1/3')).toBeInTheDocument() fireEvent.change(screen.getByPlaceholderText('models.search.placeholder'), { target: { value: 'claude' } }) diff --git a/src/renderer/pages/settings/ProviderSettings/ProviderList/ProviderEditorDrawer.tsx b/src/renderer/pages/settings/ProviderSettings/ProviderList/ProviderEditorDrawer.tsx index b3aafdf551..a2f138f44c 100644 --- a/src/renderer/pages/settings/ProviderSettings/ProviderList/ProviderEditorDrawer.tsx +++ b/src/renderer/pages/settings/ProviderSettings/ProviderList/ProviderEditorDrawer.tsx @@ -365,7 +365,7 @@ export default function ProviderEditorDrawer({ )} {duplicateSource && !duplicateNeedsBaseUrl(duplicateSource.authType) && ( -

+

{t('settings.provider.duplicate.fill_after_create')}

)} @@ -379,7 +379,7 @@ function DuplicateHeader({ source }: { source: Provider }) { const presetId = source.presetProviderId const label = presetId ? t(getProviderLabelKey(presetId)) : source.name return ( -
+
{label}
@@ -606,7 +606,7 @@ function ApiKeyField({ value, onChange }: ApiKeyFieldProps) { type="button" aria-label={t(visible ? 'settings.provider.api_key.hide_key' : 'settings.provider.api_key.show_key')} onClick={() => setVisible((v) => !v)} - className="-translate-y-1/2 absolute top-1/2 right-2 rounded-md p-1 text-muted-foreground/70 transition-colors hover:bg-accent/40 hover:text-foreground"> + className="-translate-y-1/2 absolute top-1/2 right-2 rounded-md p-1 text-muted-foreground/70 transition-colors hover:bg-(--color-surface-fg-subtle) hover:text-foreground"> {visible ? : }
diff --git a/src/renderer/pages/settings/ProviderSettings/ProviderList/ProviderList.tsx b/src/renderer/pages/settings/ProviderSettings/ProviderList/ProviderList.tsx index 31733f0b7d..e85277b164 100644 --- a/src/renderer/pages/settings/ProviderSettings/ProviderList/ProviderList.tsx +++ b/src/renderer/pages/settings/ProviderSettings/ProviderList/ProviderList.tsx @@ -1,4 +1,4 @@ -import { PageHeader } from '@cherrystudio/ui' +import { Button, PageHeader } from '@cherrystudio/ui' import { useReorder } from '@data/hooks/useReorder' import { useModels } from '@renderer/hooks/useModel' import { useProviders } from '@renderer/hooks/useProvider' @@ -33,12 +33,12 @@ export interface ProviderListProps { export default function ProviderList({ selectedProviderId, filterModeHint, onSelectProvider }: ProviderListProps) { const { t } = useTranslation() const { providers } = useProviders() + const { models: allModels } = useModels() const { applyReorderedList } = useReorder('/providers', { revalidateOnSuccess: false }) const { isSupported: isOvmsSupported } = useOvmsSupport() const [filterMode, setFilterMode] = useState(filterModeHint ?? 'all') const [searchText, setSearchText] = useState('') - const { models: allModels } = useModels(undefined, { fetchEnabled: Boolean(searchText.trim()) }) const [dragging, setDragging] = useState(false) const [contextProviderId, setContextProviderId] = useState(null) const [expandedGroups, setExpandedGroups] = useState>({}) @@ -263,18 +263,20 @@ export default function ProviderList({ selectedProviderId, filterModeHint, onSel const handleAddAnother = useCallback((template: Provider) => startAddFrom(template), [startAddFrom]) return ( -