import { useEffect, useRef, useState, type ReactNode } from 'react'; import { createPortal } from 'react-dom'; import { api } from './api'; import { AgentOperation, Fields, type Preview } from './AgentReview'; import { toolLabel, statusLabel } from './agent-display'; import { showToast } from './Toast'; export function ReviewDialog({ children, close, busy = false, }: { children: ReactNode; close: () => void; busy?: boolean; }) { const ref = useRef(null); useEffect(() => { const previous = document.activeElement as HTMLElement | null; const overflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; ref.current?.focus(); return () => { document.body.style.overflow = overflow; previous?.focus(); }; }, []); return createPortal(
{ if (e.target === e.currentTarget && !busy) close(); }} >
{ if (e.key === 'Escape' && !busy) { e.preventDefault(); close(); } if (e.key === 'Tab') { const nodes = Array.from( ref.current?.querySelectorAll( 'button:not(:disabled),input:not(:disabled),a[href],select:not(:disabled),summary', ) || [], ); const first = nodes[0], last = nodes[nodes.length - 1]; if (!first) { e.preventDefault(); return; } if ( e.shiftKey && (document.activeElement === first || document.activeElement === ref.current) ) { e.preventDefault(); last.focus(); } else if ( !e.shiftKey && (document.activeElement === last || document.activeElement === ref.current) ) { e.preventDefault(); first.focus(); } } }} >

审阅草稿

{children}
, document.body, ); } type Draft = Preview & { error?: string }; export function AgentDrafts({ initialId, close, changed, }: { initialId?: string; close: () => void; changed: () => void; }) { const [items, setItems] = useState([]); const [selected, setSelected] = useState([]); const [busy, setBusy] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [detail, setDetail] = useState(false); const [now, setNow] = useState(Date.now()); async function load(initial = false) { setLoading(true); setError(''); try { const all: Draft[] = []; let cursor: string | null = null; do { const page: { items: Draft[]; nextCursor: string | null } = await api( '/agent/drafts' + (cursor ? '?cursor=' + encodeURIComponent(cursor) : ''), ); all.push(...page.items); cursor = page.nextCursor; } while (cursor); setItems(all); setSelected(initial && initialId ? [initialId] : []); setDetail(!!initialId && !all.some((o) => o.operationId === initialId)); } catch (e) { setError(e instanceof Error ? e.message : '草稿加载失败'); } finally { setLoading(false); } } useEffect(() => { void load(true); }, [initialId]); useEffect(() => { const timer = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(timer); }, []); const valid = items.filter( (o) => o.status === 'pending' && !o.error && new Date(o.expiresAt).getTime() > now, ); const eligible = selected.filter((id) => valid.some((o) => o.operationId === id)); async function decide(approve: boolean) { if (busy || !eligible.length || eligible.length > 100) return; setBusy(true); setError(''); try { await api('/agent/operations/confirm-batch', 'POST', { approve, operationIds: eligible, }); showToast(approve ? '所选草稿已全部执行' : '所选草稿已取消', 'success'); changed(); await load(); } catch (e) { const message = e instanceof Error ? e.message : '提交失败'; setError(message + '。本批修改未执行,请刷新后重新审阅。'); showToast(message, 'error'); } finally { setBusy(false); } } async function cancelUnavailable(id: string) { if (busy) return; setBusy(true); try { await api('/agent/operations/' + id, 'POST', { approve: false }); changed(); await load(); } catch (e) { setError(e instanceof Error ? e.message : '取消失败'); } finally { setBusy(false); } } return ( {loading ? (

正在读取全部草稿…

) : detail ? ( ) : ( <>

核对每项修改后选择。一次最多同意 100 项,任一失败整批回滚。

已选择 {eligible.length} / {items.length} 项
{!items.length &&

没有等待确认的修改。

}
{items.map((o) => { const available = valid.some((v) => v.operationId === o.operationId); const p = o.impact?.parameters || {}, refs = o.impact?.references || {}; const ref = refs[String(p.id || p.sourceId || '')]; const absolute = ['balance_record', 'history_update', 'position_create'].includes( o.tool, ); return (
{o.error ? (

{o.error}

) : ( <> {o.destructive && (

删除记录并重算余额,请仔细核对。

)} {o.impact.current && (
当前记录
)} !['hidden', 'archived', 'notes'].includes(key) || (value !== false && value !== '' && value !== null), ), ) : p } references={refs} currency={ref?.currency} liability={ absolute && (ref?.side === 'liability' || p.side === 'liability') } /> {o.tool === 'position_create' && (
查看全部字段
)} 有效至 {new Date(o.expiresAt).toLocaleString()} )}
); })}
)} {error && (

{error}

)}
); }