Skip to content
Star on GitHub
Sources and outputField extraction
Style
Accent
Sound

Built by DesignMe, free to use under the MIT licence.

Expert design and dev support for startups and enterprises.

Book a call

Field extraction

The agent pulls fields out of a document and shows where each came from. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.

$npx designme add field-extraction
Northwind StudioInvoice
Invoice no. NW-2041
Issued 3 Oct 2026
Design system audit$4,800.00
Pricing page redesign$6,200.00
Agent setup, 2 seats$480.00
Total due$11,480.00
Due by 1? Nov 2026
Extracted fieldsReading page 1
01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add field-extraction

React

$npx designme add field-extraction --react

The CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro.

02

About

What it is for and what it does.

Field extraction shows a document next to the fields an agent reads from it, such as an invoice number, date and total. Each field shows how sure the agent is, lights up the part of the page it came from, and asks the person to check it when confidence is low.

  • When an agent pulls structured data out of invoices, receipts, forms or contracts.
  • When people need to see where on the page each value came from.
  • When low-confidence values should be checked by a person before the data is saved.
  • When the result goes on to another tool, like a spreadsheet, once everything is confirmed.
03

Usage

Import it and drive it from your own state.

Import

import { FieldExtraction } from '@/components/designme/field-extraction/FieldExtraction';

Example

The demo above as a React component, 58 lines you can copy.

'use client';
// The animated preview from designme.dev, rebuilt from the React component: an example of driving it from your own state.
import { useEffect, useRef, useState } from 'react';
import { FieldExtraction, FieldZone, type ExtractedField } from './FieldExtraction';
import { useScript } from '../lib/useScript';

/* ---------- Field extraction: the agent reads an invoice field by field, one date is smudged and needs a look ---------- */
const FOUND: ExtractedField[] = [
  { key: 'no', label: 'Invoice no.', value: 'NW-2041', confidence: 0.99 },
  { key: 'date', label: 'Issued', value: '2026-10-03', confidence: 0.97 },
  { key: 'total', label: 'Total', value: '$11,480.00', confidence: 0.98 },
  { key: 'due', label: 'Due by', value: '2026-11-1?', confidence: 0.41, suggestion: '2026-11-1' }
];

const INVOICE = (
  <>
    <div className="dm-fex-top"><b>Northwind Studio</b><span className="dm-muted">Invoice</span></div>
    <FieldZone field="no"><span className="dm-muted">Invoice no.</span> <span className="dm-num">NW-2041</span></FieldZone>
    <FieldZone field="date"><span className="dm-muted">Issued</span> <span className="dm-num">3 Oct 2026</span></FieldZone>
    <div className="dm-fex-tb">
      <div className="dm-fex-li"><span>Design system audit</span><span className="dm-num">$4,800.00</span></div>
      <div className="dm-fex-li"><span>Pricing page redesign</span><span className="dm-num">$6,200.00</span></div>
      <div className="dm-fex-li"><span>Agent setup, 2 seats</span><span className="dm-num">$480.00</span></div>
    </div>
    <FieldZone field="total" className="dm-fex-tot"><span>Total due</span><span className="dm-num">$11,480.00</span></FieldZone>
    <FieldZone field="due" className="dm-fex-smudge"><span className="dm-muted">Due by</span> <span className="dm-num">1? Nov 2026</span></FieldZone>
  </>
);

