import { useEffect, useState } from 'react' import { Button, Card, Col, Collapse, Empty, Row, Space, Spin, Tag, message, } from 'antd' import { ArrowLeftOutlined, BulbOutlined, DownloadOutlined, MessageOutlined, StarOutlined, WarningOutlined, } from '@ant-design/icons' import { Bar } from '@ant-design/charts' import ChatBubble from '../ChatBubble' import { intelligentEvalsApi, type IntelligentEval, type IntelligentEvalMessage, type IntelligentEvalReport, type IntelligentEvalSession, type ReportEvidence, } from '../../api' import { colors } from '../../tokens' import { shortDateTime } from '../../utils/date' import { SESSION_STATUS, severityOf } from './status' const sectionCard: React.CSSProperties = { marginBottom: 16 } function personaLabel(persona: Record): string { if (typeof persona.name === 'string' && persona.name) return persona.name if (typeof persona.background === 'string' && persona.background) return persona.background return persona.id != null ? String(persona.id) : '虚拟用户' } function EvidenceBlock({ evidence }: { evidence: ReportEvidence }) { return (
{evidence.session_id && (
会话 {evidence.session_id.slice(0, 8)} {evidence.turn_index != null ? ` · 第 ${evidence.turn_index + 1} 轮` : ''}
)} {evidence.user_said &&
用户:{evidence.user_said}
} {evidence.assistant_replied &&
回复:{evidence.assistant_replied}
}
) } function VerdictView({ verdict }: { verdict: Record }) { const entries = Object.entries(verdict) if (entries.length === 0) return null return (
会话结论
{entries.map(([k, v]) => (
{k}: {typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v)}
))}
) } function SessionMessages({ evalId, sessionId, verdict }: { evalId: string sessionId: string verdict: Record | null }) { const [messages, setMessages] = useState(null) useEffect(() => { let cancelled = false intelligentEvalsApi.listMessages(evalId, sessionId) .then((res) => { if (!cancelled) setMessages(res.data.messages) }) .catch(() => undefined) // 拦截器已弹错;收起再展开可重试 return () => { cancelled = true } }, [evalId, sessionId]) return (
{!messages && } {messages && messages.length === 0 && ( )} {messages?.map((m) => ( {m.latency_ms}ms ) : undefined} /> ))} {verdict && }
) } interface EvalReportProps { ev: IntelligentEval onBack: () => void } export default function EvalReport({ ev, onBack }: EvalReportProps) { const [exporting, setExporting] = useState(false) const report: IntelligentEvalReport | null = ev.report const reportLoading = false const sessions: IntelligentEvalSession[] = ev.sessions ?? [] const sessionsLoading = false const exportMarkdown = async () => { setExporting(true) try { await intelligentEvalsApi.downloadReportMarkdown(ev.id) message.success('已导出 Markdown 报告') } finally { setExporting(false) } } const findings = [...(report?.findings ?? [])] .sort((a, b) => severityOf(a.severity).order - severityOf(b.severity).order) // report.scores 是嵌套结构:{ overall: number, dimensions: { 维度: 分 } } // 只把 dimensions 的值当作数字维度,overall 单独取,避免 Number(对象) 产生 NaN。 const rawScores = report?.scores ?? {} const dimScores = Object.entries(rawScores.dimensions ?? {}).filter( ([, v]) => typeof v === 'number', ) const rawOverall = rawScores.overall const overall = typeof rawOverall === 'number' ? rawOverall.toFixed(1) : dimScores.length > 0 ? (dimScores.reduce((sum, [, v]) => sum + Number(v), 0) / dimScores.length).toFixed(1) : null const overallNum = overall != null ? Number(overall) : null const overallColor = overallNum == null ? colors.textMuted : overallNum >= 0.8 ? '#52c41a' : overallNum >= 0.6 ? colors.warning : '#ff4d4f' const sevCounts = findings.reduce((acc, f) => { acc[f.severity] = (acc[f.severity] ?? 0) + 1 return acc }, {} as Record) const scoreData = dimScores.map(([dimension, score]) => ({ dimension, score: Number(score) })) const scoreConfig = { data: scoreData, xField: 'dimension', yField: 'score', coordinate: { transform: [{ type: 'transpose' }] }, height: Math.max(160, scoreData.length * 44), color: colors.primary, } const summaryCard: React.CSSProperties = { display: 'flex', alignItems: 'center', gap: 10, border: `1px solid ${colors.border}`, borderRadius: 10, padding: '14px 16px', background: colors.bgContainer, boxShadow: '0 1px 3px rgba(0,0,0,0.06)', height: '100%', } const summaryIcon = (color: string): React.CSSProperties => ({ width: 42, height: 42, borderRadius: 10, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', background: `${color}1a`, color, fontSize: 20, }) return (
{ev.name} · 评估报告
{report && ( )}
{reportLoading && } {!reportLoading && !report && ( )} {report && ( <>
综合评分
{overall ?? '—'}
问题发现
{findings.length}
高 {sevCounts.high ?? 0} · 中 {sevCounts.medium ?? 0} · 低 {sevCounts.low ?? 0}
亮点
{report.highlights.length}
会话数
{ev.session_count}
{scoreData.length > 0 && ( )}
{report.summary}
{findings.length === 0 && (
未发现问题
)} { const sev = severityOf(f.severity) return { key: i, label: ( {sev.label} {f.dimension} {f.issue} ), children: (
{f.evidence.length > 0 && (
证据({f.evidence.length} 条)
{f.evidence.map((e, j) => )}
)} {f.suggestion && (
建议:{f.suggestion}
)} {f.related_sop && (
关联 SOP:{f.related_sop}
)}
), } })} />
{report.highlights.length > 0 && ( {report.highlights.map((h, i) => (
{h.dimension && {h.dimension}} {h.dimension ? ' ' : ''}{h.description}
))}
)} {report.priority_recommendations.length > 0 && ( {report.priority_recommendations.map((r, i) => (
{i + 1} {r}
))}
)} )} {sessionsLoading && } {!sessionsLoading && sessions && sessions.length === 0 && ( )} {!sessionsLoading && sessions && sessions.length > 0 && ( { const meta = SESSION_STATUS[s.status] ?? SESSION_STATUS.running return { key: s.id, label: ( {personaLabel(s.persona)} {meta.label} {s.turn_count} 轮 {s.dimension && {s.dimension}} {s.created_at && ( {shortDateTime(s.created_at)} )} ), children: (
目标:{s.goal}
), } })} /> )}
) }