Files
ai-video-fullstack/frontend/src/components/batch-test/batch-case-detail-drawer.tsx
T

199 lines
6.0 KiB
TypeScript
Raw Normal View History

2026-08-08 19:08:48 +08:00
"use client";
/**
* 批量测试用例详情面板。
* 与 prompt mode DebugDrawer overlay 相同:作为右侧 flex 半屏,无遮罩/模糊。
2026-08-08 19:08:48 +08:00
* 当前展示前端 mock 数据,后续可直接替换为真实执行事件与校验结果。
*/
import {
CheckCircle2,
Circle,
Loader2,
MessageSquareText,
Target,
TriangleAlert,
X,
2026-08-08 19:08:48 +08:00
} from "lucide-react";
import { BatchCaseStatusBadge } from "@/components/batch-test/batch-case-status";
import type { BatchRunCase } from "@/data/batch-run";
import { cn } from "@/lib/utils";
type BatchCaseDetailDrawerProps = {
item: BatchRunCase;
2026-08-08 19:08:48 +08:00
assistantName: string;
onClose: () => void;
};
export function BatchCaseDetailDrawer({
item,
assistantName,
onClose,
}: BatchCaseDetailDrawerProps) {
return (
<aside className="flex h-full min-w-0 flex-1 flex-col overflow-hidden border-l border-hairline bg-card">
<div className="flex min-h-14 shrink-0 items-center gap-3 border-b border-hairline px-4 py-3">
<button
type="button"
aria-label="关闭用例详情"
title="关闭"
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-hairline-strong bg-card text-muted-foreground shadow-sm transition-colors hover:text-foreground"
onClick={onClose}
>
<X size={16} />
</button>
<div className="min-w-0 flex-1">
<h2 className="truncate text-sm font-medium text-foreground">
{item.name}
</h2>
<p className="truncate text-xs text-muted-soft">
被测助手:{assistantName} · Mock 结果
</p>
</div>
<BatchCaseStatusBadge status={item.status} />
</div>
2026-08-08 19:08:48 +08:00
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-5">
<div className="space-y-5">
<section className="rounded-2xl border border-hairline bg-canvas-soft px-4 py-3.5">
<p className="text-xs text-muted-soft">当前状态</p>
<p className="mt-1 text-sm text-muted-foreground">
{statusDescription(item)}
</p>
</section>
2026-08-08 19:08:48 +08:00
<DetailSection
icon={<Target size={16} />}
title="预期结果"
value={item.expected}
/>
2026-08-08 19:08:48 +08:00
{(item.status === "waiting" || item.status === "running") && (
<ExecutionTimeline status={item.status} />
)}
2026-08-08 19:08:48 +08:00
{(item.status === "pass" || item.status === "fail") && (
<DetailSection
icon={<MessageSquareText size={16} />}
title="实际回复"
value={item.actual}
/>
)}
2026-08-08 19:08:48 +08:00
{item.status === "fail" && item.failReason && (
<DetailSection
icon={<TriangleAlert size={16} />}
title="失败原因(LLM 判断)"
value={item.failReason}
tone="destructive"
/>
)}
2026-08-08 19:08:48 +08:00
{item.status === "skipped" && (
<div className="rounded-2xl border border-dashed border-hairline-strong px-4 py-4 text-sm leading-6 text-muted-foreground">
该用例未执行,因此没有实际回复与判断结果。
2026-08-08 19:08:48 +08:00
</div>
)}
</div>
</div>
</aside>
2026-08-08 19:08:48 +08:00
);
}
function statusDescription(item: BatchRunCase): string {
const descriptions = {
waiting: "已加入执行队列,正在等待可用并发。",
running: "正在请求助手响应,结果尚未生成。",
pass: "执行与结果判断均已完成。",
fail: "执行完成,但实际回复未达到预期。",
skipped: "本轮运行已结束,该用例未执行。",
} as const;
return descriptions[item.status];
}
function DetailSection({
icon,
title,
value,
tone = "default",
}: {
icon: React.ReactNode;
title: string;
value: string;
tone?: "default" | "destructive";
}) {
return (
<section>
<div
className={cn(
"flex items-center gap-2 text-sm font-medium text-foreground",
tone === "destructive" && "text-destructive",
)}
>
{icon}
<h3>{title}</h3>
</div>
<p
className={cn(
"mt-2.5 rounded-2xl border border-hairline bg-canvas-soft px-4 py-3.5 text-sm leading-6 text-muted-foreground",
tone === "destructive" &&
"border-destructive/20 bg-destructive/5 text-foreground",
)}
>
{value}
</p>
</section>
);
}
function ExecutionTimeline({ status }: { status: "waiting" | "running" }) {
const isRunning = status === "running";
const steps = [
{
label: "载入测试上下文",
detail: isRunning ? "已完成" : "等待开始",
state: isRunning ? "done" : "pending",
},
{
label: "请求助手响应",
detail: isRunning ? "生成中(Mock)" : "尚未开始",
state: isRunning ? "active" : "pending",
},
{
label: "执行结果判断",
detail: "尚未开始",
state: "pending",
},
] as const;
return (
<section>
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
<Loader2 size={16} className={cn(isRunning && "animate-spin")} />
<h3>执行进度</h3>
</div>
<ol className="mt-3 rounded-2xl border border-hairline px-4 py-1">
{steps.map((step) => (
<li
key={step.label}
className="flex items-center gap-3 border-b border-hairline py-3.5 last:border-b-0"
>
{step.state === "done" ? (
<CheckCircle2 size={17} className="shrink-0 text-success" />
) : step.state === "active" ? (
<Loader2 size={17} className="shrink-0 animate-spin text-primary" />
) : (
<Circle size={17} className="shrink-0 text-muted-soft" />
)}
<span className="min-w-0 flex-1 text-sm text-foreground">
{step.label}
</span>
<span className="text-xs text-muted-soft">{step.detail}</span>
</li>
))}
</ol>
</section>
);
}