From fcd244637cd88d368801f9b1930a6dc13b967b50 Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Fri, 16 Oct 2026 03:47:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E5=AE=8C=E6=95=B4=E6=BA=90?= =?UTF-8?q?=E7=A0=81=E8=87=B3=E8=BF=9C=E7=A8=8B=EF=BC=9Ai18n=20=E5=A4=9A?= =?UTF-8?q?=E8=AF=AD=E7=A7=8D=20settings=20=E6=96=87=E6=A1=88=20+=20AI=20?= =?UTF-8?q?=E4=BE=9B=E5=BA=94=E5=95=86=E8=AE=BE=E7=BD=AE=E4=B8=8E=E7=BB=88?= =?UTF-8?q?=E7=AB=AF=E5=B7=A5=E4=BD=9C=E5=8C=BA=E8=BF=9B=E8=A1=8C=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E4=BF=AE=E6=94=B9=EF=BC=8813=20=E6=96=87=E4=BB=B6?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/layout/Sidebar.tsx | 2 +- .../settings/AiProviderSettings.tsx | 284 +++++++++++------- src/components/terminal/TerminalWorkspace.tsx | 2 +- src/i18n/locales/en/settings.json | 8 +- src/i18n/locales/ja/settings.json | 8 +- src/i18n/locales/ko/settings.json | 8 +- src/i18n/locales/ru/settings.json | 8 +- src/i18n/locales/vi/settings.json | 8 +- src/i18n/locales/zh-CN/settings.json | 8 +- src/i18n/locales/zh-TW/settings.json | 8 +- src/index.css | 55 +++- src/stores/aiProviderStore.ts | 9 +- test/components/AiProviderSettings.test.tsx | 66 ++++ 13 files changed, 350 insertions(+), 124 deletions(-) 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 && ( -
+