212 lines
6.3 KiB
TypeScript
212 lines
6.3 KiB
TypeScript
"use client";
|
||||
|
|
|
|||
|
|
/**
|
|||
|
|
* 批量测试用例详情抽屉。
|
|||
|
|
* 当前展示前端 mock 数据,后续可直接替换为真实执行事件与校验结果。
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import {
|
|||
|
|
CheckCircle2,
|
|||
|
|
Circle,
|
|||
|
|
Loader2,
|
|||
|
|
MessageSquareText,
|
|||
|
|
Target,
|
|||
|
|
TriangleAlert,
|
|||
|
|
} from "lucide-react";
|
|||
|
|
|
|||
|
|
import { BatchCaseStatusBadge } from "@/components/batch-test/batch-case-status";
|
|||
|
|
import {
|
|||
|
|
Sheet,
|
|||
|
|
SheetContent,
|
|||
|
|
SheetDescription,
|
|||
|
|
SheetHeader,
|
|||
|
|
SheetTitle,
|
|||
|
|
} from "@/components/ui/sheet";
|
|||
|
|
import type { BatchRunCase } from "@/data/batch-run";
|
|||
|
|
import { cn } from "@/lib/utils";
|
|||
|
|
|
|||
|
|
type BatchCaseDetailDrawerProps = {
|
|||
|
|
item: BatchRunCase | null;
|
|||
|
|
assistantName: string;
|
|||
|
|
onClose: () => void;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export function BatchCaseDetailDrawer({
|
|||
|
|
item,
|
|||
|
|
assistantName,
|
|||
|
|
onClose,
|
|||
|
|
}: BatchCaseDetailDrawerProps) {
|
|||
|
|
return (
|
|||
|
|
<Sheet
|
|||
|
|
open={Boolean(item)}
|
|||
|
|
onOpenChange={(open) => {
|
|||
|
|
if (!open) onClose();
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<SheetContent
|
|||
|
|
side="right"
|
|||
|
|
className="w-[min(92vw,620px)] border-hairline bg-card p-0 sm:max-w-[620px]"
|
|||
|
|
>
|
|||
|
|
{item && (
|
|||
|
|
<>
|
|||
|
|
<SheetHeader className="border-b border-hairline px-6 py-5 pr-16">
|
|||
|
|
<p className="text-xs font-medium tracking-[0.08em] text-muted-soft uppercase">
|
|||
|
|
用例详情
|
|||
|
|
</p>
|
|||
|
|
<SheetTitle className="text-lg leading-7">
|
|||
|
|
{item.name}
|
|||
|
|
</SheetTitle>
|
|||
|
|
<SheetDescription>
|
|||
|
|
被测助手:{assistantName} · 当前为前端 Mock 结果
|
|||
|
|
</SheetDescription>
|
|||
|
|
</SheetHeader>
|
|||
|
|
|
|||
|
|
<div className="min-h-0 flex-1 overflow-y-auto px-6 py-6">
|
|||
|
|
<div className="space-y-6">
|
|||
|
|
<section className="flex items-center justify-between rounded-2xl border border-hairline bg-canvas-soft px-4 py-3.5">
|
|||
|
|
<div>
|
|||
|
|
<p className="text-xs text-muted-soft">当前状态</p>
|
|||
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|||
|
|
{statusDescription(item)}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
<BatchCaseStatusBadge status={item.status} />
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<DetailSection
|
|||
|
|
icon={<Target size={16} />}
|
|||
|
|
title="预期结果"
|
|||
|
|
value={item.expected}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
{(item.status === "waiting" || item.status === "running") && (
|
|||
|
|
<ExecutionTimeline status={item.status} />
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{(item.status === "pass" || item.status === "fail") && (
|
|||
|
|
<DetailSection
|
|||
|
|
icon={<MessageSquareText size={16} />}
|
|||
|
|
title="实际回复"
|
|||
|
|
value={item.actual}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{item.status === "fail" && item.failReason && (
|
|||
|
|
<DetailSection
|
|||
|
|
icon={<TriangleAlert size={16} />}
|
|||
|
|
title="失败原因(LLM 判断)"
|
|||
|
|
value={item.failReason}
|
|||
|
|
tone="destructive"
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{item.status === "skipped" && (
|
|||
|
|
<div className="rounded-2xl border border-dashed border-hairline-strong px-4 py-4 text-sm leading-6 text-muted-foreground">
|
|||
|
|
该用例未执行,因此没有实际回复与判断结果。
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</SheetContent>
|
|||
|
|
</Sheet>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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>
|
|||
|
|
);
|
|||
|
|
}
|