import { useActionMutation, useActionQuery, } from "@agent-native/core/client/hooks"; import { useT } from "@agent-native/core/client/i18n"; import { type Icon, IconChartBar, IconCheck, IconBook, IconDotsVertical, IconExternalLink, IconFileText, IconGitMerge, IconInfoCircle, IconListDetails, IconPencil, IconShieldCheck, IconX, } from "@tabler/icons-react"; import { useMemo, useState, type KeyboardEvent } from "react"; import { useSearchParams } from "react-router"; import { type CanonicalPreviewData, CanonicalPreviewSheet, } from "@/components/brain/CanonicalPreviewSheet"; import { EmptyActionState, LoadingRows, PageHeader, StatusBadge, } from "@/components/brain/Surface"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from "@/components/ui/sheet"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { type ReviewItem, type ReviewQueueResponse } from "@/lib/brain"; import { cn } from "@/lib/utils"; type BrainT = ReturnType; type ProposalStatus = "pending" | "approved" | "rejected" | "quarantine"; interface ProposalDraft { title?: string; body?: string; rationale?: string; } interface TargetContext { label: string; detail: string; knowledgeId?: string | null; supersedesId?: string | null; } interface ProposalInsight { confidence: number | null; queueReason: string; privacyFlags: string[]; target: TargetContext; publishTier: string | null; kind: string | null; topic: string | null; status: string | null; summary: string | null; tags: string[]; approveLabel: string; } export default function ReviewRoute() { const t = useT(); const [params, setParams] = useSearchParams(); const status = proposalStatus(params.get("status")); const selectedProposalId = params.get("reviewItemId"); const [drafts, setDrafts] = useState>({}); const [notes, setNotes] = useState>({}); const [canonicalChoices, setCanonicalChoices] = useState< Record >({}); const [editingProposalIds, setEditingProposalIds] = useState>( () => new Set(), ); const [previewOpen, setPreviewOpen] = useState(false); const [canonicalPreview, setCanonicalPreview] = useState(null); const [previewProposal, setPreviewProposal] = useState( null, ); const reviewQuery = useActionQuery( "list-proposals" as any, { status } as any, ); const updateProposal = useActionMutation< unknown, { proposalId: string; title?: string; body?: string; rationale?: string; } >("update-proposal" as any); const approveProposal = useActionMutation< unknown, { proposalId: string; reviewerNotes?: string; publishCanonical?: boolean; } >("approve-proposal" as any); const previewCanonical = useActionMutation< { preview: CanonicalPreviewData }, { proposalId: string; operation: "publish"; draft?: { title?: string; body?: string; }; } >("preview-canonical-resource" as any); const rejectProposal = useActionMutation< unknown, { proposalId: string; reviewerNotes?: string } >("reject-proposal" as any); const proposals = reviewQuery.data?.proposals ?? reviewQuery.data?.items ?? []; const pendingMutation = updateProposal.isPending || approveProposal.isPending || previewCanonical.isPending || rejectProposal.isPending; const actionError = updateProposal.error ?? approveProposal.error ?? previewCanonical.error ?? rejectProposal.error; const summary = useMemo(() => { const label = status === "pending" ? t("review.pendingProposals") : status === "approved" ? t("review.approvedProposals") : t("review.rejectedProposals"); return t("review.summary", { label, count: proposals.length, itemLabel: proposals.length === 1 ? t("review.item") : t("review.items"), }); }, [proposals.length, status, t]); function updateStatus(value: string) { const next = new URLSearchParams(params); if (value === "pending") next.delete("status"); else next.set("status", value); setParams(next, { replace: true }); } function patchDraft(proposalId: string, patch: ProposalDraft) { setDrafts((current) => ({ ...current, [proposalId]: { ...current[proposalId], ...patch }, })); } function draftValue( proposal: ReviewItem, field: keyof ProposalDraft, ): string { const value = drafts[proposal.id]?.[field]; if (value !== undefined) return value; if (field === "title") return proposal.title; if (field === "body") return proposal.body ?? proposal.proposedAnswer ?? ""; return proposal.rationale ?? ""; } function hasDraftChanges(proposal: ReviewItem) { const draft = drafts[proposal.id]; if (!draft) return false; return ( (draft.title !== undefined && draft.title !== proposal.title) || (draft.body !== undefined && draft.body !== (proposal.body ?? proposal.proposedAnswer ?? "")) || (draft.rationale !== undefined && draft.rationale !== (proposal.rationale ?? "")) ); } async function saveDraft(proposal: ReviewItem) { if (!hasDraftChanges(proposal)) return; await updateProposal.mutateAsync({ proposalId: proposal.id, title: draftValue(proposal, "title"), body: draftValue(proposal, "body"), rationale: draftValue(proposal, "rationale"), }); setDrafts((current) => { const next = { ...current }; delete next[proposal.id]; return next; }); } async function approve( proposal: ReviewItem, options: { confirmedCanonical?: boolean } = {}, ) { if (canonicalChoice(proposal) && !options.confirmedCanonical) { await openCanonicalPreview(proposal); return; } await saveDraft(proposal); await approveProposal.mutateAsync({ proposalId: proposal.id, reviewerNotes: cleanNote(notes[proposal.id]), publishCanonical: canonicalChoice(proposal), }); } async function openCanonicalPreview(proposal: ReviewItem) { const result = await previewCanonical.mutateAsync({ proposalId: proposal.id, operation: "publish", draft: { title: draftValue(proposal, "title"), body: draftValue(proposal, "body"), }, }); setCanonicalPreview(result.preview); setPreviewProposal(proposal); setPreviewOpen(true); } async function approvePreviewedProposal() { if (!previewProposal) return; await approve(previewProposal, { confirmedCanonical: true }); setPreviewOpen(false); setCanonicalPreview(null); setPreviewProposal(null); } async function reject(proposal: ReviewItem) { await rejectProposal.mutateAsync({ proposalId: proposal.id, reviewerNotes: cleanNote(notes[proposal.id]), }); } function canonicalChoice(proposal: ReviewItem) { return ( canonicalChoices[proposal.id] ?? readBoolean(proposal.payload?.publishCanonical) ); } function setCanonicalChoice(proposalId: string, publishCanonical: boolean) { setCanonicalChoices((current) => ({ ...current, [proposalId]: publishCanonical, })); } function toggleProposalEditing(proposalId: string) { setEditingProposalIds((current) => { const next = new Set(current); if (next.has(proposalId)) next.delete(proposalId); else next.add(proposalId); return next; }); } function handleProposalShortcut( event: KeyboardEvent, proposal: ReviewItem, ) { if (pendingMutation || proposal.status !== "pending") return; if (isEditableTarget(event.target)) return; if (event.metaKey || event.ctrlKey || event.altKey) return; const key = event.key.toLowerCase(); if (key === "a") { event.preventDefault(); void approve(proposal); } else if (key === "r") { event.preventDefault(); void reject(proposal); } else if (key === "s" && hasDraftChanges(proposal)) { event.preventDefault(); void saveDraft(proposal); } } return (
{summary}
} />
{reviewQuery.isLoading ? ( ) : proposals.length ? (
{proposals.map((proposal) => { if (status === "quarantine") { return ( ); } const evidence = proposal.evidence ?? []; const sourceUrl = firstSourceUrl(proposal); const canReview = proposal.status === "pending"; const insight = buildProposalInsight(proposal, t); const hasChanges = hasDraftChanges(proposal); const publishCanonical = canonicalChoice(proposal); const selected = selectedProposalId === proposal.id; const editing = editingProposalIds.has(proposal.id); return ( handleProposalShortcut(event, proposal)} className={cn( "shadow-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", selected && "ring-2 ring-ring", )} aria-label={ canReview ? t("review.cardAria", { title: proposal.title }) : proposal.title } >
{proposal.title}
{formatDate(proposal.createdAt, t)} / {proposal.createdBy ?? t("review.reviewerQueue")} {hasChanges ? ( <> / {t("review.unsavedEdits")} ) : null}

{draftValue(proposal, "body") || t("review.noProposedKnowledge")}

{editing ? (
patchDraft(proposal.id, { title: event.target.value, }) } />