Remove .DS_Store from version control and update .gitignore. Enhance BatchCaseDetailDrawer and BatchRunCompletedView components to support error handling and improved UI for displaying test case statuses. Add functionality for editing and rerunning test cases. Update TestCasesPage to handle initial case selection. Refactor related components for better maintainability.
This commit is contained in:
@@ -11,40 +11,71 @@ import {
|
||||
Circle,
|
||||
Loader2,
|
||||
MessageSquareText,
|
||||
Pencil,
|
||||
RotateCcw,
|
||||
Target,
|
||||
TriangleAlert,
|
||||
Wrench,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { BatchCaseStatusBadge } from "@/components/batch-test/batch-case-status";
|
||||
import type { BatchRunCase } from "@/data/batch-run";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
firstEvaluationFailureReason,
|
||||
type BatchEvaluationResult,
|
||||
type BatchRunCase,
|
||||
type BatchToolCallRecord,
|
||||
type BatchTurnResult,
|
||||
} from "@/data/batch-run";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type BatchCaseDetailDrawerProps = {
|
||||
item: BatchRunCase;
|
||||
assistantName: string;
|
||||
onClose: () => void;
|
||||
onEdit?: () => void;
|
||||
onRerun?: () => void;
|
||||
};
|
||||
|
||||
export function BatchCaseDetailDrawer({
|
||||
item,
|
||||
assistantName,
|
||||
onClose,
|
||||
onEdit,
|
||||
onRerun,
|
||||
}: BatchCaseDetailDrawerProps) {
|
||||
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const failureReason = firstEvaluationFailureReason(item);
|
||||
|
||||
useEffect(() => {
|
||||
closeButtonRef.current?.focus();
|
||||
}, [item.id]);
|
||||
|
||||
return (
|
||||
<aside className="flex h-full min-w-0 flex-1 flex-col overflow-hidden border-l border-hairline bg-card">
|
||||
<aside
|
||||
id="batch-case-detail"
|
||||
role="complementary"
|
||||
aria-labelledby="batch-case-detail-title"
|
||||
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
|
||||
ref={closeButtonRef}
|
||||
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"
|
||||
className="flex size-10 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">
|
||||
<h2
|
||||
id="batch-case-detail-title"
|
||||
className="truncate text-sm font-medium text-foreground"
|
||||
>
|
||||
{item.name}
|
||||
</h2>
|
||||
<p className="truncate text-xs text-muted-soft">
|
||||
@@ -63,29 +94,86 @@ export function BatchCaseDetailDrawer({
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<DetailSection
|
||||
icon={<Target size={16} />}
|
||||
title="预期结果"
|
||||
value={item.expected}
|
||||
/>
|
||||
|
||||
{(item.status === "waiting" || item.status === "running") && (
|
||||
<ExecutionTimeline status={item.status} />
|
||||
<>
|
||||
<section className="rounded-2xl border border-hairline bg-canvas-soft px-4 py-3.5">
|
||||
<p className="text-xs font-medium text-foreground">执行契约</p>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-soft">
|
||||
{item.turns.length} 轮固定输入 ·{" "}
|
||||
{item.turns.reduce(
|
||||
(sum, turn) => sum + turn.evaluations.length,
|
||||
0,
|
||||
) + item.overallCriteria.length}{" "}
|
||||
项评估标准 · 全部通过才算通过
|
||||
</p>
|
||||
</section>
|
||||
<ExecutionTimeline status={item.status} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{item.status === "error" && item.executionError && (
|
||||
<section>
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-amber-600 dark:text-amber-400">
|
||||
<TriangleAlert size={16} />
|
||||
<h3>执行错误</h3>
|
||||
</div>
|
||||
<div className="mt-2.5 rounded-2xl border border-amber-500/25 bg-amber-500/[0.06] px-4 py-3.5">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<code className="text-xs font-medium text-foreground">
|
||||
{item.executionError.code}
|
||||
</code>
|
||||
<span className="rounded-full border border-amber-500/25 px-2 py-0.5 text-[10px] text-amber-600 dark:text-amber-400">
|
||||
{item.executionError.stage}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
{item.executionError.message}
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-muted-soft">
|
||||
已尝试 {item.attemptCount} 次(包含首次执行),重试后仍未恢复。
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{(item.status === "pass" || item.status === "fail") && (
|
||||
<DetailSection
|
||||
icon={<MessageSquareText size={16} />}
|
||||
title="实际回复"
|
||||
value={item.actual}
|
||||
/>
|
||||
<>
|
||||
<section>
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
||||
<MessageSquareText size={16} />
|
||||
<h3>逐轮执行结果</h3>
|
||||
</div>
|
||||
<div className="mt-3 space-y-3">
|
||||
{item.turns.map((turn) => (
|
||||
<TurnResultCard key={turn.id} turn={turn} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{item.overallCriteria.length > 0 && (
|
||||
<section>
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
||||
<Target size={16} />
|
||||
<h3>整体评估标准</h3>
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
{item.overallCriteria.map((evaluation) => (
|
||||
<EvaluationCard
|
||||
key={evaluation.id}
|
||||
evaluation={evaluation}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{item.status === "fail" && item.failReason && (
|
||||
{item.status === "fail" && failureReason && (
|
||||
<DetailSection
|
||||
icon={<TriangleAlert size={16} />}
|
||||
title="失败原因(LLM 判断)"
|
||||
value={item.failReason}
|
||||
title="失败原因"
|
||||
value={failureReason}
|
||||
tone="destructive"
|
||||
/>
|
||||
)}
|
||||
@@ -97,19 +185,175 @@ export function BatchCaseDetailDrawer({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(onEdit || onRerun) && (
|
||||
<div className="flex shrink-0 items-center justify-end gap-2 border-t border-hairline bg-card px-5 py-3">
|
||||
{onEdit && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="gap-2 rounded-full border-hairline-strong"
|
||||
onClick={onEdit}
|
||||
>
|
||||
<Pencil size={14} />
|
||||
编辑用例
|
||||
</Button>
|
||||
)}
|
||||
{onRerun && (
|
||||
<Button
|
||||
type="button"
|
||||
className="gap-2 rounded-full"
|
||||
onClick={onRerun}
|
||||
>
|
||||
<RotateCcw size={14} />
|
||||
仅重跑此项
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function statusDescription(item: BatchRunCase): string {
|
||||
const descriptions = {
|
||||
waiting: "已加入执行队列,正在等待可用并发。",
|
||||
running: "正在请求助手响应,结果尚未生成。",
|
||||
pass: "执行与结果判断均已完成。",
|
||||
fail: "执行完成,但实际回复未达到预期。",
|
||||
skipped: "本轮运行已结束,该用例未执行。",
|
||||
} as const;
|
||||
return descriptions[item.status];
|
||||
if (item.status === "waiting") {
|
||||
return item.attemptCount > 0
|
||||
? `第 ${item.attemptCount} 次执行发生错误,正在等待重试。`
|
||||
: "已加入执行队列,正在等待可用并发。";
|
||||
}
|
||||
if (item.status === "running") {
|
||||
return item.attemptCount > 1
|
||||
? `正在进行第 ${item.attemptCount} 次尝试。`
|
||||
: "正在请求助手响应,结果尚未生成。";
|
||||
}
|
||||
if (item.status === "pass") return "执行与结果判断均已完成。";
|
||||
if (item.status === "fail") return "执行完成,但实际回复未达到预期。";
|
||||
if (item.status === "error") return "执行未完成,已用尽配置的重试次数。";
|
||||
return "本轮运行已结束,该用例未执行。";
|
||||
}
|
||||
|
||||
function TurnResultCard({ turn }: { turn: BatchTurnResult }) {
|
||||
return (
|
||||
<article className="overflow-hidden rounded-2xl border border-hairline bg-canvas-soft/60">
|
||||
<div className="border-b border-hairline px-4 py-3">
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
第 {turn.index + 1} 轮
|
||||
</p>
|
||||
<div className="mt-2 grid gap-2 text-xs leading-5 sm:grid-cols-[72px_minmax(0,1fr)]">
|
||||
<span className="text-muted-soft">用户输入</span>
|
||||
<p className="whitespace-pre-wrap text-muted-foreground">
|
||||
{turn.userInput}
|
||||
</p>
|
||||
<span className="text-muted-soft">Agent 回复</span>
|
||||
<p className="whitespace-pre-wrap text-muted-foreground">
|
||||
{turn.assistantReply}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{turn.toolCalls.length > 0 && (
|
||||
<div className="space-y-2 border-b border-hairline px-4 py-3">
|
||||
<p className="inline-flex items-center gap-1.5 text-xs font-medium text-foreground">
|
||||
<Wrench size={13} />
|
||||
工具调用记录
|
||||
</p>
|
||||
{turn.toolCalls.map((toolCall) => (
|
||||
<ToolCallCard key={toolCall.id} toolCall={toolCall} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 px-4 py-3">
|
||||
{turn.evaluations.length === 0 ? (
|
||||
<p className="text-xs text-muted-soft">本轮仅推动对话,不执行评估。</p>
|
||||
) : (
|
||||
turn.evaluations.map((evaluation) => (
|
||||
<EvaluationCard key={evaluation.id} evaluation={evaluation} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolCallCard({ toolCall }: { toolCall: BatchToolCallRecord }) {
|
||||
return (
|
||||
<div className="rounded-xl border border-violet-500/20 bg-background/70 p-2.5">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<code className="text-xs text-foreground">{toolCall.functionName}</code>
|
||||
<span className="rounded-full border border-hairline px-2 py-0.5 text-[10px] text-muted-soft">
|
||||
{toolCall.outcome === "success" ? "成功" : "错误"} ·{" "}
|
||||
{toolCall.durationMs} ms
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 grid gap-2 sm:grid-cols-2">
|
||||
<CodeResult label="参数" value={toolCall.argumentsJson} />
|
||||
<CodeResult label="Mock 返回" value={toolCall.resultJson} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CodeResult({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<p className="mb-1 text-[10px] text-muted-soft">{label}</p>
|
||||
<pre className="max-h-36 overflow-auto whitespace-pre-wrap break-all rounded-lg bg-canvas-soft p-2 font-mono text-[10px] leading-4 text-muted-foreground">
|
||||
{value}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EvaluationCard({
|
||||
evaluation,
|
||||
}: {
|
||||
evaluation: BatchEvaluationResult;
|
||||
}) {
|
||||
const passed = evaluation.status === "pass";
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-xl border bg-background/70 p-3",
|
||||
passed ? "border-hairline" : "border-destructive/30 bg-destructive/5",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{passed ? (
|
||||
<CheckCircle2 size={14} className="shrink-0 text-success" />
|
||||
) : (
|
||||
<TriangleAlert size={14} className="shrink-0 text-destructive" />
|
||||
)}
|
||||
<p className="min-w-0 flex-1 text-xs font-medium text-foreground">
|
||||
{evaluation.label}
|
||||
</p>
|
||||
<span
|
||||
className={cn(
|
||||
"text-[10px] font-medium",
|
||||
passed ? "text-success" : "text-destructive",
|
||||
)}
|
||||
>
|
||||
{passed ? "通过" : "失败"}
|
||||
</span>
|
||||
</div>
|
||||
<dl className="mt-2 grid gap-x-2 gap-y-1 text-[11px] leading-4 sm:grid-cols-[52px_minmax(0,1fr)]">
|
||||
<dt className="text-muted-soft">预期</dt>
|
||||
<dd className="whitespace-pre-wrap text-muted-foreground">
|
||||
{evaluation.expected}
|
||||
</dd>
|
||||
<dt className="text-muted-soft">实际</dt>
|
||||
<dd className="whitespace-pre-wrap text-muted-foreground">
|
||||
{evaluation.actual}
|
||||
</dd>
|
||||
{evaluation.reason && (
|
||||
<>
|
||||
<dt className="text-destructive">原因</dt>
|
||||
<dd className="text-foreground">{evaluation.reason}</dd>
|
||||
</>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailSection({
|
||||
|
||||
Reference in New Issue
Block a user