import { useEffect, useMemo, useState } from 'react' import { useSearchParams } from 'react-router-dom' import { Alert, Button, Card, Col, Collapse, Descriptions, Empty, Row, Segmented, Select, Space, Spin, Statistic, Table, Tag, Tooltip, Badge, message, } from 'antd' import { CheckCircleOutlined, CloseCircleOutlined, ClearOutlined, DownloadOutlined, UserOutlined, RobotOutlined, ReloadOutlined, DiffOutlined, FileMarkdownOutlined, } from '@ant-design/icons' import { reportsApi, runsApi, type Run } from '../api' import { colors, triggerColors, triggerLabels } from '../tokens' import { formatDateTime } from '../utils/date' import { useResource } from '../hooks/useResource' interface TurnData { sent_text: string reply_text: string | null latency_ms: number | null } interface RuleResultData { rule_type: string passed: boolean score: number | null reason: string } interface CaseReport { case_id: string connectivity: boolean turns: TurnData[] results: RuleResultData[] } interface Report { run_id: string target_name: string scenario_name: string scenario_version?: number triggered_by?: string status: string started_at: string completed_at: string | null summary: { total_cases: number passed_cases: number failed_cases: number total_rules: number passed_rules: number pass_rate: number connectivity_cases: number judged_pass_rate: number | null } cases: CaseReport[] } interface CompareResult { run_a: { run_id: string; target_name: string; scenario_name: string; scenario_version?: number; triggered_by?: string; status: string; started_at: string; summary: Report['summary'] } run_b: { run_id: string; target_name: string; scenario_name: string; scenario_version?: number; triggered_by?: string; status: string; started_at: string; summary: Report['summary'] } delta: { pass_rate: number; passed_cases: number; passed_rules: number } cases: Array<{ case_id: string connectivity: boolean run_a_passed: boolean | null run_b_passed: boolean | null changed: boolean run_a_results: RuleResultData[] run_b_results: RuleResultData[] }> changed_cases: number } type ViewMode = 'single' | 'compare' export default function ReportsPage() { const [searchParams, setSearchParams] = useSearchParams() const runQuery = searchParams.get('run') ?? '' const [scenarioFilter, setScenarioFilter] = useState('') const [selectedRunId, setSelectedRunId] = useState('') const [compareRunId, setCompareRunId] = useState('') const [report, setReport] = useState(null) const [compareResult, setCompareResult] = useState(null) const [loading, setLoading] = useState(false) const [viewMode, setViewMode] = useState('single') // Keep-alive tabs never remount: refresh whenever this tab is re-activated // so runs triggered elsewhere (e.g. AI assistant) show up. const { data: runsData, reload: reloadRuns } = useResource( () => runsApi.list().then((r) => r.data.filter((run) => run.status === 'completed')), { tabPath: '/reports' }, ) const runs = useMemo(() => runsData ?? [], [runsData]) const loadReport = async (runId: string) => { setSelectedRunId(runId) setCompareResult(null) setLoading(true) try { const res = await reportsApi.get(runId) setReport(res.data as Report) } finally { setLoading(false) } } useEffect(() => { if (runQuery && runQuery !== selectedRunId) { loadReport(runQuery) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [runQuery]) const handleView = (runId: string) => { setSearchParams({ run: runId }) loadReport(runId) // 对比报告要求同场景同版本:A 变更后若 B 不可比则清空 const a = runs.find((r) => r.id === runId) const b = runs.find((r) => r.id === compareRunId) if (a && b && (a.scenario_id !== b.scenario_id || (a.scenario_version ?? 1) !== (b.scenario_version ?? 1))) setCompareRunId('') } const handleCompare = async () => { if (!selectedRunId || !compareRunId) return setLoading(true) try { const res = await reportsApi.compare(selectedRunId, compareRunId) setCompareResult(res.data as CompareResult) } catch (e: any) { const detail = e?.response?.data?.detail if (detail) message.error(detail) } finally { setLoading(false) } } const clearSelection = () => { setSelectedRunId('') setCompareRunId('') setReport(null) setCompareResult(null) setSearchParams({}) } const resetFilters = () => { setScenarioFilter('') clearSelection() } // 场景筛选项:从 run 列表聚合(含各场景 run 数量与出现过的考纲版本) const scenarioOptions = useMemo(() => { const map = new Map }>() for (const r of runs) { const entry = map.get(r.scenario_id) if (entry) { entry.count += 1 entry.versions.add(r.scenario_version ?? 1) } else { map.set(r.scenario_id, { name: r.scenario_name || r.scenario_id.slice(0, 8), count: 1, versions: new Set([r.scenario_version ?? 1]), }) } } return [...map.entries()].map(([id, s]) => { const versions = [...s.versions].sort((a, b) => a - b).map((v) => `v${v}`).join('/') return { value: id, label: `${s.name}(${s.count} · ${versions})` } }) }, [runs]) const filteredRuns = useMemo( () => (scenarioFilter ? runs.filter((r) => r.scenario_id === scenarioFilter) : runs), [runs, scenarioFilter], ) const buildOption = (r: Run) => { const passRate = r.summary?.pass_rate const pct = passRate != null ? `${Math.round(passRate * 100)}%` : '-' const scenario = r.scenario_name || r.scenario_id.slice(0, 8) const version = `v${r.scenario_version ?? 1}` const target = r.target_name || r.target_id.slice(0, 8) const time = r.started_at ? formatDateTime(r.started_at) : '' const trigger = r.triggered_by ?? 'manual' return { value: r.id, searchText: `${scenario} ${version} ${target} ${time} ${r.id} ${triggerLabels[trigger] ?? trigger}`.toLowerCase(), label: ( {scenario} {version} · {target} {time} = 0.8 ? '#3f8600' : '#cf1322', fontSize: 12, fontWeight: 600, }}>{pct} {triggerLabels[trigger] ?? trigger} ), } } const runSelectOptions = filteredRuns.map(buildOption) // 报告 B 只能选与报告 A 同场景同版本(同考纲)的 run const selectedRun = runs.find((r) => r.id === selectedRunId) const compareOptions = runs .filter((r) => r.id !== selectedRunId && selectedRun && r.scenario_id === selectedRun.scenario_id && (r.scenario_version ?? 1) === (selectedRun.scenario_version ?? 1)) .map(buildOption) const optionFilter = (input: string, opt?: { searchText?: string }) => (opt?.searchText ?? '').includes(input.toLowerCase()) return (
{/* 页头 */}

评测报告

查看评测结果详情与统计分析
{/* 选择栏:两行布局,避免对比模式下控件换行错乱 */}
{/* 第一行:视图模式 + 场景筛选 + 重置/刷新 */}
{ setViewMode(v as ViewMode); setCompareResult(null) }} options={[ { label: '单次报告', value: 'single' }, { label: '对比报告', value: 'compare', icon: }, ]} /> (v ? handleView(v) : clearSelection())} allowClear showSearch filterOption={optionFilter} options={runSelectOptions} popupMatchSelectWidth={false} /> {viewMode === 'compare' && ( <> vs