Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/352727664_codexbridge.json
T

1 line
62 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "352727664/codexbridge", "files": {"webui/src/pages/ProviderForm.jsx": {"ad38e3f5e781aa726e6a36e681f4a25c05493081": {"t": 1785595171.932739, "n": 19377, "h": "dde539c81afe7228", "c": "import { useState, useEffect } from 'react'\nimport { api } from '../lib/api'\nimport LanguageSwitcher from '../components/LanguageSwitcher'\nimport { useTranslation } from '../i18n'\n\nconst PRESETS = [\n { id: 'deepseek', name: 'DeepSeek', base_url: 'https://api.deepseek.com', model: 'deepseek-chat', has_reasoning: true },\n { id: 'qwen', name: 'Qwen', base_url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-max', has_reasoning: true },\n { id: 'kimi', name: 'Kimi', base_url: 'https://api.moonshot.cn/v1', model: 'moonshot-v1-128k', has_reasoning: false },\n { id: 'glm', name: 'GLM', base_url: 'https://open.bigmodel.cn/api/paas/v4', model: 'glm-4-plus', has_reasoning: false },\n { id: 'mimo', name: 'MiMo', base_url: 'https://api.xiaomimimo.com/v1', model: 'mimo-v2.5-pro', has_reasoning: true },\n { id: 'minimax', name: 'MiniMax', base_url: 'https://api.minimax.chat/v1', model: 'MiniMax-Text-01', has_reasoning: false },\n { id: 'baichuan', name: 'Baichuan', base_url: 'https://api.baichuan-ai.com/v1', model: 'Baichuan4', has_reasoning: false },\n { id: 'yi', name: 'Yi', base_url: 'https://api.lingyiwanwu.com/v1', model: 'yi-lightning', has_reasoning: false },\n { id: 'doubao', name: 'Doubao', base_url: 'https://ark.cn-beijing.volces.com/api/v3', model: 'doubao-1.5-pro-256k', has_reasoning: true },\n { id: 'stepfun', name: 'StepFun', base_url: 'https://api.stepfun.com/v1', model: 'step-2-16k', has_reasoning: false },\n { id: 'siliconflow', name: 'SiliconFlow', base_url: 'https://api.siliconflow.cn/v1', model: 'deepseek-ai/DeepSeek-V3', has_reasoning: true },\n { id: 'anthropic', name: 'Anthropic', base_url: 'https://api.anthropic.com', model: 'claude-sonnet-4-20250514', has_reasoning: true, api_format: 'anthropic' },\n]\n\nexport default function ProviderForm({ provider, onBack }) {\n const { t } = useTranslation()\n const isEdit = !!provider?.id\n\n const [form, setForm] = useState({\n name: '', api_format: 'openai', base_url: '', api_key: '', model: '', has_reasoning: false, note: '', website: '',\n })\n const [testing, setTesting] = useState(false)\n const [testResult, setTestResult] = useState(null)\n const [showKey, setShowKey] = useState(false)\n const [fetchingModels, setFetchingModels] = useState(false)\n const [fetchedModels, setFetchedModels] = useState([])\n const [showModelDropdown, setShowModelDropdown] = useState(false)\n const [toast, setToast] = useState(null)\n const [showPresets, setShowPresets] = useState(!isEdit)\n const [configText, setConfigText] = useState('')\n const [configError, setConfigError] = useState(null)\n\n useEffect(() => {\n if (provider?.id) {\n const models = provider.models || []\n setForm({\n name: provider.name || '', api_format: provider.api_format || 'openai', base_url: provider.base_url || '',\n api_key: provider.api_key || '', model: models[0] || '', has_reasoning: provider.has_reasoning || false,\n note: provider.note || '', website: provider.website || '',\n })\n }\n }, [provider])\n\n useEffect(() => {\n const preview = {\n name: form.name || 'provider', api_format: form.api_format, base_url: form.base_url,\n api_key: form.api_key ? 'sk-***' : '', models: form.model ? [form.model] : [], has_reasoning: form.has_reasoning,\n }\n setConfigText(JSON.stringify(preview, null, 2))\n setConfigError(null)\n }, [form.name, form.api_format, form.base_url, form.api_key, form.model, form.has_reasoning])\n\n const showToast = (msg, type = 'success') => {\n setToast({ msg, type })\n setTimeout(() => setToast(null), 2500)\n }\n\n const update = (key, val) => setForm(prev => ({ ...prev, [key]: val }))\n\n const applyPreset = (preset) => {\n setForm(prev => ({ ...prev, name: preset.name, base_url: preset.base_url, model: preset.model, has_reasoning: preset.has_reasoning, api_format: preset.api_format || 'openai' }))\n setShowPresets(false)\n }\n\n const handleTest = async () => {\n setTesting(true)\n setTestResult(null)\n try {\n const res = await api.testConnection({ base_url: form.base_url, api_key: form.api_key, api_format: form.api_format, model: form.model || '' })\n setTestResult(res)\n } catch (e) {\n setTestResult({ status: 'error', message: e.message })\n }\n setTesting(false)\n }\n\n const handleFetchModels = async () => {\n setFetchingModels(true)\n try {\n const res = await api.fetchModels({ base_url: form.base_url, api_key: form.api_key, api_format: form.api_format })\n if (res.status === 'ok' && res.models?.length) {\n setFetchedModels(res.models)\n setShowModelDropdown(true)\n showToast(t('providerForm.toastModelsFetched', { count: res.models.length }))\n } else {\n showToast(res.message || t('providerForm.toastNoModels'), 'error')\n }\n } catch (e) {\n showToast(t('providerForm.toastFetchErr') + e.message, 'error')\n }\n setFetchingModels(false)\n }\n\n const handleSave = async () => {\n if (!form.name.trim()) { showToast(t('providerForm.errNoName'), 'error'); return }\n if (!form.model.trim()) { showToast(t('providerForm.errNoModel'), 'error'); return }\n\n let configData\n try { configData = JSON.parse(configText) } catch { showToast(t('providerForm.errBadJson'), 'error'); return }\n\n const data = {\n name: configData.name || form.name.trim(), api_format: form.api_format, base_url: form.base_url.trim(),\n api_key: form.api_key.trim(), models: Array.isArray(configData.models) ? configData.models : [form.model],\n has_reasoning: configData.has_reasoning !== undefined ? configData.has_reasoning : form.has_reasoning,\n note: form.note.trim(), website: form.website.trim(),\n }\n\n try {\n if (isEdit) { await api.updateProvider(provider.id, data); showToast(t('providerForm.toastUpdated')) }\n else { await api.createProvider(data); showToast(t('providerForm.toastAdded')) }\n onBack()\n } catch (e) {\n showToast(t('providerForm.toastSaveErr') + e.message, 'error')\n }\n }\n\n const handleConfigChange = (e) => {\n const val = e.target.value\n setConfigText(val)\n try {\n const parsed = JSON.parse(val)\n setConfigError(null)\n if (parsed.name !== undefined) setForm(prev => ({ ...prev, name: parsed.name }))\n if (parsed.models && Array.isArray(parsed.models) && parsed.models.length > 0) setForm(prev => ({ ...prev, model: parsed.models[0] }))\n if (parsed.has_reasoning !== undefined) setForm(prev => ({ ...prev, has_reasoning: parsed.has_reasoning }))\n } catch { setConfigError(t('providerForm.errInvalidJson')) }\n }\n\n return (\n <div className=\"min-h-screen bg-canvas\">\n {toast && <div className={`fixed top-4 right-4 z-50 px-4 py-2.5 rounded-xl text-sm font-medium shadow-lg animate-fade-in ${toast.type === 'error' ? 'bg-error text-on-primary' : 'bg-surface-dark text-on-dark'}`}>{toast.msg}</div>}\n\n <header className=\"border-b border-hairline bg-canvas/80 backdrop-blur-md sticky top-0 z-40\">\n <div className=\"max-w-2xl mx-auto px-6 h-14 flex items-center gap-4\">\n <button onClick={onBack} className=\"w-8 h-8 flex items-center justify-center rounded-lg text-muted hover:text-ink hover:bg-surface transition-colors cursor-pointer\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M19 12H5m7-7l-7 7 7 7\"/></svg>\n </button>\n <h1 className=\"text-base font-semibold text-ink\">{isEdit ? t('providerForm.editTitle') : t('providerForm.addTitle')}</h1>\n <div className=\"flex-1\" />\n <LanguageSwitcher />\n </div>\n </header>\n\n <main className=\"max-w-2xl mx-auto px-6 py-8\">\n {showPresets && !isEdit && (\n <div className=\"mb-8 animate-fade-in\">\n <h2 className=\"text-sm font-medium text-muted mb-3\">{t('providerForm.presetHint')}</h2>\n <div className=\"grid grid-cols-2 sm:grid-cols-3 gap-2\">\n {PRESETS.map(p => (\n <button key={p.id} onClick={() => applyPreset(p)} className=\"flex items-center gap-2 px-3 py-2.5 bg-surface rounded-xl text-left hover:bg-surface-hover transition-colors cursor-pointer\">\n <div className=\"w-7 h-7 rounded-lg bg-coral-bg text-coral flex items-center justify-center text-[10px] font-bold shrink-0\">{p.name.charAt(0)}</div>\n <span className=\"text-xs font-medium text-ink truncate\">{p.name}</span>\n </button>\n ))}\n <button onClick={() => setShowPresets(false)} className=\"flex items-center gap-2 px-3 py-2.5 border border-dashed border-hairline rounded-xl text-left hover:bg-surface transition-colors cursor-pointer\">\n <div className=\"w-7 h-7 rounded-lg bg-surface flex items-center justify-center text-muted\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\"/><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/></svg>\n </div>\n <span className=\"text-xs font-medium text-muted\">{t('providerForm.custom')}</span>\n </button>\n </div>\n </div>\n )}\n\n <div className=\"space-y-6\">\n <div className=\"flex items-start gap-4\">\n <div className=\"w-16 h-16 rounded-2xl bg-surface flex items-center justify-center text-xl font-bold text-muted shrink-0 mt-1\">{form.name ? form.name.charAt(0).toUpperCase() : '?'}</div>\n <div className=\"flex-1 grid grid-cols-1 sm:grid-cols-2 gap-4\">\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">{t('providerForm.name')}</label>\n <input type=\"text\" value={form.name} onChange={e => update('name', e.target.value)} placeholder={t('providerForm.namePlaceholder')} className=\"w-full px-3 py-2 bg-canvas border border-hairline rounded-xl text-sm text-ink placeholder:text-muted/50 focus:outline-none focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors\" />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">{t('providerForm.note')}</label>\n <input type=\"text\" value={form.note} onChange={e => update('note', e.target.value)} placeholder={t('providerForm.notePlaceholder')} className=\"w-full px-3 py-2 bg-canvas border border-hairline rounded-xl text-sm text-ink placeholder:text-muted/50 focus:outline-none focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors\" />\n </div>\n </div>\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">{t('providerForm.website')}</label>\n <input type=\"url\" value={form.website} onChange={e => update('website', e.target.value)} placeholder=\"https://example.com\" className=\"w-full px-3 py-2 bg-canvas border border-hairline rounded-xl text-sm text-ink placeholder:text-muted/50 focus:outline-none focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors\" />\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">{t('providerForm.apiFormat')}</label>\n <div className=\"flex gap-2\">\n {['openai', 'anthropic'].map(fmt => (\n <button key={fmt} onClick={() => update('api_format', fmt)} className={`px-4 py-2 rounded-xl text-sm font-medium transition-colors cursor-pointer ${form.api_format === fmt ? 'bg-coral text-on-primary' : 'bg-surface text-muted hover:bg-surface-hover'}`}>\n {fmt === 'openai' ? t('providerForm.openaiCompat') : 'Anthropic'}\n </button>\n ))}\n </div>\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">{t('providerForm.apiKey')}</label>\n <div className=\"relative\">\n <input type={showKey ? 'text' : 'password'} value={form.api_key} onChange={e => update('api_key', e.target.value)} placeholder=\"sk-...\" className=\"w-full px-3 py-2 pr-10 bg-canvas border border-hairline rounded-xl text-sm text-ink placeholder:text-muted/50 focus:outline-none focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors font-mono\" />\n <button type=\"button\" onClick={() => setShowKey(!showKey)} className=\"absolute right-2 top-1/2 -translate-y-1/2 w-7 h-7 flex items-center justify-center rounded-lg text-muted hover:text-ink transition-colors cursor-pointer\">\n {showKey ? <svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg> : <svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24\"/><line x1=\"1\" y1=\"1\" x2=\"23\" y2=\"23\"/></svg>}\n </button>\n </div>\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">{t('providerForm.baseUrl')}</label>\n <input type=\"url\" value={form.base_url} onChange={e => update('base_url', e.target.value)} placeholder=\"https://api.example.com/v1\" className=\"w-full px-3 py-2 bg-canvas border border-hairline rounded-xl text-sm text-ink placeholder:text-muted/50 focus:outline-none focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors\" />\n <p className=\"text-xs text-muted mt-1\">{t('providerForm.baseUrlHelp')}</p>\n </div>\n\n <div>\n <div className=\"flex items-center justify-between mb-1.5\">\n <label className=\"text-sm font-medium text-ink\">{t('providerForm.model')}</label>\n <button onClick={handleFetchModels} disabled={fetchingModels || !form.base_url || !form.api_key} className=\"flex items-center gap-1 text-xs text-coral hover:text-coral-active transition-colors disabled:opacity-40 cursor-pointer\">\n {fetchingModels ? <svg className=\"animate-spin\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M12 2v4m0 12v4m-7.07-3.93l2.83-2.83m8.48-8.48l2.83-2.83M2 12h4m12 0h4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83\"/></svg> : <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4\"/><polyline points=\"7 10 12 15 17 10\"/><line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"/></svg>}\n {t('providerForm.fetchModels')}\n </button>\n </div>\n <div className=\"relative\">\n <input type=\"text\" value={form.model} onChange={e => { update('model', e.target.value); setShowModelDropdown(false) }} onFocus={() => { if (fetchedModels.length) setShowModelDropdown(true) }} placeholder=\"deepseek-chat\" className=\"w-full px-3 py-2 bg-canvas border border-hairline rounded-xl text-sm text-ink placeholder:text-muted/50 focus:outline-none focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors\" />\n {showModelDropdown && fetchedModels.length > 0 && (\n <div className=\"absolute z-10 w-full mt-1 bg-canvas border border-hairline rounded-xl shadow-lg max-h-48 overflow-y-auto\">\n <button onClick={() => setShowModelDropdown(false)} className=\"absolute top-2 right-2 w-6 h-6 flex items-center justify-center rounded text-muted hover:text-ink cursor-pointer\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/></svg>\n </button>\n {fetchedModels.map(m => (\n <button key={m} onClick={() => { update('model', m); setShowModelDropdown(false) }} className={`w-full text-left px-3 py-2 text-sm hover:bg-surface transition-colors cursor-pointer ${form.model === m ? 'bg-coral-bg text-coral font-medium' : 'text-ink'}`}>{m}</button>\n ))}\n </div>\n )}\n </div>\n <p className=\"text-xs text-muted mt-1\">{t('providerForm.modelHelp')}</p>\n </div>\n\n <div className=\"flex items-center gap-3\">\n <button onClick={() => update('has_reasoning', !form.has_reasoning)} className={`relative w-10 h-6 rounded-full transition-colors cursor-pointer ${form.has_reasoning ? 'bg-coral' : 'bg-hairline'}`}>\n <div className={`absolute top-1 w-4 h-4 rounded-full bg-white shadow-sm transition-all ${form.has_reasoning ? 'left-5' : 'left-1'}`} />\n </button>\n <span className=\"text-sm font-medium text-ink\">{t('providerForm.reasoning')}</span>\n </div>\n\n <div className=\"bg-surface rounded-xl p-4\">\n <div className=\"flex items-center justify-between mb-3\">\n <span className=\"text-sm font-medium text-ink\">{t('providerForm.testSection')}</span>\n <button onClick={handleTest} disabled={testing || !form.base_url || !form.api_key} className=\"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-coral text-on-primary rounded-lg hover:bg-coral-active transition-colors disabled:opacity-40 cursor-pointer\">\n {testing ? <>{t('providerForm.testing')}</> : <>{t('providerForm.testBtn')}</>}\n </button>\n </div>\n {testResult && (\n <div className={`px-3 py-2 rounded-lg text-xs ${testResult.status === 'ok' ? 'bg-success-bg text-success' : 'bg-error-bg text-error'}`}>\n <span className=\"font-medium\">{testResult.status === 'ok' ? t('providerForm.testOk') : t('providerForm.testFail')}</span>\n {testResult.latency_ms && <span className=\"ml-2 opacity-70\">{testResult.latency_ms}ms</span>}\n {testResult.message && <span className=\"ml-2\">{testResult.message}</span>}\n </div>\n )}\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">{t('providerForm.configPreview')}</label>\n <textarea value={configText} onChange={handleConfigChange} spellCheck={false} className=\"w-full bg-surface-dark rounded-xl p-4 text-xs text-on-dark/80 font-mono leading-relaxed overflow-x-auto border border-hairline focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors resize-y min-h-[180px]\" rows={12} />\n {configError && <p className=\"text-xs text-error mt-1\">{configError}</p>}\n </div>\n\n <div className=\"flex justify-end pt-4 pb-8\">\n <button onClick={handleSave} className=\"flex items-center gap-2 px-6 py-2.5 bg-coral text-on-primary rounded-xl text-sm font-medium hover:bg-coral-active transition-colors cursor-pointer\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z\"/><polyline points=\"17 21 17 13 7 13 7 21\"/><polyline points=\"7 3 7 8 15 8\"/></svg>\n {t('providerForm.save')}\n </button>\n </div>\n </div>\n </main>\n </div>\n )\n}\n"}, "68ec8644cd46f2d31bb5e6ea2422216af67142ee": {"t": 1785764163.7334774, "n": 19377, "h": "dde539c81afe7228", "c": "import { useState, useEffect } from 'react'\nimport { api } from '../lib/api'\nimport LanguageSwitcher from '../components/LanguageSwitcher'\nimport { useTranslation } from '../i18n'\n\nconst PRESETS = [\n { id: 'deepseek', name: 'DeepSeek', base_url: 'https://api.deepseek.com', model: 'deepseek-chat', has_reasoning: true },\n { id: 'qwen', name: 'Qwen', base_url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-max', has_reasoning: true },\n { id: 'kimi', name: 'Kimi', base_url: 'https://api.moonshot.cn/v1', model: 'moonshot-v1-128k', has_reasoning: false },\n { id: 'glm', name: 'GLM', base_url: 'https://open.bigmodel.cn/api/paas/v4', model: 'glm-4-plus', has_reasoning: false },\n { id: 'mimo', name: 'MiMo', base_url: 'https://api.xiaomimimo.com/v1', model: 'mimo-v2.5-pro', has_reasoning: true },\n { id: 'minimax', name: 'MiniMax', base_url: 'https://api.minimax.chat/v1', model: 'MiniMax-Text-01', has_reasoning: false },\n { id: 'baichuan', name: 'Baichuan', base_url: 'https://api.baichuan-ai.com/v1', model: 'Baichuan4', has_reasoning: false },\n { id: 'yi', name: 'Yi', base_url: 'https://api.lingyiwanwu.com/v1', model: 'yi-lightning', has_reasoning: false },\n { id: 'doubao', name: 'Doubao', base_url: 'https://ark.cn-beijing.volces.com/api/v3', model: 'doubao-1.5-pro-256k', has_reasoning: true },\n { id: 'stepfun', name: 'StepFun', base_url: 'https://api.stepfun.com/v1', model: 'step-2-16k', has_reasoning: false },\n { id: 'siliconflow', name: 'SiliconFlow', base_url: 'https://api.siliconflow.cn/v1', model: 'deepseek-ai/DeepSeek-V3', has_reasoning: true },\n { id: 'anthropic', name: 'Anthropic', base_url: 'https://api.anthropic.com', model: 'claude-sonnet-4-20250514', has_reasoning: true, api_format: 'anthropic' },\n]\n\nexport default function ProviderForm({ provider, onBack }) {\n const { t } = useTranslation()\n const isEdit = !!provider?.id\n\n const [form, setForm] = useState({\n name: '', api_format: 'openai', base_url: '', api_key: '', model: '', has_reasoning: false, note: '', website: '',\n })\n const [testing, setTesting] = useState(false)\n const [testResult, setTestResult] = useState(null)\n const [showKey, setShowKey] = useState(false)\n const [fetchingModels, setFetchingModels] = useState(false)\n const [fetchedModels, setFetchedModels] = useState([])\n const [showModelDropdown, setShowModelDropdown] = useState(false)\n const [toast, setToast] = useState(null)\n const [showPresets, setShowPresets] = useState(!isEdit)\n const [configText, setConfigText] = useState('')\n const [configError, setConfigError] = useState(null)\n\n useEffect(() => {\n if (provider?.id) {\n const models = provider.models || []\n setForm({\n name: provider.name || '', api_format: provider.api_format || 'openai', base_url: provider.base_url || '',\n api_key: provider.api_key || '', model: models[0] || '', has_reasoning: provider.has_reasoning || false,\n note: provider.note || '', website: provider.website || '',\n })\n }\n }, [provider])\n\n useEffect(() => {\n const preview = {\n name: form.name || 'provider', api_format: form.api_format, base_url: form.base_url,\n api_key: form.api_key ? 'sk-***' : '', models: form.model ? [form.model] : [], has_reasoning: form.has_reasoning,\n }\n setConfigText(JSON.stringify(preview, null, 2))\n setConfigError(null)\n }, [form.name, form.api_format, form.base_url, form.api_key, form.model, form.has_reasoning])\n\n const showToast = (msg, type = 'success') => {\n setToast({ msg, type })\n setTimeout(() => setToast(null), 2500)\n }\n\n const update = (key, val) => setForm(prev => ({ ...prev, [key]: val }))\n\n const applyPreset = (preset) => {\n setForm(prev => ({ ...prev, name: preset.name, base_url: preset.base_url, model: preset.model, has_reasoning: preset.has_reasoning, api_format: preset.api_format || 'openai' }))\n setShowPresets(false)\n }\n\n const handleTest = async () => {\n setTesting(true)\n setTestResult(null)\n try {\n const res = await api.testConnection({ base_url: form.base_url, api_key: form.api_key, api_format: form.api_format, model: form.model || '' })\n setTestResult(res)\n } catch (e) {\n setTestResult({ status: 'error', message: e.message })\n }\n setTesting(false)\n }\n\n const handleFetchModels = async () => {\n setFetchingModels(true)\n try {\n const res = await api.fetchModels({ base_url: form.base_url, api_key: form.api_key, api_format: form.api_format })\n if (res.status === 'ok' && res.models?.length) {\n setFetchedModels(res.models)\n setShowModelDropdown(true)\n showToast(t('providerForm.toastModelsFetched', { count: res.models.length }))\n } else {\n showToast(res.message || t('providerForm.toastNoModels'), 'error')\n }\n } catch (e) {\n showToast(t('providerForm.toastFetchErr') + e.message, 'error')\n }\n setFetchingModels(false)\n }\n\n const handleSave = async () => {\n if (!form.name.trim()) { showToast(t('providerForm.errNoName'), 'error'); return }\n if (!form.model.trim()) { showToast(t('providerForm.errNoModel'), 'error'); return }\n\n let configData\n try { configData = JSON.parse(configText) } catch { showToast(t('providerForm.errBadJson'), 'error'); return }\n\n const data = {\n name: configData.name || form.name.trim(), api_format: form.api_format, base_url: form.base_url.trim(),\n api_key: form.api_key.trim(), models: Array.isArray(configData.models) ? configData.models : [form.model],\n has_reasoning: configData.has_reasoning !== undefined ? configData.has_reasoning : form.has_reasoning,\n note: form.note.trim(), website: form.website.trim(),\n }\n\n try {\n if (isEdit) { await api.updateProvider(provider.id, data); showToast(t('providerForm.toastUpdated')) }\n else { await api.createProvider(data); showToast(t('providerForm.toastAdded')) }\n onBack()\n } catch (e) {\n showToast(t('providerForm.toastSaveErr') + e.message, 'error')\n }\n }\n\n const handleConfigChange = (e) => {\n const val = e.target.value\n setConfigText(val)\n try {\n const parsed = JSON.parse(val)\n setConfigError(null)\n if (parsed.name !== undefined) setForm(prev => ({ ...prev, name: parsed.name }))\n if (parsed.models && Array.isArray(parsed.models) && parsed.models.length > 0) setForm(prev => ({ ...prev, model: parsed.models[0] }))\n if (parsed.has_reasoning !== undefined) setForm(prev => ({ ...prev, has_reasoning: parsed.has_reasoning }))\n } catch { setConfigError(t('providerForm.errInvalidJson')) }\n }\n\n return (\n <div className=\"min-h-screen bg-canvas\">\n {toast && <div className={`fixed top-4 right-4 z-50 px-4 py-2.5 rounded-xl text-sm font-medium shadow-lg animate-fade-in ${toast.type === 'error' ? 'bg-error text-on-primary' : 'bg-surface-dark text-on-dark'}`}>{toast.msg}</div>}\n\n <header className=\"border-b border-hairline bg-canvas/80 backdrop-blur-md sticky top-0 z-40\">\n <div className=\"max-w-2xl mx-auto px-6 h-14 flex items-center gap-4\">\n <button onClick={onBack} className=\"w-8 h-8 flex items-center justify-center rounded-lg text-muted hover:text-ink hover:bg-surface transition-colors cursor-pointer\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M19 12H5m7-7l-7 7 7 7\"/></svg>\n </button>\n <h1 className=\"text-base font-semibold text-ink\">{isEdit ? t('providerForm.editTitle') : t('providerForm.addTitle')}</h1>\n <div className=\"flex-1\" />\n <LanguageSwitcher />\n </div>\n </header>\n\n <main className=\"max-w-2xl mx-auto px-6 py-8\">\n {showPresets && !isEdit && (\n <div className=\"mb-8 animate-fade-in\">\n <h2 className=\"text-sm font-medium text-muted mb-3\">{t('providerForm.presetHint')}</h2>\n <div className=\"grid grid-cols-2 sm:grid-cols-3 gap-2\">\n {PRESETS.map(p => (\n <button key={p.id} onClick={() => applyPreset(p)} className=\"flex items-center gap-2 px-3 py-2.5 bg-surface rounded-xl text-left hover:bg-surface-hover transition-colors cursor-pointer\">\n <div className=\"w-7 h-7 rounded-lg bg-coral-bg text-coral flex items-center justify-center text-[10px] font-bold shrink-0\">{p.name.charAt(0)}</div>\n <span className=\"text-xs font-medium text-ink truncate\">{p.name}</span>\n </button>\n ))}\n <button onClick={() => setShowPresets(false)} className=\"flex items-center gap-2 px-3 py-2.5 border border-dashed border-hairline rounded-xl text-left hover:bg-surface transition-colors cursor-pointer\">\n <div className=\"w-7 h-7 rounded-lg bg-surface flex items-center justify-center text-muted\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\"/><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/></svg>\n </div>\n <span className=\"text-xs font-medium text-muted\">{t('providerForm.custom')}</span>\n </button>\n </div>\n </div>\n )}\n\n <div className=\"space-y-6\">\n <div className=\"flex items-start gap-4\">\n <div className=\"w-16 h-16 rounded-2xl bg-surface flex items-center justify-center text-xl font-bold text-muted shrink-0 mt-1\">{form.name ? form.name.charAt(0).toUpperCase() : '?'}</div>\n <div className=\"flex-1 grid grid-cols-1 sm:grid-cols-2 gap-4\">\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">{t('providerForm.name')}</label>\n <input type=\"text\" value={form.name} onChange={e => update('name', e.target.value)} placeholder={t('providerForm.namePlaceholder')} className=\"w-full px-3 py-2 bg-canvas border border-hairline rounded-xl text-sm text-ink placeholder:text-muted/50 focus:outline-none focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors\" />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">{t('providerForm.note')}</label>\n <input type=\"text\" value={form.note} onChange={e => update('note', e.target.value)} placeholder={t('providerForm.notePlaceholder')} className=\"w-full px-3 py-2 bg-canvas border border-hairline rounded-xl text-sm text-ink placeholder:text-muted/50 focus:outline-none focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors\" />\n </div>\n </div>\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">{t('providerForm.website')}</label>\n <input type=\"url\" value={form.website} onChange={e => update('website', e.target.value)} placeholder=\"https://example.com\" className=\"w-full px-3 py-2 bg-canvas border border-hairline rounded-xl text-sm text-ink placeholder:text-muted/50 focus:outline-none focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors\" />\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">{t('providerForm.apiFormat')}</label>\n <div className=\"flex gap-2\">\n {['openai', 'anthropic'].map(fmt => (\n <button key={fmt} onClick={() => update('api_format', fmt)} className={`px-4 py-2 rounded-xl text-sm font-medium transition-colors cursor-pointer ${form.api_format === fmt ? 'bg-coral text-on-primary' : 'bg-surface text-muted hover:bg-surface-hover'}`}>\n {fmt === 'openai' ? t('providerForm.openaiCompat') : 'Anthropic'}\n </button>\n ))}\n </div>\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">{t('providerForm.apiKey')}</label>\n <div className=\"relative\">\n <input type={showKey ? 'text' : 'password'} value={form.api_key} onChange={e => update('api_key', e.target.value)} placeholder=\"sk-...\" className=\"w-full px-3 py-2 pr-10 bg-canvas border border-hairline rounded-xl text-sm text-ink placeholder:text-muted/50 focus:outline-none focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors font-mono\" />\n <button type=\"button\" onClick={() => setShowKey(!showKey)} className=\"absolute right-2 top-1/2 -translate-y-1/2 w-7 h-7 flex items-center justify-center rounded-lg text-muted hover:text-ink transition-colors cursor-pointer\">\n {showKey ? <svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg> : <svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24\"/><line x1=\"1\" y1=\"1\" x2=\"23\" y2=\"23\"/></svg>}\n </button>\n </div>\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">{t('providerForm.baseUrl')}</label>\n <input type=\"url\" value={form.base_url} onChange={e => update('base_url', e.target.value)} placeholder=\"https://api.example.com/v1\" className=\"w-full px-3 py-2 bg-canvas border border-hairline rounded-xl text-sm text-ink placeholder:text-muted/50 focus:outline-none focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors\" />\n <p className=\"text-xs text-muted mt-1\">{t('providerForm.baseUrlHelp')}</p>\n </div>\n\n <div>\n <div className=\"flex items-center justify-between mb-1.5\">\n <label className=\"text-sm font-medium text-ink\">{t('providerForm.model')}</label>\n <button onClick={handleFetchModels} disabled={fetchingModels || !form.base_url || !form.api_key} className=\"flex items-center gap-1 text-xs text-coral hover:text-coral-active transition-colors disabled:opacity-40 cursor-pointer\">\n {fetchingModels ? <svg className=\"animate-spin\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M12 2v4m0 12v4m-7.07-3.93l2.83-2.83m8.48-8.48l2.83-2.83M2 12h4m12 0h4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83\"/></svg> : <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4\"/><polyline points=\"7 10 12 15 17 10\"/><line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"/></svg>}\n {t('providerForm.fetchModels')}\n </button>\n </div>\n <div className=\"relative\">\n <input type=\"text\" value={form.model} onChange={e => { update('model', e.target.value); setShowModelDropdown(false) }} onFocus={() => { if (fetchedModels.length) setShowModelDropdown(true) }} placeholder=\"deepseek-chat\" className=\"w-full px-3 py-2 bg-canvas border border-hairline rounded-xl text-sm text-ink placeholder:text-muted/50 focus:outline-none focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors\" />\n {showModelDropdown && fetchedModels.length > 0 && (\n <div className=\"absolute z-10 w-full mt-1 bg-canvas border border-hairline rounded-xl shadow-lg max-h-48 overflow-y-auto\">\n <button onClick={() => setShowModelDropdown(false)} className=\"absolute top-2 right-2 w-6 h-6 flex items-center justify-center rounded text-muted hover:text-ink cursor-pointer\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/></svg>\n </button>\n {fetchedModels.map(m => (\n <button key={m} onClick={() => { update('model', m); setShowModelDropdown(false) }} className={`w-full text-left px-3 py-2 text-sm hover:bg-surface transition-colors cursor-pointer ${form.model === m ? 'bg-coral-bg text-coral font-medium' : 'text-ink'}`}>{m}</button>\n ))}\n </div>\n )}\n </div>\n <p className=\"text-xs text-muted mt-1\">{t('providerForm.modelHelp')}</p>\n </div>\n\n <div className=\"flex items-center gap-3\">\n <button onClick={() => update('has_reasoning', !form.has_reasoning)} className={`relative w-10 h-6 rounded-full transition-colors cursor-pointer ${form.has_reasoning ? 'bg-coral' : 'bg-hairline'}`}>\n <div className={`absolute top-1 w-4 h-4 rounded-full bg-white shadow-sm transition-all ${form.has_reasoning ? 'left-5' : 'left-1'}`} />\n </button>\n <span className=\"text-sm font-medium text-ink\">{t('providerForm.reasoning')}</span>\n </div>\n\n <div className=\"bg-surface rounded-xl p-4\">\n <div className=\"flex items-center justify-between mb-3\">\n <span className=\"text-sm font-medium text-ink\">{t('providerForm.testSection')}</span>\n <button onClick={handleTest} disabled={testing || !form.base_url || !form.api_key} className=\"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-coral text-on-primary rounded-lg hover:bg-coral-active transition-colors disabled:opacity-40 cursor-pointer\">\n {testing ? <>{t('providerForm.testing')}</> : <>{t('providerForm.testBtn')}</>}\n </button>\n </div>\n {testResult && (\n <div className={`px-3 py-2 rounded-lg text-xs ${testResult.status === 'ok' ? 'bg-success-bg text-success' : 'bg-error-bg text-error'}`}>\n <span className=\"font-medium\">{testResult.status === 'ok' ? t('providerForm.testOk') : t('providerForm.testFail')}</span>\n {testResult.latency_ms && <span className=\"ml-2 opacity-70\">{testResult.latency_ms}ms</span>}\n {testResult.message && <span className=\"ml-2\">{testResult.message}</span>}\n </div>\n )}\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">{t('providerForm.configPreview')}</label>\n <textarea value={configText} onChange={handleConfigChange} spellCheck={false} className=\"w-full bg-surface-dark rounded-xl p-4 text-xs text-on-dark/80 font-mono leading-relaxed overflow-x-auto border border-hairline focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors resize-y min-h-[180px]\" rows={12} />\n {configError && <p className=\"text-xs text-error mt-1\">{configError}</p>}\n </div>\n\n <div className=\"flex justify-end pt-4 pb-8\">\n <button onClick={handleSave} className=\"flex items-center gap-2 px-6 py-2.5 bg-coral text-on-primary rounded-xl text-sm font-medium hover:bg-coral-active transition-colors cursor-pointer\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z\"/><polyline points=\"17 21 17 13 7 13 7 21\"/><polyline points=\"7 3 7 8 15 8\"/></svg>\n {t('providerForm.save')}\n </button>\n </div>\n </div>\n </main>\n </div>\n )\n}\n"}, "d2030da84f65ed15a1e87915e48ab3ecb9e36a85": {"t": 1785764165.2528746, "n": 22237, "h": "b1581063c9a94678", "c": "import { useState, useEffect } from 'react'\nimport { api } from '../lib/api'\n\nconst PRESETS = [\n { id: 'deepseek', name: 'DeepSeek', base_url: 'https://api.deepseek.com', model: 'deepseek-chat', has_reasoning: true },\n { id: 'qwen', name: 'Qwen (通义千问)', base_url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-max', has_reasoning: true },\n { id: 'kimi', name: 'Kimi (月之暗面)', base_url: 'https://api.moonshot.cn/v1', model: 'moonshot-v1-128k', has_reasoning: false },\n { id: 'glm', name: 'GLM (智谱)', base_url: 'https://open.bigmodel.cn/api/paas/v4', model: 'glm-4-plus', has_reasoning: false },\n { id: 'mimo', name: 'MiMo (小米)', base_url: 'https://api.xiaomimimo.com/v1', model: 'mimo-v2.5-pro', has_reasoning: true },\n { id: 'minimax', name: 'MiniMax', base_url: 'https://api.minimax.chat/v1', model: 'MiniMax-Text-01', has_reasoning: false },\n { id: 'baichuan', name: 'Baichuan (百川)', base_url: 'https://api.baichuan-ai.com/v1', model: 'Baichuan4', has_reasoning: false },\n { id: 'yi', name: 'Yi (零一万物)', base_url: 'https://api.lingyiwanwu.com/v1', model: 'yi-lightning', has_reasoning: false },\n { id: 'doubao', name: 'Doubao (豆包)', base_url: 'https://ark.cn-beijing.volces.com/api/v3', model: 'doubao-1.5-pro-256k', has_reasoning: true },\n { id: 'stepfun', name: 'StepFun (阶跃星辰)', base_url: 'https://api.stepfun.com/v1', model: 'step-2-16k', has_reasoning: false },\n { id: 'siliconflow', name: 'SiliconFlow (硅基流动)', base_url: 'https://api.siliconflow.cn/v1', model: 'deepseek-ai/DeepSeek-V3', has_reasoning: true },\n { id: 'anthropic', name: 'Anthropic (Claude)', base_url: 'https://api.anthropic.com', model: 'claude-sonnet-4-20250514', has_reasoning: true, api_format: 'anthropic' },\n]\n\nexport default function ProviderForm({ provider, onBack }) {\n const isEdit = !!provider?.id\n\n const [form, setForm] = useState({\n name: '',\n api_format: 'openai',\n base_url: '',\n api_key: '',\n model: '',\n has_reasoning: false,\n note: '',\n website: '',\n })\n\n const [testing, setTesting] = useState(false)\n const [testResult, setTestResult] = useState(null)\n const [showKey, setShowKey] = useState(false)\n const [fetchingModels, setFetchingModels] = useState(false)\n const [fetchedModels, setFetchedModels] = useState([])\n const [showModelDropdown, setShowModelDropdown] = useState(false)\n const [toast, setToast] = useState(null)\n const [showPresets, setShowPresets] = useState(!isEdit)\n\n // Config preview - editable\n const [configText, setConfigText] = useState('')\n const [configError, setConfigError] = useState(null)\n\n useEffect(() => {\n if (provider?.id) {\n const models = provider.models || []\n setForm({\n name: provider.name || '',\n api_format: provider.api_format || 'openai',\n base_url: provider.base_url || '',\n api_key: provider.api_key || '',\n model: models[0] || '',\n has_reasoning: provider.has_reasoning || false,\n note: provider.note || '',\n website: provider.website || '',\n })\n }\n }, [provider])\n\n // Keep config preview in sync\n useEffect(() => {\n const preview = {\n name: form.name || 'provider',\n api_format: form.api_format,\n base_url: form.base_url,\n api_key: form.api_key ? 'sk-***' : '',\n models: form.model ? [form.model] : [],\n has_reasoning: form.has_reasoning,\n }\n setConfigText(JSON.stringify(preview, null, 2))\n setConfigError(null)\n }, [form.name, form.api_format, form.base_url, form.api_key, form.model, form.has_reasoning])\n\n const showToast = (msg, type = 'success') => {\n setToast({ msg, type })\n setTimeout(() => setToast(null), 2500)\n }\n\n const update = (key, val) => setForm(prev => ({ ...prev, [key]: val }))\n\n const applyPreset = (preset) => {\n setForm(prev => ({\n ...prev,\n name: preset.name,\n base_url: preset.base_url,\n model: preset.model,\n has_reasoning: preset.has_reasoning,\n api_format: preset.api_format || 'openai',\n }))\n setShowPresets(false)\n }\n\n const handleTest = async () => {\n setTesting(true)\n setTestResult(null)\n try {\n const res = await api.testConnection({\n base_url: form.base_url,\n api_key: form.api_key,\n api_format: form.api_format,\n model: form.model || '',\n })\n setTestResult(res)\n } catch (e) {\n setTestResult({ status: 'error', message: e.message })\n }\n setTesting(false)\n }\n\n const handleFetchModels = async () => {\n setFetchingModels(true)\n try {\n const res = await api.fetchModels({\n base_url: form.base_url,\n api_key: form.api_key,\n api_format: form.api_format,\n })\n if (res.status === 'ok' && res.models?.length) {\n setFetchedModels(res.models)\n setShowModelDropdown(true)\n showToast(`获取到 ${res.models.length} 个模型`)\n } else {\n showToast(res.message || '未获取到模型', 'error')\n }\n } catch (e) {\n showToast('获取失败: ' + e.message, 'error')\n }\n setFetchingModels(false)\n }\n\n const handleSave = async () => {\n if (!form.name.trim()) {\n showToast('请输入供应商名称', 'error')\n return\n }\n if (!form.model.trim()) {\n showToast('请输入模型名称', 'error')\n return\n }\n\n // Parse config from the editable preview\n let configData\n try {\n configData = JSON.parse(configText)\n } catch {\n showToast('配置 JSON 格式错误,请检查', 'error')\n return\n }\n\n const data = {\n name: configData.name || form.name.trim(),\n api_format: form.api_format,\n base_url: form.base_url.trim(),\n api_key: form.api_key.trim(),\n models: Array.isArray(configData.models) ? configData.models : [form.model],\n has_reasoning: configData.has_reasoning !== undefined ? configData.has_reasoning : form.has_reasoning,\n note: form.note.trim(),\n website: form.website.trim(),\n }\n\n try {\n if (isEdit) {\n await api.updateProvider(provider.id, data)\n showToast('已更新')\n } else {\n await api.createProvider(data)\n showToast('已添加')\n }\n onBack()\n } catch (e) {\n showToast('保存失败: ' + e.message, 'error')\n }\n }\n\n const handleConfigChange = (e) => {\n const val = e.target.value\n setConfigText(val)\n try {\n const parsed = JSON.parse(val)\n setConfigError(null)\n // Sync form fields from edited config\n if (parsed.name !== undefined) setForm(prev => ({ ...prev, name: parsed.name }))\n if (parsed.models && Array.isArray(parsed.models) && parsed.models.length > 0) {\n setForm(prev => ({ ...prev, model: parsed.models[0] }))\n }\n if (parsed.has_reasoning !== undefined) setForm(prev => ({ ...prev, has_reasoning: parsed.has_reasoning }))\n } catch {\n setConfigError('JSON 格式无效')\n }\n }\n\n return (\n <div className=\"min-h-screen bg-canvas\">\n {toast && (\n <div className={`fixed top-4 right-4 z-50 px-4 py-2.5 rounded-xl text-sm font-medium shadow-lg animate-fade-in ${\n toast.type === 'error' ? 'bg-error text-on-primary' : 'bg-surface-dark text-on-dark'\n }`}>\n {toast.msg}\n </div>\n )}\n\n {/* Header */}\n <header className=\"border-b border-hairline bg-canvas/80 backdrop-blur-md sticky top-0 z-40\">\n <div className=\"max-w-2xl mx-auto px-6 h-14 flex items-center gap-4\">\n <button\n onClick={onBack}\n className=\"w-8 h-8 flex items-center justify-center rounded-lg text-muted hover:text-ink hover:bg-surface transition-colors cursor-pointer\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M19 12H5m7-7l-7 7 7 7\"/></svg>\n </button>\n <h1 className=\"text-base font-semibold text-ink\">{isEdit ? '编辑供应商' : '添加供应商'}</h1>\n </div>\n </header>\n\n <main className=\"max-w-2xl mx-auto px-6 py-8\">\n {/* Preset Selector (only for new) */}\n {showPresets && !isEdit && (\n <div className=\"mb-8 animate-fade-in\">\n <h2 className=\"text-sm font-medium text-muted mb-3\">选择预设模板快速添加</h2>\n <div className=\"grid grid-cols-2 sm:grid-cols-3 gap-2\">\n {PRESETS.map(p => (\n <button\n key={p.id}\n onClick={() => applyPreset(p)}\n className=\"flex items-center gap-2 px-3 py-2.5 bg-surface rounded-xl text-left hover:bg-surface-hover transition-colors cursor-pointer\"\n >\n <div className=\"w-7 h-7 rounded-lg bg-coral-bg text-coral flex items-center justify-center text-[10px] font-bold shrink-0\">\n {p.name.charAt(0)}\n </div>\n <span className=\"text-xs font-medium text-ink truncate\">{p.name}</span>\n </button>\n ))}\n <button\n onClick={() => setShowPresets(false)}\n className=\"flex items-center gap-2 px-3 py-2.5 border border-dashed border-hairline rounded-xl text-left hover:bg-surface transition-colors cursor-pointer\"\n >\n <div className=\"w-7 h-7 rounded-lg bg-surface flex items-center justify-center text-muted\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\"/><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/></svg>\n </div>\n <span className=\"text-xs font-medium text-muted\">自定义</span>\n </button>\n </div>\n </div>\n )}\n\n <div className=\"space-y-6\">\n {/* Avatar + Name */}\n <div className=\"flex items-start gap-4\">\n <div className=\"w-16 h-16 rounded-2xl bg-surface flex items-center justify-center text-xl font-bold text-muted shrink-0 mt-1\">\n {form.name ? form.name.charAt(0).toUpperCase() : '?'}\n </div>\n <div className=\"flex-1 grid grid-cols-1 sm:grid-cols-2 gap-4\">\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">供应商名称 *</label>\n <input\n type=\"text\"\n value={form.name}\n onChange={e => update('name', e.target.value)}\n placeholder=\"例如: DeepSeek\"\n className=\"w-full px-3 py-2 bg-canvas border border-hairline rounded-xl text-sm text-ink placeholder:text-muted/50 focus:outline-none focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors\"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">备注</label>\n <input\n type=\"text\"\n value={form.note}\n onChange={e => update('note', e.target.value)}\n placeholder=\"例如: 公司专用账号\"\n className=\"w-full px-3 py-2 bg-canvas border border-hairline rounded-xl text-sm text-ink placeholder:text-muted/50 focus:outline-none focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors\"\n />\n </div>\n </div>\n </div>\n\n {/* Website */}\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">官网链接</label>\n <input\n type=\"url\"\n value={form.website}\n onChange={e => update('website', e.target.value)}\n placeholder=\"https://example.com\"\n className=\"w-full px-3 py-2 bg-canvas border border-hairline rounded-xl text-sm text-ink placeholder:text-muted/50 focus:outline-none focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors\"\n />\n </div>\n\n {/* API Format */}\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">API 格式</label>\n <div className=\"flex gap-2\">\n {['openai', 'anthropic'].map(fmt => (\n <button\n key={fmt}\n onClick={() => update('api_format', fmt)}\n className={`px-4 py-2 rounded-xl text-sm font-medium transition-colors cursor-pointer ${\n form.api_format === fmt\n ? 'bg-coral text-on-primary'\n : 'bg-surface text-muted hover:bg-surface-hover'\n }`}\n >\n {fmt === 'openai' ? 'OpenAI 兼容' : 'Anthropic'}\n </button>\n ))}\n </div>\n </div>\n\n {/* API Key */}\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">API Key</label>\n <div className=\"relative\">\n <input\n type={showKey ? 'text' : 'password'}\n value={form.api_key}\n onChange={e => update('api_key', e.target.value)}\n placeholder=\"sk-...\"\n className=\"w-full px-3 py-2 pr-10 bg-canvas border border-hairline rounded-xl text-sm text-ink placeholder:text-muted/50 focus:outline-none focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors font-mono\"\n />\n <button\n type=\"button\"\n onClick={() => setShowKey(!showKey)}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 w-7 h-7 flex items-center justify-center rounded-lg text-muted hover:text-ink transition-colors cursor-pointer\"\n >\n {showKey ? (\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg>\n ) : (\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24\"/><line x1=\"1\" y1=\"1\" x2=\"23\" y2=\"23\"/></svg>\n )}\n </button>\n </div>\n </div>\n\n {/* API Base URL */}\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">API 请求地址</label>\n <input\n type=\"url\"\n value={form.base_url}\n onChange={e => update('base_url', e.target.value)}\n placeholder=\"https://api.example.com/v1\"\n className=\"w-full px-3 py-2 bg-canvas border border-hairline rounded-xl text-sm text-ink placeholder:text-muted/50 focus:outline-none focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors\"\n />\n <p className=\"text-xs text-muted mt-1\">请填写完整请求 URL,代理会自动拼接 /chat/completions 路径</p>\n </div>\n\n {/* Model Name - Single input */}\n <div>\n <div className=\"flex items-center justify-between mb-1.5\">\n <label className=\"text-sm font-medium text-ink\">模型名称 *</label>\n <button\n onClick={handleFetchModels}\n disabled={fetchingModels || !form.base_url || !form.api_key}\n className=\"flex items-center gap-1 text-xs text-coral hover:text-coral-active transition-colors disabled:opacity-40 cursor-pointer\"\n >\n {fetchingModels ? (\n <svg className=\"animate-spin\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M12 2v4m0 12v4m-7.07-3.93l2.83-2.83m8.48-8.48l2.83-2.83M2 12h4m12 0h4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83\"/></svg>\n ) : (\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4\"/><polyline points=\"7 10 12 15 17 10\"/><line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"/></svg>\n )}\n 获取模型列表\n </button>\n </div>\n <div className=\"relative\">\n <input\n type=\"text\"\n value={form.model}\n onChange={e => {\n update('model', e.target.value)\n setShowModelDropdown(false)\n }}\n onFocus={() => { if (fetchedModels.length) setShowModelDropdown(true) }}\n placeholder=\"例如: deepseek-chat\"\n className=\"w-full px-3 py-2 bg-canvas border border-hairline rounded-xl text-sm text-ink placeholder:text-muted/50 focus:outline-none focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors\"\n />\n {showModelDropdown && fetchedModels.length > 0 && (\n <div className=\"absolute z-10 w-full mt-1 bg-canvas border border-hairline rounded-xl shadow-lg max-h-48 overflow-y-auto\">\n <button\n onClick={() => { setShowModelDropdown(false) }}\n className=\"absolute top-2 right-2 w-6 h-6 flex items-center justify-center rounded text-muted hover:text-ink cursor-pointer\"\n >\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/></svg>\n </button>\n {fetchedModels.map(m => (\n <button\n key={m}\n onClick={() => {\n update('model', m)\n setShowModelDropdown(false)\n }}\n className={`w-full text-left px-3 py-2 text-sm hover:bg-surface transition-colors cursor-pointer ${\n form.model === m ? 'bg-coral-bg text-coral font-medium' : 'text-ink'\n }`}\n >\n {m}\n </button>\n ))}\n </div>\n )}\n </div>\n <p className=\"text-xs text-muted mt-1\">填写一个模型名称,代理会转发给对应 API</p>\n </div>\n\n {/* Reasoning Toggle */}\n <div className=\"flex items-center gap-3\">\n <button\n onClick={() => update('has_reasoning', !form.has_reasoning)}\n className={`relative w-10 h-6 rounded-full transition-colors cursor-pointer ${\n form.has_reasoning ? 'bg-coral' : 'bg-hairline'\n }`}\n >\n <div className={`absolute top-1 w-4 h-4 rounded-full bg-white shadow-sm transition-all ${\n form.has_reasoning ? 'left-5' : 'left-1'\n }`} />\n </button>\n <div>\n <span className=\"text-sm font-medium text-ink\">支持推理内容</span>\n <span className=\"text-xs text-muted ml-2\">(reasoning_content / thinking 字段)</span>\n </div>\n </div>\n\n {/* Test Connection */}\n <div className=\"bg-surface rounded-xl p-4\">\n <div className=\"flex items-center justify-between mb-3\">\n <span className=\"text-sm font-medium text-ink\">测试连接</span>\n <button\n onClick={handleTest}\n disabled={testing || !form.base_url || !form.api_key}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-coral text-on-primary rounded-lg hover:bg-coral-active transition-colors disabled:opacity-40 cursor-pointer\"\n >\n {testing ? (\n <>\n <svg className=\"animate-spin\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M12 2v4m0 12v4m-7.07-3.93l2.83-2.83m8.48-8.48l2.83-2.83M2 12h4m12 0h4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83\"/></svg>\n 测试中...\n </>\n ) : (\n <>\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M13 10V3L4 14h7v7l9-11h-7z\"/></svg>\n 发送测试请求\n </>\n )}\n </button>\n </div>\n {testResult && (\n <div className={`px-3 py-2 rounded-lg text-xs ${\n testResult.status === 'ok'\n ? 'bg-success-bg text-success'\n : 'bg-error-bg text-error'\n }`}>\n <span className=\"font-medium\">\n {testResult.status === 'ok' ? '连接成功' : '连接失败'}\n </span>\n {testResult.latency_ms && <span className=\"ml-2 opacity-70\">{testResult.latency_ms}ms</span>}\n {testResult.message && <span className=\"ml-2\">{testResult.message}</span>}\n </div>\n )}\n </div>\n\n {/* Config Preview - Editable */}\n <div>\n <label className=\"block text-sm font-medium text-ink mb-1.5\">配置预览</label>\n <textarea\n value={configText}\n onChange={handleConfigChange}\n spellCheck={false}\n className=\"w-full bg-surface-dark rounded-xl p-4 text-xs text-on-dark/80 font-mono leading-relaxed overflow-x-auto border border-hairline focus:border-coral/50 focus:ring-1 focus:ring-coral/20 transition-colors resize-y min-h-[180px]\"\n rows={12}\n />\n {configError && (\n <p className=\"text-xs text-error mt-1\">{configError}</p>\n )}\n </div>\n\n {/* Save Button */}\n <div className=\"flex justify-end pt-4 pb-8\">\n <button\n onClick={handleSave}\n className=\"flex items-center gap-2 px-6 py-2.5 bg-coral text-on-primary rounded-xl text-sm font-medium hover:bg-coral-active transition-colors cursor-pointer\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\"><path d=\"M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z\"/><polyline points=\"17 21 17 13 7 13 7 21\"/><polyline points=\"7 3 7 8 15 8\"/></svg>\n 保存\n </button>\n </div>\n </div>\n </main>\n </div>\n )\n}\n"}}}}