{"repo": "cheng-ren/flowlens", "files": {"src/renderer/src/components/settings/LLMSettingsView.tsx": {"ec98c8d0a1e6109890dd4bdd0e0b45a0c0113292": {"t": 1785747362.5800986, "n": 11439, "h": "dfbe5d9956d52a5a", "c": "import React, { useEffect, useState } from \"react\";\nimport { C } from \"../../styles\";\n\nconst DEFAULT = {\n baseUrl: \"http://localhost:11434\",\n apiKey: \"\",\n model: \"qwen2.5:7b\",\n};\n\nconst PRESETS = [\n {\n label: \"Ollama 本地\",\n baseUrl: \"http://localhost:11434\",\n model: \"qwen2.5:7b\",\n apiKey: \"\",\n },\n {\n label: \"阿里百炼\",\n baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n model: \"qwen-plus\",\n apiKey: \"\",\n },\n {\n label: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com/v1\",\n model: \"deepseek-chat\",\n apiKey: \"\",\n },\n {\n label: \"OpenAI\",\n baseUrl: \"https://api.openai.com/v1\",\n model: \"gpt-4o-mini\",\n apiKey: \"\",\n },\n];\n\nconst inputStyle: React.CSSProperties = {\n width: \"100%\",\n padding: \"9px 12px\",\n background: C.bgSurface,\n border: `1px solid ${C.borderMd}`,\n borderRadius: 8,\n color: C.text1,\n fontSize: 13,\n outline: \"none\",\n boxSizing: \"border-box\",\n transition: \"border-color 0.15s\",\n fontFamily: \"inherit\",\n};\n\nconst labelStyle: React.CSSProperties = {\n fontSize: 11,\n color: C.text2,\n fontWeight: 600,\n marginBottom: 6,\n display: \"block\",\n textTransform: \"uppercase\",\n letterSpacing: \"0.07em\",\n};\n\nconst hintStyle: React.CSSProperties = {\n fontSize: 12,\n color: C.text3,\n marginTop: 5,\n lineHeight: 1.5,\n};\n\nexport function LLMSettingsView() {\n const [baseUrl, setBaseUrl] = useState(DEFAULT.baseUrl);\n const [apiKey, setApiKey] = useState(DEFAULT.apiKey);\n const [model, setModel] = useState(DEFAULT.model);\n const [showKey, setShowKey] = useState(false);\n const [status, setStatus] = useState<\"idle\" | \"saved\" | \"error\">(\"idle\");\n const [testState, setTestState] = useState<\"idle\" | \"testing\" | \"ok\" | \"fail\">(\"idle\");\n const [testMsg, setTestMsg] = useState(\"\");\n\n useEffect(() => {\n // @ts-ignore\n window.api.getLlmConfig().then((cfg: typeof DEFAULT) => {\n setBaseUrl(cfg.baseUrl || DEFAULT.baseUrl);\n setApiKey(cfg.apiKey || \"\");\n setModel(cfg.model || DEFAULT.model);\n });\n }, []);\n\n const handleTest = async () => {\n setTestState(\"testing\");\n setTestMsg(\"\");\n try {\n // @ts-ignore\n const result: { ok: boolean; reply?: string; error?: string } = await window.api.testLlmConfig({ baseUrl, apiKey, model });\n if (result.ok) {\n setTestState(\"ok\");\n setTestMsg(result.reply || \"连接成功\");\n } else {\n setTestState(\"fail\");\n setTestMsg(result.error || \"连接失败\");\n }\n } catch (e: any) {\n setTestState(\"fail\");\n setTestMsg(e?.message || \"未知错误\");\n }\n };\n\n const handleSave = async () => {\n try {\n // @ts-ignore\n await window.api.saveLlmConfig({ baseUrl, apiKey, model });\n setStatus(\"saved\");\n } catch {\n setStatus(\"error\");\n } finally {\n setTimeout(() => setStatus(\"idle\"), 2500);\n }\n };\n\n const isOllama = baseUrl.includes(\"11434\") || !apiKey;\n\n return (\n
\n
\n

\n AI 分类设置\n

\n

\n 用于商品账单的自动分类,默认使用本机 Ollama。\n

\n
\n\n {/* 快速选择服务商 */}\n
\n 快速选择服务商\n
\n {PRESETS.map((p) => {\n const active = baseUrl.trim() === p.baseUrl;\n return (\n {\n setBaseUrl(p.baseUrl);\n setModel(p.model);\n if (p.apiKey) setApiKey(p.apiKey);\n setTestState(\"idle\");\n setTestMsg(\"\");\n }}\n style={{\n padding: \"5px 12px\",\n fontSize: 12,\n fontWeight: 500,\n borderRadius: 20,\n border: `1px solid ${active ? C.accent : C.borderMd}`,\n background: active ? `${C.accent}18` : \"transparent\",\n color: active ? C.accent : C.text2,\n cursor: \"pointer\",\n transition: \"all 0.15s\",\n whiteSpace: \"nowrap\",\n }}\n onMouseEnter={(e) => {\n if (!active) {\n e.currentTarget.style.borderColor = C.accent;\n e.currentTarget.style.color = C.text1;\n }\n }}\n onMouseLeave={(e) => {\n if (!active) {\n e.currentTarget.style.borderColor = C.borderMd;\n e.currentTarget.style.color = C.text2;\n }\n }}\n >\n {p.label}\n \n );\n })}\n
\n
\n\n {/* Base URL */}\n
\n Base URL\n setBaseUrl(e.target.value)}\n placeholder={DEFAULT.baseUrl}\n style={inputStyle}\n onFocus={(e) => (e.target.style.borderColor = C.accent)}\n onBlur={(e) => (e.target.style.borderColor = C.borderMd)}\n />\n

\n {isOllama\n ? \"Ollama 本地模式,无需 API Key\"\n : \"OpenAI 兼容接口,请填写 /v1 结尾的地址,例如 https://api.deepseek.com/v1\"}\n

\n
\n\n {/* API Key */}\n
\n \n API Key\n \n (Ollama 本地可留空)\n \n \n
\n setApiKey(e.target.value)}\n placeholder=\"sk-...\"\n style={{ ...inputStyle, paddingRight: 44 }}\n onFocus={(e) => (e.target.style.borderColor = C.accent)}\n onBlur={(e) => (e.target.style.borderColor = C.borderMd)}\n />\n setShowKey((v) => !v)}\n style={{\n position: \"absolute\",\n right: 10,\n top: \"50%\",\n transform: \"translateY(-50%)\",\n background: \"transparent\",\n border: \"none\",\n color: C.text3,\n cursor: \"pointer\",\n fontSize: 14,\n padding: 2,\n }}\n >\n {showKey ? \"🙈\" : \"👁️\"}\n \n
\n
\n\n {/* 模型名称 */}\n
\n 模型名称\n setModel(e.target.value)}\n placeholder={DEFAULT.model}\n style={inputStyle}\n onFocus={(e) => (e.target.style.borderColor = C.accent)}\n onBlur={(e) => (e.target.style.borderColor = C.borderMd)}\n />\n

\n Ollama 示例:qwen2.5:7b、llama3.2 | 云端示例:deepseek-chat、gpt-4o-mini\n

\n
\n\n {/* 操作按钮行 */}\n
\n {/* 测试按钮 */}\n { if (testState !== \"testing\") e.currentTarget.style.borderColor = C.accent; e.currentTarget.style.color = C.text1; }}\n onMouseLeave={(e) => {\n e.currentTarget.style.borderColor = testState === \"ok\" ? C.green : testState === \"fail\" ? C.red : C.borderMd;\n e.currentTarget.style.color = testState === \"ok\" ? C.green : testState === \"fail\" ? C.red : C.text2;\n }}\n >\n {testState === \"testing\" ? \"测试中…\" : \"测试连接\"}\n \n\n {/* 保存按钮 */}\n (e.currentTarget.style.opacity = \"0.85\")}\n onMouseLeave={(e) => (e.currentTarget.style.opacity = \"1\")}\n >\n 保存\n \n\n {status === \"saved\" && (\n ✓ 已保存\n )}\n {status === \"error\" && (\n 保存失败\n )}\n
\n\n {/* 测试结果 */}\n {testMsg && (\n \n {testState === \"ok\" ? \"✓ 连接成功,模型回复:\" : \"✕ 连接失败:\"}\n {testMsg}\n
\n )}\n\n {/* 提示 */}\n \n
\n 本地 Ollama(推荐,免费)\n
\n 安装 Ollama 后运行:ollama pull qwen2.5:7b,无需填写 API Key。\n

\n 阿里百炼\n
\n Base URL 填 https://dashscope.aliyuncs.com/compatible-mode/v1,\n 填入百炼 API Key,模型填 qwen-plus 或 qwen-turbo。\n

\n DeepSeek / OpenAI\n
\n Base URL 填 https://api.deepseek.com/v1,\n 填入 API Key,模型填 deepseek-chat。\n
\n \n \n );\n}\n"}, "30e45b7a1a4affac49f40c88d2b49b4e46e8091a": {"t": 1785772811.5281553, "n": 11439, "h": "dfbe5d9956d52a5a", "c": "import React, { useEffect, useState } from \"react\";\nimport { C } from \"../../styles\";\n\nconst DEFAULT = {\n baseUrl: \"http://localhost:11434\",\n apiKey: \"\",\n model: \"qwen2.5:7b\",\n};\n\nconst PRESETS = [\n {\n label: \"Ollama 本地\",\n baseUrl: \"http://localhost:11434\",\n model: \"qwen2.5:7b\",\n apiKey: \"\",\n },\n {\n label: \"阿里百炼\",\n baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n model: \"qwen-plus\",\n apiKey: \"\",\n },\n {\n label: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com/v1\",\n model: \"deepseek-chat\",\n apiKey: \"\",\n },\n {\n label: \"OpenAI\",\n baseUrl: \"https://api.openai.com/v1\",\n model: \"gpt-4o-mini\",\n apiKey: \"\",\n },\n];\n\nconst inputStyle: React.CSSProperties = {\n width: \"100%\",\n padding: \"9px 12px\",\n background: C.bgSurface,\n border: `1px solid ${C.borderMd}`,\n borderRadius: 8,\n color: C.text1,\n fontSize: 13,\n outline: \"none\",\n boxSizing: \"border-box\",\n transition: \"border-color 0.15s\",\n fontFamily: \"inherit\",\n};\n\nconst labelStyle: React.CSSProperties = {\n fontSize: 11,\n color: C.text2,\n fontWeight: 600,\n marginBottom: 6,\n display: \"block\",\n textTransform: \"uppercase\",\n letterSpacing: \"0.07em\",\n};\n\nconst hintStyle: React.CSSProperties = {\n fontSize: 12,\n color: C.text3,\n marginTop: 5,\n lineHeight: 1.5,\n};\n\nexport function LLMSettingsView() {\n const [baseUrl, setBaseUrl] = useState(DEFAULT.baseUrl);\n const [apiKey, setApiKey] = useState(DEFAULT.apiKey);\n const [model, setModel] = useState(DEFAULT.model);\n const [showKey, setShowKey] = useState(false);\n const [status, setStatus] = useState<\"idle\" | \"saved\" | \"error\">(\"idle\");\n const [testState, setTestState] = useState<\"idle\" | \"testing\" | \"ok\" | \"fail\">(\"idle\");\n const [testMsg, setTestMsg] = useState(\"\");\n\n useEffect(() => {\n // @ts-ignore\n window.api.getLlmConfig().then((cfg: typeof DEFAULT) => {\n setBaseUrl(cfg.baseUrl || DEFAULT.baseUrl);\n setApiKey(cfg.apiKey || \"\");\n setModel(cfg.model || DEFAULT.model);\n });\n }, []);\n\n const handleTest = async () => {\n setTestState(\"testing\");\n setTestMsg(\"\");\n try {\n // @ts-ignore\n const result: { ok: boolean; reply?: string; error?: string } = await window.api.testLlmConfig({ baseUrl, apiKey, model });\n if (result.ok) {\n setTestState(\"ok\");\n setTestMsg(result.reply || \"连接成功\");\n } else {\n setTestState(\"fail\");\n setTestMsg(result.error || \"连接失败\");\n }\n } catch (e: any) {\n setTestState(\"fail\");\n setTestMsg(e?.message || \"未知错误\");\n }\n };\n\n const handleSave = async () => {\n try {\n // @ts-ignore\n await window.api.saveLlmConfig({ baseUrl, apiKey, model });\n setStatus(\"saved\");\n } catch {\n setStatus(\"error\");\n } finally {\n setTimeout(() => setStatus(\"idle\"), 2500);\n }\n };\n\n const isOllama = baseUrl.includes(\"11434\") || !apiKey;\n\n return (\n
\n
\n

\n AI 分类设置\n

\n

\n 用于商品账单的自动分类,默认使用本机 Ollama。\n

\n
\n\n {/* 快速选择服务商 */}\n
\n 快速选择服务商\n
\n {PRESETS.map((p) => {\n const active = baseUrl.trim() === p.baseUrl;\n return (\n {\n setBaseUrl(p.baseUrl);\n setModel(p.model);\n if (p.apiKey) setApiKey(p.apiKey);\n setTestState(\"idle\");\n setTestMsg(\"\");\n }}\n style={{\n padding: \"5px 12px\",\n fontSize: 12,\n fontWeight: 500,\n borderRadius: 20,\n border: `1px solid ${active ? C.accent : C.borderMd}`,\n background: active ? `${C.accent}18` : \"transparent\",\n color: active ? C.accent : C.text2,\n cursor: \"pointer\",\n transition: \"all 0.15s\",\n whiteSpace: \"nowrap\",\n }}\n onMouseEnter={(e) => {\n if (!active) {\n e.currentTarget.style.borderColor = C.accent;\n e.currentTarget.style.color = C.text1;\n }\n }}\n onMouseLeave={(e) => {\n if (!active) {\n e.currentTarget.style.borderColor = C.borderMd;\n e.currentTarget.style.color = C.text2;\n }\n }}\n >\n {p.label}\n \n );\n })}\n
\n
\n\n {/* Base URL */}\n
\n Base URL\n setBaseUrl(e.target.value)}\n placeholder={DEFAULT.baseUrl}\n style={inputStyle}\n onFocus={(e) => (e.target.style.borderColor = C.accent)}\n onBlur={(e) => (e.target.style.borderColor = C.borderMd)}\n />\n

\n {isOllama\n ? \"Ollama 本地模式,无需 API Key\"\n : \"OpenAI 兼容接口,请填写 /v1 结尾的地址,例如 https://api.deepseek.com/v1\"}\n

\n
\n\n {/* API Key */}\n
\n \n API Key\n \n (Ollama 本地可留空)\n \n \n
\n setApiKey(e.target.value)}\n placeholder=\"sk-...\"\n style={{ ...inputStyle, paddingRight: 44 }}\n onFocus={(e) => (e.target.style.borderColor = C.accent)}\n onBlur={(e) => (e.target.style.borderColor = C.borderMd)}\n />\n setShowKey((v) => !v)}\n style={{\n position: \"absolute\",\n right: 10,\n top: \"50%\",\n transform: \"translateY(-50%)\",\n background: \"transparent\",\n border: \"none\",\n color: C.text3,\n cursor: \"pointer\",\n fontSize: 14,\n padding: 2,\n }}\n >\n {showKey ? \"🙈\" : \"👁️\"}\n \n
\n
\n\n {/* 模型名称 */}\n
\n 模型名称\n setModel(e.target.value)}\n placeholder={DEFAULT.model}\n style={inputStyle}\n onFocus={(e) => (e.target.style.borderColor = C.accent)}\n onBlur={(e) => (e.target.style.borderColor = C.borderMd)}\n />\n

\n Ollama 示例:qwen2.5:7b、llama3.2 | 云端示例:deepseek-chat、gpt-4o-mini\n

\n
\n\n {/* 操作按钮行 */}\n
\n {/* 测试按钮 */}\n { if (testState !== \"testing\") e.currentTarget.style.borderColor = C.accent; e.currentTarget.style.color = C.text1; }}\n onMouseLeave={(e) => {\n e.currentTarget.style.borderColor = testState === \"ok\" ? C.green : testState === \"fail\" ? C.red : C.borderMd;\n e.currentTarget.style.color = testState === \"ok\" ? C.green : testState === \"fail\" ? C.red : C.text2;\n }}\n >\n {testState === \"testing\" ? \"测试中…\" : \"测试连接\"}\n \n\n {/* 保存按钮 */}\n (e.currentTarget.style.opacity = \"0.85\")}\n onMouseLeave={(e) => (e.currentTarget.style.opacity = \"1\")}\n >\n 保存\n \n\n {status === \"saved\" && (\n ✓ 已保存\n )}\n {status === \"error\" && (\n 保存失败\n )}\n
\n\n {/* 测试结果 */}\n {testMsg && (\n \n {testState === \"ok\" ? \"✓ 连接成功,模型回复:\" : \"✕ 连接失败:\"}\n {testMsg}\n
\n )}\n\n {/* 提示 */}\n \n
\n 本地 Ollama(推荐,免费)\n
\n 安装 Ollama 后运行:ollama pull qwen2.5:7b,无需填写 API Key。\n

\n 阿里百炼\n
\n Base URL 填 https://dashscope.aliyuncs.com/compatible-mode/v1,\n 填入百炼 API Key,模型填 qwen-plus 或 qwen-turbo。\n

\n DeepSeek / OpenAI\n
\n Base URL 填 https://api.deepseek.com/v1,\n 填入 API Key,模型填 deepseek-chat。\n
\n \n \n );\n}\n"}, "6458d986c68195cf1ef63df7c200d731c2b0c595": {"t": 1785772814.7605546, "n": 7731, "h": "3f717aca5ae052e6", "c": "import React, { useState } from \"react\";\nimport { C } from \"../../styles\";\n\nconst PROVIDERS = [\n { id: \"openai\", label: \"OpenAI\", icon: \"🟢\", placeholder: \"sk-...\" },\n { id: \"anthropic\", label: \"Anthropic (Claude)\", icon: \"🟣\", placeholder: \"sk-ant-...\" },\n { id: \"deepseek\", label: \"DeepSeek\", icon: \"🔵\", placeholder: \"sk-...\" },\n { id: \"zhipu\", label: \"智谱 GLM\", icon: \"🔶\", placeholder: \"...\" },\n { id: \"custom\", label: \"自定义\", icon: \"⚙️\", placeholder: \"sk-...\" },\n];\n\nconst MODELS: Record = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\n \"claude-opus-4-5\",\n \"claude-sonnet-4-5\",\n \"claude-3-5-haiku-20241022\",\n ],\n deepseek: [\"deepseek-chat\", \"deepseek-reasoner\"],\n zhipu: [\"glm-4-flash\", \"glm-4-air\", \"glm-4\"],\n custom: [],\n};\n\nconst inputStyle: React.CSSProperties = {\n width: \"100%\",\n padding: \"9px 12px\",\n background: C.bgSurface,\n border: `1px solid ${C.borderMd}`,\n borderRadius: 8,\n color: C.text1,\n fontSize: 13,\n outline: \"none\",\n boxSizing: \"border-box\",\n transition: \"border-color 0.15s\",\n};\n\nconst labelStyle: React.CSSProperties = {\n fontSize: 11,\n color: C.text3,\n fontWeight: 600,\n marginBottom: 6,\n display: \"block\",\n textTransform: \"uppercase\",\n letterSpacing: \"0.07em\",\n};\n\nexport function LLMSettingsView() {\n const [provider, setProvider] = useState(\"openai\");\n const [apiKey, setApiKey] = useState(\"\");\n const [model, setModel] = useState(\"gpt-4o\");\n const [baseUrl, setBaseUrl] = useState(\"\");\n const [showKey, setShowKey] = useState(false);\n const [saved, setSaved] = useState(false);\n\n const handleProviderChange = (id: string) => {\n setProvider(id);\n const models = MODELS[id];\n setModel(models?.[0] ?? \"\");\n if (id !== \"custom\") setBaseUrl(\"\");\n };\n\n const handleSave = () => {\n setSaved(true);\n setTimeout(() => setSaved(false), 2500);\n };\n\n const currentProvider = PROVIDERS.find((p) => p.id === provider);\n const availableModels = MODELS[provider] ?? [];\n\n return (\n
\n
\n

\n 大模型设置\n

\n

\n 配置用于账单分析与报表生成的 AI 大模型接入信息\n

\n
\n\n {/* 提供商选择 */}\n
\n 模型提供商\n
\n {PROVIDERS.map((p) => (\n handleProviderChange(p.id)}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: 6,\n padding: \"7px 14px\",\n borderRadius: 8,\n border: `1px solid ${provider === p.id ? C.accent : C.border}`,\n background: provider === p.id ? C.bgActive : C.bgSurface,\n color: provider === p.id ? C.accentSoft : C.text2,\n cursor: \"pointer\",\n fontSize: 13,\n fontWeight: provider === p.id ? 500 : 400,\n transition: \"all 0.15s\",\n }}\n >\n {p.icon}\n {p.label}\n \n ))}\n
\n
\n\n {/* API Key */}\n
\n API Key\n
\n setApiKey(e.target.value)}\n placeholder={currentProvider?.placeholder ?? \"请输入 API Key\"}\n style={{ ...inputStyle, paddingRight: 44 }}\n onFocus={(e) => ((e.target as HTMLInputElement).style.borderColor = \"#38bdf8\")}\n onBlur={(e) => ((e.target as HTMLInputElement).style.borderColor = \"#334155\")}\n />\n setShowKey((v) => !v)}\n style={{\n position: \"absolute\",\n right: 10,\n top: \"50%\",\n transform: \"translateY(-50%)\",\n background: \"transparent\",\n border: \"none\",\n color: \"#64748b\",\n cursor: \"pointer\",\n fontSize: 14,\n padding: 2,\n }}\n >\n {showKey ? \"🙈\" : \"👁️\"}\n \n
\n
\n\n {/* 模型选择 */}\n
\n 使用模型\n {availableModels.length > 0 ? (\n setModel(e.target.value)}\n style={{\n ...inputStyle,\n appearance: \"none\",\n WebkitAppearance: \"none\",\n cursor: \"pointer\",\n }}\n >\n {availableModels.map((m) => (\n \n ))}\n \n ) : (\n setModel(e.target.value)}\n placeholder=\"请输入模型名称(如 qwen-plus)\"\n style={inputStyle}\n onFocus={(e) => ((e.target as HTMLInputElement).style.borderColor = \"#38bdf8\")}\n onBlur={(e) => ((e.target as HTMLInputElement).style.borderColor = \"#334155\")}\n />\n )}\n
\n\n {/* 自定义 Base URL(custom 或通用) */}\n
\n \n Base URL\n \n (可选,用于代理或本地部署)\n \n \n setBaseUrl(e.target.value)}\n placeholder=\"https://api.openai.com/v1\"\n style={inputStyle}\n onFocus={(e) => ((e.target as HTMLInputElement).style.borderColor = \"#38bdf8\")}\n onBlur={(e) => ((e.target as HTMLInputElement).style.borderColor = \"#334155\")}\n />\n
\n\n {/* 保存按钮 */}\n
\n ((e.currentTarget as HTMLButtonElement).style.opacity = \"0.85\")}\n onMouseLeave={(e) => ((e.currentTarget as HTMLButtonElement).style.opacity = \"1\")}\n >\n 保存设置\n \n {saved && (\n \n ✓ 已保存\n \n )}\n
\n\n {/* 提示卡片 */}\n \n
\n 🔒 安全提示\n
\n API Key 仅存储在本地设备,不会上传至任何服务器。\n
\n 大模型功能将用于智能分类账单、生成财务报表摘要等场景。\n
\n
\n \n );\n}\n"}}}}