This commit is contained in:
hupenglong1
2026-05-22 19:48:47 +08:00
parent 20690cdef3
commit 5311e6d97c
31 changed files with 4620 additions and 472 deletions
@@ -1,18 +1,48 @@
"use client";
import { ExternalLinkIcon, FileIcon, RefreshCwIcon } from "lucide-react";
import {
ExternalLinkIcon,
FileIcon,
RefreshCwIcon,
XIcon,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
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 {
Sheet,
SheetClose,
SheetContent,
SheetTitle,
} from "@/components/ui/sheet";
import { cn } from "@/lib/utils";
type TableSection = {
title: string;
source_path: string;
kind: "csv" | "jsonl" | "text" | "markdown" | "missing" | "error";
columns: string[];
rows: string[][];
total_rows: number;
shown_rows: number;
error?: string;
body?: string;
};
type StepDetail = {
step: string;
run_dic: number | null;
format: "markdown" | "jsonl" | "json" | "csv" | "text" | "empty";
content: string;
format:
| "markdown"
| "jsonl"
| "json"
| "csv"
| "text"
| "empty"
| "tables";
content?: string;
sections?: TableSection[];
source_path: string | null;
fetched_at_ms: number;
error?: string;
@@ -65,6 +95,7 @@ export function StepDetailSheet({
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent
side="right"
hideClose
className="flex w-[min(50rem,calc(100vw-2rem))] flex-col gap-0 p-0 sm:max-w-2xl"
>
<header className="flex shrink-0 items-start justify-between gap-3 border-b px-5 py-3">
@@ -87,17 +118,25 @@ export function StepDetailSheet({
</div>
) : null}
</div>
<Button
type="button"
variant="ghost"
size="icon"
className="mt-0.5 size-7 shrink-0"
onClick={() => setReloadCount((c) => c + 1)}
title="刷新"
aria-label="刷新"
>
<RefreshCwIcon className="size-3.5" />
</Button>
<div className="mt-0.5 flex shrink-0 items-center gap-1">
<Button
type="button"
variant="ghost"
size="icon"
className="size-7"
onClick={() => setReloadCount((c) => c + 1)}
title="刷新"
aria-label="刷新"
>
<RefreshCwIcon className="size-3.5" />
</Button>
<SheetClose
className="inline-flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label="关闭"
>
<XIcon className="size-3.5" />
</SheetClose>
</div>
</header>
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4 text-sm">
{loading ? (
@@ -106,14 +145,16 @@ export function StepDetailSheet({
<p className="text-muted-foreground text-xs"></p>
) : detail.format === "empty" ? (
<EmptyView detail={detail} />
) : detail.format === "tables" ? (
<TablesView sections={detail.sections ?? []} />
) : detail.format === "markdown" ? (
<MarkdownView text={detail.content} />
<MarkdownView text={detail.content ?? ""} />
) : detail.format === "json" ? (
<JsonView text={detail.content} />
<JsonView text={detail.content ?? ""} />
) : detail.format === "jsonl" ? (
<JsonLinesView text={detail.content} />
<JsonLinesView text={detail.content ?? ""} />
) : (
<RawView text={detail.content} />
<RawView text={detail.content ?? ""} />
)}
</div>
{detail?.source_path && !loading ? (
@@ -939,6 +980,153 @@ function MarkdownView({ text }: { text: string }) {
);
}
function TablesView({ sections }: { sections: TableSection[] }) {
if (sections.length === 0) {
return <p className="text-muted-foreground text-xs"></p>;
}
return (
<div className="space-y-3">
{sections.map((section, idx) => (
<TableSectionCard
key={`${section.title}-${idx}`}
section={section}
defaultOpen={idx <= 1}
/>
))}
</div>
);
}
function TableSectionCard({
section,
defaultOpen,
}: {
section: TableSection;
defaultOpen: boolean;
}) {
const isMissing = section.kind === "missing" || section.kind === "error";
const isMarkdown = section.kind === "markdown";
const truncated =
section.shown_rows > 0 && section.shown_rows < section.total_rows;
const noteParts: string[] = [];
if (section.total_rows > 0) {
noteParts.push(`${section.total_rows}`);
if (truncated) {
noteParts.push(`仅显示前 ${section.shown_rows}`);
}
}
if (section.kind === "csv") noteParts.push("CSV");
else if (section.kind === "jsonl") noteParts.push("JSONL");
else if (section.kind === "text") noteParts.push("TEXT");
else if (section.kind === "markdown") noteParts.push("MARKDOWN");
return (
<details
open={defaultOpen && !isMissing}
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>
{noteParts.length > 0 ? (
<span className="rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
{noteParts.join(" · ")}
</span>
) : null}
<span className="ml-auto text-[10px] text-muted-foreground transition-transform group-open:rotate-180">
</span>
</summary>
<div className="space-y-2 px-4 py-3">
<div className="flex items-center gap-1 truncate font-mono text-[11px] text-muted-foreground">
<FileIcon className="size-3 shrink-0" />
<span className="truncate">{section.source_path}</span>
</div>
{isMissing ? (
<p className="rounded-md border border-dashed border-muted-foreground/30 bg-muted/30 px-3 py-2 text-muted-foreground text-xs">
{section.error ?? "产物文件不存在或读取失败"}
</p>
) : isMarkdown ? (
<MarkdownView text={section.body ?? ""} />
) : section.rows.length === 0 ? (
<p className="text-muted-foreground text-xs"></p>
) : (
<DataTable columns={section.columns} rows={section.rows} />
)}
</div>
</details>
);
}
function DataTable({
columns,
rows,
}: {
columns: string[];
rows: string[][];
}) {
return (
<div className="max-h-[60vh] overflow-auto rounded-md border">
<table className="w-full border-collapse text-[12px]">
<thead className="sticky top-0 z-10 bg-muted/80 backdrop-blur supports-[backdrop-filter]:bg-muted/60">
<tr>
<th className="border-b border-r px-2 py-1 text-left font-medium text-[10px] text-muted-foreground tracking-wider w-10">
#
</th>
{columns.map((c) => (
<th
key={c}
className="border-b border-r last:border-r-0 px-2 py-1 text-left font-medium whitespace-nowrap"
>
{c}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, rIdx) => (
<tr
key={rIdx}
className="border-b last:border-b-0 hover:bg-muted/20"
>
<td className="border-r px-2 py-1 align-top font-mono text-[10px] text-muted-foreground tabular-nums">
{rIdx + 1}
</td>
{columns.map((c, cIdx) => {
const cell = row[cIdx] ?? "";
return (
<td
key={c}
className="border-r last:border-r-0 px-2 py-1 align-top font-mono text-[11px]"
>
{cell === "" ? (
<span className="text-muted-foreground"></span>
) : (
<TableCell text={cell} />
)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
);
}
function TableCell({ text }: { text: string }) {
if (text.includes("\n")) {
return (
<pre className="whitespace-pre-wrap break-words font-mono text-[11px] leading-snug">
{text}
</pre>
);
}
return <span className="break-words whitespace-pre-wrap">{text}</span>;
}
function RawView({ text }: { text: string }) {
return (
<pre className="overflow-x-auto whitespace-pre-wrap break-all font-mono text-[11px] leading-relaxed">