# Extraction review

The fields an agent extracted from a document, beside its pages, for a person to accept or correct. Each field shows its value, the confidence with what it rests on, and where on the page it came from: selecting a field selects its region on the page, and selecting a region picks out its field. The person accepts a value as it is, or changes it; a changed value is marked "Changed by you" and the agent's value stays beside it. Nothing starts accepted.

The summary counts what is accepted, changed and still open, and Send review reports exactly those decisions through `onSubmit`, in the fields' order: open fields aren't sent (and with `requireAll`, Send review waits for them). A changed value that matches the agent's counts as accepted. A decision belongs to the value it was made on: when `fields` brings a new value for a field the person had decided, the field is open again and says why. Pages and regions are drawn by DocumentViewer, so the pages are the app's to render.

- Registry name: `extraction-review` (`@candor/extraction-review`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/extraction-review

## Install

```bash
npx candor-ui add extraction-review
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/extraction-review`.
It brings in: ai-content-label, button, confidence-mark, document-viewer, input, know, locale, locked, textarea, tokens, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import type { DocumentPage } from "@/components/candor/document-viewer"
import { ExtractionReview, type ExtractedField, type FieldReview } from "@/components/candor/extraction-review"

// The app renders the page (pdf.js onto a canvas, or an image from its server). An SVG stands in for it here, in the
// page's own units: 612 × 792 PDF points.
function InvoicePage() {
  return (
    <svg viewBox="0 0 612 792" aria-hidden className="bg-white text-neutral-800">
      <text x="48" y="80" fontSize="28" fontWeight="600" fill="currentColor">Invoice</text>
      <text x="400" y="74" fontSize="12" fill="currentColor">Date</text>
      <text x="400" y="96" fontSize="14" fill="currentColor">3 Nov 2026</text>
      <text x="48" y="150" fontSize="12" fill="currentColor">Bill to</text>
      <text x="48" y="172" fontSize="14" fill="currentColor">Northwind Traders Ltd</text>
      <text x="400" y="150" fontSize="12" fill="currentColor">Purchase order</text>
      <text x="400" y="172" fontSize="14" fill="currentColor">PO-7731</text>
      {[0, 1, 2, 3].map((i) => (
        <rect key={i} x="60" y={276 + i * 40} width={300 - i * 40} height="10" fill="currentColor" fillOpacity="0.15" />
      ))}
      <text x="400" y="560" fontSize="12" fill="currentColor">Total due</text>
      <text x="400" y="590" fontSize="20" fontWeight="600" fill="currentColor">€4,820.00</text>
    </svg>
  )
}

const pages: DocumentPage[] = [{ id: "p1", width: 612, height: 792, content: <InvoicePage />, alt: "Invoice INV-2041, page 1" }]

const fields: ExtractedField[] = [
  { id: "customer", label: "Customer", value: "Northwind Traders Ltd", confidence: "high", basis: "Printed text, read clearly.", region: { page: "p1", box: { x: 40, y: 134, width: 260, height: 48 } } },
  { id: "date", label: "Invoice date", value: "3 November 2026", confidence: "medium", basis: "The year is printed as two digits elsewhere on the page.", region: { page: "p1", box: { x: 392, y: 58, width: 170, height: 48 } } },
  { id: "po", label: "Purchase order", value: "PO-7731", confidence: "high", basis: "Matches the PO format.", region: { page: "p1", box: { x: 392, y: 134, width: 170, height: 48 } } },
  { id: "total", label: "Total due", value: "4820.00 EUR", confidence: "high", basis: "Printed, with the currency sign.", region: { page: "p1", box: { x: 392, y: 544, width: 170, height: 56 } } },
  { id: "terms", label: "Payment terms", value: "30 days", confidence: "low", basis: "Not on the page; taken from the customer's usual terms." },
]

export default function Example() {
  const [sent, setSent] = React.useState<FieldReview[] | null>(null)
  return (
    <div className="flex flex-col gap-3">
      <ExtractionReview title="Invoice INV-2041.pdf" pages={pages} fields={fields} agent="Intake agent" onSubmit={setSent} className="h-[40rem]" />
      {sent && <pre className="overflow-x-auto rounded-md bg-sunken p-3 text-xs">{JSON.stringify(sent, null, 2)}</pre>}
    </div>
  )
}
```

## <ExtractionReview>

Props:

- `fields: ExtractedField[]`: The extracted fields, in the order to review them.
- `pages: DocumentPage[]`: The document's pages, drawn by the app (see DocumentViewer).
- `onSubmit: (reviews: FieldReview[]) => void | Promise<void>`: Called by Send review with the fields the person accepted or changed, and nothing else. Return a promise to keep the button busy until it settles; a rejection says the review wasn't sent.
- `agent?: string | undefined`: The agent that extracted the values, named by the app ("Intake agent"). Never model text.
- `requireAll?: boolean | undefined` (default `false`): Keep Send review locked until every field is accepted or changed. Defaults to false: open fields stay open.
- `locked?: string | undefined`: Why the person can't decide these fields (they can read them but not approve them). Every decision says so.
- `title?: string | undefined`: The document's name, for the viewer ("Invoice INV-2041.pdf").
- `onPageChange?: ((pageId: string) => void) | undefined`: Passed to DocumentViewer: called with a page's id when the page shown changes, so the app can draw it.
- `onRetry?: ((pageId: string) => void) | undefined`: Passed to DocumentViewer: adds Try again to a page that couldn't be drawn.
- `headingLevel?: 1 | 2 | 3 | 4 | 5 | 6 | undefined` (default `2`): The level of the fields' heading, to fit the page's outline. Defaults to 2.
- `className?: string | undefined`

## Strings (`extractionReview`)

The fixed words, English by default. Override any of them under `extractionReview` in `<LocaleProvider strings={{ extractionReview: { … } }}>`; the table is exported as `extractionReviewStrings`.

- `heading`: Extracted fields
- `from`: Values {agent} extracted. Nothing is accepted until you accept it.
- `fromUnnamed`: Values an agent extracted. Nothing is accepted until you accept it.
- `open`: Open
- `accepted`: Accepted
- `changed`: Changed by you
- `agentValue`: {agent}'s value
- `agentValueUnnamed`: The agent's value
- `yourValue`: Your value
- `empty`: Empty
- `noBasis`: The agent gave no basis for this.
- `accept`: Accept
- `acceptField`: Accept {label}
- `change`: Change
- `changeField`: Change {label}
- `undoChange`: Undo change
- `undoChangeField`: Undo your change to {label}
- `editLabel`: Your value for {label}
- `save`: Save
- `cancel`: Cancel
- `showOnPage`: Show on page {page}
- `showOnPageField`: Show {label} on page {page}
- `noLocation`: No location recorded
- `pageMissing`: Its page isn't in this document
- `summaryAccepted`: {count} accepted
- `summaryChanged`: {count} changed by you
- `summaryOpen`: {count} still open
- `submit`: Send review
- `sending`: Sending…
- `lockedSending`: The review is being sent.
- `alreadySent`: Already sent. Change a decision to send again.
- `valueChanged`: The agent's value changed after you decided, so this field is open again.
- `openNotSent`: { one: "The open field isn't sent; it stays open.", other: "The {count} open fields aren't sent; they stay open." }
- `nothingDecided`: Accept or change a field first.
- `stillOpen`: { one: "{count} field is still open.", other: "{count} fields are still open." }
- `sent`: { one: "Sent {count} field.", other: "Sent {count} fields." }
- `failed`: The review wasn't sent. Try again.
- `none`: The agent extracted no fields from this document.

## Types

### ExtractedField

One value an agent extracted from the document. The value is model output: it is shown as text, never as HTML or a link.

```ts
{
  /** Stable for this field; reviews and the field's region use it. */
  id: string;
  /** What the field is, named by the app ("Invoice total"). */
  label: string;
  /** The value the agent extracted, as text. */
  value: string;
  /** How sure the extraction is. Left out, it shows as unknown, never as low or high. */
  confidence?: Confidence | undefined;
  /** What the confidence rests on ("Printed text, read clearly"), shown on hover and to screen readers. */
  basis?: string | undefined;
  /** Where on the page the value came from, as the extraction recorded it. Left out, the field says no location was recorded. */
  region?: {
    page: string;
    box: RegionBox;
    kind?: RegionKind | undefined;
  } | undefined;
}
```

### FieldDecision

What the person did with a field.

```ts
"accepted" | "changed"
```

### FieldReview

One field the person accepted or changed, as `onSubmit` reports it.

```ts
{
  /** The field's id. */
  id: string;
  /** "accepted": the agent's value as it is. "changed": the person's own value. */
  decision: FieldDecision;
  /** The value the person settled on: the agent's for "accepted", theirs for "changed". */
  value: string;
  /** The value the agent extracted, kept for the record. */
  agentValue: string;
}
```

## Types from other items

The props above take these. Each is defined by another item; import it from the path shown.

### Confidence

Import: `import type { Confidence } from "@/lib/candor/know"` (item `know`, https://candor.themethodic.com/docs/know.md)

```ts
"high" | "medium" | "low" | "unknown"
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
