import { useCallback, useEffect, useMemo, useState } from 'react' import { Alert, Card, Empty, Spin, Tag, Timeline } from 'antd' import { CheckCircleOutlined, ClockCircleOutlined, EditOutlined, FileTextOutlined, PlayCircleOutlined, } from '@ant-design/icons' import { intelligentEvalsApi, type DecisionLog, type ExecutionProgress, type IntelligentEval, type IntelligentEvalMessage, type IntelligentEvalSession, type TaskQueueItem, } from '../../api' import { SESSION_STATUS, decisionTypeOf } from './status' import { colors, statusColors } from '../../tokens' import { formatDateTime } from '../../utils/date' import { usePolling } from '../../hooks/usePolling' const ACTIVE_STATUSES = new Set(['planning', 'pending_approval', 'executing']) const STAGE_CONFIG = [ { key: 'planning', title: '规划', icon: EditOutlined }, { key: 'approval', title: '审批', icon: FileTextOutlined }, { key: 'executing', title: '执行', icon: PlayCircleOutlined }, { key: 'done', title: '完成', icon: CheckCircleOutlined }, ] interface ActivityEvent { time: string color: string text: string } 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 '虚拟用户' } function sessionEvents(ev: IntelligentEval): ActivityEvent[] { const events: ActivityEvent[] = [] for (const s of ev.sessions ?? []) { if (s.created_at) { events.push({ time: s.created_at, color: 'blue', text: `创建会话:${personaLabel(s.persona)} · 目标「${s.goal}」`, }) } if (s.closed_at) { const meta = SESSION_STATUS[s.status] ?? { label: s.status, color: 'default' } events.push({ time: s.closed_at, color: s.status === 'completed' ? 'green' : s.status === 'failed' ? 'red' : 'gray', text: `会话结束(${meta.label}):${personaLabel(s.persona)} · ${s.turn_count} 轮`, }) } } return events } function taskEvents(tasks: TaskQueueItem[]): ActivityEvent[] { const events: ActivityEvent[] = [] for (const t of tasks) { if (t.created_at) { events.push({ time: t.created_at, color: 'gray', text: `任务入队:${t.reason}` }) } if (t.assigned_at) { events.push({ time: t.assigned_at, color: 'blue', text: '任务被 worker 认领' }) } if (t.status === 'completed' && t.completed_at) { events.push({ time: t.completed_at, color: 'green', text: '任务完成' }) } if (t.status === 'failed' && t.completed_at) { events.push({ time: t.completed_at, color: 'red', text: `任务失败:${t.error ?? '未知原因'}` }) } } return events } function decisionEvents(logs: DecisionLog[]): ActivityEvent[] { return logs .filter((log) => log.created_at != null) .map((log) => ({ time: log.created_at as string, color: log.decision_type === 'start_analysis' ? 'green' : log.decision_type === 'wait' ? 'gray' : 'blue', text: `决策·${decisionTypeOf(log.decision_type).label}:${log.reason}`, })) } function LifecycleTimeline({ ev, currentStage, abnormal }: { ev: IntelligentEval; currentStage: string; abnormal: boolean }) { const currentIndex = STAGE_CONFIG.findIndex((s) => s.key === currentStage) // Map stages to timestamps const stageTimes: Record = { planning: ev.created_at, approval: ev.started_at, // Approval completed when execution started executing: ev.started_at, done: ev.completed_at, } return (
{STAGE_CONFIG.map((stage, idx) => { const Icon = stage.icon const isCompleted = idx < currentIndex const isCurrent = idx === currentIndex const isError = isCurrent && abnormal const bgColor = isError ? statusColors.failed : isCompleted ? statusColors.completed : isCurrent ? colors.primary : colors.bgSubtle const textColor = isCompleted || isCurrent ? '#fff' : colors.textSecondary const time = stageTimes[stage.key] return (
{isCompleted ? : } {isCurrent && (
当前
)}
{stage.title}
{time && (
{formatDateTime(time)}
)}
{idx < STAGE_CONFIG.length - 1 && (
)}
) })}
) } function TimeDistributionTable({ slots }: { slots: ExecutionProgress['slots'] }) { return (
{slots.map((slot) => { const pct = slot.planned > 0 ? Math.round((slot.completed / slot.planned) * 100) : 0 const isComplete = slot.completed >= slot.planned && slot.planned > 0 const isPast = slot.is_past return ( ) })}
时段 计划 已创建 已完成 进度 状态
{slot.time_slot} {slot.is_current && ( 当前 )} {slot.planned} 0 ? colors.text : colors.textMuted }}> {slot.created} 0 ? statusColors.completed : colors.textMuted }}> {slot.completed}
{pct}%
{isComplete ? ( 已完成 ) : isPast ? ( 未达标 ) : slot.is_current ? ( 进行中 ) : ( 待执行 )}
) } const PREVIEW_MESSAGE_COUNT = 3 const PREVIEW_MAX_CHARS = 80 function truncate(text: string): string { return text.length > PREVIEW_MAX_CHARS ? `${text.slice(0, PREVIEW_MAX_CHARS)}…` : text } const ROLE_LABEL: Record = { user: '虚拟用户', assistant: '数字员工' } function LiveSessionsCard({ stage, runningSessions, messagesBySession, nextAction, }: { stage: string runningSessions: IntelligentEvalSession[] messagesBySession: Record nextAction: string | null }) { let body if (runningSessions.length > 0) { body = (
{runningSessions.map((s) => { const messages = messagesBySession[s.id] ?? [] return (
进行中 {personaLabel(s.persona)} 目标「{s.goal}」 {messages.length} 条消息
{messages.length === 0 ? (
等待首条消息…
) : (
{messages.slice(-PREVIEW_MESSAGE_COUNT).map((m) => (
{ROLE_LABEL[m.role] ?? m.role} {truncate(m.content)}
))}
)}
) })}
) } else if (stage === 'executing') { body = (
当前没有进行中的会话:{nextAction ?? '等待下一步'}
) } else if (stage === 'done') { body =
执行已结束,无进行中的会话
} else { body =
评估尚未开始执行
} return ( {body} ) } export default function ExecutionProcess({ ev }: { ev: IntelligentEval }) { const [progress, setProgress] = useState(null) const [logs, setLogs] = useState([]) const [tasks, setTasks] = useState([]) const [messagesBySession, setMessagesBySession] = useState>({}) const [loading, setLoading] = useState(true) // 依赖稳定的 id 键而非 ev.sessions 数组身份:父组件轮询会不断替换 ev, // 若以数组为依赖,load 会每个轮询周期重建并触发重复全量加载。 const runningKey = (ev.sessions ?? []) .filter((s) => s.status === 'running') .map((s) => s.id) .sort() .join('|') const load = useCallback(async () => { try { const [progressRes, logsRes, tasksRes] = await Promise.all([ intelligentEvalsApi.getExecutionProgress(ev.id), intelligentEvalsApi.listDecisionLogs(ev.id), intelligentEvalsApi.listTasks({ eval_id: ev.id, limit: 100 }), ]) setProgress(progressRes.data) setLogs(logsRes.data.logs) setTasks(tasksRes.data.tasks) const runningIds = runningKey === '' ? [] : runningKey.split('|') if (runningIds.length > 0) { const msgResults = await Promise.all(runningIds.map((sid) => intelligentEvalsApi.listMessages(ev.id, sid))) const next: Record = {} runningIds.forEach((sid, i) => { next[sid] = msgResults[i].data.messages }) setMessagesBySession(next) } else { setMessagesBySession({}) } } finally { setLoading(false) } }, [ev.id, runningKey]) useEffect(() => { setLoading(true) void load() }, [load]) usePolling(() => { void load() }, 5000, ACTIVE_STATUSES.has(ev.status)) const activities = useMemo(() => { const all = [...sessionEvents(ev), ...taskEvents(tasks), ...decisionEvents(logs)] all.sort((a, b) => (a.time < b.time ? 1 : -1)) return all }, [ev, tasks, logs]) if (loading && progress == null) { return
} if (progress == null) { return } const abnormal = progress.abnormal_outcome != null const runningSessions = (ev.sessions ?? []).filter((s) => s.status === 'running') return (
{progress.blocker != null && ( )} {progress.next_action != null && ( )} {progress.slots.length === 0 ? ( ) : ( )} {activities.length === 0 ? ( ) : ( <> ({ color: a.color, children: (
{a.text}
{formatDateTime(a.time)}
), }))} />
{`共 ${activities.length} 条活动,完整决策历史见「决策过程」`}
)} {tasks.some((t) => t.status === 'failed') && (
存在失败任务
)}
) }