diff --git a/src/frontend/src/components/TaskModal.tsx b/src/frontend/src/components/TaskModal.tsx index 32ff12e..262f741 100644 --- a/src/frontend/src/components/TaskModal.tsx +++ b/src/frontend/src/components/TaskModal.tsx @@ -1,1188 +1,446 @@ -import { useEffect, useState, useRef, useCallback } from 'react'; -import { useStore, getPipeStatus, deptColor, stateLabel, STATE_LABEL } from '../store'; -import { api } from '../api'; -import ConfirmDialog from './ConfirmDialog'; -import ArtifactPanel from './ArtifactPanel'; -import CheckpointPanel from './CheckpointPanel'; -import { formatDashboardDateTime, formatDashboardTime } from '../time'; -import type { - Task, - TaskActivityData, - SchedulerStateData, - ActivityEntry, - TodoItem, - PhaseDuration, - TaskNode, -} from '../api'; +/** + * TaskModal v2.0 — 任务详情面板 + * 8区域:基本信息 + 状态流转 + 产出 + 评审 + 事件时间线 + 评论/决策 + 经验 + Checkpoint占位 + * Mock 数据用于 UI 预览 + */ +import { useState } from 'react'; +import { useStore } from '../store'; +import type { V2Task } from './EdictBoard'; -const NODE_STATUS_COLORS: Record = { - pending: '#6b7280', - assigned: '#6b7280', - working: '#6a9eff', - done: '#2ecc8a', - failed: '#ff5270', - blocked: '#f59e0b', - reviewing: '#818cf8', - cancelled: '#9ca3af', - waiting_human: '#f59e0b', +// ── Mock 关联数据 ── +const MOCK_COMMENTS = [ + { id: 1, author: 'pangtong-fujunshi', content: '策略逻辑确认:使用OBV作为成交量指标,5日均线作为动量信号。回测周期2023-01至2025-12,初始资金100万。', comment_type: 'handoff', created_at: '2026-05-17T08:32:00' }, + { id: 2, author: 'zhangfei-dev', content: '收到,开始编写回测脚本。预计2小时完成。', comment_type: 'progress', created_at: '2026-05-17T08:36:00' }, + { id: 3, author: 'simayi-challenger', content: '建议增加最大回撤约束(≤15%),避免极端行情下的过度亏损。', comment_type: 'review', created_at: '2026-05-17T10:15:00' }, +]; + +const MOCK_OUTPUTS = [ + { id: 1, agent: 'zhangfei-dev', content: '回测报告v1: 年化收益18.7%, 最大回撤12.3%, 夏普比1.52', content_type: 'report', attempt: 1, created_at: '2026-05-17T11:00:00' }, +]; + +const MOCK_REVIEWS = [ + { id: 'rev-001', reviewer: 'simayi-challenger', verdict: 'APPROVE', confidence: 0.85, risk_level: 'standard', summary: '代码结构清晰,逻辑正确。建议优化参数搜索范围。', rebuttal_status: 'none', debate_round: 0, created_at: '2026-05-17T11:10:00' }, +]; + +const MOCK_EVENTS = [ + { id: 1, event_type: 'task_created', agent: 'pangtong-fujunshi', detail: '创建任务:动量因子策略回测', created_at: '2026-05-17T08:30:00' }, + { id: 2, event_type: 'task_claimed', agent: 'zhangfei-dev', detail: '张飞认领任务', created_at: '2026-05-17T08:35:00' }, + { id: 3, event_type: 'task_started', agent: 'zhangfei-dev', detail: '开始执行', created_at: '2026-05-17T08:36:00' }, + { id: 4, event_type: 'comment_added', agent: 'pangtong-fujunshi', detail: '补充策略参数说明', created_at: '2026-05-17T08:32:00' }, + { id: 5, event_type: 'output_written', agent: 'zhangfei-dev', detail: '提交回测报告v1', created_at: '2026-05-17T11:00:00' }, + { id: 6, event_type: 'review_submitted', agent: 'simayi-challenger', detail: '提交审查意见:APPROVE', created_at: '2026-05-17T11:10:00' }, +]; + +const MOCK_DECISIONS = [ + { id: 1, decider: 'pangtong-fujunshi', decision_type: 'strategy', rationale: '采用OBV+5日均线组合,相比MACD减少滞后性', created_at: '2026-05-17T08:31:00' }, +]; + +const MOCK_EXPERIENCES = [ + { id: 'exp-001', pattern_type: 'best_practice', title: 'OBV动量策略回测经验', summary: 'OBV在A股市场对中盘股(市值50-200亿)信号更准确,大盘股噪声较多', tags: ['backtest', 'momentum', 'OBV'], created_at: '2026-05-17T11:15:00' }, +]; + +// ── 常量 ── +const STATUS_META: Record = { + pending: { color: '#7a9aff', label: '待认领' }, + claimed: { color: '#a07aff', label: '已认领' }, + working: { color: '#6a9eff', label: '执行中' }, + done: { color: '#2ecc8a', label: '已完成' }, + failed: { color: '#ff5270', label: '失败' }, }; -const NODE_STATUS_ICONS: Record = { - pending: '⬜', - assigned: '👤', - working: '🔄', - done: '✅', - failed: '❌', - blocked: '🚧', - reviewing: '🔍', - cancelled: '🚫', - waiting_human: '🛐', +const VALID_TRANSITIONS: Record = { + pending: ['cancelled'], + claimed: ['working', 'cancelled'], + working: ['done', 'failed', 'cancelled'], + failed: ['pending', 'cancelled'], }; -const NODE_STATUS_LABELS: Record = { - pending: '待开始', - assigned: '已分配', - working: '执行中', - done: '已完成', - failed: '失败', - blocked: '阻塞', - reviewing: '审查中', - cancelled: '已取消', - waiting_human: '等待御批', +const AGENT_EMOJI: Record = { + 'pangtong-fujunshi': '🐦', 'simayi-challenger': '🦅', 'jiangwei-infra': '🔧', + 'guanyu-dev': '⚔️', 'zhangfei-dev': '💪', 'zhaoyun-data': '📊', }; -const AGENT_LABELS: Record = { - main: '调度', - zhongshu: '中书省', - menxia: '门下省', - shangshu: '尚书省', - libu: '礼部', - hubu: '户部', - bingbu: '兵部', - xingbu: '刑部', - gongbu: '工部', - libu_hr: '吏部', - zaochao: '钦天监', +const COMMENT_TYPE_LABEL: Record = { + handoff: { icon: '🤝', label: '交接', color: '#6a9eff' }, + progress: { icon: '🔄', label: '进展', color: '#2ecc8a' }, + review: { icon: '🔍', label: '审查', color: '#f59e0b' }, + rebuttal: { icon: '⚔️', label: '反驳', color: '#818cf8' }, + debate: { icon: '🏛️', label: '辩论', color: '#ec4899' }, + observation: { icon: '👁️', label: '观察', color: '#6b7280' }, }; -const NEXT_LABELS: Record = { - Taizi: '中书省起草', - Zhongshu: '门下省审议', - Menxia: '尚书省派发', - Assigned: '开始执行', - Doing: '进入审查', - Review: '完成', +const EVENT_TYPE_ICON: Record = { + task_created: '📋', task_claimed: '👤', task_started: '⚔️', task_completed: '✅', + task_failed: '❌', comment_added: '💬', output_written: '📦', review_submitted: '🔍', + review_approved: '✅', review_rejected: '❌', observation_added: '👁️', decision_made: '🧭', }; -function fmtStalled(sec: number): string { - const v = Math.max(0, sec); - if (v < 60) return `${v}秒`; - if (v < 3600) return `${Math.floor(v / 60)}分${v % 60}秒`; - const h = Math.floor(v / 3600); - const m = Math.floor((v % 3600) / 60); - return `${h}小时${m}分`; +function fmtTime(iso: string): string { + const d = new Date(iso); + return `${d.getMonth()+1}/${d.getDate()} ${d.getHours()}:${String(d.getMinutes()).padStart(2,'0')}`; } -function fmtActivityTime(ts: number | string | undefined): string { - return formatDashboardTime(ts, { showSeconds: true }); +function fmtTimeFull(iso: string): string { + const d = new Date(iso); + return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${d.getHours()}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}`; } -export default function TaskModal() { - const modalTaskId = useStore((s) => s.modalTaskId); - const setModalTaskId = useStore((s) => s.setModalTaskId); - const liveStatus = useStore((s) => s.liveStatus); - const loadAll = useStore((s) => s.loadAll); - const toast = useStore((s) => s.toast); +// ── 区域组件 ── - const [activityData, setActivityData] = useState(null); - const [schedData, setSchedData] = useState(null); - const laTimerRef = useRef | null>(null); - const logRef = useRef(null); - const [steerMsg, setSteerMsg] = useState(''); - const [confirmDlg, setConfirmDlg] = useState<{ - title: string; message: string; okLabel: string; okClass?: string; - onOk: (reason: string) => void; - } | null>(null); - const [expandedNode, setExpandedNode] = useState(null); - // M3: Tab + Checkpoint - const [activeTab, setActiveTab] = useState<'activity' | 'artifacts'>('activity'); - const [humanInputData, setHumanInputData] = useState<{ ok: boolean; active: boolean; checkpoints: any[] } | null>(null); +function SectionLabel({ icon, title, count }: { icon: string; title: string; count?: number }) { + return ( +
+ {icon}{title} + {count !== undefined && ({count})} +
+ ); +} - const task = liveStatus?.tasks?.find((t) => t.id === modalTaskId) || null; - const isDone = task ? ['completed', 'Done', 'Cancelled', 'cancelled'].includes(task.state || task.status || '') : false; - - const fetchActivity = useCallback(async () => { - if (!modalTaskId) return; - try { - const d = await api.taskActivity(modalTaskId); - setActivityData(d); - } catch { - setActivityData(null); - } - }, [modalTaskId]); - - const fetchSched = useCallback(async () => { - if (!modalTaskId) return; - try { - const d = await api.schedulerState(modalTaskId); - setSchedData(d); - } catch { - setSchedData(null); - } - }, [modalTaskId]); - - useEffect(() => { - if (!modalTaskId || !task) return; - fetchActivity(); - fetchSched(); - - // M3: Fetch human input status - api.humanInput(modalTaskId).then(setHumanInputData).catch(() => {}); - - const isDone = ['Done', 'Cancelled'].includes(task.state); - if (!isDone) { - laTimerRef.current = setInterval(() => { - fetchActivity(); - fetchSched(); - api.humanInput(modalTaskId!).then(setHumanInputData).catch(() => {}); - }, 4000); - } - - return () => { - if (laTimerRef.current) { - clearInterval(laTimerRef.current); - laTimerRef.current = null; - } - }; - }, [modalTaskId, task?.state, fetchActivity, fetchSched]); - - // scroll log on new entries - useEffect(() => { - if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; - }, [activityData?.activity?.length]); - - if (!modalTaskId || !task) return null; - - const close = () => setModalTaskId(null); - - const stages = getPipeStatus(task); - const activeStage = stages.find((s) => s.status === 'active'); - const hb = task.status === 'completed' - ? { status: 'completed' as const, label: '✅ 任务已完成' } - : (task.heartbeat || { status: 'unknown' as const, label: '⚪ 无数据' }); - const flowLog = task.flow_log || []; - const todos = task.todos || []; - const todoDone = todos.filter((x) => x.status === 'completed').length; - const todoTotal = todos.length; - // moziplus state helpers - const planStatus = task.plan_status; - const taskState = task.status || task.state; - // 前端本地跟 ACTION_GUARDS 同样的映射表 - const ACTION_GUARDS_MODAL: Record = { - pause: ['planning', 'executing'], - resume: ['paused', 'cancelled'], - cancel: ['created', 'planning', 'executing', 'paused', 'escalated', 'failed'], - retry: ['failed'], - escalate: ['executing', 'failed'], - rollback: ['escalated'], - steer: ['executing'], - }; - const isAllowed = (action: string) => { - if (taskState === 'cancelling' || taskState === 'pausing') return false; // BUG-6: 中间态禁止操作 - const allowed = ACTION_GUARDS_MODAL[action] || []; - return allowed.includes(taskState); - }; - const canPause = isAllowed('pause'); - const canResume = isAllowed('resume'); - const canCancel = isAllowed('cancel'); - const isExecuting = taskState === 'executing'; - const isPaused = taskState === 'paused'; - const isAwaitingApproval = taskState === 'planning' && planStatus === 'approved'; - - const doTaskAction = async (action: string, reason: string) => { - try { - const r = await api.taskAction(task.id, action, reason); - if (r.ok) { - toast(r.message || '操作成功', 'ok'); - loadAll(); - close(); - } else { - toast(r.error || '操作失败', 'err'); - } - } catch { - toast('服务器连接失败', 'err'); - } +function StatusButtons({ status }: { status: string }) { + const transitions = VALID_TRANSITIONS[status] || []; + const btnMap: Record = { + working: { label: '开始执行', icon: '⚔️', bg: '#2ecc8a22', color: '#2ecc8a', border: '#2ecc8a44' }, + done: { label: '标记完成', icon: '✅', bg: '#2ecc8a22', color: '#2ecc8a', border: '#2ecc8a44' }, + failed: { label: '标记失败', icon: '❌', bg: '#ff527022', color: '#ff5270', border: '#ff527044' }, + pending: { label: '重置待认领', icon: '🔄', bg: '#6a9eff22', color: '#6a9eff', border: '#6a9eff44' }, + cancelled: { label: '取消任务', icon: '🚫', bg: '#6b728022', color: '#6b7280', border: '#6b728044' }, }; - const doReview = async (action: string) => { - const labels: Record = { approve: '准奏', reject: '封驳' }; - const comment = prompt(`${labels[action]} ${task.id}\n\n请输入批注(可留空):`); - if (comment === null) return; - try { - const r = await api.reviewAction(task.id, action, comment || ''); - if (r.ok) { - toast(`✅ ${task.id} 已${labels[action]}`, 'ok'); - loadAll(); - close(); - } else { - toast(r.error || '操作失败', 'err'); - } - } catch { - toast('服务器连接失败', 'err'); - } - }; - - const doAdvance = async () => { - const next = NEXT_LABELS[task.state] || '下一步'; - const comment = prompt(`⏩ 手动推进 ${task.id}\n当前: ${task.state} → 下一步: ${next}\n\n请输入说明(可留空):`); - if (comment === null) return; - try { - const r = await api.advanceState(task.id, comment || ''); - if (r.ok) { - toast(`⏩ ${r.message}`, 'ok'); - loadAll(); - close(); - } else { - toast(r.error || '推进失败', 'err'); - } - } catch { - toast('服务器连接失败', 'err'); - } - }; - - const doSchedAction = async (action: string) => { - if (action === 'scan') { - try { - const r = await api.schedulerScan(180); - if (r.ok) toast(`🔍 扫描完成:${r.count || 0} 个动作`, 'ok'); - else toast(r.error || '扫描失败', 'err'); - fetchSched(); - } catch { - toast('服务器连接失败', 'err'); - } - return; - } - const labels: Record = { retry: '重试', escalate: '升级', rollback: '回滚' }; - const reason = prompt(`请输入${labels[action]}原因(可留空):`); - if (reason === null) return; - const handlers: Record Promise<{ ok: boolean; message?: string; error?: string }>> = { - retry: api.schedulerRetry, - escalate: api.schedulerEscalate, - rollback: api.schedulerRollback, - }; - try { - const r = await handlers[action](task.id, reason); - if (r.ok) toast(r.message || '操作成功', 'ok'); - else toast(r.error || '操作失败', 'err'); - fetchSched(); - loadAll(); - } catch { - toast('服务器连接失败', 'err'); - } - }; - - const handleStop = () => { - const reason = prompt('请输入暂停原因(可留空):'); - if (reason === null) return; - doTaskAction('stop', reason); - }; - - // Scheduler state - const sched = schedData?.scheduler; - const stalledSec = schedData?.stalledSec || 0; + if (transitions.length === 0) return null; return ( -
-
e.stopPropagation()}> - -
-
{task.id}
-
{task.title || '(无标题)'}
+
+ {transitions.map(t => { + const b = btnMap[t]; + if (!b) return null; + return ( + + ); + })} +
+ ); +} - {/* Current Stage Banner */} - {activeStage && ( -
-
{activeStage.icon}
-
-
{activeStage.dept}
-
当前阶段:{activeStage.action}
-
- {hb.label} +function EventTimeline({ events }: { events: typeof MOCK_EVENTS }) { + return ( +
+ {events.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()).map((ev, i) => ( +
+ {/* 时间线 */} +
+ {EVENT_TYPE_ICON[ev.event_type] || '📌'} + {i < events.length - 1 &&
} +
+
+
+ {ev.detail} + {fmtTime(ev.created_at)}
- )} + {ev.agent && {AGENT_EMOJI[ev.agent] || ''} {ev.agent}} +
+
+ ))} +
+ ); +} - {/* 七步管线大图 */} -
-
- {stages.map((s, i) => ( -
-
- {(s.status === 'done' || task.status === 'completed') && } - {s.icon} - {s.status === 'active' && task.status !== 'completed' && ( - - )} +// ── 主组件 ── +export default function TaskModal() { + const modalTaskId = useStore(s => s.modalTaskId); + const setModalTaskId = useStore(s => s.setModalTaskId); + const [activeTab, setActiveTab] = useState<'overview' | 'outputs' | 'reviews' | 'experience'>('overview'); + + if (!modalTaskId) return null; + + // 用 mock 数据模拟获取任务 + const task: V2Task = { + id: modalTaskId, + title: '动量因子策略回测', + description: '基于成交量的日线动量策略回测。使用OBV指标+5日均线作为买卖信号,回测周期2023-01至2025-12,初始资金100万。持仓周期3-10天波段交易,标的选择沪深300成分股中市值50-200亿的股票。', + status: 'working', + assignee: 'zhangfei-dev', + assigned_by: 'pangtong-fujunshi', + priority: 'high', + task_type: 'backtest', + created_at: '2026-05-17T08:30:00', + updated_at: '2026-05-17T11:20:00', + claimed_at: '2026-05-17T08:35:00', + completed_at: null, + started_at: '2026-05-17T08:36:00', + retry_count: 0, + max_retries: 3, + risk_level: 'standard', + escalated: false, + deadline: '2026-05-17T18:00:00', + comments_count: 3, + outputs_count: 1, + review_status: 'pending', + latest_event: '回测脚本运行中,已完成70%数据回测', + project_id: 'demo', + }; + + const close = () => setModalTaskId(null); + const sm = STATUS_META[task.status]; + + const tabs = [ + { key: 'overview' as const, label: '📋 总览', }, + { key: 'outputs' as const, label: '📦 产出', badge: MOCK_OUTPUTS.length }, + { key: 'reviews' as const, label: '🔍 审查', badge: MOCK_REVIEWS.length }, + { key: 'experience' as const, label: '🧠 经验', badge: MOCK_EXPERIENCES.length }, + ]; + + return ( +
+
e.stopPropagation()}> + + {/* Header */} +
+
+
+
+ {task.id} + {sm.label} + 高优先级 +
+
{task.title}
+
+ +
+
+ {task.assignee && {AGENT_EMOJI[task.assignee]} {task.assignee}} + · + 创建于 {fmtTimeFull(task.created_at)} + · + 更新于 {fmtTime(task.updated_at)} +
+
+ + {/* Tab 导航 */} +
+ {tabs.map(tab => ( + + ))} +
+ + {/* Tab 内容 */} +
+ + {/* ── 总览 Tab ── */} + {activeTab === 'overview' && (<> + {/* 描述 */} +
+ +
+ {task.description} +
+
+ + {/* 状态流转按钮 */} +
+ + +
+ + {/* 基本信息网格 */} +
+ +
+ {[ + ['任务类型', task.task_type], + ['优先级', task.priority], + ['风险等级', task.risk_level], + ['重试次数', `${task.retry_count}/${task.max_retries}`], + ['认领时间', task.claimed_at ? fmtTime(task.claimed_at) : '—'], + ['开始时间', task.started_at ? fmtTime(task.started_at) : '—'], + ['截止时间', task.deadline ? fmtTime(task.deadline) : '无'], + ['分配人', task.assigned_by ? `${AGENT_EMOJI[task.assigned_by] || ''} ${task.assigned_by}` : '—'], + ].map(([k, v], i) => ( +
+ {k} + {v}
-
-
{s.dept}
-
{s.action}
-
- {s.status === 'active' && ( - - {task.status === 'completed' ? '完成' : '◀ 当前'} - - )} - {s.status === 'done' && ( - 完成 - )} - {i < stages.length - 1 && s.status === 'done' && ( - - )} -
- ))} -
-
- - {/* 任务操作按钮 */} -
- {canPause && ( - - )} - {canResume && ( - - )} - {canCancel && ( - - )} -
- - {/* M3: 人工干预横幅 */} - {humanInputData?.active && humanInputData.checkpoints.length > 0 && ( -
- { loadAll(); fetchActivity(); api.humanInput(task.id).then(setHumanInputData); }} - /> -
- )} - - {/* M3: Tab 导航 */} -
- - -
- - {/* M3: 成果物 Tab */} - {activeTab === 'artifacts' && ( - - )} - - {/* 实时动态 Tab (原有内容,只在 activity tab 时显示) */} - {activeTab === 'activity' && (<> - - {/* BUG-6: 中间态提示 */} - {taskState === 'cancelling' && ( -
- -
-
正在取消
-
等待当前节点完成后再执行取消操作...
+ ))}
- )} - {taskState === 'pausing' && ( -
- -
-
正在暂停
-
等待当前节点完成后再执行暂停操作...
+ + {/* 事件时间线 */} +
+ +
+
- )} - {/* 审批操作 */} - {isAwaitingApproval && ( -
- 📋 方案待审批 - - -
- )} - - {/* Steer 干预 */} - {isExecuting && ( -
-
🎯 Steer 干预
-
- setSteerMsg(e.target.value)} - placeholder="输入干预消息..." - style={{ - flex: 1, background: 'var(--panel)', border: '1px solid var(--line)', - borderRadius: 6, padding: '6px 10px', color: 'var(--text)', fontSize: 13, outline: 'none', - }} - onKeyDown={(e) => { - if (e.key === 'Enter' && steerMsg.trim()) { - doTaskAction('steer', steerMsg.trim()); - setSteerMsg(''); - } - }} - /> - -
-
- )} - - {/* Scheduler Section */} -
-
- 🧭 任务调度 - - {sched ? `${sched.enabled === false ? '已禁用' : '运行中'} · 阈值 ${sched.stallThresholdSec || 180}s` : '加载中...'} - -
-
-
停滞时长
{fmtStalled(stalledSec)}
-
重试次数
{sched?.retryCount || 0}
-
升级级别
{!sched?.escalationLevel ? '无' : sched.escalationLevel === 1 ? '门下省' : '尚书省'}
-
派发状态
{sched?.lastDispatchStatus || 'idle'}
-
- {sched && ( -
- {sched.lastProgressAt && 最近进展 {formatDashboardDateTime(sched.lastProgressAt)}} - {sched.lastDispatchAt && 最近派发 {formatDashboardDateTime(sched.lastDispatchAt)}} - 自动回滚 {sched.autoRollback === false ? '关闭' : '开启'} - {sched.lastDispatchAgent && 目标 {sched.lastDispatchAgent}} -
- )} -
- {isAllowed('retry') && } - {isAllowed('escalate') && } - {isAllowed('rollback') && } - -
-
- - {/* Todo List */} - {todoTotal > 0 && ( - - )} - - {/* Basic Info */} -
-
-
-
状态
-
- {stateLabel(task)} - {(task.review_round || 0) > 0 && 共磋商 {task.review_round} 轮} -
-
-
-
执行部门
-
{task.org || '—'}
-
- {task.eta && task.eta !== '-' && ( -
预计完成
{task.eta}
- )} - {task.block && task.block !== '无' && task.block !== '-' && ( -
阻塞项
{task.block}
- )} - {task.now && task.now !== '-' && ( -
-
当前进展
-
{task.now}
-
- )} - {task.ac && ( -
-
验收标准
-
{task.ac}
-
- )} -
-
- - {/* DAG 节点图 */} - - - {/* 挑战历史(磋商实录) */} - - - {/* 决策/干预记录 */} - - - {/* Flow Log */} - {flowLog.length > 0 && ( -
-
流转日志({flowLog.length} 条)
-
- {flowLog.map((fl, i) => { - const col = deptColor(fl.from || ''); + {/* 评论 */} +
+ +
+ {MOCK_COMMENTS.map(c => { + const ct = COMMENT_TYPE_LABEL[c.comment_type] || { icon: '💬', label: c.comment_type, color: '#6b7280' }; return ( -
-
{formatDashboardTime(fl.at, { showSeconds: false })}
-
-
-
- {fl.from} - - {fl.to} +
+
+
+ {AGENT_EMOJI[c.author] || '🤖'} + {c.author} + {ct.icon} {ct.label}
-
{fl.remark}
+ {fmtTime(c.created_at)}
+
{c.content}
); })}
- )} - {/* Output */} - {task.output && task.output !== '-' && task.output !== '' && ( -
-
产出物
- {task.output} -
- )} - - {/* Live Activity */} - - - {/* Pulse animation */} - ) /* end activity tab */} - - -
-
- {confirmDlg && ( - setConfirmDlg(null)} - /> - )} -
- ); -} - -function TodoSection({ todos, todoDone, todoTotal }: { todos: TodoItem[]; todoDone: number; todoTotal: number }) { - return ( -
-
-
- 子任务清单({todoDone}/{todoTotal}) -
-
-
-
-
- {Math.round((todoDone / todoTotal) * 100)}% -
-
-
- {todos.map((td) => { - const ico = td.status === 'completed' ? '✅' : td.status === 'in-progress' ? '🔄' : '⬜'; - const stLabel = td.status === 'completed' ? '已完成' : td.status === 'in-progress' ? '进行中' : '待开始'; - const stCls = td.status === 'completed' ? 's-done' : td.status === 'in-progress' ? 's-progress' : 's-notstarted'; - const itemCls = td.status === 'completed' ? 'done' : ''; - return ( -
-
- {ico} - #{td.id} - {td.title} - {stLabel} -
- {td.detail &&
{td.detail}
} -
- ); - })} -
-
- ); -} - -function LiveActivitySection({ - data, - isDone, - logRef, -}: { - data: TaskActivityData | null; - isDone: boolean; - logRef: React.RefObject; -}) { - if (!data) return null; - - const activity = data.activity || []; - const isActive = (() => { - if (!activity.length) return false; - const last = activity[activity.length - 1]; - if (!last.at) return false; - const ts = typeof last.at === 'number' ? last.at : new Date(last.at).getTime(); - return Date.now() - ts < 300000; - })(); - - const agentParts: string[] = []; - if (data.agentLabel) agentParts.push(data.agentLabel); - if (data.relatedAgents && data.relatedAgents.length > 1) agentParts.push(`${data.relatedAgents.length}个 Agent`); - if (data.lastActive) agentParts.push(`最后活跃: ${formatDashboardDateTime(data.lastActive)}`); - - // Phase durations - const phaseDurations = data.phaseDurations || []; - const maxDur = Math.max(...phaseDurations.map((p) => p.durationSec || 1), 1); - const phaseColors: Record = { - '皇上': '#eab308', '调度': '#f97316', '中书省': '#3b82f6', '门下省': '#8b5cf6', - '尚书省': '#10b981', '六部': '#06b6d4', '礼部': '#ec4899', '户部': '#f59e0b', - '兵部': '#ef4444', '刑部': '#6366f1', '工部': '#14b8a6', '吏部': '#d946ef', - }; - - // Todos summary - const ts = data.todosSummary; - - // Resource summary - const rs = data.resourceSummary; - - // Group non-flow activity by agent - const flowItems = activity.filter((a) => a.kind === 'flow'); - const nonFlow = activity.filter((a) => a.kind !== 'flow'); - const grouped = new Map(); - nonFlow.forEach((a) => { - const key = a.agent || 'unknown'; - if (!grouped.has(key)) grouped.set(key, []); - grouped.get(key)!.push(a); - }); - - return ( -
-
- - - {isDone ? '执行回顾' : '实时动态'} - - {agentParts.join(' · ') || '加载中...'} -
- - {/* Phase Bars */} - {phaseDurations.length > 0 && ( -
-
- ⏱ 阶段耗时 - {data.totalDuration && 总耗时 {data.totalDuration}} -
- {phaseDurations.map((p, i) => { - const pct = Math.max(5, Math.round(((p.durationSec || 1) / maxDur) * 100)); - const color = phaseColors[p.phase] || '#6b7280'; - return ( -
- {p.phase} -
-
-
- - {p.durationText} - {p.ongoing && ●进行中} - -
- ); - })} -
- )} - - {/* Todos Progress */} - {ts && ( -
-
- 📊 执行进度 - = 100 ? '#22c55e' : ts.percent >= 50 ? '#60a5fa' : 'var(--text)' }}>{ts.percent}% - ✅{ts.completed} 🔄{ts.inProgress} ⬜{ts.notStarted} / 共{ts.total}项 -
-
-
-
-
-
- )} - - {/* Resource Summary */} - {rs && (rs.totalTokens || rs.totalCost) && ( -
- 📈 资源消耗 - {rs.totalTokens != null && 🔢 {rs.totalTokens.toLocaleString()} tokens} - {rs.totalCost != null && 💰 ${rs.totalCost.toFixed(4)}} - {rs.totalElapsedSec != null && ( - - ⏳ {rs.totalElapsedSec >= 60 ? `${Math.floor(rs.totalElapsedSec / 60)}分` : ''}{rs.totalElapsedSec % 60}秒 - - )} -
- )} - - {/* Timeline 时序表 */} - {data.timeline && data.timeline.length > 0 && ( -
-
📜 运行轨迹
-
- {data.timeline.map((t, i) => ( -
- {formatDashboardTime(t.time, { showSeconds: true })} - {t.phase} - {t.node_name && {t.node_name}} - {t.action} - {t.agent && {t.agent}} - {t.verdict && {t.verdict}} - {t.detail && {t.detail}} -
- ))} -
-
- )} - - {/* Activity Log (原有详细日志) */} -
}> - {/* Flow entries */} - {flowItems.length > 0 && ( -
- {flowItems.map((a, i) => ( -
- 📋 - {a.from}{a.to} {a.remark || ''} - {fmtActivityTime(a.at)} -
- ))} -
- )} - - {/* grouped entries 已删除,与 timeline 重复;仅保留空状态提示 */} - {!flowItems.length && grouped.size === 0 && ( -
- {data.message || data.error || 'Agent 尚未上报进展(等待 Agent 调用 progress 命令)'} -
- )} -
-
- ); -} - -function ActivityEntryView({ entry: a }: { entry: ActivityEntry }) { - const time = fmtActivityTime(a.at); - const agBadge = a.agent ? ( - - {AGENT_LABELS[a.agent] || a.agent} - - ) : null; - - if (a.kind === 'progress') { - return ( -
- 🔄 - {agBadge}当前进展:{a.text} - {time} -
- ); - } - - if (a.kind === 'todos') { - const items = a.items || []; - const diffMap = new Map(); - if (a.diff) { - (a.diff.changed || []).forEach((c) => diffMap.set(c.id, { type: 'changed', from: c.from, to: c.to })); - (a.diff.added || []).forEach((c) => diffMap.set(c.id, { type: 'added' })); - } - return ( -
-
{agBadge}📝 执行计划
- {items.map((td) => { - const icon = td.status === 'completed' ? '✅' : td.status === 'in-progress' ? '🔄' : '⬜'; - const d = diffMap.get(String(td.id)); - const style: React.CSSProperties = td.status === 'completed' - ? { opacity: 0.5, textDecoration: 'line-through' } - : td.status === 'in-progress' - ? { color: '#60a5fa', fontWeight: 'bold' } - : {}; - return ( -
- {icon} {td.title} - {d && d.type === 'changed' && d.to === 'completed' && ✨刚完成} - {d && d.type === 'changed' && d.to !== 'completed' && ↻{d.from}→{d.to}} - {d && d.type === 'added' && 🆕新增} -
- ); - })} - {a.diff?.removed?.map((r) => ( -
🗑 {r.title}
- ))} -
- ); - } - - if (a.kind === 'assistant') { - return ( - <> - {a.thinking && ( -
- 💭 - {agBadge}{a.thinking} - {time} -
- )} - {a.tools?.map((tc, i) => ( -
- 🔧 - {agBadge}{tc.name}{tc.input_preview || ''} - {time} -
- ))} - {a.text && ( -
- 🤖 - {agBadge}{a.text} - {time} -
- )} - - ); - } - - if (a.kind === 'tool_result') { - const ok = a.exitCode === 0 || a.exitCode === null || a.exitCode === undefined; - return ( -
- {ok ? '✅' : '❌'} - {agBadge}{a.tool || ''}{a.output ? a.output.substring(0, 150) : ''} - {time} -
- ); - } - - if (a.kind === 'user') { - return ( -
- 📥 - {agBadge}{a.text || ''} - {time} -
- ); - } - - return null; -} - -// ── P2-A: DAG 节点图 ── - -interface NodeData { - node_id: string; - name: string; - agent_id: string; - status: string; - depends_on?: string[]; - output?: string; -} - -function DagNodesSection({ - nodes, - expandedNode, - setExpandedNode, - toast, -}: { - nodes: NodeData[]; - expandedNode: string | null; - setExpandedNode: (id: string | null) => void; - toast: (msg: string, type: 'ok' | 'err') => void; -}) { - if (!nodes.length) return null; - - const statusIcon: Record = { - pending: '⏳', assigned: '📮', working: '⚔️', reviewing: '🔎', - done: '✅', failed: '❌', blocked: '🚧', cancelled: '🚫', - }; - const statusColor: Record = { - pending: '#6b7280', assigned: '#6a9eff', working: '#6a9eff', - reviewing: '#818cf8', done: '#2ecc8a', failed: '#ff5270', blocked: '#f59e0b', cancelled: '#9ca3af', - }; - - const doneCount = nodes.filter((n) => n.status === 'done').length; - - return ( -
-
⚔️ 各营进展({doneCount}/{nodes.length} 完成)
-
- {nodes.map((n) => { - const isExpanded = expandedNode === n.node_id; - const color = statusColor[n.status] || '#6b7280'; - return ( -
-
{ - if (n.status === 'done') setExpandedNode(isExpanded ? null : n.node_id); - }} - > - {statusIcon[n.status] || '❓'} - {n.name} - - {n.agent_id} - - - {n.status} - - {n.depends_on && n.depends_on.length > 0 && ( - - ← {n.depends_on.join(', ')} - - )} -
- {isExpanded && n.output && ( -
-
📦 产出
- {n.output.substring(0, 500)}{n.output.length > 500 ? '...' : ''} -
- + {/* 决策记录 */} +
+ + {MOCK_DECISIONS.map(d => ( +
+
+ {AGENT_EMOJI[d.decider]} {d.decider} + {fmtTime(d.created_at)}
+
{d.decision_type}
+
{d.rationale}
- )} + ))}
- ); - })} -
-
- ); -} -// ── P2-A: 挑战历史(磋商实录) ── - -function ChallengeHistorySection({ - sourceMeta, - reviewRound, -}: { - sourceMeta?: Record; - reviewRound: number; -}) { - const challenges = (sourceMeta?.challenges || []) as Array<{ - round?: number; - challenger?: string; - verdict?: string; - feedback?: string; - timestamp?: string; - node_name?: string; - }>; - - if (!challenges.length && reviewRound === 0) return null; - - return ( -
-
🦅 磋商实录{reviewRound > 0 ? `(${reviewRound} 轮)` : ''}
- {!challenges.length ? ( -
无挑战记录
- ) : ( -
- {challenges.map((c, i) => { - const isPass = c.verdict === 'APPROVE' || c.verdict === 'pass'; - return ( -
-
- {isPass ? '✅' : '🔄'} - - {c.node_name ? `${c.node_name} ` : ''}第 {c.round || i + 1} 轮 - - - {c.challenger || '司马懿'} - - - {c.verdict || 'ITERATE'} - + {/* Checkpoint 占位 */} +
+ +
+
🛐
+
Checkpoint 功能开发中
+
+ 验证/决策/执行三种检查点将在 v2.7 版本提供
- {c.feedback && ( -
- {c.feedback.substring(0, 200)}{c.feedback.length > 200 ? '...' : ''} -
- )}
- ); - })} -
- )} -
- ); -} +
+ )} -// ── P2-A: 决策/干预记录 ── - -function DecisionInterventionSection({ - sourceMeta, -}: { - sourceMeta?: Record; -}) { - const decisions = (sourceMeta?.decisions || []) as Array<{ - type?: string; - by?: string; - reason?: string; - timestamp?: string; - }>; - const interventions = (sourceMeta?.interventions || []) as Array<{ - action?: string; - by?: string; - reason?: string; - timestamp?: string; - }>; - - if (!decisions.length && !interventions.length) return null; - - return ( -
-
📋 决策与干预
-
- {decisions.map((d, i) => ( -
- 🧭 决策 - {d.by || '系统'} - {d.type || ''} - {d.reason && ( -
- {d.reason.substring(0, 150)} + {/* ── 产出 Tab ── */} + {activeTab === 'outputs' && (<> + + {MOCK_OUTPUTS.length === 0 ? ( +
+ 暂无产出物。Agent 完成后将自动提交。 +
+ ) : ( +
+ {MOCK_OUTPUTS.map(o => ( +
+
+ {AGENT_EMOJI[o.agent]} {o.agent} + 尝试 #{o.attempt} · {fmtTime(o.created_at)} +
+
{o.content}
+
+ + {o.content_type} + +
+
+ ))}
)} -
- ))} - {interventions.map((iv, i) => ( -
- ⚡ 干预 - {iv.by || '指挥'} - {iv.action || ''} - {iv.reason && ( -
- {iv.reason.substring(0, 150)} + )} + + {/* ── 审查 Tab ── */} + {activeTab === 'reviews' && (<> + + {MOCK_REVIEWS.map(r => ( +
+
+
+ {AGENT_EMOJI[r.reviewer]} {r.reviewer} + {r.verdict === 'APPROVE' ? '✅ 通过' : '❌ 驳回'} +
+ {fmtTime(r.created_at)} +
+
+ 置信度: {(r.confidence * 100).toFixed(0)}% + 风险: {r.risk_level} + 辩论轮次: {r.debate_round} +
+
{r.summary}
+
+ ))} + {MOCK_REVIEWS.length === 0 && ( +
+ 暂无审查记录。审查流水线将在产出提交后自动触发。
)} -
- ))} + )} + + {/* ── 经验 Tab ── */} + {activeTab === 'experience' && (<> + + {MOCK_EXPERIENCES.map(exp => ( +
+
+ {exp.title} + {fmtTime(exp.created_at)} +
+
{exp.summary}
+
+ {exp.tags.map(t => ( + + {t} + + ))} +
+
+ ))} + {MOCK_EXPERIENCES.length === 0 && ( +
+ 暂无经验沉淀。任务完成后将自动触发一级蒸馏。 +
+ )} + )} +
);