"use client"; /** 批量测试已完成结果视图。 */ import { ChevronDown, ChevronRight, Settings2 } from "lucide-react"; import { useRef, useState } from "react"; import { BatchCaseDetailDrawer } from "@/components/batch-test/batch-case-detail-drawer"; import { BatchCaseStatusBadge, getBatchCaseStatusLabel, } from "@/components/batch-test/batch-case-status"; import { BATCH_ERROR_STRATEGY_LABEL, BATCH_FAILURE_STRATEGY_LABEL, countByStatus, formatRunDuration, formatRunTime, type BatchRunSnapshot, } from "@/data/batch-run"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; type ResultFilter = "all" | "fail" | "error"; function stopReasonLabel(run: BatchRunSnapshot): string { if (run.stopReason === "manual") return "已手动停止"; if (run.stopReason === "assertion_failure") return "因断言失败停止"; if (run.stopReason === "execution_error") return "因执行错误停止"; return ""; } export function BatchRunCompletedView({ run, onEditCase, onRerunCase, }: { run: BatchRunSnapshot; onEditCase: (caseId: string) => void; onRerunCase: (caseId: string) => void; }) { const [selectedCaseId, setSelectedCaseId] = useState(null); const [filter, setFilter] = useState("all"); const detailRowRefs = useRef(new Map()); const selectedCase = run.cases.find((item) => item.id === selectedCaseId) ?? null; const detailOpen = Boolean(selectedCase); const counts = countByStatus(run.cases); const total = run.cases.length; const outcome = getRunOutcome(run, counts); const visibleCases = filter === "fail" ? run.cases.filter((item) => item.status === "fail") : filter === "error" ? run.cases.filter((item) => item.status === "error") : run.cases; function closeDetail() { const currentId = selectedCaseId; setSelectedCaseId(null); if (!currentId) return; window.requestAnimationFrame(() => { detailRowRefs.current.get(currentId)?.focus(); }); } function handleCaseRowKeyDown( event: React.KeyboardEvent, caseId: string, ) { if (event.key !== "Enter" && event.key !== " ") return; event.preventDefault(); setSelectedCaseId(caseId); } return (
{/* 结果总览 */}

{run.title}

{run.finishedAt && (

完成于 {formatRunTime(run.finishedAt)} {run.stopReason ? ` · ${stopReasonLabel(run)}` : ""}

)}
{outcome.label}

测试结果

{counts.pass} / {total} 通过

{counts.skipped > 0 && ( )}
被测助手
{run.assistantName}
评估模型
{run.config.evaluatorModelResourceName}
{run.config.evaluatorModel && (
{run.config.evaluatorModel}
)}
测试范围
{run.config.suiteCount} 个测试集 · {total} 个用例
运行参数
{run.config.concurrency} 并发 · {run.config.timeoutSecs} 秒超时 · {formatRunDuration(run.startedAt, run.finishedAt)}
查看运行设置

断言失败: {BATCH_FAILURE_STRATEGY_LABEL[run.config.failureStrategy]}

执行错误: 重试 {run.config.errorRetryCount} 次, {BATCH_ERROR_STRATEGY_LABEL[run.config.errorStrategy]}

{/* 用例列表 */}

用例列表

{visibleCases.map((item) => { const index = run.cases.findIndex( (caseItem) => caseItem.id === item.id, ); return ( { if (element) { detailRowRefs.current.set(item.id, element); } else { detailRowRefs.current.delete(item.id); } }} role="button" tabIndex={0} aria-label={`查看用例 ${item.name} 详情,状态:${getBatchCaseStatusLabel(item.status)}`} aria-expanded={selectedCaseId === item.id} aria-controls="batch-case-detail" onClick={() => setSelectedCaseId(item.id)} onKeyDown={(event) => handleCaseRowKeyDown(event, item.id) } className={cn( "cursor-pointer border-b border-hairline transition-colors outline-none last:border-b-0 hover:bg-canvas-soft/70 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/40", selectedCaseId === item.id && "bg-canvas-soft", )} > ); })} {visibleCases.length === 0 && ( )}
# 测试用例 状态 详情
{index + 1} {item.name} 查看
{filter === "error" ? "本次运行没有执行错误。" : "本次运行没有断言失败用例。"}
{selectedCase && ( onEditCase(selectedCase.id)} onRerun={() => onRerunCase(selectedCase.id)} /> )}
); } type BatchCounts = ReturnType; function getRunOutcome(run: BatchRunSnapshot, counts: BatchCounts) { if (run.status === "cancelled") { return { label: "运行已停止", className: "bg-surface-strong text-muted-foreground", }; } if (counts.error > 0) { return { label: "存在执行错误", className: "bg-amber-500/10 text-amber-600 dark:text-amber-400", }; } if (counts.fail > 0) { return { label: "存在断言失败", className: "bg-destructive/10 text-destructive", }; } if (counts.skipped > 0) { return { label: "部分未执行", className: "bg-surface-strong text-muted-foreground", }; } if (counts.pass > 0) { return { label: "全部通过", className: "bg-success/10 text-success", }; } return { label: "暂无结果", className: "bg-surface-strong text-muted-foreground", }; } function ResultCount({ tone, label, count, }: { tone: "success" | "destructive" | "warning" | "muted"; label: string; count: number; }) { const dotClass = { success: "bg-success", destructive: "bg-destructive", warning: "bg-amber-500", muted: "bg-muted-soft", }[tone]; return (
{label} {count}
); } function ResultDistribution({ counts, total, }: { counts: BatchCounts; total: number; }) { const segments = [ { key: "pass", count: counts.pass, className: "bg-success" }, { key: "fail", count: counts.fail, className: "bg-destructive" }, { key: "error", count: counts.error, className: "bg-amber-500" }, { key: "skipped", count: counts.skipped, className: "bg-muted-soft" }, ].filter((item) => item.count > 0); return (
{total > 0 && segments.map((segment) => ( ))}
); }