Fundamental_Analysis/frontend/src/app/config/page.tsx
2025-10-21 20:17:14 +08:00

229 lines
8.7 KiB
TypeScript
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.

"use client";
import { useEffect, useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
type Config = {
llm?: {
provider?: "gemini" | "openai";
gemini?: { api_key?: string; base_url?: string };
openai?: { api_key?: string; base_url?: string };
};
data_sources?: {
tushare?: { api_key?: string };
finnhub?: { api_key?: string };
jp_source?: { api_key?: string };
};
database?: { url?: string };
prompts?: { info?: string; finance?: string };
};
export default function ConfigPage() {
const [cfg, setCfg] = useState<Config | null>(null);
const [saving, setSaving] = useState(false);
const [msg, setMsg] = useState<string | null>(null);
const [health, setHealth] = useState<string>("unknown");
// form inputs (敏感字段不回显,留空表示保持现有值)
const [provider, setProvider] = useState<"gemini" | "openai">("gemini");
const [geminiBaseUrl, setGeminiBaseUrl] = useState("");
const [geminiKey, setGeminiKey] = useState(""); // 留空则保留
const [openaiBaseUrl, setOpenaiBaseUrl] = useState("");
const [openaiKey, setOpenaiKey] = useState(""); // 留空则保留
const [tushareKey, setTushareKey] = useState(""); // 留空则保留
const [finnhubKey, setFinnhubKey] = useState(""); // 留空则保留
const [jpKey, setJpKey] = useState(""); // 留空则保留
const [dbUrl, setDbUrl] = useState("");
const [promptInfo, setPromptInfo] = useState("");
const [promptFinance, setPromptFinance] = useState("");
async function loadConfig() {
try {
const res = await fetch("/api/config");
const data: Config = await res.json();
setCfg(data);
// 非敏感字段可回显
setProvider((data.llm?.provider as any) ?? "gemini");
setGeminiBaseUrl(data.llm?.gemini?.base_url ?? "");
setOpenaiBaseUrl(data.llm?.openai?.base_url ?? "");
setDbUrl(data.database?.url ?? "");
setPromptInfo(data.prompts?.info ?? "");
setPromptFinance(data.prompts?.finance ?? "");
} catch {
setMsg("加载配置失败");
}
}
async function saveConfig() {
if (!cfg) return;
setSaving(true);
setMsg(null);
try {
// 构造覆盖配置:敏感字段若为空则沿用现有值
const next: Config = {
llm: {
provider,
gemini: {
base_url: geminiBaseUrl,
api_key: geminiKey || cfg.llm?.gemini?.api_key || undefined,
},
openai: {
base_url: openaiBaseUrl,
api_key: openaiKey || cfg.llm?.openai?.api_key || undefined,
},
},
data_sources: {
tushare: { api_key: tushareKey || cfg.data_sources?.tushare?.api_key || undefined },
finnhub: { api_key: finnhubKey || cfg.data_sources?.finnhub?.api_key || undefined },
jp_source: { api_key: jpKey || cfg.data_sources?.jp_source?.api_key || undefined },
},
database: { url: dbUrl },
prompts: { info: promptInfo, finance: promptFinance },
};
const res = await fetch("/api/config", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(next),
});
const ok = await res.json();
if (ok?.status === "ok") {
setMsg("保存成功");
await loadConfig();
// 清空敏感输入(避免页面存储)
setGeminiKey("");
setOpenaiKey("");
setTushareKey("");
setFinnhubKey("");
setJpKey("");
} else {
setMsg("保存失败");
}
} catch {
setMsg("保存失败");
} finally {
setSaving(false);
}
}
async function testHealth() {
try {
const res = await fetch("/health");
const h = await res.json();
setHealth(h?.status ?? "unknown");
} catch {
setHealth("error");
}
}
useEffect(() => {
loadConfig();
testHealth();
}, []);
return (
<div className="space-y-6">
<header className="space-y-2">
<h1 className="text-2xl font-semibold"></h1>
<p className="text-sm text-muted-foreground">
LLM
</p>
</header>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>GET /health</CardDescription>
</CardHeader>
<CardContent className="flex items-center gap-2">
<Badge variant={health === "ok" ? "secondary" : "outline"}>{health}</Badge>
<Button variant="outline" onClick={testHealth}></Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>LLM </CardTitle>
<CardDescription>Gemini / OpenAI</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex gap-2">
<label className="text-sm w-28">Provider</label>
<select
className="border rounded px-2 py-1 bg-background"
value={provider}
onChange={(e) => setProvider(e.target.value as any)}
>
<option value="gemini">Gemini</option>
<option value="openai">OpenAI</option>
</select>
</div>
<div className="flex gap-2">
<label className="text-sm w-28">Gemini Base URL</label>
<Input placeholder="可留空" value={geminiBaseUrl} onChange={(e) => setGeminiBaseUrl(e.target.value)} />
</div>
<div className="flex gap-2 items-center">
<label className="text-sm w-28">Gemini Key</label>
<Input type="password" placeholder="留空=保持现值" value={geminiKey} onChange={(e) => setGeminiKey(e.target.value)} />
</div>
<div className="flex gap-2">
<label className="text-sm w-28">OpenAI Base URL</label>
<Input placeholder="可留空" value={openaiBaseUrl} onChange={(e) => setOpenaiBaseUrl(e.target.value)} />
</div>
<div className="flex gap-2 items-center">
<label className="text-sm w-28">OpenAI Key</label>
<Input type="password" placeholder="留空=保持现值" value={openaiKey} onChange={(e) => setOpenaiKey(e.target.value)} />
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>TuShare / Finnhub / JP</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex gap-2 items-center">
<label className="text-sm w-28">TuShare</label>
<Input type="password" placeholder="留空=保持现值" value={tushareKey} onChange={(e) => setTushareKey(e.target.value)} />
</div>
<div className="flex gap-2 items-center">
<label className="text-sm w-28">Finnhub</label>
<Input type="password" placeholder="留空=保持现值" value={finnhubKey} onChange={(e) => setFinnhubKey(e.target.value)} />
</div>
<div className="flex gap-2 items-center">
<label className="text-sm w-28">JP Source</label>
<Input type="password" placeholder="留空=保持现值" value={jpKey} onChange={(e) => setJpKey(e.target.value)} />
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex gap-2">
<label className="text-sm w-28">DB URL</label>
<Input placeholder="postgresql+asyncpg://..." value={dbUrl} onChange={(e) => setDbUrl(e.target.value)} />
</div>
<div className="flex gap-2">
<label className="text-sm w-28">Prompt Info</label>
<Input placeholder="模板info" value={promptInfo} onChange={(e) => setPromptInfo(e.target.value)} />
</div>
<div className="flex gap-2">
<label className="text-sm w-28">Prompt Finance</label>
<Input placeholder="模板finance" value={promptFinance} onChange={(e) => setPromptFinance(e.target.value)} />
</div>
<div className="flex gap-2">
<Button onClick={saveConfig} disabled={saving}>{saving ? "保存中…" : "保存配置"}</Button>
{msg && <span className="text-xs text-muted-foreground">{msg}</span>}
</div>
</CardContent>
</Card>
</div>
);
}