finance-tools/src/components/AiSummaryCard.tsx

108 lines
3.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.

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>
);
}