import { useState } from 'react' import { Tag, Tooltip } from 'antd' import { CaretDownOutlined, CaretRightOutlined, CheckCircleFilled, CloseCircleFilled, ThunderboltOutlined, } from '@ant-design/icons' import type { CaseState } from '../hooks/useRunSession' import type { CaseSnapshot } from '../api' import GeneratedMessages from './GeneratedMessages' import TurnList from './TurnList' import { colors, statusColors } from '../tokens' import { formatRuleScore, ruleTypeLabel } from '../utils/ruleLabels' interface CaseBlockProps { cs: CaseState snapshot?: CaseSnapshot defaultCollapsed?: boolean } export default function CaseBlock({ cs, snapshot, defaultCollapsed }: CaseBlockProps) { const [collapsed, setCollapsed] = useState(!!defaultCollapsed) const allPassed = cs.ruleResults.length > 0 && cs.ruleResults.every((r) => r.passed) const hasFailed = cs.ruleResults.some((r) => !r.passed) const color = cs.status === 'running' ? statusColors.running : cs.status === 'done' && allPassed ? statusColors.completed : cs.status === 'done' && hasFailed ? statusColors.failed : statusColors.pending const rulePassed = cs.ruleResults.filter((r) => r.passed).length const ruleTotal = cs.ruleResults.length const typeLabel = snapshot?.type === 'dynamic' ? 'AI 动态' : snapshot?.type === 'multi_turn' ? '多轮' : snapshot?.type === 'single' ? '单轮' : cs.isDynamic ? 'AI 动态' : null return (
setCollapsed((v) => !v)} style={{ background: `${color}0d`, padding: '8px 12px', display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', borderBottom: collapsed ? 'none' : `1px solid ${color}22`, }} > {collapsed ? : } {cs.caseId} {typeLabel && ( {typeLabel === 'AI 动态' && } {typeLabel} )} {cs.turns.length} 轮 {ruleTotal > 0 && ( <> {' · '} {rulePassed}/{ruleTotal} 规则通过 )}
{!collapsed && (
{/* Expectations panel */} {snapshot && } {/* AI generated messages */} {cs.generatedMessages && cs.generatedMessages.length > 0 && ( )} {/* Turns + case-level errors */} {/* Rule results footer */} {cs.ruleResults.length > 0 && (
{cs.ruleResults.map((r, i) => ( {r.passed ? '✓' : '✗'} {ruleTypeLabel(r.rule_type)} {r.score != null && {formatRuleScore(r.rule_type, r.score)}} ))}
)}
)}
) } function StatusDot({ status, allPassed, hasFailed }: { status: CaseState['status']; allPassed: boolean; hasFailed: boolean }) { if (status === 'running') { return } if (status === 'done') { if (hasFailed) return if (allPassed) return return } return } function ExpectationsPanel({ snapshot }: { snapshot: CaseSnapshot }) { const e = snapshot.expectations const items: { label: string; value: string }[] = [] if (e.keywords_include?.length) items.push({ label: '需包含', value: e.keywords_include.join('、') }) if (e.keywords_exclude?.length) items.push({ label: '需排除', value: e.keywords_exclude.join('、') }) if (e.response_time_max_ms) items.push({ label: '响应上限', value: `${e.response_time_max_ms} ms` }) if (e.coherence_min_score != null) items.push({ label: '连贯性最低分', value: String(e.coherence_min_score) }) if (e.intent) items.push({ label: '目标意图', value: e.intent }) if (snapshot.type === 'dynamic' && snapshot.prompt) items.push({ label: '生成提示', value: snapshot.prompt }) if (items.length === 0) return null return (
{items.map((it, i) => ( {it.label}:{it.value} ))}
) }