/* Inbox mode — real API data */ function InboxMode({ onHome, initialSelected = null }) { const [selected, setSelected] = React.useState(initialSelected); const [filter, setFilter] = React.useState('all'); const inboxQ = useApi(() => API.inbox(), []); const notifs = inboxQ.data?.items || []; if (selected) { return ( setSelected(null)} onHome={onHome} onReplied={() => { setSelected(null); inboxQ.reload(); }} /> ); } if (inboxQ.loading) { return (
); } if (inboxQ.error) { return (
); } if (notifs.length === 0) { return (
all clear} />
Все нотификации обработаны
Агент работает автономно. Когда потребуется решение — появится здесь.
); } const KINDS = ['all', 'phase', 'todo', 'cons', 'system']; const filtered = filter === 'all' ? notifs : notifs.filter(n => notifIcon(n.type).kind === filter); return (
awaiting} />
{[['all', 'Все'], ['phase', 'Phase'], ['todo', 'Todo'], ['cons', 'Consilium'], ['system', 'System']].map(([id, l]) => ( ))}
{filtered.map(n => { const ico = notifIcon(n.type); return (
setSelected(n.id)}>
{ico.label}
{n.label || n.type}
{relTime(n.created_at)}
{n.project} · {n.scope_entity_type || n.type}
{n.project} {n.label || n.type} {n.scope_entity_id && {n.scope_entity_id}}
); })}
); } // ── Detail view — loads full notification ───────────────────── function NotifDetail({ id, onBack, onHome, onReplied }) { const detailQ = useApi(() => API.notification(id), [id]); const [sending, setSending] = React.useState(false); const [replyText, setReplyText] = React.useState(''); const sendReply = async (response) => { setSending(true); try { await API.replyNotif(id, response || replyText); onReplied(); } catch (e) { alert('Ошибка: ' + e.message); } finally { setSending(false); } }; if (detailQ.loading) { return (
); } if (!detailQ.data || detailQ.error) { return (
); } const notif = detailQ.data; const ico = notifIcon(notif.type); const ctx = notif.context || {}; const scope = [notif.scope_entity_type, notif.scope_entity_id].filter(Boolean).join(' · '); return (
{scope && {scope}} {onHome && }
{ico.label}
{notif.label || notif.type}
{notif.project} · {relTime(notif.created_at)} ago
{notif.type === 'phase_plan_review' && ( )} {notif.type === 'consilium_iteration_review' && ( )} {notif.type === 'todo_escalation' && ( )} {notif.type === 'todo_unsure' && ( )} {notif.type === 'ultimate_replacement_confirm' && ( )} {notif.type === 'subproject_merge_approval' && ( )} {(notif.type === 'arc_closure' || notif.type === 'linter_degraded') && ( )} {!['phase_plan_review','consilium_iteration_review','todo_escalation','todo_unsure', 'ultimate_replacement_confirm','subproject_merge_approval','arc_closure','linter_degraded'].includes(notif.type) && ( )}
); } // ── Type-specific detail UIs ────────────────────────────────── function PhaseReview({ ctx, notif, sendReply, sending }) { const phases = ctx.phases || []; const handleApprove = () => { if (ctx.arc_id && notif.project) { API.approveArc(notif.project, ctx.arc_id).then(() => sendReply('approve')).catch(e => sendReply('approve')); } else { sendReply('approve'); } }; return (
арка
{ctx.arc_title || ctx.arc_id || '—'}
{ctx.arc_id} · auto_proceed = {ctx.auto_proceed ? '1' : '0'}
awaiting
{phases.length > 0 && (
план фаз · {phases.length} шт
{phases.map((p, i) => (
{String(i + 1).padStart(2, '0')}
{p.title}
{p.desc &&
{p.desc}
} {p.est &&
est: {p.est}
}
))}
)}
); } function ConsiliumReview({ ctx, replyText, setReplyText, sendReply, sending }) { return (
change · iteration {ctx.iteration}/{ctx.max}
{ctx.change}
{(ctx.m1 || ctx.m2) && (
независимые оценки
{ctx.m1 && (
M1
compat:{ctx.m1.compatibility}
rec:{ctx.m1.recommendation}
{ctx.m1.warning && (
⚠ {ctx.m1.warning}
)}
)} {ctx.m2 && (
M2
compat:{ctx.m2.compatibility}
rec:{ctx.m2.recommendation}
{ctx.m2.warning && (
⚠ {ctx.m2.warning}
)}
)}
)}
{ctx.m1 && } {ctx.m2 && }