auto-sync: 2026-05-17 13:22:21

This commit is contained in:
cfdaily
2026-05-17 13:22:21 +08:00
parent 4100e2176c
commit bad461ab2a
+145 -141
View File
@@ -1,9 +1,9 @@
/**
* TaskModal v2.0 — 任务详情面板
* store 读取 v2taskDetailexpand=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 && (