finance-tools/src/components/AiSummaryCard.tsx

108 lines
3.7 KiB
TypeScript
Raw Normal View History

2026-06-21 20:28:06 +00:00
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AppIcon } from "./PixelIcons";
import { Card, Tag, Loading } from "./ui";
import { api } from "../lib/api";
type Props = {
symbol: string;
scope: "stock" | "company" | "fund";
title?: string;
auto?: boolean;
aiFocus: { symbol: string; subPage: string; label: string; cardTitle?: string };
};
function verdictTone(v?: string): "up" | "down" | "gold" {
if (v === "偏正面") return "up";
if (v === "謹慎") return "down";
return "gold";
}
export default function AiSummaryCard({ symbol, scope, title = "AI 今日總結", auto = true, aiFocus }: Props) {
const qc = useQueryClient();
const q = useQuery({
queryKey: ["ai-summary", symbol, scope],
queryFn: () => api.aiSummary(symbol, scope, auto),
enabled: !!symbol && auto,
staleTime: 12 * 3600_000,
retry: 1,
});
const syncMut = useMutation({
mutationFn: () => api.syncAiSummary(symbol, scope),
onSuccess: (data) => {
qc.setQueryData(["ai-summary", symbol, scope], data);
},
});
const summary = q.data?.summary;
const loading = q.isLoading || syncMut.isPending;
return (
<Card
className="mt"
title={title}
ico={<AppIcon name="scroll" size={22} framed variant="hero" />}
ai
aiFocus={{ ...aiFocus, cardTitle: title }}
onRefresh={() => syncMut.mutate()}
refreshing={loading}
refreshLabel="重新分析"
>
<div className="ai-summary-toolbar">
<span className="small muted">
{q.data?.cached
? `今日已分析 · ${q.data?.summary?.day || ""} · 讀資料庫(省 token`
: q.data?.mcpUsed
? "MCP + 本機資料已送 AI 總結"
: "整合本機財報/公司研究;可串 MCP 補充"}
</span>
</div>
{loading && !summary ? (
<Loading label="MCP 取資料並產生總結…" />
) : summary ? (
<>
<div className="fund-verdict-row">
{summary.verdict ? <Tag tone={verdictTone(summary.verdict)}>{summary.verdict}</Tag> : null}
{summary.provider ? <span className="small muted">via {summary.provider}</span> : null}
</div>
<div className="interpret-box">
<div className="interpret-label">AI </div>
<p>{summary.summaryZh}</p>
{summary.bullets?.length ? (
<ul className="interpret-bullets">
{summary.bullets.map((b, i) => (
<li key={i}>{b}</li>
))}
</ul>
) : null}
</div>
{summary.risks?.length ? (
<div className="ai-summary-risks mt-s">
<h4 className="small" style={{ color: "var(--crimson)", margin: "0 0 6px" }}>
</h4>
<ul className="interpret-bullets">
{summary.risks.map((r, i) => (
<li key={i}>{r}</li>
))}
</ul>
</div>
) : null}
{summary.sources?.length ? (
<p className="small muted mt-s">{summary.sources.join(" · ")}</p>
) : null}
{q.data?.skipReason ? <p className="small muted">{q.data.skipReason}</p> : null}
{q.data?.aiError ? <p className="small muted">AI {q.data.aiError}</p> : null}
</>
) : (
<p className="muted">
<button type="button" className="btn-primary" style={{ marginLeft: 8 }} disabled={loading} onClick={() => syncMut.mutate()}>
AI
</button>
</p>
)}
</Card>
);
}