Installation
npx shadcn@latest add @pixeldosa/ai-triage-tableRequires `cn` at `@/lib/utils`. Installing this pulls in AI Action Toolbar and Confidence Meter. Pass `unchanged` whenever you have it — showing what the AI deliberately left alone is half of what makes review cheap.
Usage
"use client";
import { AITriageTable } from "@/components/ui/ai-triage-table";
<AITriageTable
title="Enrich 48 accounts"
items={proposals} // { id, label, before?, after, confidence, provenance? }[]
unchanged={untouched} // { id, label }[]
onApply={(acceptedIds) => save(acceptedIds)}
/>
The problem it solves
AI now produces changes faster than people can check them. Research on AI products calls this the audit burden: people aren't doing less work, they're supervising the machine.
Every other review component helps someone check AI output. This one reduces how much checking there is — without ever quietly accepting anything.
Features
- Attention order, not arrival order. Low-confidence changes open first because they're where errors live. High-confidence changes start collapsed to a count.
- What didn't change is one line. "40 checked and left unchanged" — nobody scans forty rows to confirm nothing happened. Expand it if you want to see them.
- Before and after, side by side. The old value struck through, the proposal beside it, the source underneath.
- Nothing is written until Apply. Decisions are staged; every row has a one-step Undo. Apply states exactly how many changes it will commit, and that anything unreviewed stays as it is.
What it refuses to do
It is not "Accept all" with better styling.
- Bulk accept exists only for the high-confidence group — low and medium changes must be decided one by one.
- And only after that group has been opened. Until then it says "Open to review before accepting" in plain text, rather than showing a disabled button that explains nothing.
Accepting is where risk lives, so that's where friction stays. Everything else gets out of the way.
Examples
Only pass what actually changed as items. If the AI produced a value identical to the
current one, it belongs in unchanged:
const items = results.filter((r) => r.after !== r.before);
const unchanged = results.filter((r) => r.after === r.before).map(({ id, label }) => ({ id, label }));
Gating a consequential apply behind an approval:
const [pending, setPending] = React.useState<string[] | null>(null);
<AITriageTable items={items} unchanged={unchanged} onApply={setPending} />
{pending ? (
<AIApprovalGate
action={`Update ${pending.length} customer records`}
reversible
impact={[{ label: "Records", value: String(pending.length) }]}
onApprove={() => save(pending)}
onReject={() => setPending(null)}
/>
) : null}
Props
| Prop | Type | Description |
|---|---|---|
items | TriageItem[] | Required. { id, label, before?, after, confidence, provenance? }. |
unchanged | { id, label }[] | What the AI examined and left alone. |
onApply | (acceptedIds: string[]) => void | Required. The only point anything is committed. |
title | string | Defaults to "Review proposed changes". |
headingLevel | 2 | 3 | 4 | 5 | 6 | Heading level, so the component fits your page's outline. Defaults to 3. |
Accessibility
- A labelled
<section>with a live "N of M reviewed" count. - Each group header is a real disclosure with
aria-expandedandaria-controls. - Collapsed groups are
inert— hidden rows can't be tabbed into or read out. - Every row's decision controls are a keyboard toolbar; Undo is always reachable.
- Before/after is marked by strikethrough and underline plus a screen-reader "changes to", never colour alone.
When not to use
One field is Smart Field. One form is AI Form Fill. A text rewrite is Diff Accept.
Use this when AI output arrives in volume.
why it's built this way
Engineering Notes
Built in direct response to the audit burden: AI now produces changes faster than people can check them, and every earlier component in this system enables review without reducing it, which at volume turns the user into a full-time auditor. Triage reduces review in three ways. It orders by attention rather than by arrival — low confidence first, high confidence collapsed — so effort lands where errors are likely. It collapses what the AI left unchanged into a count, since confirming that nothing happened is otherwise the most tedious part of reviewing a batch. And it states consequences at the point of commitment ('Apply 5', 'not reviewed will stay as they are'). The hard boundary is that it must not become Accept-all by another name. Bulk acceptance is limited to the high-confidence group, and gated on that group having been opened — tracked as an explicit inspected set, with the reason stated in text rather than hidden behind a disabled button. Decisions are staged rather than committed, so every row's Undo is simply removing a staged decision, with no undo timer to manage. Collapsed groups use the house grid-template-rows pattern with overflow-hidden, and are marked inert while closed: a 0fr row hides content visually but otherwise leaves its buttons focusable and exposed to assistive technology.
how it moves, and when it doesn't
Motion Notes
Deliberately quiet: this is a user-turn surface, so nothing moves on its own and nothing shimmers. Groups expand and collapse through the house grid-template-rows transition at `--pd-duration-fast`, the disclosure chevrons rotate at `--pd-duration-instant`, and a decided row fades to reduced opacity over `--pd-duration-fast` so the eye moves on to what remains without the row disappearing — its Undo stays reachable. Action colours transition through the composed AI Action Toolbar. There is no entrance animation and no motion tied to counts changing, since numbers updating in a review surface should read as information rather than event. Under prefers-reduced-motion every transition drops to instant and the grouping, counts and decided-state opacity still carry all the information.