feat(observability): 执行可视化 — 节点级实时轨迹(运行·观测)
把任务执行做成可观测:Dispatcher 在每个节点/阶段发结构化 ExecEvent, 经独立 NATS 通道回流,前端逐节点点亮(状态/耗时/工具入参产出)。 - shared: contract.ExecEvent + ExecSubject(sundynix.exec.<id>,与 Token 流分流); bus.PublishExec/CompleteExec/SubscribeExec(core NATS,复用结束头) - dispatcher: execTracer(自增 Seq 保序 + span 自动计耗时); Orchestrator 加 ExecSink;通用图(init 召回 / 各 tool 入参→产出 / prompt / model 首token+token数)与报告编排(规划大纲 / 各章并行 start-end / 渲染)全程埋点 - gateway: SubscribeExec + GET /tasks/:id/exec SSE(与 token 流并行) - desktop: streamExec + deriveNodes(按 node 归并 start/end/error/info); 复用组件 ExecTrace(竖向轨道,按 kind 着色,运行中脉冲灯); 新 RunsView(运行·观测:轨迹+输出双栏);BottomDrawer 轨迹/工具调用 tab 接真实数据; ReportView 加执行轨迹栏;左导航「运行」置就绪 实测: - 报告任务 /exec:规划(2680ms,4章) → 4 章并行(seq 交错,各~7-8s 重叠=真并行, 每章带 docs 知识库检索预览+成稿字数) → 渲染(docx 落盘) - 通用图 /exec:tool:kb_search(678ms,入参→Milvus 产出) → prompt(2消息) → model(首token 860ms / 4 tokens) - 浏览器(Preview):报告页执行轨迹逐节点点亮、章节带耗时/字数/检索片段,完成后下载 Word Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,9 +7,10 @@ import { StudioView } from "./studio/StudioView";
|
||||
import { MemoryView } from "./views/MemoryView";
|
||||
import { KbView } from "./views/KbView";
|
||||
import { ReportView } from "./views/ReportView";
|
||||
import { RunsView } from "./views/RunsView";
|
||||
import { Home } from "./views/Home";
|
||||
import { Placeholder } from "./views/Placeholder";
|
||||
import { submitTask, streamTokens, type Identity } from "./lib/api";
|
||||
import { submitTask, streamTokens, streamExec, type Identity } from "./lib/api";
|
||||
import type { TaskDsl } from "./lib/dsl";
|
||||
import { emptyRun, type RunState } from "./lib/run";
|
||||
|
||||
@@ -28,11 +29,14 @@ export default function App() {
|
||||
const [run, setRun] = useState<RunState>(emptyRun);
|
||||
const closeRef = useRef<(() => void) | null>(null);
|
||||
|
||||
const execCloseRef = useRef<(() => void) | null>(null);
|
||||
|
||||
const onRun = useCallback(
|
||||
async (dsl: TaskDsl) => {
|
||||
closeRef.current?.();
|
||||
execCloseRef.current?.();
|
||||
const t0 = Date.now();
|
||||
setRun({ phase: "submitting", output: "", events: [{ t: 0, label: "提交任务" }] });
|
||||
setRun({ phase: "submitting", output: "", events: [{ t: 0, label: "提交任务" }], exec: [] });
|
||||
try {
|
||||
const taskId = await submitTask(dsl, identity);
|
||||
let first = true;
|
||||
@@ -42,6 +46,13 @@ export default function App() {
|
||||
taskId,
|
||||
events: [...r.events, { t: Date.now() - t0, label: `已发布 ${taskId}` }],
|
||||
}));
|
||||
// 执行轨迹(运行·观测):与 token 流并行订阅,逐节点点亮。
|
||||
execCloseRef.current = streamExec(
|
||||
taskId,
|
||||
(ev) => setRun((r) => ({ ...r, exec: [...r.exec, ev] })),
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
closeRef.current = streamTokens(
|
||||
taskId,
|
||||
(tok) =>
|
||||
@@ -84,6 +95,8 @@ export default function App() {
|
||||
<KbView />
|
||||
) : view === "report" ? (
|
||||
<ReportView identity={identity} />
|
||||
) : view === "runs" ? (
|
||||
<RunsView run={run} />
|
||||
) : view === "memory" ? (
|
||||
<MemoryView identity={identity} />
|
||||
) : (
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { deriveNodes, type NodeTrace, type RunPhase } from "../lib/run";
|
||||
import type { ExecEvent } from "../lib/api";
|
||||
|
||||
// 各节点类别的图标与配色(与后端 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: "渲染" },
|
||||
};
|
||||
|
||||
function meta(kind: string) {
|
||||
return KIND[kind] ?? { icon: "•", 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" />;
|
||||
}
|
||||
|
||||
// ExecTrace 把执行事件流渲染为竖向轨道:每个节点一颗灯,实时点亮 + 耗时 + 入参/产出。
|
||||
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>
|
||||
);
|
||||
}
|
||||
const total = nodes.reduce((s, n) => s + (n.ms ?? 0), 0);
|
||||
return (
|
||||
<div className={compact ? "" : "p-1"}>
|
||||
{!compact && (
|
||||
<div className="mb-2 flex items-center gap-3 px-1 text-[11px] text-slate-500">
|
||||
<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>}
|
||||
</div>
|
||||
)}
|
||||
<ol className="relative ml-2 space-y-2 border-l border-line pl-4">
|
||||
{nodes.map((n) => {
|
||||
const m = meta(n.kind);
|
||||
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="flex items-center gap-2">
|
||||
<span className={`text-sm leading-none ${m.cls}`}>{m.icon}</span>
|
||||
<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>}
|
||||
</div>
|
||||
{n.detail && <p className="mt-1 break-words text-[11px] leading-relaxed text-slate-400">{n.detail}</p>}
|
||||
{n.notes.map((note, i) => (
|
||||
<p key={i} className="mt-1 break-words border-l border-line pl-2 text-[11px] text-slate-500">
|
||||
{note}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -47,6 +47,38 @@ export function streamTokens(
|
||||
return () => es.close();
|
||||
}
|
||||
|
||||
// 执行轨迹事件(与后端 contract.ExecEvent 对应):运行·观测的节点级实时事件。
|
||||
export interface ExecEvent {
|
||||
seq: number;
|
||||
ts: number;
|
||||
node: string; // init / tool:wiki_search / prompt / model / plan / section:0 / render / task / compile
|
||||
kind: string; // memory|tool|prompt|model|plan|section|render|system
|
||||
phase: string; // start|end|error|info
|
||||
label: string;
|
||||
detail?: string;
|
||||
ms?: number;
|
||||
}
|
||||
|
||||
// streamExec: 订阅 SSE /api/v1/tasks/:id/exec —— 与 token 流并行,逐节点点亮执行轨迹。
|
||||
export function streamExec(
|
||||
taskId: string,
|
||||
onEvent: (ev: ExecEvent) => void,
|
||||
onDone: () => void,
|
||||
onError?: (e: unknown) => void,
|
||||
): () => void {
|
||||
const es = new EventSource(`${GATEWAY}/api/v1/tasks/${taskId}/exec`);
|
||||
es.addEventListener("exec", (e) => onEvent(JSON.parse((e as MessageEvent).data) as ExecEvent));
|
||||
es.addEventListener("done", () => {
|
||||
es.close();
|
||||
onDone();
|
||||
});
|
||||
es.onerror = (e) => {
|
||||
es.close();
|
||||
onError?.(e);
|
||||
};
|
||||
return () => es.close();
|
||||
}
|
||||
|
||||
// 入库进度事件(与后端 contract.IngestEvent 对应)。
|
||||
export interface IngestEvent {
|
||||
stage: string;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// 运行状态 —— 跨 Studio 与底部抽屉共享。
|
||||
import type { ExecEvent } from "./api";
|
||||
|
||||
export type RunPhase = "idle" | "submitting" | "streaming" | "done" | "error";
|
||||
|
||||
export interface RunEvent {
|
||||
@@ -11,7 +13,58 @@ export interface RunState {
|
||||
taskId?: string;
|
||||
output: string;
|
||||
events: RunEvent[];
|
||||
exec: ExecEvent[]; // 后端回流的节点级执行轨迹(运行·观测)
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const emptyRun: RunState = { phase: "idle", output: "", events: [] };
|
||||
export const emptyRun: RunState = { phase: "idle", output: "", events: [], exec: [] };
|
||||
|
||||
// ---- 执行轨迹派生:把扁平 ExecEvent 流归并为按节点聚合的轨迹 ----
|
||||
|
||||
export type NodeStatus = "running" | "done" | "error" | "info";
|
||||
|
||||
export interface NodeTrace {
|
||||
node: string;
|
||||
kind: string; // memory|tool|prompt|model|plan|section|render|system
|
||||
label: string;
|
||||
status: NodeStatus;
|
||||
ms?: number;
|
||||
detail?: string;
|
||||
notes: string[]; // info 子事件(如检索到的参考资料预览)
|
||||
order: number;
|
||||
}
|
||||
|
||||
// deriveNodes 把事件流按 node 归并:start→running,end→done(带耗时),error→error,info→点事件/附注。
|
||||
export function deriveNodes(events: ExecEvent[]): NodeTrace[] {
|
||||
const map = new Map<string, NodeTrace>();
|
||||
let order = 0;
|
||||
for (const e of events) {
|
||||
let n = map.get(e.node);
|
||||
if (!n) {
|
||||
n = { node: e.node, kind: e.kind, label: e.label, status: "info", notes: [], order: order++ };
|
||||
map.set(e.node, n);
|
||||
}
|
||||
if (e.label) n.label = e.label;
|
||||
n.kind = e.kind;
|
||||
switch (e.phase) {
|
||||
case "start":
|
||||
if (n.status !== "done" && n.status !== "error") n.status = "running";
|
||||
break;
|
||||
case "end":
|
||||
n.status = "done";
|
||||
n.ms = e.ms;
|
||||
if (e.detail) n.detail = e.detail;
|
||||
break;
|
||||
case "error":
|
||||
n.status = "error";
|
||||
n.ms = e.ms;
|
||||
if (e.detail) n.detail = e.detail;
|
||||
break;
|
||||
case "info":
|
||||
if (e.detail) n.notes.push(e.detail);
|
||||
else if (e.label) n.notes.push(e.label);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return [...map.values()].sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import type { RunState } from "../lib/run";
|
||||
import { deriveNodes, type RunState } from "../lib/run";
|
||||
import { ExecTrace } from "../components/ExecTrace";
|
||||
|
||||
type Tab = "output" | "trace" | "tools" | "cite" | "eval";
|
||||
const TABS: Array<{ key: Tab; label: string }> = [
|
||||
@@ -43,24 +44,14 @@ export function BottomDrawer({ run }: { run: RunState }) {
|
||||
</button>
|
||||
</div>
|
||||
{open && (
|
||||
<div className="h-40 overflow-auto p-3 text-xs">
|
||||
<div className="h-44 overflow-auto p-3 text-xs">
|
||||
{tab === "output" && (
|
||||
<pre className="whitespace-pre-wrap font-mono leading-relaxed text-emerald-300">
|
||||
{run.output || "在编排页搭图 → 运行,模型注入画像与历史后流式作答,token 在此呈现。"}
|
||||
</pre>
|
||||
)}
|
||||
{tab === "trace" && (
|
||||
<ul className="space-y-1 text-slate-400">
|
||||
{run.events.length === 0 && <li className="text-slate-600">尚无运行。</li>}
|
||||
{run.events.map((e, i) => (
|
||||
<li key={i}>
|
||||
<span className="text-slate-600">+{e.t}ms</span> · {e.label}
|
||||
</li>
|
||||
))}
|
||||
{run.events.length > 0 && <li className="mt-2 text-[11px] text-slate-600">(节点级轨迹待后端回流节点事件后逐节点点亮)</li>}
|
||||
</ul>
|
||||
)}
|
||||
{tab === "tools" && <p className="text-slate-600">工具调用日志:每次 sundynix.tools.* 的请求/响应(需后端回流工具事件)。</p>}
|
||||
{tab === "trace" && <ExecTrace events={run.exec} phase={run.phase} />}
|
||||
{tab === "tools" && <ToolCalls run={run} />}
|
||||
{tab === "cite" && <p className="text-slate-600">引用列表:RAG 答案的来源块(源文档 + 分数 + 来源徽标)。</p>}
|
||||
{tab === "eval" && <p className="text-slate-600">评测:忠实度 / 完整度质量门结果(需 harness eval)。</p>}
|
||||
</div>
|
||||
@@ -68,3 +59,32 @@ export function BottomDrawer({ run }: { run: RunState }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ToolCalls:从执行事件里筛出工具调用节点,逐条展示入参 → 产出 + 耗时/状态。
|
||||
function ToolCalls({ run }: { run: RunState }) {
|
||||
const tools = deriveNodes(run.exec).filter((n) => n.kind === "tool");
|
||||
if (tools.length === 0) {
|
||||
return <p className="text-slate-600">本次运行暂无工具调用。图里挂了检索/工具节点,或报告挂了知识库时,每次 sundynix.tools.* 调用会在此列出入参与产出。</p>;
|
||||
}
|
||||
return (
|
||||
<ul className="space-y-1.5">
|
||||
{tools.map((t) => (
|
||||
<li key={t.node} className="rounded-lg border border-line bg-ink-950/60 px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-amber-300">⚙</span>
|
||||
<span className="font-mono text-[11px] text-slate-200">{t.node.replace(/^tool:/, "")}</span>
|
||||
<span
|
||||
className={`rounded px-1.5 py-0.5 text-[9px] ${
|
||||
t.status === "error" ? "bg-rose-500/15 text-rose-300" : t.status === "running" ? "bg-cyan-500/15 text-cyan-300" : "bg-emerald-500/15 text-emerald-300"
|
||||
}`}
|
||||
>
|
||||
{t.status === "error" ? "失败" : t.status === "running" ? "调用中" : "成功"}
|
||||
</span>
|
||||
{t.ms != null && t.ms > 0 && <span className="ml-auto font-mono text-[10px] text-slate-500">{t.ms} ms</span>}
|
||||
</div>
|
||||
{t.detail && <p className="mt-1 break-words text-[11px] leading-relaxed text-slate-400">{t.detail}</p>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const ITEMS: Item[] = [
|
||||
{ key: "studio", label: "编排", icon: "◆", group: "BUILD", ready: true },
|
||||
{ key: "kb", label: "知识库", icon: "▣", group: "BUILD", ready: true },
|
||||
{ key: "report", label: "报告", icon: "▦", group: "BUILD", ready: true },
|
||||
{ key: "runs", label: "运行", icon: "▸", group: "RUN" },
|
||||
{ key: "runs", label: "运行", icon: "▸", group: "RUN", ready: true },
|
||||
{ key: "memory", label: "记忆", icon: "◇", group: "MANAGE", ready: true },
|
||||
{ key: "market", label: "市场", icon: "⌧", group: "MANAGE" },
|
||||
{ key: "admin", label: "管理", icon: "⚙", group: "MANAGE" },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { generateReport, streamTokens, reportDownloadUrl, type Identity } from "../lib/api";
|
||||
import { generateReport, streamTokens, streamExec, reportDownloadUrl, type Identity, type ExecEvent } from "../lib/api";
|
||||
import { ExecTrace } from "../components/ExecTrace";
|
||||
|
||||
type Phase = "idle" | "running" | "done" | "error";
|
||||
|
||||
@@ -10,22 +11,32 @@ export function ReportView({ identity }: { identity: Identity }) {
|
||||
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);
|
||||
|
||||
const running = phase === "running";
|
||||
|
||||
const onGenerate = async () => {
|
||||
if (!topic.trim() || running) return;
|
||||
closeRef.current?.();
|
||||
execCloseRef.current?.();
|
||||
setPhase("running");
|
||||
setOut("");
|
||||
setExec([]);
|
||||
setErr("");
|
||||
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),
|
||||
@@ -95,21 +106,30 @@ export function ReportView({ identity }: { identity: Identity }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 实时进度 / 正文 */}
|
||||
<div className="flex min-h-0 flex-1 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>
|
||||
{/* 执行轨迹 + 报告正文 */}
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { ExecTrace } from "../components/ExecTrace";
|
||||
import { deriveNodes, type RunState } from "../lib/run";
|
||||
|
||||
// 运行·观测:把最近一次运行的执行轨迹实时可视化(节点逐个点亮 + 工具入参/产出 + 耗时),
|
||||
// 右侧并列模型输出。数据来自 Studio 运行时订阅的 sundynix.exec.<id> 事件流。
|
||||
export function RunsView({ run }: { run: RunState }) {
|
||||
const nodes = deriveNodes(run.exec);
|
||||
const tools = nodes.filter((n) => n.kind === "tool");
|
||||
const phaseText =
|
||||
run.phase === "streaming" ? "执行中" : run.phase === "done" ? "完成" : run.phase === "error" ? "出错" : run.phase === "submitting" ? "提交中" : "就绪";
|
||||
const phaseCls =
|
||||
run.phase === "streaming" ? "text-cyan-400" : run.phase === "done" ? "text-emerald-400" : run.phase === "error" ? "text-rose-400" : "text-slate-500";
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col gap-4 overflow-hidden p-6">
|
||||
<header className="flex items-center gap-3">
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-slate-100">运行 · 观测</h1>
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
实时执行轨迹:每个节点(记忆/工具/提示词/模型,或报告的规划/分章/渲染)逐个点亮,附入参产出与耗时。
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-4 text-xs">
|
||||
<span className="text-slate-500">
|
||||
任务 <span className="font-mono text-slate-300">{run.taskId ?? "—"}</span>
|
||||
</span>
|
||||
<span className={phaseCls}>● {phaseText}</span>
|
||||
<span className="text-slate-500">{nodes.length} 节点 · {tools.length} 次工具调用</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid min-h-0 flex-1 grid-cols-[1.2fr_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={run.exec} phase={run.phase} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 模型输出 */}
|
||||
<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">
|
||||
{run.output ? (
|
||||
<pre className="whitespace-pre-wrap break-words font-sans text-sm leading-relaxed text-slate-300">{run.output}</pre>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-center text-xs text-slate-600">
|
||||
在「编排」页搭图并运行,或在「报告」页生成报告,<br />执行轨迹与输出会实时出现在这里。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user