affectUI › Components › Agents
Review Center
A focused workspace for what your agents want to do – the queue sorted by risk, one item at a time with the shaky spans marked, Approve, Edit, Reject and Skip on single keys, a stamp as each one leaves and the next one rising in.
Part of affectUI Pro. Install: npx shadcn@latest add https://www.affectui.com/r/review-center.json · Import from @/components/ui/review-center
When to use
- Human in the loop at volume: a person signs off what agents produced – code changes, migrations, plans, contract edits, refunds, payments – one item after another
- Code review for agents: a multi-file change as file rows with +/- counts and a unified diff with line numbers, the risky lines flagged with the agent's own note
- When the reviewer has to see why the agent wants something and where it is unsure, not only what it wants
- Review queues with a service level: oldest items and items close to their deadline need to stand out
When not to use
- A board of background tasks with their status – use Agent Inbox
- A handful of yes/no questions answered in passing – use Decision Inbox
- One action that waits for a go, with a countdown – use Approval Card
- An agent asking for access to a tool or scope – use Permission Request
- An agent stuck on a decision that lays out its options and their consequences – use Decision Tree
Usage
import { ReviewCenter, type ReviewItem } from "@/components/ui/review-center";
const items: ReviewItem[] = [
{
id: "refund-4821",
title: "Refund order #4821 for Maya Chen",
agent: "Support agent", model: "Sonnet 5.5",
type: "Refund", risk: "high", confidence: 0.62,
createdAt: Date.now() - 26 * 60_000,
reasoning: "Her blender arrived cracked – damaged items are covered within 30 days.",
fields: [
// A flagged span: marked in the proposal, its note shows on hover or with F
{ label: "Amount", value: [{ text: "$184.00", note: "Includes $20 express shipping.", source: "Refund policy §4" }] },
{ label: "Order", value: "#4821" },
],
bodyLabel: "Reply to Maya",
body: "Hi Maya, I'm so sorry your blender arrived cracked …",
},
];
export function Reviews() {
return (
<ReviewCenter
items={items}
reviewed={24}
onDecide={(item, result) => save(item.id, result)} // { decision, reason?, text?, fields? }
onUndo={(item) => revert(item.id)}
/>
);
}Props
| Name | Type | Default | Description |
|---|---|---|---|
items | ReviewItem[] | { id, title, agent, model?, icon?, type, risk, confidence, createdAt, reasoning?, fields?, body?, bodyLabel?, format?, sources? } – everything waiting; decided items hide themselves, new ids slide into the queue | |
risk | "high" | "medium" | "low" | Per item – sorts the queue (high first), feeds the High risk filter and shows as a small tag next to the agent (high and medium) | |
confidence | number | Per item, 0–1 – below lowConfidence the item counts as Unsure | |
fields / body | ReviewText | Per item – a string, or an array of strings and { text, note, source? } flags; flags are marked and explain themselves on hover, focus or F. Edit (E) works on these two | |
files | ReviewFile[] | Per item – a code change: { path, added?, removed?, diff?, start? } – a row per file, a unified diff with line numbers and a light syntax tint under the files that have one | |
steps | ReviewStep[] | Per item – a plan: { title, tool?, icon?, detail? } as numbered steps; a step with a flag gets an amber number | |
format | "text" | "code" | "text" | Per item – "code" shows the body as a diff, like a file's |
onDecide | (item, result) => void | Approved or rejected – result is { decision, reason?, text?, fields? }; text and fields only when the reviewer edited them | |
onUndo | (item, result) => void | The last decision was taken back – put the item back if you dropped it from items | |
onSkip | (item) => void | The reviewer moved on without deciding; the row is marked skipped | |
reviewed | number | 0 | How many were reviewed before (e.g. today) – the header counts on from here |
total | number | reviewed + waiting | The total in the header |
sla | number | 1800000 | How long an item may wait, in ms – waiting times turn amber at 75% and red past it |
now | number | The clock (epoch ms) – defaults to the real one, ticking every 10s | |
lowConfidence | number | 0.7 | Below this an item is Unsure |
reasons | string[] | ["Wrong info", "Tone", "Not allowed", "Other"] | The quick reasons for a rejection, picked with 1–4 |
order | "risk" | "oldest" | "given" | "risk" | High risk first then oldest, oldest first, or as passed |
filter / defaultFilter | "all" | "high" | "unsure" | "all" | The queue's filter, controlled or uncontrolled |
onFilterChange | (filter) => void | The filter changed | |
ref | Ref<ReviewCenterHandle> | approve, reject(reason?), edit, setDraft, save, cancel, skip, next, previous, undo, peek(index), select(id) | |
title | string | "Review center" | Heading of the bar |
className | string | Extra class on the root |
Keyboard
- A: Approves the current item
- E: Edits the proposal in place – Cmd/Ctrl+Enter approves with the edits, Esc cancels
- R: Rejects – then 1–4 picks the reason, Esc cancels
- S: Skips to the next item without deciding
- J / K: Next and previous item (ArrowDown and ArrowUp work too)
- F: Shows the agent's note on the next flagged span
- Z: Undoes the last decision (Cmd/Ctrl+Z too)
- Esc: Closes the reason picker, the editor or a note
Accessibility
- Shortcuts work while the focus is anywhere inside – a click on plain content keeps them working; buttons carry aria-keyshortcuts
- Every action is a real button; the reason picker is a labelled group
- Queue rows show only the title, the type and the waiting time; as buttons they are labelled with title, type, risk, confidence and waiting time; the current one has aria-current
- Flagged spans are focusable and point to their note with aria-describedby (role tooltip)
- Decisions, skips and undos are announced in a polite live region, with the new count
- When the row or field you were on leaves, the focus moves to the next item instead of getting lost
Responsive
- Built on a container query: wide, the queue sits on the left and the item on the right, in a fixed-height workspace
- Under 640px the queue goes on top, three rows high, the item under it; its box glides to each item's height; buttons share the width
- Under 420px the agent line wraps between its parts instead of cutting them, button icons step aside and the flag counter hides – flags are tapped directly; diffs scroll sideways
- Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"
Requirements
- React 19
Related
- Decision Tree – An agent stuck on a decision lays out its options as a tree – the consequences of each branch, the risky ones marked, its own pick highlighted – and you choose a path or write your own
- Agent Inbox – All your background agent tasks in one calm list – live clocks and status tags, filter tabs with rolling counts, rows that unfold to the last steps with Approve, Reject, Retry or Open, and glide to their new place
- Decision Inbox – Open decisions with two quick answers each – answered ones confirm and fold away
- Approval Card – An agent asks before it acts – risk badge, preview, Approve and Deny with shortcuts, a pausing countdown, and a smooth morph into a resolved row
Guides
- Human-in-the-loop UI: approvals, permissions and autonomy – UI patterns for keeping people in control of AI agents – approval cards, permission requests, autonomy levels, steering and reviewing changes.