修改
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
@plugin "@tailwindcss/typography";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import { ExternalLinkIcon, FileIcon, RefreshCwIcon } from "lucide-react";
|
import { ExternalLinkIcon, FileIcon, RefreshCwIcon } from "lucide-react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet";
|
import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -106,7 +107,7 @@ export function StepDetailSheet({
|
|||||||
) : detail.format === "empty" ? (
|
) : detail.format === "empty" ? (
|
||||||
<EmptyView detail={detail} />
|
<EmptyView detail={detail} />
|
||||||
) : detail.format === "markdown" ? (
|
) : detail.format === "markdown" ? (
|
||||||
<MarkdownText text={detail.content} />
|
<MarkdownView text={detail.content} />
|
||||||
) : detail.format === "json" ? (
|
) : detail.format === "json" ? (
|
||||||
<JsonView text={detail.content} />
|
<JsonView text={detail.content} />
|
||||||
) : detail.format === "jsonl" ? (
|
) : detail.format === "jsonl" ? (
|
||||||
@@ -166,54 +167,774 @@ function EmptyView({ detail }: { detail: StepDetail }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type JsonValue =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| JsonValue[]
|
||||||
|
| { [key: string]: JsonValue };
|
||||||
|
|
||||||
function JsonView({ text }: { text: string }) {
|
function JsonView({ text }: { text: string }) {
|
||||||
const pretty = useMemo(() => {
|
const parsed = useMemo<{ ok: true; value: JsonValue } | { ok: false }>(() => {
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(JSON.parse(text), null, 2);
|
return { ok: true, value: JSON.parse(text) as JsonValue };
|
||||||
} catch {
|
} catch {
|
||||||
return text;
|
return { ok: false };
|
||||||
}
|
}
|
||||||
}, [text]);
|
}, [text]);
|
||||||
|
|
||||||
|
if (!parsed.ok) {
|
||||||
return (
|
return (
|
||||||
<pre className="overflow-x-auto rounded-md border bg-muted/30 p-3 font-mono text-[11px] leading-relaxed">
|
<pre className="overflow-x-auto rounded-md border bg-muted/30 p-3 font-mono text-[11px] leading-relaxed">
|
||||||
{pretty}
|
{text}
|
||||||
</pre>
|
</pre>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = parsed.value;
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-[10px] text-muted-foreground">
|
||||||
|
{value.length} 项
|
||||||
|
</div>
|
||||||
|
{value.map((item, idx) => (
|
||||||
|
<details
|
||||||
|
key={idx}
|
||||||
|
open={idx === 0}
|
||||||
|
className="rounded-lg border bg-card shadow-sm"
|
||||||
|
>
|
||||||
|
<summary className="cursor-pointer border-b px-4 py-2 font-mono text-[11px] text-muted-foreground hover:bg-muted/30">
|
||||||
|
#{idx}
|
||||||
|
</summary>
|
||||||
|
<div className="px-4 py-3">
|
||||||
|
<JsonValueView value={item} depth={0} />
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (value !== null && typeof value === "object") {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border bg-card shadow-sm">
|
||||||
|
<div className="space-y-3 px-4 py-3">
|
||||||
|
<JsonObjectView value={value as Record<string, JsonValue>} depth={0} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border bg-muted/20 px-3 py-2 font-mono text-[12px]">
|
||||||
|
<JsonPrimitive value={value} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function JsonObjectView({
|
||||||
|
value,
|
||||||
|
depth,
|
||||||
|
}: {
|
||||||
|
value: Record<string, JsonValue>;
|
||||||
|
depth: number;
|
||||||
|
}) {
|
||||||
|
const keys = Object.keys(value);
|
||||||
|
if (keys.length === 0) {
|
||||||
|
return (
|
||||||
|
<span className="font-mono text-[11px] text-muted-foreground">
|
||||||
|
{`{}`}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{keys.map((k) => (
|
||||||
|
<Field key={k} label={k}>
|
||||||
|
<JsonValueView value={value[k]} depth={depth + 1} />
|
||||||
|
</Field>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function JsonValueView({
|
||||||
|
value,
|
||||||
|
depth,
|
||||||
|
}: {
|
||||||
|
value: JsonValue;
|
||||||
|
depth: number;
|
||||||
|
}) {
|
||||||
|
if (value === null || typeof value !== "object") {
|
||||||
|
return <JsonPrimitive value={value} />;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.length === 0) {
|
||||||
|
return (
|
||||||
|
<span className="font-mono text-[11px] text-muted-foreground">
|
||||||
|
[]
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const allPrimitive = value.every(
|
||||||
|
(v) => v === null || typeof v !== "object",
|
||||||
|
);
|
||||||
|
if (allPrimitive) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{value.map((v, i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className="inline-flex rounded border bg-muted/30 px-1.5 py-0.5 font-mono text-[11px]"
|
||||||
|
>
|
||||||
|
<JsonPrimitive value={v} />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const allObjects = value.every(
|
||||||
|
(v) => v !== null && typeof v === "object" && !Array.isArray(v),
|
||||||
|
);
|
||||||
|
if (allObjects) {
|
||||||
|
const cols = Array.from(
|
||||||
|
new Set(
|
||||||
|
value.flatMap((v) =>
|
||||||
|
Object.keys(v as Record<string, JsonValue>),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const flat = cols.length <= 8 && depth <= 1;
|
||||||
|
if (flat) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto rounded-md border">
|
||||||
|
<table className="w-full border-collapse text-[12px]">
|
||||||
|
<thead className="bg-muted/50">
|
||||||
|
<tr>
|
||||||
|
{cols.map((c) => (
|
||||||
|
<th
|
||||||
|
key={c}
|
||||||
|
className="border-b px-2 py-1 text-left font-medium"
|
||||||
|
>
|
||||||
|
{c}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{value.map((row, i) => (
|
||||||
|
<tr
|
||||||
|
key={i}
|
||||||
|
className="border-b last:border-b-0 hover:bg-muted/20"
|
||||||
|
>
|
||||||
|
{cols.map((c) => {
|
||||||
|
const cell = (row as Record<string, JsonValue>)[c];
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
key={c}
|
||||||
|
className="px-2 py-1 align-top font-mono text-[11px]"
|
||||||
|
>
|
||||||
|
{cell === undefined ? (
|
||||||
|
<span className="text-muted-foreground">—</span>
|
||||||
|
) : (
|
||||||
|
<JsonValueView value={cell} depth={depth + 2} />
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{value.map((v, i) => (
|
||||||
|
<details key={i} className="rounded-md border bg-muted/20">
|
||||||
|
<summary className="cursor-pointer px-3 py-1 font-mono text-[10px] text-muted-foreground hover:bg-muted/40">
|
||||||
|
#{i}
|
||||||
|
</summary>
|
||||||
|
<div className="space-y-2 px-3 pt-1 pb-2">
|
||||||
|
<JsonValueView value={v} depth={depth + 1} />
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const obj = value as Record<string, JsonValue>;
|
||||||
|
if (depth >= 1) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2 border-l-2 border-muted pl-3">
|
||||||
|
<JsonObjectView value={obj} depth={depth} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<JsonObjectView value={obj} depth={depth} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function JsonPrimitive({ value }: { value: JsonValue }) {
|
||||||
|
if (value === null) {
|
||||||
|
return (
|
||||||
|
<span className="font-mono text-[11px] text-muted-foreground italic">
|
||||||
|
null
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (typeof value === "boolean") {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"font-mono text-[11px]",
|
||||||
|
value
|
||||||
|
? "text-emerald-600 dark:text-emerald-400"
|
||||||
|
: "text-rose-600 dark:text-rose-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{String(value)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return (
|
||||||
|
<span className="font-mono text-[12px] text-foreground">{value}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (typeof value !== "string") return null;
|
||||||
|
const str = value;
|
||||||
|
if (str.includes("\n")) {
|
||||||
|
return (
|
||||||
|
<pre className="overflow-x-auto whitespace-pre-wrap break-words font-mono text-[11px] leading-relaxed">
|
||||||
|
{str}
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <span className="break-words text-foreground">{str}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type IterationEntry = {
|
||||||
|
iteration?: number;
|
||||||
|
runDic?: number;
|
||||||
|
timestamp?: string;
|
||||||
|
hypothesis?: string;
|
||||||
|
intervention?: { type?: string; summary?: string } | string;
|
||||||
|
prediction?: Record<string, number | string>;
|
||||||
|
results?: Record<string, number | string>;
|
||||||
|
verdict?: string;
|
||||||
|
root_cause_findings?: Array<{
|
||||||
|
pattern?: string;
|
||||||
|
class?: string;
|
||||||
|
cases?: number;
|
||||||
|
}>;
|
||||||
|
error_delta?: { persistent?: number; new?: number; fixed?: number };
|
||||||
|
next_hypothesis?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
const KNOWN_KEYS = new Set([
|
||||||
|
"iteration",
|
||||||
|
"runDic",
|
||||||
|
"timestamp",
|
||||||
|
"hypothesis",
|
||||||
|
"intervention",
|
||||||
|
"prediction",
|
||||||
|
"results",
|
||||||
|
"verdict",
|
||||||
|
"root_cause_findings",
|
||||||
|
"error_delta",
|
||||||
|
"next_hypothesis",
|
||||||
|
]);
|
||||||
|
|
||||||
function JsonLinesView({ text }: { text: string }) {
|
function JsonLinesView({ text }: { text: string }) {
|
||||||
const lines = useMemo(
|
const entries = useMemo(() => {
|
||||||
() =>
|
return text
|
||||||
text
|
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map((l) => l.trim())
|
.map((l) => l.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((l) => {
|
.map((l) => {
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(JSON.parse(l), null, 2);
|
return { ok: true as const, data: JSON.parse(l) as IterationEntry };
|
||||||
} catch {
|
} catch {
|
||||||
return l;
|
return { ok: false as const, raw: l };
|
||||||
}
|
}
|
||||||
}),
|
});
|
||||||
[text],
|
}, [text]);
|
||||||
);
|
|
||||||
|
if (entries.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<p className="text-muted-foreground text-xs">无记录。</p>
|
||||||
<div className="text-[10px] text-muted-foreground">{lines.length} 行</div>
|
);
|
||||||
{lines.map((l, idx) => (
|
}
|
||||||
<details
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-[10px] text-muted-foreground">
|
||||||
|
{entries.length} 条记录(最新在最后)
|
||||||
|
</div>
|
||||||
|
{entries.map((entry, idx) =>
|
||||||
|
entry.ok ? (
|
||||||
|
<IterationCard
|
||||||
key={idx}
|
key={idx}
|
||||||
open={idx === 0}
|
entry={entry.data}
|
||||||
className="rounded-md border bg-muted/20"
|
defaultOpen={idx === entries.length - 1}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="rounded-md border border-destructive/40 bg-destructive/5 p-3 font-mono text-[11px] text-destructive"
|
||||||
>
|
>
|
||||||
<summary className="cursor-pointer px-3 py-1.5 font-mono text-[11px] text-muted-foreground hover:bg-muted/40">
|
行 #{idx + 1} 无法解析为 JSON
|
||||||
行 #{idx + 1}
|
<pre className="mt-1 whitespace-pre-wrap break-all text-muted-foreground">
|
||||||
|
{entry.raw}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function IterationCard({
|
||||||
|
entry,
|
||||||
|
defaultOpen,
|
||||||
|
}: {
|
||||||
|
entry: IterationEntry;
|
||||||
|
defaultOpen: boolean;
|
||||||
|
}) {
|
||||||
|
const intervention =
|
||||||
|
typeof entry.intervention === "string"
|
||||||
|
? { summary: entry.intervention }
|
||||||
|
: (entry.intervention ?? {});
|
||||||
|
const otherKeys = Object.keys(entry).filter((k) => !KNOWN_KEYS.has(k));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<details
|
||||||
|
open={defaultOpen}
|
||||||
|
className="group rounded-lg border bg-card shadow-sm"
|
||||||
|
>
|
||||||
|
<summary className="flex cursor-pointer items-center gap-2 border-b px-4 py-2.5 hover:bg-muted/30">
|
||||||
|
<span className="rounded bg-primary/10 px-2 py-0.5 font-mono text-[11px] font-semibold text-primary">
|
||||||
|
R{entry.iteration ?? "?"}
|
||||||
|
</span>
|
||||||
|
{entry.runDic != null ? (
|
||||||
|
<span className="rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
|
||||||
|
runDic={entry.runDic}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{entry.verdict ? <VerdictPill verdict={entry.verdict} /> : null}
|
||||||
|
<span className="ml-auto truncate font-mono text-[10px] text-muted-foreground">
|
||||||
|
{entry.timestamp ?? ""}
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div className="space-y-3 px-4 py-3 text-[13px]">
|
||||||
|
{entry.hypothesis ? (
|
||||||
|
<Field label="假设" tone="primary">
|
||||||
|
{entry.hypothesis}
|
||||||
|
</Field>
|
||||||
|
) : null}
|
||||||
|
{intervention.summary || intervention.type ? (
|
||||||
|
<Field label="干预">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
{intervention.type ? (
|
||||||
|
<span className="rounded bg-amber-500/15 px-2 py-0.5 font-mono text-[11px] text-amber-700 dark:text-amber-300">
|
||||||
|
{intervention.type}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{intervention.summary ? (
|
||||||
|
<span className="text-foreground">{intervention.summary}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
) : null}
|
||||||
|
{entry.prediction || entry.results ? (
|
||||||
|
<MetricsCompare
|
||||||
|
prediction={entry.prediction}
|
||||||
|
results={entry.results}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{entry.error_delta ? (
|
||||||
|
<Field label="错误增量">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<DeltaPill
|
||||||
|
label="persistent"
|
||||||
|
value={entry.error_delta.persistent}
|
||||||
|
tone="muted"
|
||||||
|
/>
|
||||||
|
<DeltaPill
|
||||||
|
label="new"
|
||||||
|
value={entry.error_delta.new}
|
||||||
|
tone="rose"
|
||||||
|
/>
|
||||||
|
<DeltaPill
|
||||||
|
label="fixed"
|
||||||
|
value={entry.error_delta.fixed}
|
||||||
|
tone="emerald"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
) : null}
|
||||||
|
{entry.root_cause_findings && entry.root_cause_findings.length > 0 ? (
|
||||||
|
<Field label="根因发现">
|
||||||
|
<div className="overflow-x-auto rounded-md border">
|
||||||
|
<table className="w-full border-collapse text-[12px]">
|
||||||
|
<thead className="bg-muted/50">
|
||||||
|
<tr>
|
||||||
|
<th className="border-b px-2 py-1 text-left font-medium">
|
||||||
|
pattern
|
||||||
|
</th>
|
||||||
|
<th className="border-b px-2 py-1 text-left font-medium">
|
||||||
|
class
|
||||||
|
</th>
|
||||||
|
<th className="border-b px-2 py-1 text-right font-medium">
|
||||||
|
cases
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{entry.root_cause_findings.map((f, i) => (
|
||||||
|
<tr
|
||||||
|
key={i}
|
||||||
|
className="border-b last:border-b-0 hover:bg-muted/20"
|
||||||
|
>
|
||||||
|
<td className="px-2 py-1">{f.pattern ?? "—"}</td>
|
||||||
|
<td className="px-2 py-1">
|
||||||
|
{f.class ? (
|
||||||
|
<span className="rounded bg-muted px-1.5 py-0.5 font-mono text-[10px]">
|
||||||
|
{f.class}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
"—"
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1 text-right font-mono">
|
||||||
|
{f.cases ?? "—"}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
) : null}
|
||||||
|
{entry.next_hypothesis ? (
|
||||||
|
<Field label="下一步假设" tone="accent">
|
||||||
|
{entry.next_hypothesis}
|
||||||
|
</Field>
|
||||||
|
) : null}
|
||||||
|
{otherKeys.length > 0 ? (
|
||||||
|
<details className="rounded-md border bg-muted/20">
|
||||||
|
<summary className="cursor-pointer px-3 py-1.5 text-[11px] text-muted-foreground hover:bg-muted/40">
|
||||||
|
其他字段({otherKeys.length})
|
||||||
</summary>
|
</summary>
|
||||||
<pre className="overflow-x-auto px-3 pt-0 pb-2 font-mono text-[11px] leading-relaxed">
|
<pre className="overflow-x-auto px-3 pt-0 pb-2 font-mono text-[11px] leading-relaxed">
|
||||||
{l}
|
{JSON.stringify(
|
||||||
|
Object.fromEntries(otherKeys.map((k) => [k, entry[k]])),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}
|
||||||
</pre>
|
</pre>
|
||||||
</details>
|
</details>
|
||||||
))}
|
) : null}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Field({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
tone,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
tone?: "primary" | "accent";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"rounded-md border bg-muted/20 px-3 py-2 leading-relaxed",
|
||||||
|
tone === "primary" && "border-primary/30 bg-primary/5",
|
||||||
|
tone === "accent" && "border-amber-500/30 bg-amber-500/5",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function VerdictPill({ verdict }: { verdict: string }) {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
pass: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300",
|
||||||
|
partial: "bg-amber-500/15 text-amber-700 dark:text-amber-300",
|
||||||
|
miss: "bg-rose-500/15 text-rose-700 dark:text-rose-300",
|
||||||
|
replication: "bg-sky-500/15 text-sky-700 dark:text-sky-300",
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"rounded-full px-2 py-0.5 font-medium text-[10px] tracking-wider",
|
||||||
|
map[verdict.toLowerCase()] ?? "bg-muted text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{verdict.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeltaPill({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
tone,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: number | undefined;
|
||||||
|
tone: "rose" | "emerald" | "muted";
|
||||||
|
}) {
|
||||||
|
if (value == null) return null;
|
||||||
|
const toneMap: Record<typeof tone, string> = {
|
||||||
|
rose: "bg-rose-500/15 text-rose-700 dark:text-rose-300",
|
||||||
|
emerald: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300",
|
||||||
|
muted: "bg-muted text-muted-foreground",
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-mono text-[11px]",
|
||||||
|
toneMap[tone],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="opacity-70">{label}</span>
|
||||||
|
<span className="font-semibold">{value}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MetricsCompare({
|
||||||
|
prediction,
|
||||||
|
results,
|
||||||
|
}: {
|
||||||
|
prediction?: Record<string, number | string>;
|
||||||
|
results?: Record<string, number | string>;
|
||||||
|
}) {
|
||||||
|
const keys = useMemo(() => {
|
||||||
|
const set = new Set<string>();
|
||||||
|
Object.keys(prediction ?? {}).forEach((k) => set.add(k));
|
||||||
|
Object.keys(results ?? {}).forEach((k) => set.add(k));
|
||||||
|
return Array.from(set);
|
||||||
|
}, [prediction, results]);
|
||||||
|
|
||||||
|
if (keys.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field label="指标">
|
||||||
|
<div className="overflow-x-auto rounded-md border">
|
||||||
|
<table className="w-full border-collapse text-[12px]">
|
||||||
|
<thead className="bg-muted/50">
|
||||||
|
<tr>
|
||||||
|
<th className="border-b px-2 py-1 text-left font-medium">
|
||||||
|
metric
|
||||||
|
</th>
|
||||||
|
<th className="border-b px-2 py-1 text-right font-medium">
|
||||||
|
prediction
|
||||||
|
</th>
|
||||||
|
<th className="border-b px-2 py-1 text-right font-medium">
|
||||||
|
result
|
||||||
|
</th>
|
||||||
|
<th className="border-b px-2 py-1 text-right font-medium">
|
||||||
|
Δ
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{keys.map((k) => {
|
||||||
|
const p = prediction?.[k];
|
||||||
|
const r = results?.[k];
|
||||||
|
const pn = typeof p === "number" ? p : Number(p);
|
||||||
|
const rn = typeof r === "number" ? r : Number(r);
|
||||||
|
const delta =
|
||||||
|
Number.isFinite(pn) && Number.isFinite(rn)
|
||||||
|
? rn - pn
|
||||||
|
: null;
|
||||||
|
const deltaTone =
|
||||||
|
delta == null
|
||||||
|
? ""
|
||||||
|
: delta > 0
|
||||||
|
? "text-emerald-600 dark:text-emerald-400"
|
||||||
|
: delta < 0
|
||||||
|
? "text-rose-600 dark:text-rose-400"
|
||||||
|
: "text-muted-foreground";
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={k}
|
||||||
|
className="border-b last:border-b-0 hover:bg-muted/20"
|
||||||
|
>
|
||||||
|
<td className="px-2 py-1 font-mono text-[11px]">{k}</td>
|
||||||
|
<td className="px-2 py-1 text-right font-mono">
|
||||||
|
{p ?? "—"}
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1 text-right font-mono font-semibold">
|
||||||
|
{r ?? "—"}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1 text-right font-mono",
|
||||||
|
deltaTone,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{delta == null
|
||||||
|
? "—"
|
||||||
|
: `${delta > 0 ? "+" : ""}${delta.toFixed(2)}`}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PROSE_CLASSES = cn(
|
||||||
|
"prose prose-sm max-w-none dark:prose-invert",
|
||||||
|
"prose-headings:scroll-m-20 prose-headings:font-semibold",
|
||||||
|
"prose-h1:mt-4 prose-h1:mb-2 prose-h1:text-lg",
|
||||||
|
"prose-h2:mt-4 prose-h2:mb-2 prose-h2:text-base prose-h2:border-b prose-h2:border-border prose-h2:pb-1",
|
||||||
|
"prose-h3:mt-3 prose-h3:mb-1.5 prose-h3:text-sm",
|
||||||
|
"prose-p:my-2 prose-p:leading-relaxed",
|
||||||
|
"prose-a:text-primary prose-a:no-underline hover:prose-a:underline",
|
||||||
|
"prose-strong:text-foreground prose-strong:font-semibold",
|
||||||
|
"prose-blockquote:my-2 prose-blockquote:border-l-4 prose-blockquote:border-muted-foreground/30 prose-blockquote:pl-3 prose-blockquote:text-muted-foreground prose-blockquote:not-italic",
|
||||||
|
"prose-ul:my-2 prose-ol:my-2 prose-li:my-0.5",
|
||||||
|
"prose-hr:my-3 prose-hr:border-border",
|
||||||
|
"prose-table:my-3 prose-table:text-xs",
|
||||||
|
"prose-thead:bg-muted prose-th:px-2 prose-th:py-1 prose-th:font-semibold prose-th:text-left",
|
||||||
|
"prose-td:px-2 prose-td:py-1 prose-tr:border-b prose-tr:border-border",
|
||||||
|
"prose-code:rounded prose-code:bg-muted prose-code:px-1.5 prose-code:py-0.5 prose-code:text-[0.85em] prose-code:before:content-none prose-code:after:content-none",
|
||||||
|
"prose-pre:my-2 prose-pre:rounded-md prose-pre:border prose-pre:border-border prose-pre:bg-muted/40 prose-pre:p-3 prose-pre:text-[12px] prose-pre:text-foreground",
|
||||||
|
);
|
||||||
|
|
||||||
|
type MarkdownSection = { title: string | null; body: string };
|
||||||
|
|
||||||
|
const HIDDEN_MD_SECTION_PATTERNS: RegExp[] = [
|
||||||
|
/^(?:\d+(?:\.\d+)*\s+)?根因归类/,
|
||||||
|
/^(?:\d+(?:\.\d+)*\s+)?下一轮假设候选/,
|
||||||
|
/^(?:\d+(?:\.\d+)*\s+)?error_registry\s+baseline/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
function isHiddenHeading(title: string): boolean {
|
||||||
|
const trimmed = title.trim();
|
||||||
|
return HIDDEN_MD_SECTION_PATTERNS.some((re) => re.test(trimmed));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripHiddenSections(text: string): string {
|
||||||
|
const lines = text.split("\n");
|
||||||
|
const out: string[] = [];
|
||||||
|
let skipUntilLevel: number | null = null;
|
||||||
|
for (const line of lines) {
|
||||||
|
const m = line.match(/^(#{1,6})\s+(.+?)\s*$/);
|
||||||
|
if (m) {
|
||||||
|
const level = m[1].length;
|
||||||
|
const title = m[2];
|
||||||
|
if (skipUntilLevel !== null && level <= skipUntilLevel) {
|
||||||
|
skipUntilLevel = null;
|
||||||
|
}
|
||||||
|
if (skipUntilLevel === null && isHiddenHeading(title)) {
|
||||||
|
skipUntilLevel = level;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (skipUntilLevel !== null) continue;
|
||||||
|
out.push(line);
|
||||||
|
}
|
||||||
|
return out.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMarkdownSections(text: string): MarkdownSection[] {
|
||||||
|
const sections: MarkdownSection[] = [];
|
||||||
|
let current: MarkdownSection = { title: null, body: "" };
|
||||||
|
const lines = text.split("\n");
|
||||||
|
for (const line of lines) {
|
||||||
|
const m = line.match(/^##\s+(.+?)\s*$/);
|
||||||
|
if (m) {
|
||||||
|
if (current.title !== null || current.body.trim().length > 0) {
|
||||||
|
sections.push(current);
|
||||||
|
}
|
||||||
|
current = { title: m[1], body: "" };
|
||||||
|
} else {
|
||||||
|
current.body += (current.body ? "\n" : "") + line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current.title !== null || current.body.trim().length > 0) {
|
||||||
|
sections.push(current);
|
||||||
|
}
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MarkdownView({ text }: { text: string }) {
|
||||||
|
const sections = useMemo(() => {
|
||||||
|
const stripped = stripHiddenSections(text);
|
||||||
|
return splitMarkdownSections(stripped).filter(
|
||||||
|
(s) => s.title === null || !isHiddenHeading(s.title),
|
||||||
|
);
|
||||||
|
}, [text]);
|
||||||
|
|
||||||
|
if (sections.length === 0) {
|
||||||
|
return (
|
||||||
|
<p className="text-muted-foreground text-xs">空内容。</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{sections.map((section, idx) =>
|
||||||
|
section.title === null ? (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="rounded-lg border bg-card px-4 py-3 shadow-sm"
|
||||||
|
>
|
||||||
|
<article className={PROSE_CLASSES}>
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
|
{section.body}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<details
|
||||||
|
key={idx}
|
||||||
|
open={idx <= 1}
|
||||||
|
className="group rounded-lg border bg-card shadow-sm"
|
||||||
|
>
|
||||||
|
<summary className="flex cursor-pointer items-center gap-2 border-b px-4 py-2.5 hover:bg-muted/30">
|
||||||
|
<span className="font-semibold text-foreground text-sm">
|
||||||
|
{section.title}
|
||||||
|
</span>
|
||||||
|
<span className="ml-auto text-[10px] text-muted-foreground transition-transform group-open:rotate-180">
|
||||||
|
▾
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div className="px-4 py-3">
|
||||||
|
<article className={PROSE_CLASSES}>
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
|
{section.body}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+49
@@ -26,6 +26,7 @@
|
|||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.5",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.4.13",
|
"@biomejs/biome": "^2.4.13",
|
||||||
"@tailwindcss/postcss": "^4.2.4",
|
"@tailwindcss/postcss": "^4.2.4",
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "^25.6.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@@ -4676,6 +4678,19 @@
|
|||||||
"tailwindcss": "4.2.4"
|
"tailwindcss": "4.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/typography": {
|
||||||
|
"version": "0.5.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
|
||||||
|
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"postcss-selector-parser": "6.0.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/debug": {
|
"node_modules/@types/debug": {
|
||||||
"version": "4.1.13",
|
"version": "4.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
|
||||||
@@ -4930,6 +4945,19 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cssesc": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"cssesc": "bin/cssesc"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -6422,6 +6450,20 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss-selector-parser": {
|
||||||
|
"version": "6.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||||
|
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cssesc": "^3.0.0",
|
||||||
|
"util-deprecate": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss/node_modules/nanoid": {
|
"node_modules/postcss/node_modules/nanoid": {
|
||||||
"version": "3.3.12",
|
"version": "3.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||||
@@ -7247,6 +7289,13 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vfile": {
|
"node_modules/vfile": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.5",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.4.13",
|
"@biomejs/biome": "^2.4.13",
|
||||||
"@tailwindcss/postcss": "^4.2.4",
|
"@tailwindcss/postcss": "^4.2.4",
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "^25.6.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
|||||||
@@ -212,6 +212,9 @@ for k in prev:
|
|||||||
4. **跨轮 diff**:对每个 B case 查 `error_registry.jsonl`,标记 `persistent` / `new` / `regressed`。
|
4. **跨轮 diff**:对每个 B case 查 `error_registry.jsonl`,标记 `persistent` / `new` / `regressed`。
|
||||||
5. 按 query 内容归类 pattern:过召、丢失、误判、噪声、意图漂移等。
|
5. 按 query 内容归类 pattern:过召、丢失、误判、噪声、意图漂移等。
|
||||||
6. 每个 pattern 列 2–5 个典型 case,**优先列 new/regressed 的**。
|
6. 每个 pattern 列 2–5 个典型 case,**优先列 new/regressed 的**。
|
||||||
|
- **每条 case 必须包含 `label:` 字段**,无论用哪种排版(多行、按桶分组、紧凑一行)。
|
||||||
|
- **禁止**只写 `query → pred` 而省略 `label`。读者要靠 `label` 才能判断 pred 对不对。
|
||||||
|
- 按桶/簇/类型分组列 case 时(如"多约束路线规划 (N 条)"这种小标题),组内每条仍然必须含 `label`。
|
||||||
7. 做归因(见 2.4)。
|
7. 做归因(见 2.4)。
|
||||||
8. 更新 `error_registry.jsonl`:写入本轮所有 B case 的 `(query_hash, runDic, was_wrong=True)`。
|
8. 更新 `error_registry.jsonl`:写入本轮所有 B case 的 `(query_hash, runDic, was_wrong=True)`。
|
||||||
9. 写入 `results/workflow<runDic>.md`。
|
9. 写入 `results/workflow<runDic>.md`。
|
||||||
|
|||||||
Reference in New Issue
Block a user