auto-sync: 2026-05-17 13:22:21
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* TaskModal v2.0 — 任务详情面板
|
||||
* 从 store 读取 v2taskDetail(expand=all API 返回)
|
||||
* TaskModal v2.0 — 任务详情面板(真实 API 对接版)
|
||||
* 数据来源:store.v2taskDetail(通过 loadV2TaskDetail 从后端 expand=all API 加载)
|
||||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useStore, type V2Task } from '../store';
|
||||
import { useEffect } from 'react';
|
||||
import { useStore } from '../store';
|
||||
|
||||
// ── 常量 ──
|
||||
const STATUS_META: Record<string, { color: string; label: string }> = {
|
||||
@@ -17,6 +17,7 @@ const STATUS_META: Record<string, { color: string; label: string }> = {
|
||||
cancelled: { color: '#6b7280', label: '已取消' },
|
||||
};
|
||||
|
||||
// 对齐后端 VALID_TRANSITIONS
|
||||
const VALID_TRANSITIONS: Record<string, string[]> = {
|
||||
pending: ['claimed', 'cancelled'],
|
||||
claimed: ['working', 'pending', 'cancelled'],
|
||||
@@ -52,30 +53,27 @@ const COMMENT_TYPE_LABEL: Record<string, { icon: string; label: string; color: s
|
||||
};
|
||||
|
||||
const EVENT_TYPE_ICON: Record<string, string> = {
|
||||
task_created: '📋', task_claimed: '👤', task_working: '⚔️',
|
||||
task_review: '🔍', task_done: '✅', task_failed: '❌', task_blocked: '🚧',
|
||||
comment_added: '💬', output_written: '📦', task_reviewed: '🔍',
|
||||
decision_recorded: '🧭', observation_added: '👁️', daemon_tick: '⏱️',
|
||||
task_created: '📋', task_claimed: '👤', task_started: '⚔️', task_completed: '✅',
|
||||
task_failed: '❌', comment_added: '💬', output_written: '📦', review_submitted: '🔍',
|
||||
review_approved: '✅', review_rejected: '❌', observation_added: '👁️', decision_made: '🧭',
|
||||
status_changed: '🔄', task_blocked: '🚧',
|
||||
};
|
||||
|
||||
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')}`;
|
||||
try {
|
||||
const d = new Date(iso.includes('T') ? iso : iso.replace(' ', 'T') + 'Z');
|
||||
return `${d.getMonth()+1}/${d.getDate()} ${d.getHours()}:${String(d.getMinutes()).padStart(2,'0')}`;
|
||||
} catch { return iso; }
|
||||
}
|
||||
|
||||
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')}`;
|
||||
}
|
||||
|
||||
function parseDetail(raw: string): any {
|
||||
try { return JSON.parse(raw); } catch { return {}; }
|
||||
try {
|
||||
const d = new Date(iso.includes('T') ? iso : iso.replace(' ', 'T') + 'Z');
|
||||
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')}`;
|
||||
} catch { return iso; }
|
||||
}
|
||||
|
||||
// ── 子组件 ──
|
||||
|
||||
function SectionLabel({ icon, title, count }: { icon: string; title: string; count?: number }) {
|
||||
return (
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text)', marginBottom: 8, display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
@@ -85,12 +83,8 @@ function SectionLabel({ icon, title, count }: { icon: string; title: string; cou
|
||||
);
|
||||
}
|
||||
|
||||
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] || [];
|
||||
|
||||
function StatusButtons({ status }: { status: string }) {
|
||||
const transitions = VALID_TRANSITIONS[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' },
|
||||
@@ -104,30 +98,13 @@ function StatusButtons({ task }: { task: V2Task }) {
|
||||
|
||||
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} onClick={() => handleClick(t)} style={{
|
||||
<button key={t} style={{
|
||||
padding: '5px 12px', borderRadius: 6, fontSize: 11, cursor: 'pointer',
|
||||
background: b.bg, color: b.color, border: `1px solid ${b.border}`, fontWeight: 600,
|
||||
}}>{b.icon} {b.label}</button>
|
||||
@@ -138,30 +115,25 @@ function StatusButtons({ task }: { task: V2Task }) {
|
||||
}
|
||||
|
||||
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 || ''));
|
||||
if (!events || events.length === 0) return <div style={{ fontSize: 12, color: 'var(--muted)', textAlign: 'center', padding: 16 }}>暂无事件记录</div>;
|
||||
const sorted = [...events].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
{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>
|
||||
{sorted.map((ev, i) => (
|
||||
<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 }}>{ev.detail || ev.event_type}</span>
|
||||
{ev.created_at && <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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -170,11 +142,11 @@ function EventTimeline({ events }: { events: any[] }) {
|
||||
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 taskDetail = useStore(s => s.v2taskDetail);
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'outputs' | 'reviews' | 'experience'>('overview');
|
||||
|
||||
// 加载详情
|
||||
// 打开时加载详情
|
||||
useEffect(() => {
|
||||
if (modalTaskId) {
|
||||
setActiveTab('overview');
|
||||
@@ -184,34 +156,47 @@ export default function TaskModal() {
|
||||
|
||||
if (!modalTaskId) return null;
|
||||
|
||||
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 task = taskDetail;
|
||||
if (!task) {
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(0,0,0,0.6)', display: 'flex', justifyContent: 'center', alignItems: 'center' }} onClick={() => setModalTaskId(null)}>
|
||||
<div style={{ background: 'var(--bg)', border: '1px solid var(--line)', borderRadius: 16, padding: 40, textAlign: 'center' }}>
|
||||
<div style={{ fontSize: 24, marginBottom: 8 }}>⏳</div>
|
||||
<div style={{ fontSize: 14, color: 'var(--muted)' }}>加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 close = () => setModalTaskId(null);
|
||||
|
||||
const comments = (task as any).comments || [];
|
||||
const outputs = (task as any).outputs || [];
|
||||
const reviews = (task as any).reviews || [];
|
||||
const decisions = (task as any).decisions || [];
|
||||
const events = (task as any).events || [];
|
||||
const experiences = (task as any).experiences || [];
|
||||
|
||||
const tabs = [
|
||||
{ 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 },
|
||||
{ key: 'outputs' as const, label: '📦 产出', badge: outputs.length },
|
||||
{ key: 'reviews' as const, label: '🔍 审查', badge: reviews.length },
|
||||
{ key: 'experience' as const, label: '🧠 经验', badge: 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 }}>
|
||||
@@ -239,20 +224,26 @@ export default function TaskModal() {
|
||||
<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>
|
||||
|
||||
{/* Content */}
|
||||
{/* Tab Content */}
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '16px 20px' }}>
|
||||
|
||||
{/* ── 总览 ── */}
|
||||
{activeTab === 'overview' && (<>
|
||||
{/* 描述 */}
|
||||
{task.description && (
|
||||
@@ -262,21 +253,21 @@ export default function TaskModal() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 状态按钮 */}
|
||||
{/* 状态操作 */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<SectionLabel icon="🔄" title="状态操作" />
|
||||
<StatusButtons task={task} />
|
||||
<StatusButtons status={task.status} />
|
||||
</div>
|
||||
|
||||
{/* 信息网格 */}
|
||||
{/* 任务信息 */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<SectionLabel icon="📊" title="任务信息" />
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
||||
{[
|
||||
['类型', task.task_type || '—'],
|
||||
['任务类型', task.task_type || '—'],
|
||||
['优先级', `${task.priority} (${pm.label})`],
|
||||
['风险', task.risk_level || '—'],
|
||||
['重试', `${task.retry_count}/${task.max_retries}`],
|
||||
['风险等级', 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) : '无'],
|
||||
@@ -299,40 +290,41 @@ export default function TaskModal() {
|
||||
</div>
|
||||
|
||||
{/* 评论 */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<SectionLabel icon="💬" title="评论/交接" count={comments.length} />
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{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 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11 }}>
|
||||
<span>{AGENT_EMOJI[c.author] || '🤖'}</span>
|
||||
<span style={{ fontWeight: 600 }}>{c.author}</span>
|
||||
<span style={{ fontSize: 9, padding: '1px 4px', borderRadius: 3, background: ct.color + '22', color: ct.color }}>{ct.icon} {ct.label}</span>
|
||||
{comments.length > 0 && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<SectionLabel icon="💬" title="评论/交接" count={comments.length} />
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{comments.map((c: any, i: number) => {
|
||||
const ct = COMMENT_TYPE_LABEL[c.comment_type] || COMMENT_TYPE_LABEL.general;
|
||||
return (
|
||||
<div key={c.id || i} style={{ padding: '8px 12px', background: 'var(--panel2)', borderRadius: 8 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11 }}>
|
||||
<span>{AGENT_EMOJI[c.author] || '🤖'}</span>
|
||||
<span style={{ fontWeight: 600 }}>{c.author}</span>
|
||||
<span style={{ fontSize: 9, padding: '1px 4px', borderRadius: 3, background: ct.color + '22', color: ct.color }}>{ct.icon} {ct.label}</span>
|
||||
</div>
|
||||
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{c.created_at ? fmtTime(c.created_at) : ''}</span>
|
||||
</div>
|
||||
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{fmtTime(c.created_at)}</span>
|
||||
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.5 }}>{c.body || c.content}</div>
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* 决策 */}
|
||||
{/* 决策记录 */}
|
||||
{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 }}>
|
||||
{decisions.map((d: any, i: number) => (
|
||||
<div key={d.id || i} 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>
|
||||
<span style={{ fontSize: 11, fontWeight: 600 }}>{AGENT_EMOJI[d.decider] || '🤖'} {d.decider}</span>
|
||||
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{d.created_at ? fmtTime(d.created_at) : ''}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.5 }}>{d.rationale}</div>
|
||||
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.5 }}>{d.rationale || d.decision}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -344,66 +336,78 @@ export default function TaskModal() {
|
||||
<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>
|
||||
</>)}
|
||||
|
||||
{/* ── 产出 ── */}
|
||||
{activeTab === 'outputs' && (<>
|
||||
<SectionLabel icon="📦" title="产出物" count={outputs.length} />
|
||||
{outputs.length === 0 ? (
|
||||
<div style={{ padding: 20, textAlign: 'center', color: 'var(--muted)', fontSize: 12 }}>暂无产出物</div>
|
||||
<div style={{ padding: 20, textAlign: 'center', color: 'var(--muted)', fontSize: 12 }}>暂无产出物。Agent 完成后将自动提交。</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{outputs.map((o: any) => (
|
||||
<div key={o.id} style={{ padding: '12px 16px', background: 'var(--panel2)', borderRadius: 8, borderLeft: '3px solid #2ecc8a' }}>
|
||||
{outputs.map((o: any, i: number) => (
|
||||
<div key={o.id || i} 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)' }}>{fmtTime(o.created_at)}</span>
|
||||
</div>
|
||||
<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>
|
||||
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{o.created_at ? fmtTime(o.created_at) : ''}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.6 }}>{o.summary || o.title || o.content_path || '(无摘要)'}</div>
|
||||
{o.output_type && <div style={{ marginTop: 6 }}><span style={{ fontSize: 10, padding: '1px 5px', borderRadius: 3, background: '#2ecc8a22', color: '#2ecc8a' }}>{o.output_type}</span></div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>)}
|
||||
|
||||
{/* ── 审查 ── */}
|
||||
{activeTab === 'reviews' && (<>
|
||||
<SectionLabel icon="🔍" title="审查记录" count={reviews.length} />
|
||||
{reviews.length === 0 ? (
|
||||
<div style={{ padding: 20, textAlign: 'center', color: 'var(--muted)', fontSize: 12 }}>暂无审查记录</div>
|
||||
<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 }}>
|
||||
reviews.map((r: any, i: number) => (
|
||||
<div key={r.id || i} style={{
|
||||
padding: '12px 16px', background: 'var(--panel2)', borderRadius: 8, marginBottom: 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>
|
||||
<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>
|
||||
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{r.created_at ? fmtTime(r.created_at) : ''}</span>
|
||||
</div>
|
||||
{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 style={{ display: 'flex', gap: 8, marginBottom: 6, fontSize: 10, color: 'var(--muted)' }}>
|
||||
{r.confidence != null && <span>置信度: {(r.confidence * 100).toFixed(0)}%</span>}
|
||||
{r.round != null && <span>轮次: {r.round}</span>}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.5 }}>{r.summary || '(无摘要)'}</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</>)}
|
||||
|
||||
{/* ── 经验 ── */}
|
||||
{activeTab === 'experience' && (<>
|
||||
<SectionLabel icon="🧠" title="经验沉淀" count={experiences.length} />
|
||||
{experiences.length === 0 ? (
|
||||
<div style={{ padding: 20, textAlign: 'center', color: 'var(--muted)', fontSize: 12 }}>暂无经验沉淀</div>
|
||||
<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 }}>
|
||||
experiences.map((exp: any, i: number) => (
|
||||
<div key={exp.experience_id || i} style={{
|
||||
padding: '12px 16px', background: 'var(--panel2)', borderRadius: 8, marginBottom: 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>
|
||||
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{exp.created_at ? fmtTime(exp.created_at) : ''}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#a0aec0', lineHeight: 1.5, marginBottom: 6 }}>{exp.summary}</div>
|
||||
{exp.tags && (
|
||||
|
||||
Reference in New Issue
Block a user