export default function FieldExtractionDemo() {
  const [fields, setFields] = useState<ExtractedField[]>([]);
  const [reading, setReading] = useState(false);
  const [status, setStatus] = useState('Reading page 1');
  const [send, setSend] = useState<'idle' | 'sending' | 'sent'>('idle');
  const later = useRef<ReturnType<typeof setTimeout>>(undefined);
  useEffect(() => () => clearTimeout(later.current), []);
  useScript(async ({ wait }) => {
    await wait(300); setReading(true);
    for (const f of FOUND) { await wait(560); setFields(list => [...list, f]); }
    await wait(400); setReading(false); setStatus('1 field needs a look');
  });
  // the day is the smudged digit; anything left unfinished becomes the 14th, as the agent guessed
  const fix = (key: string, value: string) => {
    setFields(list => list.map(f => f.key === key ? { ...f, value: /\d\d$/.test(value) ? value : '2026-11-14', fixed: true } : f));
    setStatus('All fields confirmed');
  };
  const sendRow = () => {
    setSend('sending');
    later.current = setTimeout(() => { setSend('sent'); setStatus('1 row added to Invoices 2026'); }, 800);
  };
  return (
    <div style={{ width: '100%' }}>
      <FieldExtraction page={INVOICE} fields={fields} reading={reading} status={status} checkHint="Type the day you see" onFix={fix}
        sendLabel="Send to Sheets" sentLabel="Added to Invoices" sendState={send} onSend={sendRow} />
    </div>
  );
}
04

Props

Everything FieldExtraction takes.

PropTypeWhat it does
pagerequiredReactNodeThe page being read, on the left. Wrap the parts the fields come from in FieldZone
fieldsrequiredExtractedField[]The fields found so far, in order. Each new one slides in and its zone lights up for a moment
readingbooleanA scan line passes down the page each time this turns true, and Send waits while it is true
statusstringThe quiet line by the title, like "Reading page 1". Crossfades when it changes
checkHintstringReplaces the status while a field is being checked
onFix(key: string, value: string) => voidCalled when a checked field is confirmed with Enter or by clicking away
titlestring
sendLabelstringThe button under the fields. It waits until every field is sure or checked
sendState'idle' | 'sending' | 'sent'idle shows sendLabel, sending a spinner, sent a check and sentLabel
sentLabelstring
onSend() => void
classNamestring
05

Code

The full source, as the CLI installs it.

FieldExtraction.tsxReact · 134 lines
'use client';
import { createContext, useContext, useEffect, useRef, useState, type KeyboardEvent, type ReactNode } from 'react';
import { cx } from '../lib/cx';
import { useIsoLayoutEffect } from '../lib/hooks';
import { Icon } from '../lib/icons';
import { Spin, Swap, Tip } from '../lib/motion';
import '../designme.css';
import './field-extraction.css';

export interface ExtractedField {
  /** Ties the field to the FieldZone it was read from */
  key: string;
  /** Like "Invoice no." */
  label: string;
  value: string;
  /** 0 to 1. Under 0.6 the field asks the person to check it */
  confidence: number;
  /** What the value starts as when the person checks it, like the value without its unsure digit */
  suggestion?: string;
  /** The person has checked it. Shows "Fixed" with a full bar */
  fixed?: boolean;
}

export interface FieldExtractionProps {
  /** The page being read, on the left. Wrap the parts the fields come from in FieldZone */
  page: ReactNode;
  /** The fields found so far, in order. Each new one slides in and its zone lights up for a moment */
  fields: ExtractedField[];
  /** A scan line passes down the page each time this turns true, and Send waits while it is true */
  reading?: boolean;
  /** The quiet line by the title, like "Reading page 1". Crossfades when it changes */
  status?: string;
  /** Replaces the status while a field is being checked */
  checkHint?: string;
  /** Called when a checked field is confirmed with Enter or by clicking away */
  onFix?: (key: string, value: string) => void;
  title?: string;
  /** The button under the fields. It waits until every field is sure or checked */
  sendLabel?: string;
  /** idle shows sendLabel, sending a spinner, sent a check and sentLabel */
  sendState?: 'idle' | 'sending' | 'sent';
  sentLabel?: string;
  onSend?: () => void;
  className?: string;
}

const needsCheck = (f: ExtractedField) => f.confidence < 0.6 && !f.fixed;

type Zones = { lit: string | null; scan: string[]; warn: string[] };
const ZoneContext = createContext<Zones>({ lit: null, scan: [], warn: [] });

