AI form fill
ProFills a Form from a document, an email or a record. Suggestions stream in under each field with where they came from and how sure the model is; people accept or dismiss them one by one or all at once, and Accept all can be undone.
npx prodular-ui add ai-form-fillPart of Prodular UI Pro. Preview and read the docs freely, install with a license. See pricing
An expense from a receipt
Fill from receipt streams five fields from a simulated model. The purpose comes from the calendar with low confidence; hover the meter for the quote.
"use client"
import * as React from "react"import { z } from "zod"
import { AiFormFill, AiFormFillBar, AiFormFillSuggestion, type AiFormFillResult } from "@/components/ui/ai-form-fill"import { Form, FormField, FormSubmit, useAppForm } from "@/components/ui/form"import { Input } from "@/components/ui/input"import { NumberInput } from "@/components/ui/number-input"import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"import { Textarea } from "@/components/ui/textarea"
const categories = [ { value: "travel", label: "Travel" }, { value: "meals", label: "Meals with clients" }, { value: "software", label: "Software" }, { value: "office", label: "Office" },]
const schema = z.object({ merchant: z.string().trim().min(1, "Who was paid?"), amount: z.number({ error: "Enter the total." }).positive("Enter the total."), date: z.string().min(1, "Enter the date on the receipt."), category: z.string().min(1, "Pick a category."), purpose: z.string().trim().min(10, "Say why the company paid for this."),})type Expense = z.infer<typeof schema>
// A stand-in for your model reading the receipt: fields arrive one by one, like a streamed object.const extracted: AiFormFillResult = { merchant: { value: "Tavares Rico", source: { label: "receipt, line 1", quote: "TAVARES RICO, Rua da Misericórdia 37, Lisboa" }, confidence: 0.97 }, date: { value: "2026-10-14", display: "Oct 14, 2026", source: { label: "receipt, line 3", quote: "14/10/2026 21:42" }, confidence: 0.94 }, amount: { value: 486.4, display: "€486.40", source: { label: "receipt, total", quote: "TOTAL EUR 486,40" }, confidence: 0.92 }, category: { value: "meals", display: "Meals with clients", source: { label: "merchant type" }, confidence: 0.71 }, purpose: { value: "Dinner with the Kestrel Health finance team after the renewal workshop in Lisbon.", source: { label: "your calendar, Oct 14", quote: "Kestrel renewal workshop, dinner at 20:30" }, confidence: 0.58, },}
async function* readReceipt(signal: AbortSignal) { for (const [name, suggestion] of Object.entries(extracted)) { await new Promise((r) => setTimeout(r, 350)) if (signal.aborted) return yield { [name]: suggestion } }}
export default function AiFormFillReceipt() { const form = useAppForm(schema, { defaultValues: { merchant: "", amount: undefined, date: "", category: "", purpose: "" } as unknown as Expense }) const [saved, setSaved] = React.useState(false)
return ( <Form form={form} className="w-full max-w-lg" onSubmit={async () => { await new Promise((r) => setTimeout(r, 500)) setSaved(true) }} > <AiFormFill run={readReceipt} source="receipt-tavares-rico.jpg"> <AiFormFillBar label="Fill from receipt" /> <FormField<Expense, "merchant"> name="merchant" label="Merchant"> {(field) => ( <> <Input {...field} /> <AiFormFillSuggestion name="merchant" label="merchant" /> </> )} </FormField> <div className="grid gap-6 sm:grid-cols-2"> <FormField<Expense, "amount"> name="amount" label="Amount"> {(field) => ( <> <NumberInput value={field.value ?? null} onValueChange={(v) => field.onChange(v ?? undefined)} prefix="€" format={{ minimumFractionDigits: 2 }} /> <AiFormFillSuggestion name="amount" label="amount" /> </> )} </FormField> <FormField<Expense, "date"> name="date" label="Date"> {(field) => ( <> <Input {...field} type="date" /> <AiFormFillSuggestion name="date" label="date" /> </> )} </FormField> </div> <FormField<Expense, "category"> name="category" label="Category"> {(field) => ( <> <Select items={categories} value={field.value || null} onValueChange={(v) => field.onChange(v ?? "")}> <SelectTrigger className="w-full" aria-invalid={field["aria-invalid"]}> <SelectValue placeholder="Choose a category" /> </SelectTrigger> <SelectContent> {categories.map((c) => ( <SelectItem key={c.value} value={c.value}> {c.label} </SelectItem> ))} </SelectContent> </Select> <AiFormFillSuggestion name="category" label="category" /> </> )} </FormField> <FormField<Expense, "purpose"> name="purpose" label="Business purpose"> {(field) => ( <> <Textarea {...field} rows={3} /> <AiFormFillSuggestion name="purpose" label="business purpose" /> </> )} </FormField> </AiFormFill> <div className="flex items-center gap-3"> <FormSubmit>Submit expense</FormSubmit> {saved && <span className="text-[13px] text-muted-foreground">Submitted for approval.</span>} </div> </Form> )}Anatomy
<Form form={form} onSubmit={save}> <AiFormFill run={readReceipt} source="receipt.jpg"> <AiFormFillBar /> <FormField name="merchant" label="Merchant"> {(field) => (<><Input {...field} /><AiFormFillSuggestion name="merchant" /></>)} </FormField> </AiFormFill></Form>Guidance
Use it when
- Forms people fill from something they already have: receipts, invoices, emails, contracts, CRM records.
- Sources and confidence so people know which suggestions to check.
- Inside Form, with AiFormFillSuggestion in each field's render function under its control.
Avoid
- Writing values straight into fields. Suggestions only apply when accepted.
- Free text editing. Use AI field actions for rewriting what people wrote.
Rules for AI agents
Published in llms-full.txt, the registry and the MCP server, so coding agents follow them when they build with this component.
- Wrap the fields in AiFormFill inside Form and pass run(signal). On the server, extract an object with the same field names, for example with the AI SDK's streamObject and a Zod schema, and return its partialObjectStream.
- Each suggestion is { value, display?, source?: { label, quote? }, confidence? }. value must have the field's type; display is what people read.
- Suggestions equal to the current value are skipped. Accepting validates the field through React Hook Form.
- Place AiFormFillBar at the top of the form and AiFormFillSuggestion name="field" right after each control.
- Throw from run with a message that names the source to show an error with Try again.
API
AiFormFill
| Prop | Type | Default | Description |
|---|---|---|---|
| run | (signal) => AsyncIterable<Partial<AiFormFillResult>> | Promise<AiFormFillResult> | Reads the source and returns suggestions by field name. | |
| source | string | What is read, as people know it. |
AiFormFillBar
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | "Fill with AI" | The start button. |
AiFormFillSuggestion
| Prop | Type | Default | Description |
|---|---|---|---|
| name | string | The field this suggestion belongs to. |
useAiFormFill
| Prop | Type | Default | Description |
|---|---|---|---|
| returns | { status, pending, start, cancel, accept, reject, acceptAll, rejectAll, undo, canUndo } | For your own controls. |
Installation
The Prodular UI CLI adds the source to components/ui with everything it depends on. In a project that already uses shadcn/ui, the shadcn CLI installs it from the Prodular registry too. See Installation for the first setup.
npx prodular-ui add ai-form-fill# or, with the registry in components.jsonnpx shadcn@latest add @prodular/ai-form-fill