修改
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user