/** A part of the page a field was read from. It lights up while its field is read, hovered or checked */
export function FieldZone({ field, className, children }: { field: string; className?: string; children: ReactNode }) {
  const z = useContext(ZoneContext);
  return <div className={cx('dm-fex-z', z.scan.includes(field) && 'dm-scan', z.warn.includes(field) && 'dm-warn', z.lit === field && 'dm-hl', className)}>{children}</div>;
}

/** The value while it is being checked: the person types into the field itself */
function Editing({ start, onDone }: { start: string; onDone: (value: string) => void }) {
  const ref = useRef<HTMLSpanElement>(null);
  const done = useRef(false);
  useIsoLayoutEffect(() => {
    const v = ref.current; if (!v) return;
    v.textContent = start; v.focus();
    const s = getSelection(); s?.selectAllChildren(v); s?.collapseToEnd();
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
  const finish = () => { if (done.current) return; done.current = true; onDone(ref.current?.textContent ?? start); };
  const key = (e: KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); finish(); } };
  return <span ref={ref} className="dm-v dm-num" contentEditable="true" suppressContentEditableWarning role="textbox" aria-label="Value" onKeyDown={key} onBlur={finish} />;
}

/** Fields an agent reads off a document, each with how sure it is. Unsure ones ask the person to check them. */
export function FieldExtraction({ page, fields, reading = false, status = '', checkHint = 'Type the value you see', onFix, title = 'Extracted fields',
  sendLabel = 'Send', sendState = 'idle', sentLabel = 'Sent', onSend, className }: FieldExtractionProps) {
  const [hover, setHover] = useState<string | null>(null);
  const [editing, setEditing] = useState<string | null>(null);

  // a new field lights its zone for half a second
  const [scan, setScan] = useState<string[]>([]);
  const known = useRef(new Set(fields.map(f => f.key)));
  const timers = useRef<ReturnType<typeof setTimeout>[]>([]);
  useEffect(() => () => timers.current.forEach(clearTimeout), []);
  useEffect(() => {
    const fresh = fields.map(f => f.key).filter(k => !known.current.has(k));
    if (!fresh.length) return;
    fresh.forEach(k => known.current.add(k));
    setScan(s => [...s, ...fresh]);
    timers.current.push(setTimeout(() => setScan(s => s.filter(k => !fresh.includes(k))), 500));
  }, [fields]);

  // each time reading starts, a new scan line passes down the page; it stays parked below the page after
  const [passes, setPasses] = useState(reading ? 1 : 0);
  const [wasReading, setWasReading] = useState(reading);
  if (reading !== wasReading) { setWasReading(reading); if (reading) setPasses(p => p + 1); }

  const zones: Zones = { lit: editing ?? hover, scan, warn: fields.filter(needsCheck).map(f => f.key) };
  const ready = !reading && fields.length > 0 && !fields.some(needsCheck);

  return (
    <ZoneContext.Provider value={zones}>
      <div data-dm="" className={cx('dm-fex', className)}>
        <div className="dm-fex-doc">
          <div className="dm-fex-pg">{page}</div>
          {passes > 0 && <div key={passes} className="dm-fex-beam dm-go" aria-hidden="true" />}
        </div>
        <div className="dm-fex-side dm-panel">
          <div className="dm-fex-sh"><b>{title}</b><Swap className="dm-muted dm-fex-st">{editing ? checkHint : status}</Swap></div>
          <div className="dm-fex-list">
            {fields.map(f => {
              const warn = needsCheck(f), pct = Math.round(f.confidence * 100);
              return (
                <div key={f.key} className={cx('dm-fex-r', warn && 'dm-warn', 'dm-enter')} onMouseEnter={() => setHover(f.key)} onMouseLeave={() => setHover(h => h === f.key ? null : h)}>
                  <span className="dm-k">{f.label}</span>
                  {editing === f.key
                    ? <Editing key="edit" start={f.suggestion ?? f.value} onDone={v => { setEditing(null); onFix?.(f.key, v); }} />
                    : <span key="view" className="dm-v dm-num">{f.value}</span>}
                  {warn
                    ? <span className="dm-fex-cf"><Tip label={`${pct}% sure, check it`}><button type="button" className="dm-fex-fix" onClick={() => setEditing(f.key)}><Icon name="edit" size="xs" />Check</button></Tip></span>
                    : <span key={f.fixed ? 'fixed' : 'sure'} className={cx('dm-fex-cf', f.fixed && 'dm-enter')}><i><b style={{ width: (f.fixed ? 100 : pct) + '%' }} /></i>{f.fixed ? 'Fixed' : pct + '%'}</span>}
                </div>
              );
            })}
          </div>
          <div className="dm-fex-f">
            <button type="button" className={cx('dm-pill dm-sm', sendState === 'sent' ? 'dm-soft' : 'dm-primary')} disabled={!ready} onClick={() => { if (sendState === 'idle') onSend?.(); }}>
              {sendState === 'sending' ? <Spin /> : sendState === 'sent' ? <><Icon name="check" size="xs" />{sentLabel}</> : sendLabel}
            </button>
          </div>
        </div>
      </div>
    </ZoneContext.Provider>
  );
}
field-extraction.cssReact · 47 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Field extraction ---------- */
.dm-fex{display:grid;grid-template-columns:minmax(0,1fr) clamp(240px,44%,316px);gap:14px;width:100%;container:fex/inline-size}
/* the document and the fields side by side while the component has room for both, whatever the screen; one above the other below that */
@container fex (max-width:560px){.dm-fex-doc,.dm-fex-side{grid-column:1/-1}}
.dm-fex-doc{border-radius:18px;background:var(--dm-chip);padding:var(--dm-s-5);display:grid;place-items:center;min-width:0;position:relative;overflow:hidden}
/* the invoice is paper: white ink-on-paper in light mode, a dark document in dark mode */
.dm-fex-pg{--dm-pg:#fff;--dm-pg-fg:#232323;--dm-pg-mut:#6a6a6a;--dm-pg-line:#e8e8e8;--dm-pg-hl:var(--dm-accent-mark,var(--dm-accent));--dm-pg-ring:var(--dm-accent-mark,var(--dm-accent));--dm-pg-warn:232,155,20;--dm-pg-wring:#e89b14;--dm-pg-sh:0 1px 2px rgba(0,0,0,.08),0 8px 30px rgba(0,0,0,.08);
  width:min(330px,100%);background:var(--dm-pg);color:var(--dm-pg-fg);border-radius:6px;padding:var(--dm-s-5) 22px;box-shadow:var(--dm-pg-sh);font-size:12px;display:flex;flex-direction:column;gap:var(--dm-s-2);position:relative}
