diff --git a/web/src/app/(app)/reviews/page.tsx b/web/src/app/(app)/reviews/page.tsx index 382b913..35884db 100644 --- a/web/src/app/(app)/reviews/page.tsx +++ b/web/src/app/(app)/reviews/page.tsx @@ -1,17 +1,19 @@ "use client"; import Link from "next/link"; -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { AppShell } from "@/components/AppShell"; import { AgentTimeline } from "@/components/AgentTimeline"; import { ProposalReviewCard } from "@/components/ProposalReviewCard"; -import { approveReviewItem, listAgentTimeline, listApprovalQueue, rejectReviewItem } from "@/lib/review-client"; +import { approveReviewItem, batchReviewItems, listAgentTimeline, listApprovalQueue, rejectReviewItem } from "@/lib/review-client"; import type { AgentTimelineItem, ApprovalQueueItem } from "@/lib/types"; export default function ReviewsPage() { const [approvalQueue, setApprovalQueue] = useState([]); const [timeline, setTimeline] = useState([]); const [selectedApprovalId, setSelectedApprovalId] = useState(null); + const [selectedBatchIds, setSelectedBatchIds] = useState>(new Set()); + const [reviewNote, setReviewNote] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); @@ -37,6 +39,29 @@ export default function ReviewsPage() { () => approvalQueue.find((item) => item.id === selectedApprovalId) ?? approvalQueue[0] ?? null, [approvalQueue, selectedApprovalId], ); + + const batchMode = selectedBatchIds.size > 0; + + const toggleBatchItem = useCallback((id: string) => { + setSelectedBatchIds((current) => { + const next = new Set(current); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }, []); + + const selectAllForBatch = useCallback(() => { + setSelectedBatchIds(new Set(approvalQueue.map((item) => item.id))); + }, [approvalQueue]); + + const clearBatch = useCallback(() => { + setSelectedBatchIds(new Set()); + }, []); + const operatorWorkflows = [ { id: "workflow-approvals", @@ -70,12 +95,13 @@ export default function ReviewsPage() { } setIsSubmitting(true); + const note = reviewNote.trim() || undefined; try { const updated = decision === "approved" - ? await approveReviewItem(featuredProposal) - : await rejectReviewItem(featuredProposal); + ? await approveReviewItem(featuredProposal, note) + : await rejectReviewItem(featuredProposal, note); setApprovalQueue((current) => { const nextQueue = current.filter((item) => item.id !== featuredProposal.id); @@ -88,13 +114,14 @@ export default function ReviewsPage() { { id: updated.id, actor: updated.owner, - action: `human ${decision === "approved" ? "approved" : "rejected"} proposal`, + action: `human ${decision} proposal`, timestamp: new Date().toISOString(), status: updated.status, summary: updated.after ?? updated.title, }, ...current, ]); + setReviewNote(""); } catch (err) { setError(err instanceof Error ? err.message : "Unable to update review state"); } finally { @@ -102,6 +129,41 @@ export default function ReviewsPage() { } } + async function handleBatchDecision(decision: "approved" | "rejected") { + const batchItems = approvalQueue.filter((item) => selectedBatchIds.has(item.id)); + if (batchItems.length === 0) return; + + setIsSubmitting(true); + const note = reviewNote.trim() || undefined; + + try { + await batchReviewItems(batchItems, decision, note); + + setApprovalQueue((current) => { + const nextQueue = current.filter((item) => !selectedBatchIds.has(item.id)); + setSelectedApprovalId(nextQueue[0]?.id ?? null); + return nextQueue; + }); + setTimeline((current) => [ + { + id: `batch-${Date.now()}`, + actor: "reviewer", + action: `batch ${decision} ${batchItems.length} proposals`, + timestamp: new Date().toISOString(), + status: decision, + summary: note ?? `${batchItems.length} items ${decision}`, + }, + ...current, + ]); + setSelectedBatchIds(new Set()); + setReviewNote(""); + } catch (err) { + setError(err instanceof Error ? err.message : "Batch review failed"); + } finally { + setIsSubmitting(false); + } + } + return (
Approval queue
-
- severity:medium+ +
+ {batchMode ? ( + <> + {selectedBatchIds.size} selected + + + ) : ( + + )} status:pending - owner:any
{error ?
{error}
: null} + + {batchMode ? ( +
+ + +
+ ) : null} +
{approvalQueue.map((item) => (