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>
This commit is contained in:
@@ -1,19 +1,21 @@
|
||||
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 [err, setErr] = useState("");
|
||||
const closeRef = useRef<(() => void) | null>(null);
|
||||
const execCloseRef = useRef<(() => void) | null>(null);
|
||||
|
||||
@@ -26,7 +28,6 @@ export function ReportView({ identity }: { identity: Identity }) {
|
||||
setPhase("running");
|
||||
setOut("");
|
||||
setExec([]);
|
||||
setErr("");
|
||||
setTaskId("");
|
||||
try {
|
||||
const id = await generateReport(identity, topic.trim(), kb.trim() || undefined);
|
||||
@@ -40,20 +41,25 @@ export function ReportView({ identity }: { identity: Identity }) {
|
||||
closeRef.current = streamTokens(
|
||||
id,
|
||||
(tok) => setOut((o) => o + tok),
|
||||
() => setPhase("done"),
|
||||
() => {
|
||||
setErr("连接中断");
|
||||
setPhase("done");
|
||||
toast.push("success", "报告已生成,可下载 Word");
|
||||
},
|
||||
() => {
|
||||
setPhase("error");
|
||||
toast.push("error", "报告流连接中断");
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
setErr((e as Error).message);
|
||||
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-y-auto p-6">
|
||||
<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">
|
||||
@@ -61,75 +67,53 @@ export function ReportView({ identity }: { identity: Identity }) {
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* 输入区 */}
|
||||
<div className="grid grid-cols-[1fr_220px] gap-3 rounded-xl border border-line bg-ink-900 p-4 shadow-card">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-[11px] font-medium text-slate-400">报告主题</label>
|
||||
<input
|
||||
<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 年国产大模型产业现状与趋势分析"
|
||||
className="rounded-lg border border-line bg-ink-950 px-3 py-2 text-sm text-slate-200 outline-none placeholder:text-slate-600 focus:border-violet-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-[11px] font-medium text-slate-400">知识库(可选)</label>
|
||||
<input
|
||||
value={kb}
|
||||
onChange={(e) => setKb(e.target.value)}
|
||||
placeholder="如 docs,留空则不挂检索"
|
||||
className="rounded-lg border border-line bg-ink-950 px-3 py-2 text-sm text-slate-200 outline-none placeholder:text-slate-600 focus:border-violet-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center gap-3">
|
||||
<button
|
||||
onClick={onGenerate}
|
||||
disabled={running || !topic.trim()}
|
||||
className="rounded-lg bg-gradient-to-r from-violet-500 to-cyan-500 px-4 py-2 text-sm font-medium text-white shadow-glow transition disabled:cursor-not-allowed disabled:opacity-40"
|
||||
</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"
|
||||
>
|
||||
{running ? "生成中…" : "生成报告"}
|
||||
</button>
|
||||
{phase === "done" && taskId && (
|
||||
<a
|
||||
href={reportDownloadUrl(taskId)}
|
||||
className="rounded-lg border border-violet-500/60 px-4 py-2 text-sm font-medium text-violet-300 transition hover:bg-violet-500/10"
|
||||
>
|
||||
⬇ 下载 Word
|
||||
</a>
|
||||
)}
|
||||
<span className="text-xs text-slate-500">
|
||||
{running && "正在编排,实时进度见下方…"}
|
||||
{phase === "done" && "已完成"}
|
||||
{phase === "error" && <span className="text-rose-400">出错:{err}</span>}
|
||||
</span>
|
||||
</div>
|
||||
<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">
|
||||
<section className="flex min-h-0 flex-col rounded-xl border border-line bg-ink-900 shadow-card">
|
||||
<div className="border-b border-line px-4 py-2.5 text-[11px] font-medium text-slate-400">执行轨迹</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-4">
|
||||
<ExecTrace events={exec} phase={running ? "streaming" : phase === "done" ? "done" : phase === "error" ? "error" : "idle"} />
|
||||
</div>
|
||||
</section>
|
||||
<Panel title="执行轨迹" icon={FileText}>
|
||||
<ExecTrace events={exec} phase={tracePhase} />
|
||||
</Panel>
|
||||
|
||||
<section className="flex min-h-0 flex-col rounded-xl border border-line bg-ink-900 shadow-card">
|
||||
<div className="flex items-center gap-2 border-b border-line px-4 py-2.5 text-[11px] text-slate-500">
|
||||
<span className={`h-2 w-2 rounded-full ${running ? "animate-pulse bg-cyan-400" : phase === "done" ? "bg-emerald-400" : "bg-slate-600"}`} />
|
||||
报告正文 · {taskId || "未开始"}
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-4">
|
||||
{out ? (
|
||||
<pre className="whitespace-pre-wrap break-words font-sans text-sm leading-relaxed text-slate-300">{out}</pre>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-sm text-slate-600">
|
||||
输入主题并点击「生成报告」,这里将实时显示规划与撰写过程。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user