:root[data-theme="dark"] .dm-fex-doc{background:color-mix(in srgb,var(--dm-chip) 40%,var(--dm-stage))}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .dm-fex-doc{background:color-mix(in srgb,var(--dm-chip) 40%,var(--dm-stage))}}
:root[data-theme="dark"] .dm-fex-pg{--dm-pg:var(--dm-raised);--dm-pg-fg:var(--dm-fg);--dm-pg-mut:var(--dm-muted);--dm-pg-line:var(--dm-line);--dm-pg-warn:240,168,48;--dm-pg-wring:#f0a830;--dm-pg-sh:0 0 0 1px var(--dm-line),0 10px 30px rgba(0,0,0,.4)}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .dm-fex-pg{--dm-pg:var(--dm-raised);--dm-pg-fg:var(--dm-fg);--dm-pg-mut:var(--dm-muted);--dm-pg-line:var(--dm-line);--dm-pg-warn:240,168,48;--dm-pg-wring:#f0a830;--dm-pg-sh:0 0 0 1px var(--dm-line),0 10px 30px rgba(0,0,0,.4)}}
.dm-fex-pg .dm-muted{color:var(--dm-pg-mut)}
/* a label under the scan's tint or a highlight leans toward the ink, so it keeps 4.5:1 with any accent */
.dm-fex-z:is(.dm-scan,.dm-hl) .dm-muted{color:color-mix(in srgb,var(--dm-pg-mut) 70%,var(--dm-pg-fg))}
.dm-fex-top{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px;font-size:13px}
.dm-fex-z{position:relative;padding:2px var(--dm-s-1);margin:0 -4px;border-radius:var(--dm-r-xs);transition:background .3s,box-shadow .3s}
.dm-fex-z.dm-scan{background:color-mix(in srgb,var(--dm-pg-hl) 8%,transparent)}
.dm-fex-z.dm-hl{background:color-mix(in srgb,var(--dm-pg-hl) 12%,transparent);box-shadow:0 0 0 1.5px var(--dm-pg-ring)}
.dm-fex-z.dm-warn.dm-hl{background:rgba(var(--dm-pg-warn),.14);box-shadow:0 0 0 1.5px var(--dm-pg-wring)}
.dm-fex-tb{border-top:1px solid var(--dm-pg-line);border-bottom:1px solid var(--dm-pg-line);padding:6px 0;display:flex;flex-direction:column;gap:var(--dm-s-1)}
.dm-fex-li{display:flex;justify-content:space-between;gap:10px}
.dm-fex-tot{display:flex;justify-content:space-between;font-weight:500;font-size:13px}
.dm-fex-smudge .dm-num{filter:blur(.4px)}
.dm-fex-beam{position:absolute;left:0;right:0;height:40px;background:linear-gradient(180deg,transparent,color-mix(in srgb,var(--dm-pg-hl) 12%,transparent),transparent);pointer-events:none;top:-40px}
.dm-fex-beam.dm-go{animation:dm-fexb 2.6s var(--dm-ease-io) both}
@keyframes dm-fexb{to{top:100%}}
.dm-fex-side{padding:var(--dm-s-3);display:flex;flex-direction:column;gap:var(--dm-s-2);min-width:0}
.dm-fex-sh{display:flex;justify-content:space-between;align-items:baseline;font-size:13px;padding:2px var(--dm-s-1)}
.dm-fex-sh b{font-weight:500}
.dm-fex-st{font-size:12px}
.dm-fex-list{display:flex;flex-direction:column;gap:2px;flex:1}
.dm-fex-r{display:grid;grid-template-columns:70px 1fr auto;align-items:center;gap:var(--dm-s-2);padding:7px 6px;border-radius:10px;font-size:13px;transition:background .2s}
@media (hover:hover) and (pointer:fine){.dm-fex-r:hover{background:var(--dm-row)}}
.dm-fex-r .dm-k{font-size:12px;color:var(--dm-muted)}
.dm-fex-r .dm-v{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;outline:0;border-radius:6px;padding:1px var(--dm-s-1);margin:0 -4px}
.dm-fex-r .dm-v[contenteditable="true"]{background:var(--dm-surface);box-shadow:0 0 0 1px color-mix(in srgb,var(--dm-field-line) 50%,transparent)}
.dm-fex-r .dm-v[contenteditable="true"]:focus{box-shadow:0 0 0 1px color-mix(in srgb,var(--dm-field-line-strong) 50%,transparent)}
.dm-fex-cf{display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--dm-muted)}
.dm-fex-cf i{width:22px;height:4px;border-radius:2px;background:var(--dm-line);overflow:hidden;display:block}
.dm-fex-cf i b{display:block;height:100%;border-radius:2px;background:var(--dm-ok);transform-origin:left;animation:dm-ssrel .7s var(--dm-ease) both}
.dm-fex-r.dm-warn .dm-fex-cf i b{background:var(--dm-warn)}
.dm-fex-fix{font-size:12px;color:var(--dm-warn);display:inline-flex;align-items:center;gap:var(--dm-s-1)}
@keyframes dm-ssrel{from{transform:scaleX(0)}}
.dm-fex-f{display:flex;justify-content:flex-end}
field-extraction.htmlHTML · 26 lines
<div class="card wide" data-c="field-extraction">
  <div class="stage-in">
    <div class="fex">
      <div class="fex-doc">
        <div class="fex-pg">
          <div class="fex-top"><b>Northwind Studio</b><span class="muted">Invoice</span></div>
          <div class="fex-z" data-z="no"><span class="muted">Invoice no.</span> <span class="num">NW-2041</span></div>
          <div class="fex-z" data-z="date"><span class="muted">Issued</span> <span class="num">3 Oct 2026</span></div>
          <div class="fex-tb">
            <div class="fex-li"><span>Design system audit</span><span class="num">$4,800.00</span></div>
            <div class="fex-li"><span>Pricing page redesign</span><span class="num">$6,200.00</span></div>
            <div class="fex-li"><span>Agent setup, 2 seats</span><span class="num">$480.00</span></div>
          </div>
          <div class="fex-z fex-tot" data-z="total"><span>Total due</span><span class="num">$11,480.00</span></div>
          <div class="fex-z fex-smudge" data-z="due"><span class="muted">Due by</span> <span class="num">1? Nov 2026</span></div>
        </div>
      </div>
      <div class="fex-side panel">
        <div class="fex-sh"><b>Extracted fields</b><span class="muted fex-st">Reading page 1</span></div>
        <div class="fex-list"></div>
        <div class="fex-f"><button class="pill primary sm fex-go" disabled>Send to Sheets</button></div>
      </div>
    </div>
  </div>
