import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { Check, CheckCircle2, ChevronDown, Plus, Save, Trash2, Wifi } from 'lucide-react' import type { AiBillingMode, AiProviderConfig } from '../../../shared/types/ai.types' import { AI_PROVIDER_FAMILY_ORDER, createCustomProvider, createProviderFromPreset, presetsByFamily, resolveProviderFamily, } from '../../ai/providerPresets' import { useAiProviderStore } from '../../stores/aiProviderStore' import Button from '../common/Button' type TestState = 'idle' | 'testing' | 'success' | 'error' function selectedModel(provider: AiProviderConfig | null) { if (!provider) return null return provider.models.find((model) => model.id === provider.defaultModelId) || null } 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(), allowInsecureHttp: provider.allowInsecureHttp, model, credential: apiKey.trim() ? `entered:${apiKey.trim()}` : provider.hasApiKey ? 'stored' : 'missing', }) } function validDraft(provider: AiProviderConfig | null, apiKey: string): boolean { if (!provider?.name.trim() || !provider.baseUrl.trim() || !selectedModel(provider)?.model.trim()) return false return provider.authStyle === 'none' || provider.hasApiKey === true || apiKey.trim().length > 0 } export default function AiProviderSettings() { const { t } = useTranslation(['settings', 'common']) const store = useAiProviderStore() const [selectedId, setSelectedId] = useState('') const [presetId, setPresetId] = useState('deepseek') const [draft, setDraft] = useState(null) const [apiKey, setApiKey] = useState('') const [manualModel, setManualModel] = useState(false) const [advancedOpen, setAdvancedOpen] = useState(false) const [testState, setTestState] = useState('idle') const [verifiedSnapshot, setVerifiedSnapshot] = useState(null) const [message, setMessage] = useState('') const [testAnswer, setTestAnswer] = useState('') const [saving, setSaving] = useState(false) useEffect(() => { void store.load() }, []) // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (!selectedId && store.providers[0]) setSelectedId(store.providers[0].id) }, [selectedId, store.providers]) useEffect(() => { const provider = store.providers.find((item) => item.id === selectedId) if (!provider) return setDraft(structuredClone(provider)) setApiKey('') setManualModel(provider.models.length === 0) setAdvancedOpen(resolveProviderFamily(provider) === 'custom' || provider.presetId === 'vllm') setTestState('idle') setVerifiedSnapshot(null) setMessage('') setTestAnswer('') }, [selectedId, store.providers]) 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 family = draft ? resolveProviderFamily(draft) : null const needsEndpoint = family === 'custom' || family === 'local' || draft?.presetId === 'vllm' const chooseDraft = (provider: AiProviderConfig) => { setSelectedId(provider.id) setDraft(provider) setApiKey('') setManualModel(provider.models.length === 0) setAdvancedOpen(resolveProviderFamily(provider) === 'custom' || provider.presetId === 'vllm') setTestState('idle') setVerifiedSnapshot(null) setMessage('') setTestAnswer('') } const addPreset = () => chooseDraft(createProviderFromPreset(presetId)) const addCustom = () => chooseDraft(createCustomProvider()) const updateDraft = (changes: Partial) => { if (!draft) return setDraft({ ...draft, ...changes }) if (testState !== 'idle') setMessage(t('ai.retest')) } const chooseModel = (modelId: string) => { if (!draft) return if (modelId === '__manual') { setManualModel(true) return } setManualModel(false) updateDraft({ defaultModelId: modelId }) } const defaultManualBilling = (): AiBillingMode => { if (!draft) return 'payg' const resolved = resolveProviderFamily(draft) if (resolved === 'local') return 'local' if (resolved === 'plan') return 'plan' return 'payg' } const updateManualModel = (modelId: string) => { if (!draft) return const active = selectedModel(draft) if (active) { updateDraft({ models: draft.models.map((model) => model.id === active.id ? { ...model, model: modelId, label: modelId || t('ai.customModel') } : model ), }) return } const id = `manual-${Date.now().toString(36)}` updateDraft({ models: [ { id, model: modelId, label: modelId || t('ai.customModel'), capability: 'chat', billing: defaultManualBilling(), }, ], defaultModelId: id, }) } const testConnection = async () => { if (!draft || !currentModel || !canTest) return setTestState('testing') setVerifiedSnapshot(null) setMessage(t('ai.testingMessage')) setTestAnswer('') const snapshot = currentSnapshot try { const result = await window.opsxApi.aiTestProvider({ provider: draft, model: currentModel, apiKey: apiKey.trim() || undefined, }) if (!result.success) throw new Error(result.error || t('ai.testFailed')) setTestState('success') setVerifiedSnapshot(snapshot) setTestAnswer(result.answer?.trim() || '') setMessage(t('ai.available', { latency: result.latencyMs ?? '-' })) } catch (error) { setTestState('error') setMessage(error instanceof Error ? error.message : String(error)) } } const save = async () => { if (!draft || !verified) return setSaving(true) try { await store.saveProvider(draft, apiKey) if (!store.activeProviderId || !isStored) await store.setActiveProvider(draft.id) setApiKey('') setMessage(t('ai.saved')) } catch (error) { setMessage(error instanceof Error ? error.message : String(error)) setTestState('error') } finally { setSaving(false) } } const remove = async () => { if (!draft) return if (isStored) await store.deleteProvider(draft.id) setDraft(null) setSelectedId('') setApiKey('') setVerifiedSnapshot(null) setMessage('') setTestAnswer('') } return (
{!draft ? (
{t('ai.emptyHint')}
) : (
updateDraft({ name: event.target.value })} className="ui-input-sm" /> {manualModel || draft.models.length === 0 ? ( updateManualModel(event.target.value)} placeholder="deepseek-v4-pro" className="ui-input-sm font-mono" /> ) : ( )} {manualModel && draft.models.length > 0 && (
)} setApiKey(event.target.value)} placeholder={ draft.hasApiKey ? t('ai.savedKeepBlank') : draft.authStyle === 'none' ? t('ai.notRequired') : 'sk-...' } className="ui-input-sm" /> {(needsEndpoint || advancedOpen) && ( updateDraft({ baseUrl: event.target.value })} placeholder="https://api.example.com/v1" className="ui-input-sm font-mono" disabled={!!currentModel?.baseUrl && !needsEndpoint} /> )}
{advancedOpen && (
updateDraft({ allowInsecureHttp: event.target.checked })} className="h-3.5 w-3.5 accent-blue-500" />
)}
{isStored && store.activeProviderId !== draft.id && ( )}
{verified && } {message}
{verified && testAnswer &&

{testAnswer}

}
)}
) } function FormRow({ label, children }: { label: string; children: React.ReactNode }) { return ( ) } function CompactField({ children, wide = false }: { children: React.ReactNode; wide?: boolean }) { return
{children}
}