2026-08-08 19:08:48 +08:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
/**
|
2026-08-09 17:08:53 +08:00
|
|
|
* 批量测试用例详情面板。
|
|
|
|
|
* 与 prompt mode DebugDrawer overlay 相同:作为右侧 flex 半屏,无遮罩/模糊。
|
2026-08-08 19:08:48 +08:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
CheckCircle2,
|
|
|
|
|
Circle,
|
|
|
|
|
Loader2,
|
|
|
|
|
MessageSquareText,
|
2026-08-10 09:33:29 +08:00
|
|
|
Pencil,
|
|
|
|
|
RotateCcw,
|
2026-08-08 19:08:48 +08:00
|
|
|
Target,
|
|
|
|
|
TriangleAlert,
|
2026-08-10 09:33:29 +08:00
|
|
|
Wrench,
|
2026-08-09 17:08:53 +08:00
|
|
|
X,
|
2026-08-08 19:08:48 +08:00
|
|
|
} from "lucide-react";
|
2026-08-10 09:33:29 +08:00
|
|
|
import { useEffect, useRef } from "react";
|
2026-08-08 19:08:48 +08:00
|
|
|
|
|
|
|
|
import { BatchCaseStatusBadge } from "@/components/batch-test/batch-case-status";
|
2026-08-10 09:33:29 +08:00
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import {
|
|
|
|
|
firstEvaluationFailureReason,
|
|
|
|
|
type BatchEvaluationResult,
|
|
|
|
|
type BatchRunCase,
|
|
|
|
|
type BatchToolCallRecord,
|
|
|
|
|
type BatchTurnResult,
|
|
|
|
|
} from "@/data/batch-run";
|
2026-08-08 19:08:48 +08:00
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
|
|
|
|
|
type BatchCaseDetailDrawerProps = {
|
2026-08-09 17:08:53 +08:00
|
|
|
item: BatchRunCase;
|
2026-08-08 19:08:48 +08:00
|
|
|
assistantName: string;
|
|
|
|
|
onClose: () => void;
|
2026-08-10 09:33:29 +08:00
|
|
|
onEdit?: () => void;
|
|
|
|
|
onRerun?: () => void;
|
2026-08-08 19:08:48 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function BatchCaseDetailDrawer({
|
|
|
|
|
item,
|
|
|
|
|
assistantName,
|
|
|
|
|
onClose,
|
2026-08-10 09:33:29 +08:00
|
|
|
onEdit,
|
|
|
|
|
onRerun,
|
2026-08-08 19:08:48 +08:00
|
|
|
}: BatchCaseDetailDrawerProps) {
|
2026-08-10 09:33:29 +08:00
|
|
|
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
|
|
|
|
const failureReason = firstEvaluationFailureReason(item);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
closeButtonRef.current?.focus();
|
|
|
|
|
}, [item.id]);
|
|
|
|
|
|
2026-08-08 19:08:48 +08:00
|
|
|
return (
|
2026-08-10 09:33:29 +08:00
|
|
|
<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"
|
|
|
|
|
>
|
2026-08-09 17:08:53 +08:00
|
|
|
<div className="flex min-h-14 shrink-0 items-center gap-3 border-b border-hairline px-4 py-3">
|
|
|
|
|
<button
|
2026-08-10 09:33:29 +08:00
|
|
|
ref={closeButtonRef}
|
2026-08-09 17:08:53 +08:00
|
|
|
type="button"
|
|
|
|
|
aria-label="关闭用例详情"
|
|
|
|
|
title="关闭"
|
2026-08-10 09:33:29 +08:00
|
|
|
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"
|
2026-08-09 17:08:53 +08:00
|
|
|
onClick={onClose}
|
|
|
|
|
>
|
|
|
|
|
<X size={16} />
|
|
|
|
|
</button>
|
|
|
|
|
<div className="min-w-0 flex-1">
|
2026-08-10 09:33:29 +08:00
|
|
|
<h2
|
|
|
|
|
id="batch-case-detail-title"
|
|
|
|
|
className="truncate text-sm font-medium text-foreground"
|
|
|
|
|
>
|
2026-08-09 17:08:53 +08:00
|
|
|
{item.name}
|
|
|
|
|
</h2>
|
|
|
|
|
<p className="truncate text-xs text-muted-soft">
|
2026-08-10 13:49:24 +08:00
|
|
|
被测助手:{assistantName} · Pipeline 结果
|
2026-08-09 17:08:53 +08:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<BatchCaseStatusBadge status={item.status} />
|
|
|
|
|
</div>
|
2026-08-08 19:08:48 +08:00
|
|
|
|
2026-08-09 17:08:53 +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
|
|
|
|
2026-08-09 17:08:53 +08:00
|
|
|
{(item.status === "waiting" || item.status === "running") && (
|
2026-08-10 09:33:29 +08:00
|
|
|
<>
|
|
|
|
|
<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">
|
2026-08-10 13:49:24 +08:00
|
|
|
正在执行固定文字脚本;完成后将展示每一轮回复、工具调用和全部评估标准。
|
2026-08-10 09:33:29 +08:00
|
|
|
</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>
|
2026-08-09 17:08:53 +08:00
|
|
|
)}
|
2026-08-08 19:08:48 +08:00
|
|
|
|
2026-08-09 17:08:53 +08:00
|
|
|
{(item.status === "pass" || item.status === "fail") && (
|
2026-08-10 09:33:29 +08:00
|
|
|
<>
|
|
|
|
|
<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>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
2026-08-09 17:08:53 +08:00
|
|
|
)}
|
2026-08-08 19:08:48 +08:00
|
|
|
|
2026-08-10 09:33:29 +08:00
|
|
|
{item.status === "fail" && failureReason && (
|
2026-08-09 17:08:53 +08:00
|
|
|
<DetailSection
|
|
|
|
|
icon={<TriangleAlert size={16} />}
|
2026-08-10 09:33:29 +08:00
|
|
|
title="失败原因"
|
|
|
|
|
value={failureReason}
|
2026-08-09 17:08:53 +08:00
|
|
|
tone="destructive"
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2026-08-08 19:08:48 +08:00
|
|
|
|
2026-08-09 17:08:53 +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>
|
2026-08-09 17:08:53 +08:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-08-10 09:33:29 +08:00
|
|
|
|
|
|
|
|
{(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>
|
|
|
|
|
)}
|
2026-08-09 17:08:53 +08:00
|
|
|
</aside>
|
2026-08-08 19:08:48 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function statusDescription(item: BatchRunCase): string {
|
2026-08-10 09:33:29 +08:00
|
|
|
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} />
|
2026-08-10 13:49:24 +08:00
|
|
|
<CodeResult label="工具返回" value={toolCall.resultJson} />
|
2026-08-10 09:33:29 +08:00
|
|
|
</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>
|
|
|
|
|
);
|
2026-08-08 19:08:48 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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: "请求助手响应",
|
2026-08-10 13:49:24 +08:00
|
|
|
detail: isRunning ? "Pipeline 生成中" : "尚未开始",
|
2026-08-08 19:08:48 +08:00
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|