</div>
field-extraction.cssHTML · 46 lines
/* ---------- Field extraction ---------- */
.fex{display:grid;grid-template-columns:minmax(0,1fr) clamp(240px,44%,316px);gap:14px;width:100%;container:fex/inline-size}
/* the document and the fields side by side while the component has room for both, whatever the screen; one above the other below that */
@container fex (max-width:560px){.fex-doc,.fex-side{grid-column:1/-1}}
.fex-doc{border-radius:18px;background:var(--chip);padding:var(--s-5);display:grid;place-items:center;min-width:0;position:relative;overflow:hidden}
/* the invoice is paper: white ink-on-paper in light mode, a dark document in dark mode */
.fex-pg{--pg:#fff;--pg-fg:#232323;--pg-mut:#6a6a6a;--pg-line:#e8e8e8;--pg-hl:var(--accent-mark,var(--accent));--pg-ring:var(--accent-mark,var(--accent));--pg-warn:232,155,20;--pg-wring:#e89b14;--pg-sh:0 1px 2px rgba(0,0,0,.08),0 8px 30px rgba(0,0,0,.08);
  width:min(330px,100%);background:var(--pg);color:var(--pg-fg);border-radius:6px;padding:var(--s-5) 22px;box-shadow:var(--pg-sh);font-size:12px;display:flex;flex-direction:column;gap:var(--s-2);position:relative}
