import { useEffect, useState } from 'react' import { Button, Card, Col, Collapse, Empty, Row, Space, Spin, Statistic, Tag, message, } from 'antd' import { ArrowLeftOutlined, DownloadOutlined } 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 [report, setReport] = useState(ev.report) const [reportLoading, setReportLoading] = useState(!ev.report) const [sessions, setSessions] = useState(null) const [sessionsLoading, setSessionsLoading] = useState(true) const [exporting, setExporting] = useState(false) useEffect(() => { let cancelled = false setReport(ev.report) setReportLoading(!ev.report) if (!ev.report) { intelligentEvalsApi.getReport(ev.id) .then((res) => { if (!cancelled) setReport(res.data) }) .catch(() => undefined) .finally(() => { if (!cancelled) setReportLoading(false) }) } setSessionsLoading(true) intelligentEvalsApi.listSessions(ev.id) .then((res) => { if (!cancelled) setSessions(res.data.sessions) }) .catch(() => undefined) .finally(() => { if (!cancelled) setSessionsLoading(false) }) return () => { cancelled = true } }, [ev.id, ev.report]) 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) const scores = Object.entries(report?.scores ?? {}) const overall = scores.length > 0 ? (scores.reduce((sum, [, v]) => sum + Number(v), 0) / scores.length).toFixed(1) : null const sevCounts = findings.reduce((acc, f) => { acc[f.severity] = (acc[f.severity] ?? 0) + 1 return acc }, {} as Record) const scoreData = scores.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, } return (
{ev.name} · 评估报告
{report && ( )}
{reportLoading && } {!reportLoading && !report && ( )} {report && ( <>
高 {sevCounts.high ?? 0} · 中 {sevCounts.medium ?? 0} · 低 {sevCounts.low ?? 0}
{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}
), } })} /> )}
) }