diff --git a/web/src/app/(app)/reviews/page.tsx b/web/src/app/(app)/reviews/page.tsx index 8fbf293..73b3166 100644 --- a/web/src/app/(app)/reviews/page.tsx +++ b/web/src/app/(app)/reviews/page.tsx @@ -1,21 +1,19 @@ "use client"; -import Link from "next/link"; import { useCallback, useEffect, useMemo, useState } from "react"; import { AppShell } from "@/components/AppShell"; -import { AgentTimeline } from "@/components/AgentTimeline"; -import { ProposalReviewCard } from "@/components/ProposalReviewCard"; import { Badge, - Button, - ListItemButton, Panel, - PanelBody, - PanelHeader, - PanelTitle, - StatusBadge, - Textarea, } from "@/components/ui/Primitives"; +import { + ProposalDiffCard, + ReviewDecisionBar, + ReviewNoteField, + ReviewQueueList, + ReviewTimeline, + ReviewWorkflowNav, +} from "@/components/reviews/ReviewWorkflow"; import { approveReviewItem, batchReviewItems, listAgentTimeline, listApprovalQueue, rejectReviewItem } from "@/lib/review-client"; import { toast } from "@/lib/toast"; import type { AgentTimelineItem, ApprovalQueueItem } from "@/lib/types"; @@ -93,14 +91,6 @@ export default function ReviewsPage() { }, ] as const; - function getWorkflowHref(workflow: (typeof operatorWorkflows)[number]) { - if (workflow.id === "workflow-agent-activity") { - return "/reviews"; - } - - return "/reviews"; - } - async function handleDecision(decision: "approved" | "rejected") { if (!featuredProposal) { return; @@ -189,131 +179,41 @@ export default function ReviewsPage() { actions={Operator workflow shell} > - - - Operator workflows - - - {operatorWorkflows.map((workflow) => ( - - {workflow.name} - Owner: {workflow.owner} - - - {workflow.status} - - Queue: {workflow.queueCount} - SLA {workflow.sla} - - - ))} - - + - - Approval queue - - {batchMode ? ( - <> - {selectedBatchIds.size} selected - Clear - > - ) : ( - Select all - )} - status:pending - - + void handleBatchDecision(decision)} + /> {error ? {error} : null} - {batchMode ? ( - - void handleBatchDecision("approved")} - > - Approve {selectedBatchIds.size} - - void handleBatchDecision("rejected")} - variant="destructive" - > - Reject {selectedBatchIds.size} - - - ) : null} - - - {approvalQueue.map((item) => ( - batchMode ? toggleBatchItem(item.id) : setSelectedApprovalId(item.id)} - style={{ - display: "flex", - justifyContent: "space-between", - gap: "var(--nl-space-3)", - alignItems: "center", - flexWrap: "wrap", - textAlign: "left", - }} - > - - {item.title} - {item.owner} - - - {batchMode ? ( - {selectedBatchIds.has(item.id) ? "selected" : "unselected"} - ) : null} - {item.severity} - - {item.status} - - - - ))} - + batchMode ? toggleBatchItem(id) : setSelectedApprovalId(id)} + /> - - - setReviewNote(e.target.value)} - placeholder="Optional: add a note explaining your decision…" - rows={2} - /> - - + {featuredProposal && !batchMode ? ( - void handleDecision("approved")} onReject={() => void handleDecision("rejected")} isSubmitting={isSubmitting} /> ) : null} - + ); } diff --git a/web/src/components/reviews/ReviewWorkflow.tsx b/web/src/components/reviews/ReviewWorkflow.tsx new file mode 100644 index 0000000..4b43e07 --- /dev/null +++ b/web/src/components/reviews/ReviewWorkflow.tsx @@ -0,0 +1,216 @@ +import Link from "next/link"; +import { AgentTimeline } from "@/components/AgentTimeline"; +import { ProposalReviewCard } from "@/components/ProposalReviewCard"; +import { + Badge, + Button, + ListItemButton, + Panel, + PanelBody, + PanelHeader, + PanelTitle, + StatusBadge, + Textarea, +} from "@/components/ui/Primitives"; +import type { AgentTimelineItem, ApprovalQueueItem } from "@/lib/types"; + +type ReviewWorkflow = { + id: string; + name: string; + owner: string; + queueCount: number; + sla: string; + status: "healthy" | "at_risk"; +}; + +type ReviewDecision = "approved" | "rejected"; + +export function ReviewWorkflowNav({ workflows }: { workflows: readonly ReviewWorkflow[] }) { + return ( + + + Operator workflows + + + {workflows.map((workflow) => ( + + {workflow.name} + Owner: {workflow.owner} + + + {workflow.status} + + Queue: {workflow.queueCount} + SLA {workflow.sla} + + + ))} + + + ); +} + +export function ReviewDecisionBar({ + batchMode, + selectedCount, + isSubmitting, + onSelectAll, + onClear, + onBatchDecision, +}: { + batchMode: boolean; + selectedCount: number; + isSubmitting: boolean; + onSelectAll: () => void; + onClear: () => void; + onBatchDecision: (decision: ReviewDecision) => void; +}) { + return ( + <> + + Approval queue + + {batchMode ? ( + <> + {selectedCount} selected + + Clear + + > + ) : ( + + Select all + + )} + status:pending + + + + {batchMode ? ( + + onBatchDecision("approved")} + > + Approve {selectedCount} + + onBatchDecision("rejected")} + variant="destructive" + > + Reject {selectedCount} + + + ) : null} + > + ); +} + +export function ReviewQueueList({ + items, + batchMode, + selectedBatchIds, + selectedApprovalId, + onSelectItem, +}: { + items: ApprovalQueueItem[]; + batchMode: boolean; + selectedBatchIds: Set; + selectedApprovalId: string | null; + onSelectItem: (id: string) => void; +}) { + return ( + + {items.map((item) => ( + onSelectItem(item.id)} + style={{ + display: "flex", + justifyContent: "space-between", + gap: "var(--nl-space-3)", + alignItems: "center", + flexWrap: "wrap", + textAlign: "left", + }} + > + + {item.title} + {item.owner} + + + {batchMode ? ( + {selectedBatchIds.has(item.id) ? "selected" : "unselected"} + ) : null} + {item.severity} + + {item.status} + + + + ))} + + ); +} + +export function ReviewNoteField({ + value, + onChange, +}: { + value: string; + onChange: (value: string) => void; +}) { + return ( + + + onChange(event.target.value)} + placeholder="Optional: add a note explaining your decision..." + rows={2} + /> + + + ); +} + +export function ProposalDiffCard({ + proposal, + isSubmitting, + onApprove, + onReject, +}: { + proposal: ApprovalQueueItem | null; + isSubmitting: boolean; + onApprove: () => void; + onReject: () => void; +}) { + if (!proposal) { + return null; + } + + return ( + + ); +} + +export function ReviewTimeline({ items }: { items: AgentTimelineItem[] }) { + return ; +}