:root[data-theme="dark"] .fex-doc{background:color-mix(in srgb,var(--chip) 40%,var(--stage))}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .fex-doc{background:color-mix(in srgb,var(--chip) 40%,var(--stage))}}
:root[data-theme="dark"] .fex-pg{--pg:var(--raised);--pg-fg:var(--fg);--pg-mut:var(--muted);--pg-line:var(--line);--pg-warn:240,168,48;--pg-wring:#f0a830;--pg-sh:0 0 0 1px var(--line),0 10px 30px rgba(0,0,0,.4)}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .fex-pg{--pg:var(--raised);--pg-fg:var(--fg);--pg-mut:var(--muted);--pg-line:var(--line);--pg-warn:240,168,48;--pg-wring:#f0a830;--pg-sh:0 0 0 1px var(--line),0 10px 30px rgba(0,0,0,.4)}}
.fex-pg .muted{color:var(--pg-mut)}
/* a label under the scan's tint or a highlight leans toward the ink, so it keeps 4.5:1 with any accent */
.fex-z:is(.scan,.hl) .muted{color:color-mix(in srgb,var(--pg-mut) 70%,var(--pg-fg))}
.fex-top{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px;font-size:13px}
.fex-z{position:relative;padding:2px var(--s-1);margin:0 -4px;border-radius:var(--r-xs);transition:background .3s,box-shadow .3s}
.fex-z.scan{background:color-mix(in srgb,var(--pg-hl) 8%,transparent)}
.fex-z.hl{background:color-mix(in srgb,var(--pg-hl) 12%,transparent);box-shadow:0 0 0 1.5px var(--pg-ring)}
.fex-z.warn.hl{background:rgba(var(--pg-warn),.14);box-shadow:0 0 0 1.5px var(--pg-wring)}
.fex-tb{border-top:1px solid var(--pg-line);border-bottom:1px solid var(--pg-line);padding:6px 0;display:flex;flex-direction:column;gap:var(--s-1)}
.fex-li{display:flex;justify-content:space-between;gap:10px}
.fex-tot{display:flex;justify-content:space-between;font-weight:500;font-size:13px}
.fex-smudge .num{filter:blur(.4px)}
.fex-beam{position:absolute;left:0;right:0;height:40px;background:linear-gradient(180deg,transparent,color-mix(in srgb,var(--pg-hl) 12%,transparent),transparent);pointer-events:none;top:-40px}
.fex-beam.go{animation:fexb 2.6s var(--ease-io) both}
@keyframes fexb{to{top:100%}}
.fex-side{padding:var(--s-3);display:flex;flex-direction:column;gap:var(--s-2);min-width:0}
.fex-sh{display:flex;justify-content:space-between;align-items:baseline;font-size:13px;padding:2px var(--s-1)}
.fex-sh b{font-weight:500}
.fex-st{font-size:12px}
.fex-list{display:flex;flex-direction:column;gap:2px;flex:1}
.fex-r{display:grid;grid-template-columns:70px 1fr auto;align-items:center;gap:var(--s-2);padding:7px 6px;border-radius:10px;font-size:13px;transition:background .2s}
@media (hover:hover) and (pointer:fine){.fex-r:hover{background:var(--row)}}
.fex-r .k{font-size:12px;color:var(--muted)}
.fex-r .v{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;outline:0;border-radius:6px;padding:1px var(--s-1);margin:0 -4px}
.fex-r .v[contenteditable="true"]{background:var(--surface);box-shadow:0 0 0 1px color-mix(in srgb,var(--field-line) 50%,transparent)}
.fex-r .v[contenteditable="true"]:focus{box-shadow:0 0 0 1px color-mix(in srgb,var(--field-line-strong) 50%,transparent)}
.fex-cf{display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--muted)}
.fex-cf i{width:22px;height:4px;border-radius:2px;background:var(--line);overflow:hidden;display:block}
.fex-cf i b{display:block;height:100%;border-radius:2px;background:var(--ok);transform-origin:left;animation:ssrel .7s var(--ease) both}
.fex-r.warn .fex-cf i b{background:var(--warn)}
.fex-fix{font-size:12px;color:var(--warn);display:inline-flex;align-items:center;gap:var(--s-1)}
@keyframes ssrel{from{transform:scaleX(0)}}
.fex-f{display:flex;justify-content:flex-end}
field-extraction.jsHTML · 23 lines
DM.def('field-extraction', (r, c) => {
  const F = [['no','Invoice no.','NW-2041',.99],['date','Issued','2026-10-03',.97],['total','Total','$11,480.00',.98],['due','Due by','2026-11-1?',.41]];
  const list = $('.fex-list', r), st = $('.fex-st', r), go = $('.fex-go', r), doc = $('.fex-doc', r), pg = $('.fex-pg', r);
  const z = k => $(`.fex-z[data-z="${k}"]`, r);
  function row(f){
    const warn = f[3] < .6;
    const el = html(`<div class="fex-r${warn ? ' warn' : ''}" data-k="${f[0]}"><span class="k">${f[1]}</span><span class="v num">${f[2]}</span><span class="fex-cf">${warn ? `<button class="fex-fix" data-tip="${Math.round(f[3] * 100)}% sure, check it">${I('edit','xs')}Check</button>` : `<i><b style="width:${f[3] * 100}%"></b></i>${Math.round(f[3] * 100)}%`}</span></div>`);
    if (warn) z(f[0]).classList.add('warn');
    el.onmouseenter = () => z(f[0]).classList.add('hl'); el.onmouseleave = () => z(f[0]).classList.remove('hl');
    const fx = $('.fex-fix', el);
    if (fx) fx.onclick = () => { const v = $('.v', el); v.contentEditable = 'true'; v.textContent = '2026-11-1'; v.focus({ preventScroll:true }); const s = document.getSelection(); s.selectAllChildren(v); s.collapseToEnd(); z(f[0]).classList.add('hl'); swap(st, 'Type the day you see');
      const done = () => { v.contentEditable = 'false'; z(f[0]).classList.remove('hl', 'warn'); el.classList.remove('warn'); const cf = $('.fex-cf', el); cf.innerHTML = `<i><b style="width:100%"></b></i>Fixed`; enter(cf); go.disabled = false; swap(st, 'All fields confirmed'); };
      v.onkeydown = e => { if (e.key === 'Enter'){ e.preventDefault(); if (!/\d\d$/.test(v.textContent)) v.textContent = '2026-11-14'; done(); } }; v.onblur = () => { if (v.isContentEditable){ if (!/\d\d$/.test(v.textContent)) v.textContent = '2026-11-14'; done(); } }; };
    return el;
  }
  go.onclick = () => c.run(async () => { go.innerHTML = '<span class="spin"></span>'; await c.wait(800); go.innerHTML = I('check','xs') + 'Added to Invoices'; go.className = 'pill soft sm fex-go'; swap(st, '1 row added to Invoices 2026'); });
  c.run(async () => {
    const beam = html('<div class="fex-beam"></div>'); doc.appendChild(beam); await c.wait(300); beam.classList.add('go');
    for (const f of F){ await c.wait(560); const zz = z(f[0]); zz.classList.add('scan'); const el = row(f); list.appendChild(el); enter(el); c.later(500, () => zz.classList.remove('scan')); }
    await c.wait(400); swap(st, '1 field needs a look');
  });
});
06

Questions

What people ask about it.

How do I link a field to the spot on the page it came from?
Wrap that part of the page in <FieldZone field="key"> using the same key as the field. The component handles the highlight.
What counts as a field that needs checking?
Any field with confidence below 0.6 that is not marked fixed. Those fields block the send button until the person checks them.
Does checking a field update its value by itself?
No. It calls onFix with the key and the typed value. Update the field in your state and set fixed: true to show it as confirmed.
Can I stream fields in one by one?
Yes. Append to fields as the agent finds them. Each new field slides in and briefly lights its zone on the page.
07

More in Sources and output

What the agent read, what it produced and where each answer came from.

Code viewerCode the agent wrote, with line notes you can ask about.
Terminal streamShell commands the agent runs, with their output as it streams in.
Citation hover cardHover a citation to see the exact passage it came from.
Source stackEverything the agent read, folded into one quiet pill.
Metric insightA number, its trend, and the agent's read on why it moved.
Details panelProperties of a run you can change in place.
Task tableAgent tasks in a sortable table with bulk actions.