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:
Blizzard
2026-06-12 16:39:42 +08:00
parent 190c191ce4
commit 72bd43965f
25 changed files with 715 additions and 348 deletions
@@ -1,47 +1,53 @@
import {
Cpu,
Bookmark,
Wrench,
MessageSquareText,
Sparkles,
ListTree,
FileText,
FileOutput,
CheckCircle2,
XCircle,
Loader2,
Circle,
type LucideIcon,
} from "lucide-react";
import { deriveNodes, type NodeTrace, type RunPhase } from "../lib/run";
import type { ExecEvent } from "../lib/api";
import { cn } from "../ui";
// 各节点类别的图标与配色(与后端 ExecEvent.kind 对应)。
const KIND: Record<string, { icon: string; cls: string; name: string }> = {
system: { icon: "▸", cls: "text-slate-400", name: "系统" },
memory: { icon: "◇", cls: "text-violet-300", name: "记忆召回" },
tool: { icon: "⚙", cls: "text-amber-300", name: "工具调用" },
prompt: { icon: "▤", cls: "text-sky-300", name: "提示词" },
model: { icon: "✦", cls: "text-emerald-300", name: "模型推理" },
plan: { icon: "◷", cls: "text-cyan-300", name: "规划" },
section: { icon: "¶", cls: "text-indigo-300", name: "章节" },
render: { icon: "▦", cls: "text-rose-300", name: "渲染" },
const KIND: Record<string, { icon: LucideIcon; cls: string; name: string }> = {
system: { icon: Cpu, cls: "text-slate-400", name: "系统" },
memory: { icon: Bookmark, cls: "text-brand-400", name: "记忆召回" },
tool: { icon: Wrench, cls: "text-warn", name: "工具调用" },
prompt: { icon: MessageSquareText, cls: "text-sky-300", name: "提示词" },
model: { icon: Sparkles, cls: "text-success", name: "模型推理" },
plan: { icon: ListTree, cls: "text-accent-400", name: "规划" },
section: { icon: FileText, cls: "text-indigo-300", name: "章节" },
render: { icon: FileOutput, cls: "text-danger", name: "渲染" },
};
function meta(kind: string) {
return KIND[kind] ?? { icon: "•", cls: "text-slate-400", name: kind };
return KIND[kind] ?? { icon: Circle, cls: "text-slate-400", name: kind };
}
function StatusDot({ status }: { status: NodeTrace["status"] }) {
if (status === "running")
return <span className="h-2.5 w-2.5 animate-pulse rounded-full bg-cyan-400 shadow-[0_0_8px_rgba(34,211,238,0.8)]" />;
if (status === "done") return <span className="h-2.5 w-2.5 rounded-full bg-emerald-400" />;
if (status === "error") return <span className="h-2.5 w-2.5 rounded-full bg-rose-500" />;
return <span className="h-2.5 w-2.5 rounded-full bg-slate-600" />;
if (status === "running") return <Loader2 className="h-4 w-4 animate-spin text-accent-400" strokeWidth={2.4} />;
if (status === "done") return <CheckCircle2 className="h-4 w-4 text-success" strokeWidth={2.2} />;
if (status === "error") return <XCircle className="h-4 w-4 text-danger" strokeWidth={2.2} />;
return <Circle className="h-4 w-4 text-slate-600" strokeWidth={2} />;
}
// ExecTrace 把执行事件流渲染为竖向轨道:每个节点一颗灯,实时点亮 + 耗时 + 入参/产出。
export function ExecTrace({
events,
phase,
compact,
}: {
events: ExecEvent[];
phase?: RunPhase;
compact?: boolean;
}) {
export function ExecTrace({ events, phase, compact }: { events: ExecEvent[]; phase?: RunPhase; compact?: boolean }) {
const nodes = deriveNodes(events);
if (nodes.length === 0) {
return (
<div className="flex h-full items-center justify-center px-4 py-8 text-center text-xs text-slate-600">
/
<br />
Word
<div className="flex h-full flex-col items-center justify-center gap-1 px-4 py-8 text-center text-xs text-slate-600">
<span></span>
<span className="text-slate-700"> / </span>
</div>
);
}
@@ -53,28 +59,27 @@ export function ExecTrace({
<span>{nodes.length} </span>
<span>·</span>
<span> {total} ms</span>
{phase === "streaming" && <span className="text-cyan-400"> </span>}
{phase === "done" && <span className="text-emerald-400"> </span>}
{phase === "error" && <span className="text-rose-400"> </span>}
{phase === "streaming" && <span className="text-accent-400"> </span>}
{phase === "done" && <span className="text-success"> </span>}
{phase === "error" && <span className="text-danger"> </span>}
</div>
)}
<ol className="relative ml-2 space-y-2 border-l border-line pl-4">
{nodes.map((n) => {
const m = meta(n.kind);
const Icon = m.icon;
return (
<li key={n.node} className="relative">
<span className="absolute -left-[22px] top-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-ink-900">
<StatusDot status={n.status} />
</span>
<div className="rounded-lg border border-line bg-ink-950/60 px-3 py-2">
<div className="rounded-md border border-line bg-ink-950/60 px-3 py-2">
<div className="flex items-center gap-2">
<span className={`text-sm leading-none ${m.cls}`}>{m.icon}</span>
<Icon className={cn("h-3.5 w-3.5 shrink-0", m.cls)} strokeWidth={2} />
<span className="text-xs font-medium text-slate-200">{n.label}</span>
<span className={`rounded px-1.5 py-0.5 text-[9px] ${m.cls} bg-white/5`}>{m.name}</span>
{n.ms != null && n.ms > 0 && (
<span className="ml-auto font-mono text-[10px] text-slate-500">{n.ms} ms</span>
)}
{n.status === "running" && <span className="ml-auto text-[10px] text-cyan-400"></span>}
<span className={cn("rounded bg-white/5 px-1.5 py-0.5 text-[9px]", m.cls)}>{m.name}</span>
{n.ms != null && n.ms > 0 && <span className="ml-auto font-mono text-[10px] text-slate-500">{n.ms} ms</span>}
{n.status === "running" && <span className="ml-auto text-[10px] text-accent-400"></span>}
</div>
{n.detail && <p className="mt-1 break-words text-[11px] leading-relaxed text-slate-400">{n.detail}</p>}
{n.notes.map((note, i) => (