Skip to content
Tier

Extraction review

Pro

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.

Markdown
Version 5.2.0· other versions

This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).

Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0 and 2.8.0: the page came later, or had another name then.

Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.

Preview

Loading the preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/extraction-review

Brings in: ai-content-label, bidi, button, confidence-mark, document-viewer, heading-level, heading-scope, input, know, locale, locked, textarea, tokens, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"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>

PropTypeDefaultDescription
fieldsrequiredExtractedField[]—The extracted fields, in the order to review them.
pagesrequiredDocumentPage[]—The document's pages, drawn by the app (see DocumentViewer).
onSubmitrequired(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.
agentstring | undefined—The agent that extracted the values, named by the app ("Intake agent"). Never model text.
requireAllboolean | undefinedfalseKeep Send review locked until every field is accepted or changed. Defaults to false: open fields stay open.
lockedstring | null | undefined—Why the person can't decide these fields (they can read them but not approve them). Every decision says so. `null` or left out: unlocked.
titlestring | 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.
headingLevelHeadingLevel | undefined—The level of the fields' heading. Defaults to one below the nearest `HeadingScope`, else 2.
classNamestring | undefined—Extra classes for the outer element.

Strings

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

KeyEnglish
headingExtracted fields
fromValues {agent} extracted. Nothing is accepted until you accept it.
fromUnnamedValues an agent extracted. Nothing is accepted until you accept it.
openOpen
acceptedAccepted
changedChanged by you
agentValue{agent}'s value
agentValueUnnamedThe agent's value
yourValueYour value
emptyEmpty
noBasisThe agent gave no basis for this.
acceptAccept
acceptFieldAccept {label}
changeChange
changeFieldChange {label}
undoChangeUndo change
undoChangeFieldUndo your change to {label}
editLabelYour value for {label}
saveSave
cancelCancel
showOnPageShow on page {page}
showOnPageFieldShow {label} on page {page}
noLocationNo location recorded
pageMissingIts page isn't in this document
summaryAccepted{count} accepted
summaryChanged{count} changed by you
summaryOpen{count} still open
submitSend review
sendingSending…
lockedSendingThe review is being sent.
alreadySentAlready sent. Change a decision to send again.
valueChangedThe 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." }
nothingDecidedAccept 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." }
failedThe review wasn't sent. Try again.
noneThe 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.
{ /** 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.
"accepted" | "changed"
FieldReview
One field the person accepted or changed, as `onSubmit` reports it.
{ /** 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.

HeadingLevel from @/lib/candor/heading-level
A heading's level in the page's outline: `1` is `<h1>`, `6` is `<h6>`. Every Candor item that draws a heading takes its `headingLevel` as this type; the level is where the heading sits in the outline, never how large it looks.
1 | 2 | 3 | 4 | 5 | 6
Confidence from @/lib/candor/know
"high" | "medium" | "low" | "unknown"