feat(memory): P1 长期记忆升级 —— 异步攒批 Consolidate + 软删 + importance/last_seen #1
@@ -1,13 +1,12 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Activity, History, Wrench } from "lucide-react";
|
import { Activity, FileText, History, Wrench } from "lucide-react";
|
||||||
import { ExecTrace } from "../components/ExecTrace";
|
import { ExecTrace } from "../components/ExecTrace";
|
||||||
import { ChartView } from "../components/ChartView";
|
import { Markdown } from "../components/Markdown";
|
||||||
import { extractChartBlocks, hasChart } from "../lib/chartspec";
|
|
||||||
import { deriveNodes, emptyRun, type RunState } from "../lib/run";
|
import { deriveNodes, emptyRun, type RunState } from "../lib/run";
|
||||||
import { listRuns, taskEval, streamExec, streamTokens, type RunSummary, type EvalResult } from "../lib/api";
|
import { listRuns, taskEval, streamExec, streamTokens, type RunSummary, type EvalResult } from "../lib/api";
|
||||||
import { Tabs, Dot, Badge, EmptyState, cn, type TabDef } from "../ui";
|
import { Tabs, Panel, Dot, Badge, EmptyState, cn, type TabDef } from "../ui";
|
||||||
|
|
||||||
type DetailTab = "trace" | "output" | "tools" | "eval";
|
type DetailTab = "trace" | "tools" | "eval";
|
||||||
|
|
||||||
const STATUS_DOT: Record<string, "success" | "danger" | "warn" | "running" | "neutral"> = {
|
const STATUS_DOT: Record<string, "success" | "danger" | "warn" | "running" | "neutral"> = {
|
||||||
done: "success", failed: "danger", timeout: "danger", rejected: "danger",
|
done: "success", failed: "danger", timeout: "danger", rejected: "danger",
|
||||||
@@ -23,8 +22,8 @@ function relTime(iso: string): string {
|
|||||||
return `${Math.floor(d / 86_400_000)} 天前`;
|
return `${Math.floor(d / 86_400_000)} 天前`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 运行 · 观测:左侧运行历史,右侧 tab 切换查看选中运行的 轨迹/输出/工具调用/评测(复盘)。
|
// 运行 · 观测:左=运行历史,中=tab 切换(轨迹/工具/评测)+状态,右=模型输出(Markdown)。
|
||||||
// 选中历史运行后从 Redis 回放(exec/token 流对完成任务即回放);「当前运行」置顶沿用实时订阅。
|
// 选中历史运行从 Redis 回放;「当前运行」置顶沿用实时订阅。
|
||||||
export function RunsView({ run }: { run: RunState }) {
|
export function RunsView({ run }: { run: RunState }) {
|
||||||
const [runs, setRuns] = useState<RunSummary[]>([]);
|
const [runs, setRuns] = useState<RunSummary[]>([]);
|
||||||
const [sel, setSel] = useState<string | null>(null); // null = 当前实时运行
|
const [sel, setSel] = useState<string | null>(null); // null = 当前实时运行
|
||||||
@@ -53,7 +52,7 @@ export function RunsView({ run }: { run: RunState }) {
|
|||||||
taskEval(taskId).then(setEvalRes).catch(() => {});
|
taskEval(taskId).then(setEvalRes).catch(() => {});
|
||||||
}, [run.taskId]);
|
}, [run.taskId]);
|
||||||
|
|
||||||
// 新的实时运行开始 → 自动切回「当前运行」(若此前选着历史项),并回到轨迹标签。
|
// 新实时运行开始 → 切回「当前运行」+ 回到轨迹标签。
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (run.taskId) { closeRef.current?.(); setSel(null); setEvalRes(null); setTab("trace"); }
|
if (run.taskId) { closeRef.current?.(); setSel(null); setEvalRes(null); setTab("trace"); }
|
||||||
}, [run.taskId]);
|
}, [run.taskId]);
|
||||||
@@ -69,23 +68,28 @@ export function RunsView({ run }: { run: RunState }) {
|
|||||||
const tools = nodes.filter((n) => n.kind === "tool");
|
const tools = nodes.filter((n) => n.kind === "tool");
|
||||||
const tabs: TabDef<DetailTab>[] = [
|
const tabs: TabDef<DetailTab>[] = [
|
||||||
{ key: "trace", label: "执行轨迹", count: nodes.length },
|
{ key: "trace", label: "执行轨迹", count: nodes.length },
|
||||||
{ key: "output", label: "模型输出" },
|
|
||||||
{ key: "tools", label: "工具调用", count: tools.length },
|
{ key: "tools", label: "工具调用", count: tools.length },
|
||||||
{ key: "eval", label: "评测" },
|
{ key: "eval", label: "评测" },
|
||||||
];
|
];
|
||||||
const empty = !cur.taskId && cur.exec.length === 0 && !cur.output;
|
const empty = !cur.taskId && cur.exec.length === 0 && !cur.output;
|
||||||
|
|
||||||
|
// 运行状态指示(沿用原底部抽屉的「完成 ✓ / 流式中… / 出错」)。
|
||||||
|
const statusCls =
|
||||||
|
cur.phase === "streaming" ? "text-accent-400" : cur.phase === "done" ? "text-success" : cur.phase === "error" ? "text-danger" : "text-slate-500";
|
||||||
|
const statusText =
|
||||||
|
cur.phase === "streaming" ? "流式中…" : cur.phase === "done" ? "完成 ✓" : cur.phase === "error" ? `✗ ${cur.error ?? "出错"}` : cur.phase === "submitting" ? "提交中…" : "就绪";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-col gap-3 overflow-hidden p-6">
|
<div className="flex h-full min-h-0 flex-col gap-3 overflow-hidden p-6">
|
||||||
<header className="flex items-center gap-3">
|
<header className="flex items-center gap-3">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h1 className="text-lg font-semibold text-slate-100">运行 · 观测</h1>
|
<h1 className="text-lg font-semibold text-slate-100">运行 · 观测</h1>
|
||||||
<p className="mt-1 text-xs text-slate-500">运行历史复盘:选一次运行,切换标签查看节点轨迹、模型输出、工具调用与评测。</p>
|
<p className="mt-1 text-xs text-slate-500">运行历史复盘:选一次运行,左查轨迹/工具/评测(切换标签),右看模型输出。</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-slate-500">{nodes.length} 节点 · {tools.length} 次工具调用</span>
|
<span className="text-xs text-slate-500">{nodes.length} 节点 · {tools.length} 次工具调用</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="grid min-h-0 flex-1 grid-cols-[250px_1fr] gap-3">
|
<div className="grid min-h-0 flex-1 grid-cols-[240px_1fr_1fr] gap-3">
|
||||||
{/* 运行历史列表 */}
|
{/* 运行历史列表 */}
|
||||||
<div className="flex min-h-0 flex-col rounded-lg border border-line bg-ink-900">
|
<div className="flex min-h-0 flex-col rounded-lg border border-line bg-ink-900">
|
||||||
<div className="flex items-center gap-2 border-b border-line px-3 py-2 text-xs font-medium text-slate-400">
|
<div className="flex items-center gap-2 border-b border-line px-3 py-2 text-xs font-medium text-slate-400">
|
||||||
@@ -115,18 +119,17 @@ export function RunsView({ run }: { run: RunState }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 详情:tab 切换 轨迹/输出/工具/评测 */}
|
{/* 中:tab 切换 轨迹/工具/评测 + 状态指示 */}
|
||||||
<div className="flex min-h-0 flex-col rounded-lg border border-line bg-ink-900">
|
<div className="flex min-h-0 flex-col rounded-lg border border-line bg-ink-900">
|
||||||
<div className="border-b border-line px-2">
|
<div className="flex items-center border-b border-line px-2">
|
||||||
<Tabs tabs={tabs} value={tab} onChange={setTab} />
|
<Tabs tabs={tabs} value={tab} onChange={setTab} />
|
||||||
|
<span className={cn("ml-auto pr-2 text-[11px]", statusCls)}>{statusText}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-h-0 flex-1 overflow-auto p-4">
|
<div className="min-h-0 flex-1 overflow-auto p-4">
|
||||||
{empty ? (
|
{empty ? (
|
||||||
<EmptyState icon={Activity} title="选择一次运行" desc="左侧点选历史运行即可回放轨迹与输出;或在编排/报告页发起新运行。" />
|
<EmptyState icon={Activity} title="选择一次运行" desc="左侧点选历史运行即可回放轨迹;或在编排/报告页发起新运行。" />
|
||||||
) : tab === "trace" ? (
|
) : tab === "trace" ? (
|
||||||
<ExecTrace events={cur.exec} phase={cur.phase} />
|
<ExecTrace events={cur.exec} phase={cur.phase} />
|
||||||
) : tab === "output" ? (
|
|
||||||
<OutputView output={cur.output} />
|
|
||||||
) : tab === "tools" ? (
|
) : tab === "tools" ? (
|
||||||
<ToolCalls run={cur} />
|
<ToolCalls run={cur} />
|
||||||
) : (
|
) : (
|
||||||
@@ -134,30 +137,16 @@ export function RunsView({ run }: { run: RunState }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// OutputView:渲染模型输出。含 ```chart 块时分段渲染(文本 + SVG 图表),否则纯文本。
|
{/* 右:模型输出(Markdown,固定面板,与之前一致) */}
|
||||||
function OutputView({ output }: { output: string }) {
|
<Panel title="模型输出" icon={FileText}>
|
||||||
if (!output) {
|
{cur.output ? (
|
||||||
return <p className="text-xs text-slate-600">本次运行暂无输出(或尚未产出 token)。</p>;
|
<Markdown text={cur.output} className="text-sm" />
|
||||||
}
|
|
||||||
if (!hasChart(output)) {
|
|
||||||
return <pre className="whitespace-pre-wrap font-mono text-[13px] leading-relaxed text-slate-200">{output}</pre>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{extractChartBlocks(output).map((seg, i) =>
|
|
||||||
seg.kind === "chart" ? (
|
|
||||||
<ChartView key={i} spec={seg.spec} />
|
|
||||||
) : (
|
) : (
|
||||||
seg.text.trim() && (
|
<EmptyState icon={Activity} title="尚无输出" desc="选择一次运行查看其模型输出;或发起新运行,token 在此流式呈现。" />
|
||||||
<pre key={i} className="whitespace-pre-wrap font-mono text-[13px] leading-relaxed text-slate-200">{seg.text}</pre>
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)}
|
)}
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user