修改
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import { ExternalLinkIcon, FileIcon, RefreshCwIcon } from "lucide-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 { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -106,7 +107,7 @@ export function StepDetailSheet({
|
||||
) : detail.format === "empty" ? (
|
||||
<EmptyView detail={detail} />
|
||||
) : detail.format === "markdown" ? (
|
||||
<MarkdownText text={detail.content} />
|
||||
<MarkdownView text={detail.content} />
|
||||
) : detail.format === "json" ? (
|
||||
<JsonView text={detail.content} />
|
||||
) : 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 }) {
|
||||
const pretty = useMemo(() => {
|
||||
const parsed = useMemo<{ ok: true; value: JsonValue } | { ok: false }>(() => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(text), null, 2);
|
||||
return { ok: true, value: JSON.parse(text) as JsonValue };
|
||||
} catch {
|
||||
return text;
|
||||
return { ok: false };
|
||||
}
|
||||
}, [text]);
|
||||
|
||||
if (!parsed.ok) {
|
||||
return (
|
||||
<pre className="overflow-x-auto rounded-md border bg-muted/30 p-3 font-mono text-[11px] leading-relaxed">
|
||||
{text}
|
||||
</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 (
|
||||
<pre className="overflow-x-auto rounded-md border bg-muted/30 p-3 font-mono text-[11px] leading-relaxed">
|
||||
{pretty}
|
||||
</pre>
|
||||
<div className="rounded-md border bg-muted/20 px-3 py-2 font-mono text-[12px]">
|
||||
<JsonPrimitive value={value} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function JsonLinesView({ text }: { text: string }) {
|
||||
const lines = useMemo(
|
||||
() =>
|
||||
text
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)
|
||||
.map((l) => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(l), null, 2);
|
||||
} catch {
|
||||
return l;
|
||||
}
|
||||
}),
|
||||
[text],
|
||||
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">
|
||||
<div className="text-[10px] text-muted-foreground">{lines.length} 行</div>
|
||||
{lines.map((l, idx) => (
|
||||
<details
|
||||
key={idx}
|
||||
open={idx === 0}
|
||||
className="rounded-md border bg-muted/20"
|
||||
>
|
||||
<summary className="cursor-pointer px-3 py-1.5 font-mono text-[11px] text-muted-foreground hover:bg-muted/40">
|
||||
行 #{idx + 1}
|
||||
</summary>
|
||||
<pre className="overflow-x-auto px-3 pt-0 pb-2 font-mono text-[11px] leading-relaxed">
|
||||
{l}
|
||||
</pre>
|
||||
</details>
|
||||
))}
|
||||
<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 }) {
|
||||
const entries = useMemo(() => {
|
||||
return text
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)
|
||||
.map((l) => {
|
||||
try {
|
||||
return { ok: true as const, data: JSON.parse(l) as IterationEntry };
|
||||
} catch {
|
||||
return { ok: false as const, raw: l };
|
||||
}
|
||||
});
|
||||
}, [text]);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return (
|
||||
<p className="text-muted-foreground text-xs">无记录。</p>
|
||||
);
|
||||
}
|
||||
|
||||
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}
|
||||
entry={entry.data}
|
||||
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"
|
||||
>
|
||||
行 #{idx + 1} 无法解析为 JSON
|
||||
<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>
|
||||
<pre className="overflow-x-auto px-3 pt-0 pb-2 font-mono text-[11px] leading-relaxed">
|
||||
{JSON.stringify(
|
||||
Object.fromEntries(otherKeys.map((k) => [k, entry[k]])),
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
</pre>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Generated
+7369
-7320
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
@@ -39,6 +40,7 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.13",
|
||||
"@tailwindcss/postcss": "^4.2.4",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
|
||||
@@ -212,6 +212,9 @@ for k in prev:
|
||||
4. **跨轮 diff**:对每个 B case 查 `error_registry.jsonl`,标记 `persistent` / `new` / `regressed`。
|
||||
5. 按 query 内容归类 pattern:过召、丢失、误判、噪声、意图漂移等。
|
||||
6. 每个 pattern 列 2–5 个典型 case,**优先列 new/regressed 的**。
|
||||
- **每条 case 必须包含 `label:` 字段**,无论用哪种排版(多行、按桶分组、紧凑一行)。
|
||||
- **禁止**只写 `query → pred` 而省略 `label`。读者要靠 `label` 才能判断 pred 对不对。
|
||||
- 按桶/簇/类型分组列 case 时(如"多约束路线规划 (N 条)"这种小标题),组内每条仍然必须含 `label`。
|
||||
7. 做归因(见 2.4)。
|
||||
8. 更新 `error_registry.jsonl`:写入本轮所有 B case 的 `(query_hash, runDic, was_wrong=True)`。
|
||||
9. 写入 `results/workflow<runDic>.md`。
|
||||
|
||||
Reference in New Issue
Block a user