diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 8f941fa..44d03cf 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -474,7 +474,7 @@ export default function Sidebar({ onNewSession, onQuickConnect, onEditSession, o <>
diff --git a/src/components/settings/AiProviderSettings.tsx b/src/components/settings/AiProviderSettings.tsx index e7f3924..7bd6e08 100644 --- a/src/components/settings/AiProviderSettings.tsx +++ b/src/components/settings/AiProviderSettings.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { Check, CheckCircle2, ChevronDown, Plus, Save, Trash2, Wifi } from 'lucide-react' +import { Check, CheckCircle2, ChevronDown, Cpu, Plus, Save, Trash2, Wifi } from 'lucide-react' import type { AiBillingMode, AiProviderConfig } from '../../../shared/types/ai.types' import { AI_PROVIDER_FAMILY_ORDER, @@ -23,7 +23,6 @@ function connectionSnapshot(provider: AiProviderConfig, apiKey: string): string const model = selectedModel(provider) return JSON.stringify({ id: provider.id, - name: provider.name.trim(), protocol: provider.protocol, authStyle: provider.authStyle, baseUrl: provider.baseUrl.trim(), @@ -77,8 +76,17 @@ export default function AiProviderSettings() { const currentSnapshot = useMemo(() => (draft ? connectionSnapshot(draft, apiKey) : ''), [apiKey, draft]) const verified = !!currentSnapshot && verifiedSnapshot === currentSnapshot && testState === 'success' const currentModel = selectedModel(draft) - const isStored = !!draft && store.providers.some((provider) => provider.id === draft.id) - const canTest = validDraft(draft, apiKey) && testState !== 'testing' + const storedProvider = store.providers.find((provider) => provider.id === draft?.id) + const isStored = !!storedProvider + const dirty = + !!draft && (!storedProvider || JSON.stringify(draft) !== JSON.stringify(storedProvider) || !!apiKey.trim()) + const connectionChanged = !storedProvider || currentSnapshot !== connectionSnapshot(storedProvider, '') + const needsTest = connectionChanged && !verified + const busy = saving || testState === 'testing' + const canTest = validDraft(draft, apiKey) && !busy + const canSave = dirty && validDraft(draft, apiKey) && !needsTest && !busy + const displayedMessage = + verifiedSnapshot && verifiedSnapshot !== currentSnapshot ? (dirty && needsTest ? t('ai.retest') : '') : message const family = draft ? resolveProviderFamily(draft) : null const needsEndpoint = family === 'custom' || family === 'local' || draft?.presetId === 'vllm' @@ -100,7 +108,6 @@ export default function AiProviderSettings() { const updateDraft = (changes: Partial) => { if (!draft) return setDraft({ ...draft, ...changes }) - if (testState !== 'idle') setMessage(t('ai.retest')) } const chooseModel = (modelId: string) => { @@ -172,13 +179,16 @@ export default function AiProviderSettings() { } const save = async () => { - if (!draft || !verified) return + if (!draft || !canSave) return setSaving(true) try { await store.saveProvider(draft, apiKey) - if (!store.activeProviderId || !isStored) await store.setActiveProvider(draft.id) + if (!store.activeProviderId) await store.setActiveProvider(draft.id) setApiKey('') - setMessage(t('ai.saved')) + setMessage('') + setTestAnswer('') + setVerifiedSnapshot(null) + setTestState('idle') } catch (error) { setMessage(error instanceof Error ? error.message : String(error)) setTestState('error') @@ -199,38 +209,41 @@ export default function AiProviderSettings() { } return ( -
-
- setPresetId(event.target.value)} + className="ui-select-pill min-w-0 flex-1" + > + {AI_PROVIDER_FAMILY_ORDER.map((familyId) => ( + + {presetsByFamily(familyId).map((preset) => ( + + ))} + ))} - - ))} - - -
+ + +
-
- -
+
{!draft ? ( -
+
{t('ai.emptyHint')}
) : ( -
-
+
+
+
+ +
+
+

{draft.name || t('ai.name')}

+

{t('ai.configuration')}

+
+ + {dirty ? t('ai.unsaved') : store.activeProviderId === draft.id ? t('ai.active') : t('ai.saved')} + +
+
)} - + {(needsEndpoint || advancedOpen) && ( - + setAdvancedOpen((open) => !open)} - className="flex items-center gap-1 px-0.5 text-xs text-txt-faint hover:text-txt-muted" + aria-expanded={advancedOpen} + aria-controls="ai-provider-advanced" + className="flex w-full items-center gap-2 border-t border-border-subtle px-5 py-3 text-xs font-medium text-txt-muted hover:bg-surface-hover" > {t('ai.advanced')} {advancedOpen && ( -
+