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}
))}
)
}