import { useEffect, useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' import { Alert, Button, Empty, message, Progress, Select, Space, Tabs, Tooltip, Typography, } from 'antd' import { FileTextOutlined, PlayCircleOutlined, ReloadOutlined, StopOutlined, } from '@ant-design/icons' import { reportsApi, runsApi, scenariosApi, targetsApi, type Run, type Scenario, type Target } from '../api' import RunList from '../components/RunList' import CaseBlock from '../components/CaseBlock' import RuleOverview from '../components/RuleOverview' import CaseDetail from '../components/CaseDetail' import { useRunSession } from '../hooks/useRunSession' import { useTicker } from '../hooks/useTicker' import { useOnTabActive } from '../hooks/useOnTabActive' import { colors, statusColors, statusLabels } from '../tokens' import { formatDateTime, elapsedStr } from '../utils/date' import { passRateColor } from '../utils/colors' export default function RunsPage() { const navigate = useNavigate() const [runs, setRuns] = useState([]) const [targets, setTargets] = useState([]) const [scenarios, setScenarios] = useState([]) const [loading, setLoading] = useState(true) const [targetId, setTargetId] = useState() const [scenarioId, setScenarioId] = useState() const [starting, setStarting] = useState(false) const [tabKey, setTabKey] = useState(null) const [tabOverrideId, setTabOverrideId] = useState(null) const [focusCaseId, setFocusCaseId] = useState(null) const session = useRunSession() const loadLists = async () => { setLoading(true) try { const [r, t, s] = await Promise.all([ runsApi.list(), targetsApi.list(), scenariosApi.list(), ]) // API 已按 started_at DESC 排序,直接使用,最新的在前 setRuns(r.data) setTargets(t.data) setScenarios(s.data) } finally { setLoading(false) } } useEffect(() => { loadLists() }, []) // Keep-alive tabs never remount: refresh the list whenever the tab is // re-activated so runs triggered elsewhere (e.g. AI assistant) show up. useOnTabActive('/runs', loadLists) useEffect(() => { if (!session.isLive) return const id = window.setInterval(() => { runsApi.list().then((r) => setRuns(r.data)).catch(() => {}) }, 3000) return () => window.clearInterval(id) }, [session.isLive]) useEffect(() => { if (session.completed) { runsApi.list().then((r) => setRuns(r.data)).catch(() => {}) } }, [session.completed]) // 提取为共享函数,handleStart 和 onRerun 共用 const startRun = async (tId: string, sId: string) => { setStarting(true) try { const res = await runsApi.start(tId, sId) await loadLists() session.select(res.data, { live: true }) setTabOverrideId(null) setFocusCaseId(null) } finally { setStarting(false) } } const handleStart = async () => { if (!targetId || !scenarioId) { message.warning('请选择评测对象和场景') return } await startRun(targetId, scenarioId) message.success('评测已启动') } const handleSelect = (r: Run) => { session.select(r) setTabOverrideId(null) setFocusCaseId(null) } const handleOpenReport = (runId: string) => { navigate(`/reports?run=${runId}`) } return (
{/* 页头 */}

评测执行

启动评测任务并查看实时执行详情
{/* 主体 grid — 贴边铺满 */}
{/* LEFT: launcher + list */}
{/* 启动区:两行布局 */}
(opt?.label as string ?? '').toLowerCase().includes(input.toLowerCase())} placeholder="选择评测场景" options={scenarios.map((s) => ({ label: `${s.name}(v${s.version ?? 1})`, value: s.id }))} style={{ flex: 1 }} size="middle" />
{/* RIGHT: detail workspace */}
{ setTabKey(k) setTabOverrideId(session.run?.id ?? null) }} focusCaseId={focusCaseId} onFocusCase={(id) => { setFocusCaseId(id); setTabKey('detail'); setTabOverrideId(session.run?.id ?? null) }} onCancel={async () => { try { await session.cancel() message.success('已发送停止请求') } catch { message.error('停止失败') } }} onOpenReport={handleOpenReport} onExportJson={(runId) => reportsApi.download(runId, 'json')} onRerun={async () => { if (!session.run) return await startRun(session.run.target_id, session.run.scenario_id) message.success('已重新启动') }} />
) } // ─── Detail Workspace ───────────────────────────────────────────────────────── interface DetailWorkspaceProps { run: Run | null cases: ReturnType['cases'] progress: ReturnType['progress'] snapshots: ReturnType['scenarioSnapshot'] isLive: boolean finalStatus: ReturnType['finalStatus'] errorInfo: ReturnType['errorInfo'] tabKey: string | null tabOverrideId: string | null onTabChange: (k: string) => void focusCaseId: string | null onFocusCase: (id: string) => void onCancel: () => void onOpenReport: (runId: string) => void onExportJson: (runId: string) => void onRerun: () => void } function DetailWorkspace({ run, cases, progress, snapshots, isLive, finalStatus, errorInfo, tabKey, tabOverrideId, onTabChange, focusCaseId, onFocusCase, onCancel, onOpenReport, onExportJson, onRerun, }: DetailWorkspaceProps) { useTicker(isLive) const status = run?.status ?? 'pending' const autoTab = useMemo(() => { if (isLive) return 'log' if (status === 'completed') return 'overview' if (status === 'failed') return 'detail' return 'log' }, [isLive, status]) if (!run) { return (
选择左侧记录或启动新评测} />
) } const summary = (run.summary || {}) as Record const passRate = summary.pass_rate as number | undefined const totalCases = summary.total_cases as number | undefined const passedCases = summary.passed_cases as number | undefined const totalRules = summary.total_rules as number | undefined const passedRules = summary.passed_rules as number | undefined const activeKey = tabOverrideId === run.id && tabKey ? tabKey : autoTab const finalAlert = (() => { if (finalStatus === 'completed') return if (finalStatus === 'cancelled') return if (finalStatus === 'failed') return return null })() return ( <> {/* KPI bar */}
{run.id.slice(0, 8)}… {isLive ? ( ) : ( {status === 'completed' && ( <> )} )}
{progress && progress.total > 0 && (
进度 {progress.done}/{progress.total}{progress.currentIndex ? ` · 第 ${progress.currentIndex} 个` : ''}
)} {finalAlert &&
{finalAlert}
}
{/* Tab 导航栏(只渲染头部,内容区由下方 flex 面板独立控制) */}
0 ? ` (${cases.length})` : ''}` }, { key: 'overview', label: '规则总览' }, { key: 'detail', label: '用例明细' }, ]} />
{/* 实时对话 — 独立 flex 子元素,直接 overflowY: auto */}
{cases.length === 0 ? ( ) : ( cases.map((c) => ( )) )}
{/* 规则总览 */}
onFocusCase(id)} />
{/* 用例明细 — CaseDetail 内部两栏各自独立滚动 */}
) } function StatusBadge({ status, finalStatus }: { status: string; finalStatus: ReturnType['finalStatus'] }) { const effective = finalStatus ?? status const color = statusColors[effective] ?? colors.textMuted const label = statusLabels[effective] ?? effective const isRunning = effective === 'running' || effective === 'pending' return (
{isRunning ? : } {label}
) } function KpiItem({ label, value, subtitle, valueColor }: { label: string value: string subtitle?: string valueColor?: string }) { return (
{label}
{value}
{subtitle &&
{subtitle}
}
) }