diff --git a/src/frontend/src/components/TaskModal.tsx b/src/frontend/src/components/TaskModal.tsx index b1f587a..8a6975b 100644 --- a/src/frontend/src/components/TaskModal.tsx +++ b/src/frontend/src/components/TaskModal.tsx @@ -1,43 +1,9 @@ /** * TaskModal v2.0 — 任务详情面板 - * 8区域:基本信息 + 状态流转 + 产出 + 评审 + 事件时间线 + 评论/决策 + 经验 + Checkpoint占位 - * Mock 数据用于 UI 预览 + * 从 store 读取 v2taskDetail(expand=all API 返回) */ -import { useState } from 'react'; -import { useStore } from '../store'; -import type { V2Task } from './EdictBoard'; - -// ── 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' }, -]; +import { useState, useEffect } from 'react'; +import { useStore, type V2Task } from '../store'; // ── 常量 ── const STATUS_META: Record = { @@ -51,7 +17,6 @@ const STATUS_META: Record = { cancelled: { color: '#6b7280', label: '已取消' }, }; -// P1: 对齐后端 VALID_TRANSITIONS(db.py) const VALID_TRANSITIONS: Record = { pending: ['claimed', 'cancelled'], claimed: ['working', 'pending', 'cancelled'], @@ -63,37 +28,53 @@ const VALID_TRANSITIONS: Record = { cancelled: [], }; +const PRIORITY_META: Record = { + 1: { color: '#6b7280', label: '低' }, 2: { color: '#6b7280', label: '低' }, + 3: { color: '#3b82f6', label: '中' }, 4: { color: '#3b82f6', label: '中' }, + 5: { color: '#f59e0b', label: '高' }, 6: { color: '#f59e0b', label: '高' }, + 7: { color: '#ff5270', label: '紧急' }, 8: { color: '#ff5270', label: '紧急' }, + 9: { color: '#ff5270', label: '紧急' }, 10: { color: '#ff5270', label: '紧急' }, +}; + const AGENT_EMOJI: Record = { 'pangtong-fujunshi': '🐦', 'simayi-challenger': '🦅', 'jiangwei-infra': '🔧', 'guanyu-dev': '⚔️', 'zhangfei-dev': '💪', 'zhaoyun-data': '📊', }; 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' }, + 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' }, + general: { icon: '💬', label: '评论', color: '#6b7280' }, }; 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: '🧭', + task_created: '📋', task_claimed: '👤', task_claimed: '👤', task_working: '⚔️', + task_review: '🔍', task_done: '✅', task_failed: '❌', task_blocked: '🚧', + comment_added: '💬', output_written: '📦', task_reviewed: '🔍', + decision_recorded: '🧭', observation_added: '👁️', daemon_tick: '⏱️', }; function fmtTime(iso: string): string { + if (!iso) return '—'; const d = new Date(iso); return `${d.getMonth()+1}/${d.getDate()} ${d.getHours()}:${String(d.getMinutes()).padStart(2,'0')}`; } function fmtTimeFull(iso: string): string { + if (!iso) return '—'; 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')}`; + return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${d.getHours()}:${String(d.getMinutes()).padStart(2,'0')}`; } -// ── 区域组件 ── +function parseDetail(raw: string): any { + try { return JSON.parse(raw); } catch { return {}; } +} + +// ── 子组件 ── function SectionLabel({ icon, title, count }: { icon: string; title: string; count?: number }) { return ( @@ -104,31 +85,51 @@ function SectionLabel({ icon, title, count }: { icon: string; title: string; cou ); } -function StatusButtons({ status }: { status: string }) { - const transitions = VALID_TRANSITIONS[status] || []; +function StatusButtons({ task }: { task: V2Task }) { + const selectedProjectId = useStore(s => s.selectedProjectId); + const loadV2Tasks = useStore(s => s.loadV2Tasks); + const toast = useStore(s => s.toast); + const transitions = VALID_TRANSITIONS[task.status] || []; + const btnMap: Record = { - claimed: { label: '认领任务', icon: '👤', bg: '#a07aff22', color: '#a07aff', border: '#a07aff44' }, - working: { label: '开始执行', icon: '⚔️', bg: '#2ecc8a22', color: '#2ecc8a', border: '#2ecc8a44' }, - review: { label: '提交审查', icon: '🔍', bg: '#818cf822', color: '#818cf8', border: '#818cf844' }, - done: { label: '标记完成', icon: '✅', bg: '#2ecc8a22', color: '#2ecc8a', border: '#2ecc8a44' }, - failed: { label: '标记失败', icon: '❌', bg: '#ff527022', color: '#ff5270', border: '#ff527044' }, - blocked: { label: '标记阻塞', icon: '🚧', bg: '#f59e0b22', color: '#f59e0b', border: '#f59e0b44' }, - pending: { label: '重置待认领', icon: '🔄', bg: '#6a9eff22', color: '#6a9eff', border: '#6a9eff44' }, + claimed: { label: '认领任务', icon: '👤', bg: '#a07aff22', color: '#a07aff', border: '#a07aff44' }, + working: { label: '开始执行', icon: '⚔️', bg: '#2ecc8a22', color: '#2ecc8a', border: '#2ecc8a44' }, + review: { label: '提交审查', icon: '🔍', bg: '#818cf822', color: '#818cf8', border: '#818cf844' }, + done: { label: '标记完成', icon: '✅', bg: '#2ecc8a22', color: '#2ecc8a', border: '#2ecc8a44' }, + failed: { label: '标记失败', icon: '❌', bg: '#ff527022', color: '#ff5270', border: '#ff527044' }, + blocked: { label: '标记阻塞', icon: '🚧', bg: '#f59e0b22', color: '#f59e0b', border: '#f59e0b44' }, + pending: { label: '重置待认领', icon: '🔄', bg: '#6a9eff22', color: '#6a9eff', border: '#6a9eff44' }, cancelled: { label: '取消任务', icon: '🚫', bg: '#6b728022', color: '#6b7280', border: '#6b728044' }, }; if (transitions.length === 0) return null; + const handleClick = async (newStatus: string) => { + if (!selectedProjectId) return; + try { + const res = await fetch(`/api/projects/${selectedProjectId}/tasks/${task.id}/status`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: newStatus }), + }); + const data = await res.json(); + if (data.ok) { + toast(`✅ 状态已更新为 ${STATUS_META[newStatus]?.label || newStatus}`, 'ok'); + loadV2Tasks(); + } else { + toast(`❌ 状态更新失败`, 'err'); + } + } catch { toast('❌ 网络错误', 'err'); } + }; + return (
{transitions.map(t => { const b = btnMap[t]; if (!b) return null; return ( - ); })} @@ -136,25 +137,31 @@ function StatusButtons({ status }: { status: string }) { ); } -function EventTimeline({ events }: { events: typeof MOCK_EVENTS }) { +function EventTimeline({ events }: { events: any[] }) { + if (!events || events.length === 0) return
暂无事件
; + const sorted = [...events].sort((a, b) => (b.created_at || '').localeCompare(a.created_at || '')); 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)} + {sorted.map((ev, i) => { + const detail = parseDetail(ev.detail || '{}'); + return ( +
+
+ {EVENT_TYPE_ICON[ev.event_type] || '📌'} + {i < sorted.length - 1 &&
} +
+
+
+ + {detail.from && detail.to ? `${STATUS_META[detail.from]?.label || detail.from} → ${STATUS_META[detail.to]?.label || detail.to}` : ev.event_type} + + {fmtTime(ev.created_at)} +
+ {ev.agent && {AGENT_EMOJI[ev.agent] || ''} {ev.agent}}
- {ev.agent && {AGENT_EMOJI[ev.agent] || ''} {ev.agent}}
-
- ))} + ); + })}
); } @@ -163,59 +170,48 @@ function EventTimeline({ events }: { events: typeof MOCK_EVENTS }) { export default function TaskModal() { const modalTaskId = useStore(s => s.modalTaskId); const setModalTaskId = useStore(s => s.setModalTaskId); + const v2taskDetail = useStore(s => s.v2taskDetail); + const loadV2TaskDetail = useStore(s => s.loadV2TaskDetail); const [activeTab, setActiveTab] = useState<'overview' | 'outputs' | 'reviews' | 'experience'>('overview'); + // 加载详情 + useEffect(() => { + if (modalTaskId) { + setActiveTab('overview'); + loadV2TaskDetail(modalTaskId); + } + }, [modalTaskId]); + 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: 7, - 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 task = v2taskDetail; + if (!task) return ( +
setModalTaskId(null)}> +
e.stopPropagation()}>加载中...
+
+ ); - const close = () => setModalTaskId(null); - const sm = STATUS_META[task.status]; + const close = () => { setModalTaskId(null); }; + const sm = STATUS_META[task.status] || STATUS_META.pending; + const pm = PRIORITY_META[task.priority] || { color: '#6b7280', label: `P${task.priority}` }; 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 }, + { key: 'overview' as const, label: '📋 总览' }, + { key: 'outputs' as const, label: '📦 产出', badge: (task.outputs || []).length }, + { key: 'reviews' as const, label: '🔍 审查', badge: (task.reviews || []).length }, + { key: 'experience' as const, label: '🧠 经验', badge: (task.experiences || []).length }, ]; + const comments = task.comments || []; + const outputs = task.outputs || []; + const reviews = task.reviews || []; + const events = task.events || []; + const decisions = task.decisions || []; + const experiences = task.experiences || []; + return ( -
-
e.stopPropagation()}> +
+
e.stopPropagation()}> {/* Header */}
@@ -224,14 +220,14 @@ export default function TaskModal() {
{task.id} {sm.label} - 紧急优先级 + {pm.label}优先级
{task.title}
- {task.assignee && {AGENT_EMOJI[task.assignee]} {task.assignee}} + {task.assignee && {AGENT_EMOJI[task.assignee] || '🤖'} {task.assignee}} · 创建于 {fmtTimeFull(task.created_at)} · @@ -239,54 +235,48 @@ export default function TaskModal() {
- {/* Tab 导航 */} + {/* Tabs */}
{tabs.map(tab => ( ))}
- {/* Tab 内容 */} + {/* Content */}
- {/* ── 总览 Tab ── */} {activeTab === 'overview' && (<> {/* 描述 */} -
- -
- {task.description} + {task.description && ( +
+ +
{task.description}
-
+ )} - {/* 状态流转按钮 */} + {/* 状态按钮 */}
- +
- {/* 基本信息网格 */} + {/* 信息网格 */}
{[ - ['任务类型', task.task_type], - ['优先级', task.priority], - ['风险等级', task.risk_level], - ['重试次数', `${task.retry_count}/${task.max_retries}`], + ['类型', task.task_type || '—'], + ['优先级', `${task.priority} (${pm.label})`], + ['风险', 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) : '无'], @@ -302,18 +292,18 @@ export default function TaskModal() { {/* 事件时间线 */}
- +
- +
{/* 评论 */}
- +
- {MOCK_COMMENTS.map(c => { - const ct = COMMENT_TYPE_LABEL[c.comment_type] || { icon: '💬', label: c.comment_type, color: '#6b7280' }; + {comments.map((c: any) => { + const ct = COMMENT_TYPE_LABEL[c.comment_type] || COMMENT_TYPE_LABEL.general; return (
@@ -324,64 +314,56 @@ export default function TaskModal() {
{fmtTime(c.created_at)}
-
{c.content}
+
{c.body}
); })} + {comments.length === 0 &&
暂无评论
}
- {/* 决策记录 */} -
- - {MOCK_DECISIONS.map(d => ( -
-
- {AGENT_EMOJI[d.decider]} {d.decider} - {fmtTime(d.created_at)} + {/* 决策 */} + {decisions.length > 0 && ( +
+ + {decisions.map((d: any) => ( +
+
+ {AGENT_EMOJI[d.decider] || ''} {d.decider} + {fmtTime(d.created_at)} +
+
{d.rationale}
-
{d.decision_type}
-
{d.rationale}
-
- ))} -
+ ))} +
+ )} {/* Checkpoint 占位 */}
-
+
🛐
Checkpoint 功能开发中
-
- 验证/决策/执行三种检查点将在 v2.7 版本提供 -
+
v2.7 提供
)} - {/* ── 产出 Tab ── */} {activeTab === 'outputs' && (<> - - {MOCK_OUTPUTS.length === 0 ? ( -
- 暂无产出物。Agent 完成后将自动提交。 -
+ + {outputs.length === 0 ? ( +
暂无产出物
) : (
- {MOCK_OUTPUTS.map(o => ( + {outputs.map((o: any) => (
- {AGENT_EMOJI[o.agent]} {o.agent} - 尝试 #{o.attempt} · {fmtTime(o.created_at)} + {AGENT_EMOJI[o.agent] || '🤖'} {o.agent} + {fmtTime(o.created_at)}
-
{o.content}
-
- - {o.content_type} - +
{o.summary || o.title}
+
+ {o.output_type}
))} @@ -389,66 +371,50 @@ export default function TaskModal() { )} )} - {/* ── 审查 Tab ── */} {activeTab === 'reviews' && (<> - - {MOCK_REVIEWS.map(r => ( -
-
-
- {AGENT_EMOJI[r.reviewer]} {r.reviewer} - {r.verdict === 'APPROVE' ? '✅ 通过' : '❌ 驳回'} + + {reviews.length === 0 ? ( +
暂无审查记录
+ ) : ( + reviews.map((r: any) => ( +
+
+
+ {AGENT_EMOJI[r.reviewer] || '🤖'} {r.reviewer} + + {r.verdict === 'approve' ? '✅ 通过' : '❌ 驳回'} + +
+ {fmtTime(r.created_at)}
- {fmtTime(r.created_at)} + {r.confidence != null &&
置信度: {(r.confidence * 100).toFixed(0)}%
} +
{r.summary}
-
- 置信度: {(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)} + + {experiences.length === 0 ? ( +
暂无经验沉淀
+ ) : ( + experiences.map((exp: any) => ( +
+
+ {exp.title} + {fmtTime(exp.created_at)} +
+
{exp.summary}
+ {exp.tags && ( +
+ {String(exp.tags).split(',').map((t: string) => ( + {t.trim()} + ))} +
+ )}
-
{exp.summary}
-
- {exp.tags.map(t => ( - - {t} - - ))} -
-
- ))} - {MOCK_EXPERIENCES.length === 0 && ( -
- 暂无经验沉淀。任务完成后将自动触发一级蒸馏。 -
+ )) )} )}