auto-sync: 2026-05-17 13:10:24

This commit is contained in:
cfdaily
2026-05-17 13:10:24 +08:00
parent 7c03512a88
commit 367c7c9aed
+200 -234
View File
@@ -1,43 +1,9 @@
/**
* TaskModal v2.0 — 任务详情面板
* 8区域:基本信息 + 状态流转 + 产出 + 评审 + 事件时间线 + 评论/决策 + 经验 + Checkpoint占位
* Mock 数据用于 UI 预览
* 从 store 读取 v2taskDetailexpand=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<string, { color: string; label: string }> = {
@@ -51,7 +17,6 @@ const STATUS_META: Record<string, { color: string; label: string }> = {
cancelled: { color: '#6b7280', label: '已取消' },
};
// P1: 对齐后端 VALID_TRANSITIONSdb.py
const VALID_TRANSITIONS: Record<string, string[]> = {
pending: ['claimed', 'cancelled'],
claimed: ['working', 'pending', 'cancelled'],
@@ -63,37 +28,53 @@ const VALID_TRANSITIONS: Record<string, string[]> = {
cancelled: [],
};
const PRIORITY_META: Record<number, { color: string; label: string }> = {
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<string, string> = {
'pangtong-fujunshi': '🐦', 'simayi-challenger': '🦅', 'jiangwei-infra': '🔧',
'guanyu-dev': '⚔️', 'zhangfei-dev': '💪', 'zhaoyun-data': '📊',
};
const COMMENT_TYPE_LABEL: Record<string, { icon: string; label: string; color: string }> = {
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<string, string> = {
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<string, { label: string; icon: string; bg: string; color: string; border: string }> = {
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 (
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
{transitions.map(t => {
const b = btnMap[t];
if (!b) return null;
return (
<button key={t} style={{
<button key={t} onClick={() => handleClick(t)} style={{
padding: '5px 12px', borderRadius: 6, fontSize: 11, cursor: 'pointer',
background: b.bg, color: b.color, border: `1px solid ${b.border}`,
fontWeight: 600,
background: b.bg, color: b.color, border: `1px solid ${b.border}`, fontWeight: 600,
}}>{b.icon} {b.label}</button>
);
})}
@@ -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 <div style={{ fontSize: 12, color: 'var(--muted)', textAlign: 'center', padding: 16 }}></div>;
const sorted = [...events].sort((a, b) => (b.created_at || '').localeCompare(a.created_at || ''));
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
{events.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()).map((ev, i) => (
<div key={ev.id} style={{ display: 'flex', gap: 8, padding: '6px 0', position: 'relative' }}>
{/* 时间线 */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: 24, flexShrink: 0 }}>
<span style={{ fontSize: 13 }}>{EVENT_TYPE_ICON[ev.event_type] || '📌'}</span>
{i < events.length - 1 && <div style={{ width: 1, flex: 1, background: 'var(--line)', marginTop: 2 }} />}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: 12, fontWeight: 500 }}>{ev.detail}</span>
<span style={{ fontSize: 10, color: 'var(--muted)', flexShrink: 0, marginLeft: 8 }}>{fmtTime(ev.created_at)}</span>
{sorted.map((ev, i) => {
const detail = parseDetail(ev.detail || '{}');
return (
<div key={ev.id || i} style={{ display: 'flex', gap: 8, padding: '6px 0', position: 'relative' }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: 24, flexShrink: 0 }}>
<span style={{ fontSize: 13 }}>{EVENT_TYPE_ICON[ev.event_type] || '📌'}</span>
{i < sorted.length - 1 && <div style={{ width: 1, flex: 1, background: 'var(--line)', marginTop: 2 }} />}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: 12, fontWeight: 500 }}>
{detail.from && detail.to ? `${STATUS_META[detail.from]?.label || detail.from}${STATUS_META[detail.to]?.label || detail.to}` : ev.event_type}
</span>
<span style={{ fontSize: 10, color: 'var(--muted)', flexShrink: 0, marginLeft: 8 }}>{fmtTime(ev.created_at)}</span>
</div>
{ev.agent && <span style={{ fontSize: 10, color: 'var(--muted)' }}>{AGENT_EMOJI[ev.agent] || ''} {ev.agent}</span>}
</div>
{ev.agent && <span style={{ fontSize: 10, color: 'var(--muted)' }}>{AGENT_EMOJI[ev.agent] || ''} {ev.agent}</span>}
</div>
</div>
))}
);
})}
</div>
);
}
@@ -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 (
<div style={{ position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(0,0,0,0.6)', display: 'flex', justifyContent: 'center', alignItems: 'flex-start', padding: '40px 20px' }} onClick={() => setModalTaskId(null)}>
<div style={{ background: 'var(--bg)', border: '1px solid var(--line)', borderRadius: 16, padding: 40, color: 'var(--muted)' }} onClick={e => e.stopPropagation()}>...</div>
</div>
);
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 (
<div style={{
position: 'fixed', inset: 0, zIndex: 1000,
background: 'rgba(0,0,0,0.6)', display: 'flex', justifyContent: 'center', alignItems: 'flex-start',
padding: '40px 20px', overflowY: 'auto', backdropFilter: 'blur(4px)',
}} onClick={close}>
<div style={{
background: 'var(--bg)', border: '1px solid var(--line)', borderRadius: 16,
width: '100%', maxWidth: 720, maxHeight: 'calc(100vh - 80px)', overflow: 'hidden',
display: 'flex', flexDirection: 'column',
boxShadow: '0 8px 40px rgba(0,0,0,0.5)',
}} onClick={e => e.stopPropagation()}>
<div style={{ position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(0,0,0,0.6)', display: 'flex', justifyContent: 'center', alignItems: 'flex-start', padding: '40px 20px', overflowY: 'auto', backdropFilter: 'blur(4px)' }} onClick={close}>
<div style={{ background: 'var(--bg)', border: '1px solid var(--line)', borderRadius: 16, width: '100%', maxWidth: 720, maxHeight: 'calc(100vh - 80px)', overflow: 'hidden', display: 'flex', flexDirection: 'column', boxShadow: '0 8px 40px rgba(0,0,0,0.5)' }} onClick={e => e.stopPropagation()}>
{/* Header */}
<div style={{ padding: '16px 20px', borderBottom: '1px solid var(--line)', flexShrink: 0 }}>
@@ -224,14 +220,14 @@ export default function TaskModal() {
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<span style={{ fontSize: 11, color: 'var(--acc)', fontWeight: 700, letterSpacing: '.04em' }}>{task.id}</span>
<span style={{ fontSize: 10, padding: '2px 6px', borderRadius: 4, background: sm.color + '22', color: sm.color, fontWeight: 600 }}>{sm.label}</span>
<span style={{ fontSize: 10, padding: '2px 6px', borderRadius: 4, background: '#ff527022', color: '#ff5270', fontWeight: 600 }}></span>
<span style={{ fontSize: 10, padding: '2px 6px', borderRadius: 4, background: pm.color + '22', color: pm.color, fontWeight: 600 }}>{pm.label}</span>
</div>
<div style={{ fontSize: 18, fontWeight: 700, lineHeight: 1.3 }}>{task.title}</div>
</div>
<button onClick={close} style={{ background: 'none', border: 'none', color: 'var(--muted)', fontSize: 18, cursor: 'pointer', padding: 4 }}></button>
</div>
<div style={{ fontSize: 12, color: 'var(--muted)', marginTop: 6 }}>
{task.assignee && <span>{AGENT_EMOJI[task.assignee]} {task.assignee}</span>}
{task.assignee && <span>{AGENT_EMOJI[task.assignee] || '🤖'} {task.assignee}</span>}
<span style={{ margin: '0 8px' }}>·</span>
<span> {fmtTimeFull(task.created_at)}</span>
<span style={{ margin: '0 8px' }}>·</span>
@@ -239,54 +235,48 @@ export default function TaskModal() {
</div>
</div>
{/* Tab 导航 */}
{/* Tabs */}
<div style={{ display: 'flex', borderBottom: '1px solid var(--line)', padding: '0 20px', flexShrink: 0 }}>
{tabs.map(tab => (
<button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{
padding: '10px 16px', fontSize: 12, fontWeight: 600,
border: 'none', background: 'none', cursor: 'pointer',
padding: '10px 16px', fontSize: 12, fontWeight: 600, border: 'none', background: 'none', cursor: 'pointer',
color: activeTab === tab.key ? 'var(--acc)' : 'var(--muted)',
borderBottom: activeTab === tab.key ? '2px solid var(--acc)' : '2px solid transparent',
display: 'flex', alignItems: 'center', gap: 4, transition: 'all .15s',
}}>
{tab.label}
{tab.badge ? <span style={{
fontSize: 9, background: activeTab === tab.key ? 'var(--acc)' : '#2a3550',
color: activeTab === tab.key ? '#000' : '#8899aa',
borderRadius: 8, padding: '0 5px', minWidth: 16, textAlign: 'center',
}}>{tab.badge}</span> : null}
{tab.badge ? <span style={{ fontSize: 9, background: activeTab === tab.key ? 'var(--acc)' : '#2a3550', color: activeTab === tab.key ? '#000' : '#8899aa', borderRadius: 8, padding: '0 5px', minWidth: 16, textAlign: 'center' }}>{tab.badge}</span> : null}
</button>
))}
</div>
{/* Tab 内容 */}
{/* Content */}
<div style={{ flex: 1, overflowY: 'auto', padding: '16px 20px' }}>
{/* ── 总览 Tab ── */}
{activeTab === 'overview' && (<>
{/* 描述 */}
<div style={{ marginBottom: 16 }}>
<SectionLabel icon="📝" title="需求描述" />
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.6, padding: '8px 12px', background: 'var(--panel2)', borderRadius: 8 }}>
{task.description}
{task.description && (
<div style={{ marginBottom: 16 }}>
<SectionLabel icon="📝" title="需求描述" />
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.6, padding: '8px 12px', background: 'var(--panel2)', borderRadius: 8 }}>{task.description}</div>
</div>
</div>
)}
{/* 状态流转按钮 */}
{/* 状态按钮 */}
<div style={{ marginBottom: 16 }}>
<SectionLabel icon="🔄" title="状态操作" />
<StatusButtons status={task.status} />
<StatusButtons task={task} />
</div>
{/* 基本信息网格 */}
{/* 信息网格 */}
<div style={{ marginBottom: 16 }}>
<SectionLabel icon="📊" title="任务信息" />
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
{[
['任务类型', 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() {
{/* 事件时间线 */}
<div style={{ marginBottom: 16 }}>
<SectionLabel icon="📜" title="事件时间线" count={MOCK_EVENTS.length} />
<SectionLabel icon="📜" title="事件时间线" count={events.length} />
<div style={{ padding: '8px 12px', background: 'var(--panel2)', borderRadius: 8 }}>
<EventTimeline events={MOCK_EVENTS} />
<EventTimeline events={events} />
</div>
</div>
{/* 评论 */}
<div style={{ marginBottom: 16 }}>
<SectionLabel icon="💬" title="评论/交接" count={MOCK_COMMENTS.length} />
<SectionLabel icon="💬" title="评论/交接" count={comments.length} />
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{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 (
<div key={c.id} style={{ padding: '8px 12px', background: 'var(--panel2)', borderRadius: 8 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
@@ -324,64 +314,56 @@ export default function TaskModal() {
</div>
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{fmtTime(c.created_at)}</span>
</div>
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.5 }}>{c.content}</div>
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.5 }}>{c.body}</div>
</div>
);
})}
{comments.length === 0 && <div style={{ fontSize: 12, color: 'var(--muted)', textAlign: 'center', padding: 12 }}></div>}
</div>
</div>
{/* 决策记录 */}
<div style={{ marginBottom: 16 }}>
<SectionLabel icon="🧭" title="决策记录" count={MOCK_DECISIONS.length} />
{MOCK_DECISIONS.map(d => (
<div key={d.id} style={{ padding: '8px 12px', background: 'var(--panel2)', borderRadius: 8, borderLeft: '3px solid #6a9eff' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<span style={{ fontSize: 11, fontWeight: 600 }}>{AGENT_EMOJI[d.decider]} {d.decider}</span>
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{fmtTime(d.created_at)}</span>
{/* 决策 */}
{decisions.length > 0 && (
<div style={{ marginBottom: 16 }}>
<SectionLabel icon="🧭" title="决策记录" count={decisions.length} />
{decisions.map((d: any) => (
<div key={d.id} style={{ padding: '8px 12px', background: 'var(--panel2)', borderRadius: 8, borderLeft: '3px solid #6a9eff', marginBottom: 6 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<span style={{ fontSize: 11, fontWeight: 600 }}>{AGENT_EMOJI[d.decider] || ''} {d.decider}</span>
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{fmtTime(d.created_at)}</span>
</div>
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.5 }}>{d.rationale}</div>
</div>
<div style={{ fontSize: 10, color: 'var(--muted)', marginBottom: 2 }}>{d.decision_type}</div>
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.5 }}>{d.rationale}</div>
</div>
))}
</div>
))}
</div>
)}
{/* Checkpoint 占位 */}
<div style={{ marginBottom: 16 }}>
<SectionLabel icon="🛐" title="Checkpoint" />
<div style={{
padding: '16px 20px', background: 'var(--panel2)', borderRadius: 8,
border: '1px dashed #2a3550', textAlign: 'center',
}}>
<div style={{ padding: '16px 20px', background: 'var(--panel2)', borderRadius: 8, border: '1px dashed #2a3550', textAlign: 'center' }}>
<div style={{ fontSize: 24, marginBottom: 8 }}>🛐</div>
<div style={{ fontSize: 12, color: 'var(--muted)', fontWeight: 600 }}>Checkpoint </div>
<div style={{ fontSize: 11, color: '#4a5568', marginTop: 4 }}>
// v2.7
</div>
<div style={{ fontSize: 11, color: '#4a5568', marginTop: 4 }}>v2.7 </div>
</div>
</div>
</>)}
{/* ── 产出 Tab ── */}
{activeTab === 'outputs' && (<>
<SectionLabel icon="📦" title="产出物" count={MOCK_OUTPUTS.length} />
{MOCK_OUTPUTS.length === 0 ? (
<div style={{ padding: 20, textAlign: 'center', color: 'var(--muted)', fontSize: 12 }}>
Agent
</div>
<SectionLabel icon="📦" title="产出物" count={outputs.length} />
{outputs.length === 0 ? (
<div style={{ padding: 20, textAlign: 'center', color: 'var(--muted)', fontSize: 12 }}></div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{MOCK_OUTPUTS.map(o => (
{outputs.map((o: any) => (
<div key={o.id} style={{ padding: '12px 16px', background: 'var(--panel2)', borderRadius: 8, borderLeft: '3px solid #2ecc8a' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
<span style={{ fontSize: 11, fontWeight: 600 }}>{AGENT_EMOJI[o.agent]} {o.agent}</span>
<span style={{ fontSize: 10, color: 'var(--muted)' }}> #{o.attempt} · {fmtTime(o.created_at)}</span>
<span style={{ fontSize: 11, fontWeight: 600 }}>{AGENT_EMOJI[o.agent] || '🤖'} {o.agent}</span>
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{fmtTime(o.created_at)}</span>
</div>
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.6 }}>{o.content}</div>
<div style={{ marginTop: 8, display: 'flex', gap: 6 }}>
<span style={{ fontSize: 10, padding: '1px 5px', borderRadius: 3, background: '#2ecc8a22', color: '#2ecc8a' }}>
{o.content_type}
</span>
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.6 }}>{o.summary || o.title}</div>
<div style={{ marginTop: 6 }}>
<span style={{ fontSize: 10, padding: '1px 5px', borderRadius: 3, background: '#2ecc8a22', color: '#2ecc8a' }}>{o.output_type}</span>
</div>
</div>
))}
@@ -389,66 +371,50 @@ export default function TaskModal() {
)}
</>)}
{/* ── 审查 Tab ── */}
{activeTab === 'reviews' && (<>
<SectionLabel icon="🔍" title="审查记录" count={MOCK_REVIEWS.length} />
{MOCK_REVIEWS.map(r => (
<div key={r.id} style={{
padding: '12px 16px', background: 'var(--panel2)', borderRadius: 8,
borderLeft: `3px solid ${r.verdict === 'APPROVE' ? '#2ecc8a' : '#ff5270'}`,
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 11, fontWeight: 600 }}>{AGENT_EMOJI[r.reviewer]} {r.reviewer}</span>
<span style={{
fontSize: 10, padding: '1px 6px', borderRadius: 4, fontWeight: 600,
background: r.verdict === 'APPROVE' ? '#2ecc8a22' : '#ff527022',
color: r.verdict === 'APPROVE' ? '#2ecc8a' : '#ff5270',
}}>{r.verdict === 'APPROVE' ? '✅ 通过' : '❌ 驳回'}</span>
<SectionLabel icon="🔍" title="审查记录" count={reviews.length} />
{reviews.length === 0 ? (
<div style={{ padding: 20, textAlign: 'center', color: 'var(--muted)', fontSize: 12 }}></div>
) : (
reviews.map((r: any) => (
<div key={r.id} style={{ padding: '12px 16px', background: 'var(--panel2)', borderRadius: 8, borderLeft: `3px solid ${r.verdict === 'approve' ? '#2ecc8a' : '#ff5270'}`, marginBottom: 8 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 11, fontWeight: 600 }}>{AGENT_EMOJI[r.reviewer] || '🤖'} {r.reviewer}</span>
<span style={{ fontSize: 10, padding: '1px 6px', borderRadius: 4, fontWeight: 600, background: r.verdict === 'approve' ? '#2ecc8a22' : '#ff527022', color: r.verdict === 'approve' ? '#2ecc8a' : '#ff5270' }}>
{r.verdict === 'approve' ? '✅ 通过' : '❌ 驳回'}
</span>
</div>
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{fmtTime(r.created_at)}</span>
</div>
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{fmtTime(r.created_at)}</span>
{r.confidence != null && <div style={{ fontSize: 10, color: 'var(--muted)', marginBottom: 4 }}>: {(r.confidence * 100).toFixed(0)}%</div>}
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.5 }}>{r.summary}</div>
</div>
<div style={{ display: 'flex', gap: 8, marginBottom: 6, fontSize: 10, color: 'var(--muted)' }}>
<span>: {(r.confidence * 100).toFixed(0)}%</span>
<span>: {r.risk_level}</span>
<span>: {r.debate_round}</span>
</div>
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.5 }}>{r.summary}</div>
</div>
))}
{MOCK_REVIEWS.length === 0 && (
<div style={{ padding: 20, textAlign: 'center', color: 'var(--muted)', fontSize: 12 }}>
线
</div>
))
)}
</>)}
{/* ── 经验 Tab ── */}
{activeTab === 'experience' && (<>
<SectionLabel icon="🧠" title="经验沉淀" count={MOCK_EXPERIENCES.length} />
{MOCK_EXPERIENCES.map(exp => (
<div key={exp.id} style={{
padding: '12px 16px', background: 'var(--panel2)', borderRadius: 8,
borderLeft: `3px solid ${exp.pattern_type === 'best_practice' ? '#2ecc8a' : exp.pattern_type === 'pitfall' ? '#ff5270' : '#6a9eff'}`,
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<span style={{ fontSize: 12, fontWeight: 600 }}>{exp.title}</span>
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{fmtTime(exp.created_at)}</span>
<SectionLabel icon="🧠" title="经验沉淀" count={experiences.length} />
{experiences.length === 0 ? (
<div style={{ padding: 20, textAlign: 'center', color: 'var(--muted)', fontSize: 12 }}></div>
) : (
experiences.map((exp: any) => (
<div key={exp.id} style={{ padding: '12px 16px', background: 'var(--panel2)', borderRadius: 8, borderLeft: `3px solid ${exp.pattern_type === 'best_practice' ? '#2ecc8a' : exp.pattern_type === 'pitfall' ? '#ff5270' : '#6a9eff'}`, marginBottom: 8 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<span style={{ fontSize: 12, fontWeight: 600 }}>{exp.title}</span>
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{fmtTime(exp.created_at)}</span>
</div>
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.5, marginBottom: 6 }}>{exp.summary}</div>
{exp.tags && (
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{String(exp.tags).split(',').map((t: string) => (
<span key={t} style={{ fontSize: 9, padding: '1px 5px', borderRadius: 3, background: '#6a9eff22', color: '#6a9eff' }}>{t.trim()}</span>
))}
</div>
)}
</div>
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.5, marginBottom: 6 }}>{exp.summary}</div>
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{exp.tags.map(t => (
<span key={t} style={{ fontSize: 9, padding: '1px 5px', borderRadius: 3, background: '#6a9eff22', color: '#6a9eff' }}>
{t}
</span>
))}
</div>
</div>
))}
{MOCK_EXPERIENCES.length === 0 && (
<div style={{ padding: 20, textAlign: 'center', color: 'var(--muted)', fontSize: 12 }}>
</div>
))
)}
</>)}
</div>