Files
sundynix-agentix/sundynix-desktop/frontend/src/views/ReportView.tsx
T
Blizzard 72bd43965f feat(desktop): 工业化升级 A —— 设计系统地基(primitives + lucide + 语义令牌)
把"手搓内联 class + Unicode 字符图标"换成统一组件与真实图标,为后续工业化打底。

- 依赖:装 lucide-react(描线图标,按需 tree-shake)
- 令牌:tailwind.config 加语义色 brand/accent/success/warn/danger + 圆角档位;
  强调色字面量(violet/cyan/emerald…)收敛到令牌,便于整体换肤
- primitives(src/ui,零重依赖自建):Button/Input/Textarea/Select/Field/Card/Panel/
  Badge/Dot/Tabs/Skeleton/EmptyState/Dialog/Toast(+useToast)/cn,桶文件统一引入
- 迁移:TopBar/LeftNav/BottomDrawer + Home/Report/Runs/Kb/Placeholder/ExecTrace/
  MemoryPanel/StudioView 全部换 primitives + lucide 图标;导航/能力卡/按钮告别
  ▤◆▣▦ 等 Unicode 字符;错误改用全局 Toast;空状态用 EmptyState
- App 包 ToastProvider

验证:tsc + vite build 通过;浏览器(Preview)走查工作台/报告页——真实图标、统一卡片/
按钮/输入;跑报告端到端正常(执行轨迹 lucide 状态图标点亮、章节耗时/字数/检索片段、
完成弹 Toast + 下载 Word)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 16:39:42 +08:00

121 lines
4.6 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 { useRef, useState } from "react";
import { Play, Download, FileText } from "lucide-react";
import { generateReport, streamTokens, streamExec, reportDownloadUrl, type Identity, type ExecEvent } from "../lib/api";
import { ExecTrace } from "../components/ExecTrace";
import { Button, Input, Field, Panel, Dot, EmptyState, useToast } from "../ui";
type Phase = "idle" | "running" | "done" | "error";
// 报告生成:输入主题(+可选知识库) → 触发后端专用编排
// (规划大纲 → 各章并行检索+撰写 → 渲染 Word),实时看进度与正文,完成后下载 .docx。
export function ReportView({ identity }: { identity: Identity }) {
const toast = useToast();
const [topic, setTopic] = useState("");
const [kb, setKb] = useState("");
const [phase, setPhase] = useState<Phase>("idle");
const [out, setOut] = useState("");
const [exec, setExec] = useState<ExecEvent[]>([]);
const [taskId, setTaskId] = useState("");
const closeRef = useRef<(() => void) | null>(null);
const execCloseRef = useRef<(() => void) | null>(null);
const running = phase === "running";
const onGenerate = async () => {
if (!topic.trim() || running) return;
closeRef.current?.();
execCloseRef.current?.();
setPhase("running");
setOut("");
setExec([]);
setTaskId("");
try {
const id = await generateReport(identity, topic.trim(), kb.trim() || undefined);
setTaskId(id);
execCloseRef.current = streamExec(
id,
(ev) => setExec((xs) => [...xs, ev]),
() => {},
() => {},
);
closeRef.current = streamTokens(
id,
(tok) => setOut((o) => o + tok),
() => {
setPhase("done");
toast.push("success", "报告已生成,可下载 Word");
},
() => {
setPhase("error");
toast.push("error", "报告流连接中断");
},
);
} catch (e) {
setPhase("error");
toast.push("error", (e as Error).message);
}
};
const tracePhase = running ? "streaming" : phase === "done" ? "done" : phase === "error" ? "error" : "idle";
return (
<div className="flex h-full min-h-0 flex-col gap-4 overflow-hidden p-6">
<header>
<h1 className="text-lg font-semibold text-slate-100"></h1>
<p className="mt-1 text-xs text-slate-500">
+ LLM Word(.docx)
</p>
</header>
<div className="grid grid-cols-[1fr_220px_auto_auto] items-end gap-3 rounded-lg border border-line bg-ink-900 p-4 shadow-card">
<Field label="报告主题">
<Input
value={topic}
onChange={(e) => setTopic(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && onGenerate()}
placeholder="如:2026 年国产大模型产业现状与趋势分析"
/>
</Field>
<Field label="知识库(可选)">
<Input value={kb} onChange={(e) => setKb(e.target.value)} placeholder="如 docs,留空则不挂检索" />
</Field>
<Button variant="primary" icon={Play} onClick={onGenerate} disabled={running || !topic.trim()}>
{running ? "生成中…" : "生成报告"}
</Button>
{phase === "done" && taskId ? (
<a
href={reportDownloadUrl(taskId)}
className="inline-flex h-9 items-center gap-1.5 rounded-md border border-brand/60 px-4 text-sm font-medium text-brand-400 transition hover:bg-brand/10"
>
<Download className="h-4 w-4" />
Word
</a>
) : (
<span className="h-9" />
)}
</div>
<div className="grid min-h-0 flex-1 grid-cols-[340px_1fr] gap-4">
<Panel title="执行轨迹" icon={FileText}>
<ExecTrace events={exec} phase={tracePhase} />
</Panel>
<Panel
title={
<span className="flex items-center gap-2">
<Dot tone={running ? "running" : phase === "done" ? "success" : "neutral"} pulse={running} />
· {taskId || "未开始"}
</span>
}
>
{out ? (
<pre className="whitespace-pre-wrap break-words font-sans text-sm leading-relaxed text-slate-300">{out}</pre>
) : (
<EmptyState icon={FileText} title="尚未生成报告" desc="输入主题并点击「生成报告」,这里将实时显示规划与撰写过程。" />
)}
</Panel>
</div>
</div>
);
}