1 line
33 KiB
JSON
1 line
33 KiB
JSON
{"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 | 云端示例: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 | 云端示例: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"}}}} |