Files

1 line
33 KiB
JSON
Raw Permalink 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": "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 <div style={{ padding: 30, maxWidth: 520 }}>\n <div style={{ marginBottom: 28 }}>\n <h2 style={{ margin: \"0 0 5px\", fontSize: 17, fontWeight: 600, color: C.text1 }}>\n AI 分类设置\n </h2>\n <p style={{ margin: 0, fontSize: 13, color: C.text2 }}>\n 用于商品账单的自动分类,默认使用本机 Ollama。\n </p>\n </div>\n\n {/* 快速选择服务商 */}\n <div style={{ marginBottom: 20 }}>\n <span style={labelStyle}>快速选择服务商</span>\n <div style={{ display: \"flex\", gap: 8, flexWrap: \"wrap\" }}>\n {PRESETS.map((p) => {\n const active = baseUrl.trim() === p.baseUrl;\n return (\n <button\n key={p.label}\n onClick={() => {\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 </button>\n );\n })}\n </div>\n </div>\n\n {/* Base URL */}\n <div style={{ marginBottom: 20 }}>\n <span style={labelStyle}>Base URL</span>\n <input\n type=\"text\"\n value={baseUrl}\n onChange={(e) => 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 <p style={hintStyle}>\n {isOllama\n ? \"Ollama 本地模式,无需 API Key\"\n : \"OpenAI 兼容接口,请填写 /v1 结尾的地址,例如 https://api.deepseek.com/v1\"}\n </p>\n </div>\n\n {/* API Key */}\n <div style={{ marginBottom: 20 }}>\n <span style={labelStyle}>\n API Key\n <span style={{ color: C.text3, marginLeft: 6, fontWeight: 400, textTransform: \"none\" }}>\n (Ollama 本地可留空)\n </span>\n </span>\n <div style={{ position: \"relative\" }}>\n <input\n type={showKey ? \"text\" : \"password\"}\n value={apiKey}\n onChange={(e) => 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 <button\n onClick={() => 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 </button>\n </div>\n </div>\n\n {/* 模型名称 */}\n <div style={{ marginBottom: 28 }}>\n <span style={labelStyle}>模型名称</span>\n <input\n type=\"text\"\n value={model}\n onChange={(e) => 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 <p style={hintStyle}>\n Ollama 示例:qwen2.5:7b、llama3.2&emsp;|&emsp;云端示例:deepseek-chat、gpt-4o-mini\n </p>\n </div>\n\n {/* 操作按钮行 */}\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 10, flexWrap: \"wrap\" }}>\n {/* 测试按钮 */}\n <button\n onClick={handleTest}\n disabled={testState === \"testing\"}\n style={{\n padding: \"9px 20px\",\n background: \"transparent\",\n border: `1px solid ${testState === \"ok\" ? C.green : testState === \"fail\" ? C.red : C.borderMd}`,\n borderRadius: 8,\n color: testState === \"ok\" ? C.green : testState === \"fail\" ? C.red : C.text2,\n fontSize: 13,\n fontWeight: 500,\n cursor: testState === \"testing\" ? \"default\" : \"pointer\",\n transition: \"all 0.15s\",\n opacity: testState === \"testing\" ? 0.6 : 1,\n }}\n onMouseEnter={(e) => { 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 </button>\n\n {/* 保存按钮 */}\n <button\n onClick={handleSave}\n style={{\n padding: \"9px 26px\",\n background: `linear-gradient(135deg, ${C.accent}, ${C.violet})`,\n border: \"none\",\n borderRadius: 8,\n color: \"white\",\n fontSize: 13,\n fontWeight: 600,\n cursor: \"pointer\",\n transition: \"opacity 0.15s\",\n }}\n onMouseEnter={(e) => (e.currentTarget.style.opacity = \"0.85\")}\n onMouseLeave={(e) => (e.currentTarget.style.opacity = \"1\")}\n >\n 保存\n </button>\n\n {status === \"saved\" && (\n <span style={{ fontSize: 13, color: C.green }}>✓ 已保存</span>\n )}\n {status === \"error\" && (\n <span style={{ fontSize: 13, color: C.red }}>保存失败</span>\n )}\n </div>\n\n {/* 测试结果 */}\n {testMsg && (\n <div\n style={{\n marginTop: 12,\n padding: \"10px 14px\",\n borderRadius: 8,\n background: testState === \"ok\"\n ? \"rgba(62,207,142,0.07)\"\n : \"rgba(248,81,73,0.07)\",\n border: `1px solid ${testState === \"ok\" ? \"rgba(62,207,142,0.2)\" : \"rgba(248,81,73,0.2)\"}`,\n fontSize: 13,\n color: testState === \"ok\" ? C.green : C.red,\n lineHeight: 1.6,\n wordBreak: \"break-all\",\n }}\n >\n {testState === \"ok\" ? \"✓ 连接成功,模型回复:\" : \"✕ 连接失败:\"}\n <span style={{ color: C.text2, marginLeft: 4 }}>{testMsg}</span>\n </div>\n )}\n\n {/* 提示 */}\n <div\n style={{\n marginTop: 28,\n padding: \"14px 16px\",\n background: C.bgSurface,\n borderRadius: 10,\n border: `1px solid ${C.border}`,\n }}\n >\n <div style={{ fontSize: 12, color: C.text2, lineHeight: 1.9 }}>\n <strong style={{ color: C.text2 }}>本地 Ollama(推荐,免费)</strong>\n <br />\n 安装 Ollama 后运行:<code style={{ color: C.accentSoft }}>ollama pull qwen2.5:7b</code>,无需填写 API Key。\n <br /><br />\n <strong style={{ color: C.text2 }}>阿里百炼</strong>\n <br />\n Base URL 填 <code style={{ color: C.accentSoft }}>https://dashscope.aliyuncs.com/compatible-mode/v1</code>,\n 填入百炼 API Key,模型填 <code style={{ color: C.accentSoft }}>qwen-plus</code> 或 <code style={{ color: C.accentSoft }}>qwen-turbo</code>。\n <br /><br />\n <strong style={{ color: C.text2 }}>DeepSeek / OpenAI</strong>\n <br />\n Base URL 填 <code style={{ color: C.accentSoft }}>https://api.deepseek.com/v1</code>,\n 填入 API Key,模型填 <code style={{ color: C.accentSoft }}>deepseek-chat</code>。\n </div>\n </div>\n </div>\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 <div style={{ padding: 30, maxWidth: 520 }}>\n <div style={{ marginBottom: 28 }}>\n <h2 style={{ margin: \"0 0 5px\", fontSize: 17, fontWeight: 600, color: C.text1 }}>\n AI 分类设置\n </h2>\n <p style={{ margin: 0, fontSize: 13, color: C.text2 }}>\n 用于商品账单的自动分类,默认使用本机 Ollama。\n </p>\n </div>\n\n {/* 快速选择服务商 */}\n <div style={{ marginBottom: 20 }}>\n <span style={labelStyle}>快速选择服务商</span>\n <div style={{ display: \"flex\", gap: 8, flexWrap: \"wrap\" }}>\n {PRESETS.map((p) => {\n const active = baseUrl.trim() === p.baseUrl;\n return (\n <button\n key={p.label}\n onClick={() => {\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 </button>\n );\n })}\n </div>\n </div>\n\n {/* Base URL */}\n <div style={{ marginBottom: 20 }}>\n <span style={labelStyle}>Base URL</span>\n <input\n type=\"text\"\n value={baseUrl}\n onChange={(e) => 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 <p style={hintStyle}>\n {isOllama\n ? \"Ollama 本地模式,无需 API Key\"\n : \"OpenAI 兼容接口,请填写 /v1 结尾的地址,例如 https://api.deepseek.com/v1\"}\n </p>\n </div>\n\n {/* API Key */}\n <div style={{ marginBottom: 20 }}>\n <span style={labelStyle}>\n API Key\n <span style={{ color: C.text3, marginLeft: 6, fontWeight: 400, textTransform: \"none\" }}>\n (Ollama 本地可留空)\n </span>\n </span>\n <div style={{ position: \"relative\" }}>\n <input\n type={showKey ? \"text\" : \"password\"}\n value={apiKey}\n onChange={(e) => 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 <button\n onClick={() => 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 </button>\n </div>\n </div>\n\n {/* 模型名称 */}\n <div style={{ marginBottom: 28 }}>\n <span style={labelStyle}>模型名称</span>\n <input\n type=\"text\"\n value={model}\n onChange={(e) => 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 <p style={hintStyle}>\n Ollama 示例:qwen2.5:7b、llama3.2&emsp;|&emsp;云端示例:deepseek-chat、gpt-4o-mini\n </p>\n </div>\n\n {/* 操作按钮行 */}\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 10, flexWrap: \"wrap\" }}>\n {/* 测试按钮 */}\n <button\n onClick={handleTest}\n disabled={testState === \"testing\"}\n style={{\n padding: \"9px 20px\",\n background: \"transparent\",\n border: `1px solid ${testState === \"ok\" ? C.green : testState === \"fail\" ? C.red : C.borderMd}`,\n borderRadius: 8,\n color: testState === \"ok\" ? C.green : testState === \"fail\" ? C.red : C.text2,\n fontSize: 13,\n fontWeight: 500,\n cursor: testState === \"testing\" ? \"default\" : \"pointer\",\n transition: \"all 0.15s\",\n opacity: testState === \"testing\" ? 0.6 : 1,\n }}\n onMouseEnter={(e) => { 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 </button>\n\n {/* 保存按钮 */}\n <button\n onClick={handleSave}\n style={{\n padding: \"9px 26px\",\n background: `linear-gradient(135deg, ${C.accent}, ${C.violet})`,\n border: \"none\",\n borderRadius: 8,\n color: \"white\",\n fontSize: 13,\n fontWeight: 600,\n cursor: \"pointer\",\n transition: \"opacity 0.15s\",\n }}\n onMouseEnter={(e) => (e.currentTarget.style.opacity = \"0.85\")}\n onMouseLeave={(e) => (e.currentTarget.style.opacity = \"1\")}\n >\n 保存\n </button>\n\n {status === \"saved\" && (\n <span style={{ fontSize: 13, color: C.green }}>✓ 已保存</span>\n )}\n {status === \"error\" && (\n <span style={{ fontSize: 13, color: C.red }}>保存失败</span>\n )}\n </div>\n\n {/* 测试结果 */}\n {testMsg && (\n <div\n style={{\n marginTop: 12,\n padding: \"10px 14px\",\n borderRadius: 8,\n background: testState === \"ok\"\n ? \"rgba(62,207,142,0.07)\"\n : \"rgba(248,81,73,0.07)\",\n border: `1px solid ${testState === \"ok\" ? \"rgba(62,207,142,0.2)\" : \"rgba(248,81,73,0.2)\"}`,\n fontSize: 13,\n color: testState === \"ok\" ? C.green : C.red,\n lineHeight: 1.6,\n wordBreak: \"break-all\",\n }}\n >\n {testState === \"ok\" ? \"✓ 连接成功,模型回复:\" : \"✕ 连接失败:\"}\n <span style={{ color: C.text2, marginLeft: 4 }}>{testMsg}</span>\n </div>\n )}\n\n {/* 提示 */}\n <div\n style={{\n marginTop: 28,\n padding: \"14px 16px\",\n background: C.bgSurface,\n borderRadius: 10,\n border: `1px solid ${C.border}`,\n }}\n >\n <div style={{ fontSize: 12, color: C.text2, lineHeight: 1.9 }}>\n <strong style={{ color: C.text2 }}>本地 Ollama(推荐,免费)</strong>\n <br />\n 安装 Ollama 后运行:<code style={{ color: C.accentSoft }}>ollama pull qwen2.5:7b</code>,无需填写 API Key。\n <br /><br />\n <strong style={{ color: C.text2 }}>阿里百炼</strong>\n <br />\n Base URL 填 <code style={{ color: C.accentSoft }}>https://dashscope.aliyuncs.com/compatible-mode/v1</code>,\n 填入百炼 API Key,模型填 <code style={{ color: C.accentSoft }}>qwen-plus</code> 或 <code style={{ color: C.accentSoft }}>qwen-turbo</code>。\n <br /><br />\n <strong style={{ color: C.text2 }}>DeepSeek / OpenAI</strong>\n <br />\n Base URL 填 <code style={{ color: C.accentSoft }}>https://api.deepseek.com/v1</code>,\n 填入 API Key,模型填 <code style={{ color: C.accentSoft }}>deepseek-chat</code>。\n </div>\n </div>\n </div>\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<string, string[]> = {\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 <div style={{ padding: 30, maxWidth: 560 }}>\n <div style={{ marginBottom: 26 }}>\n <h2 style={{ margin: \"0 0 5px\", fontSize: 17, fontWeight: 600, color: C.text1 }}>\n 大模型设置\n </h2>\n <p style={{ margin: 0, fontSize: 13, color: C.text2 }}>\n 配置用于账单分析与报表生成的 AI 大模型接入信息\n </p>\n </div>\n\n {/* 提供商选择 */}\n <div style={{ marginBottom: 20 }}>\n <span style={labelStyle}>模型提供商</span>\n <div style={{ display: \"flex\", gap: 8, flexWrap: \"wrap\" }}>\n {PROVIDERS.map((p) => (\n <button\n key={p.id}\n onClick={() => 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 <span>{p.icon}</span>\n {p.label}\n </button>\n ))}\n </div>\n </div>\n\n {/* API Key */}\n <div style={{ marginBottom: 20 }}>\n <span style={labelStyle}>API Key</span>\n <div style={{ position: \"relative\" }}>\n <input\n type={showKey ? \"text\" : \"password\"}\n value={apiKey}\n onChange={(e) => 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 <button\n onClick={() => 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 </button>\n </div>\n </div>\n\n {/* 模型选择 */}\n <div style={{ marginBottom: 20 }}>\n <span style={labelStyle}>使用模型</span>\n {availableModels.length > 0 ? (\n <select\n value={model}\n onChange={(e) => setModel(e.target.value)}\n style={{\n ...inputStyle,\n appearance: \"none\",\n WebkitAppearance: \"none\",\n cursor: \"pointer\",\n }}\n >\n {availableModels.map((m) => (\n <option key={m} value={m}>\n {m}\n </option>\n ))}\n </select>\n ) : (\n <input\n type=\"text\"\n value={model}\n onChange={(e) => 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 </div>\n\n {/* 自定义 Base URL(custom 或通用) */}\n <div style={{ marginBottom: 28 }}>\n <span style={labelStyle}>\n Base URL\n <span\n style={{ color: \"#334155\", marginLeft: 6, fontWeight: 400, textTransform: \"none\" }}\n >\n (可选,用于代理或本地部署)\n </span>\n </span>\n <input\n type=\"text\"\n value={baseUrl}\n onChange={(e) => 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 </div>\n\n {/* 保存按钮 */}\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 14 }}>\n <button\n onClick={handleSave}\n style={{\n padding: \"9px 26px\",\n background: `linear-gradient(135deg, ${C.accent}, ${C.violet})`,\n border: \"none\",\n borderRadius: 8,\n color: \"white\",\n fontSize: 13,\n fontWeight: 600,\n cursor: \"pointer\",\n transition: \"opacity 0.15s\",\n }}\n onMouseEnter={(e) => ((e.currentTarget as HTMLButtonElement).style.opacity = \"0.85\")}\n onMouseLeave={(e) => ((e.currentTarget as HTMLButtonElement).style.opacity = \"1\")}\n >\n 保存设置\n </button>\n {saved && (\n <span style={{ fontSize: 13, color: \"#34d399\", display: \"flex\", alignItems: \"center\", gap: 4 }}>\n ✓ 已保存\n </span>\n )}\n </div>\n\n {/* 提示卡片 */}\n <div\n style={{\n marginTop: 32,\n padding: 16,\n background: C.bgSurface,\n borderRadius: 10,\n border: `1px solid ${C.border}`,\n }}\n >\n <div style={{ fontSize: 12, color: C.text2, lineHeight: 1.8 }}>\n <strong style={{ color: C.text3, fontWeight: 600 }}>🔒 安全提示</strong>\n <br />\n API Key 仅存储在本地设备,不会上传至任何服务器。\n <br />\n 大模型功能将用于智能分类账单、生成财务报表摘要等场景。\n </div>\n </div>\n </div>\n );\n}\n"}}}}