Extract from text
Paste raw notes → typed summary, action items, urgency, and due date.
Hand the model a blob of text — meeting notes, an email, a Slack thread — and ask it to produce a structured object. The hook's shape contract ensures the result is parseable and typed.
Skeleton
Summary: —
Action items:
Urgency: —
Due: —
const [text, setText] = useState(SAMPLE);const [out, , ai] = useSmartState({ summary: '', actionItems: [''], urgency: '', dueDate: '' },'Structured fields extracted from raw text',);<textarea value={text} onChange={(e) => setText(e.target.value)} rows={5} /><button onClick={() => ai.generate(text)}>Extract</button><p>Summary: {out.summary}</p><ul>{out.actionItems.filter(Boolean).map((a, i) => <li key={i}>{a}</li>)}</ul><p>Urgency: {out.urgency}</p><p>Due: {out.dueDate}</p>Stylized
A sample picker, monospace textarea, urgency badge with traffic-light colors, and numbered action items.
Click extract to pull a summary, action items, urgency and due date.
'use client';import { useState } from 'react';import { SmartProvider, useSmartState } from '@extedcoud/smart-components';import { makeSmartStateMock } from '@/lib/mock-client';const client = makeSmartStateMock(500);const SAMPLES = [ { label: 'Q3 budget', text: `Meeting notes — 2026-05-13Discussed the Q3 budget revision with finance. Need to circulate the updated forecast by Friday. Maya will own the slide deck; Jordan to finalize headcount numbers. Sign-off from VP Eng required before submission. Tight timeline — blocking the Q3 OKRs review.`, }, { label: 'Migration deploy', text: `Deploy review:Migration ready in staging, awaiting validation. Need a rollback plan documented before we schedule the production window. Estimated 1-week timeline.`, },];const URGENCY_STYLES: Record<string, string> = { high: 'bg-rose-500/15 text-rose-700 ring-rose-500/30 dark:text-rose-300', medium: 'bg-amber-500/15 text-amber-700 ring-amber-500/30 dark:text-amber-300', low: 'bg-emerald-500/15 text-emerald-700 ring-emerald-500/30 dark:text-emerald-300',};function Demo() { const [sample, setSample] = useState(SAMPLES[0]); const [text, setText] = useState(SAMPLES[0].text); const [out, , ai] = useSmartState( { summary: '', actionItems: [''], urgency: '', dueDate: '' }, 'Structured fields extracted from raw text', ); const loading = ai.status === 'loading'; const items = out.actionItems.filter(Boolean); const filled = !!out.summary; return ( <div className="w-full max-w-xl"> <div className="rounded-2xl border border-fd-border bg-fd-card p-5 shadow-sm"> <div className="flex flex-wrap gap-2"> {SAMPLES.map((s) => { const active = s.label === sample.label; return ( <button key={s.label} onClick={() => { setSample(s); setText(s.text); }} className={`inline-flex h-8 min-h-[44px] items-center rounded-full border px-3 text-xs font-medium transition ${ active ? 'border-fd-foreground bg-fd-foreground text-fd-background' : 'border-fd-border bg-fd-background text-fd-muted-foreground hover:bg-fd-accent' }`} style={{ touchAction: 'manipulation' }} > {s.label} </button> ); })} </div> <label htmlFor="smart-state-extract-text" className="mt-4 block text-xs font-medium uppercase tracking-wider text-fd-muted-foreground" > Raw notes </label> <textarea id="smart-state-extract-text" value={text} onChange={(e) => setText(e.target.value)} rows={5} style={{ fontSize: 14, fontFamily: 'ui-monospace, monospace' }} className="mt-2 w-full resize-none rounded-lg border border-fd-border bg-fd-background px-3 py-2 text-fd-foreground outline-none focus:border-fd-foreground" /> <button onClick={() => ai.generate(text)} disabled={loading || !text.trim()} className="mt-3 inline-flex h-10 min-h-[44px] w-full items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-violet-500 to-fuchsia-500 text-sm font-semibold text-white shadow transition hover:opacity-90 disabled:opacity-50" style={{ touchAction: 'manipulation' }} > {loading ? 'Extracting…' : '✦ Extract structured data'} </button> <div className={`mt-5 rounded-xl border border-fd-border bg-fd-background p-4 transition ${loading ? 'animate-pulse' : ''}`}> {!filled ? ( <p className="py-6 text-center text-sm text-fd-muted-foreground"> Click extract to pull a summary, action items, urgency and due date. </p> ) : ( <> <div className="flex flex-wrap items-center gap-2"> {out.urgency && ( <span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-[11px] font-medium uppercase tracking-wider ring-1 ${ URGENCY_STYLES[out.urgency.toLowerCase()] ?? URGENCY_STYLES.low }`} > {out.urgency} urgency </span> )} {out.dueDate && ( <span className="inline-flex items-center rounded-full bg-fd-secondary/60 px-2.5 py-0.5 text-[11px] font-medium text-fd-foreground"> Due {out.dueDate} </span> )} </div> <p className="mt-3 text-sm text-fd-foreground">{out.summary}</p> {items.length > 0 && ( <ul className="mt-4 space-y-2"> {items.map((a, i) => ( <li key={i} className="flex items-start gap-2 text-sm text-fd-foreground"> <span className="mt-0.5 flex h-4 w-4 flex-none items-center justify-center rounded-full bg-violet-500/15 text-[10px] font-bold text-violet-600 dark:text-violet-300"> {i + 1} </span> <span>{a}</span> </li> ))} </ul> )} </> )} </div> </div> </div> );}export default function SmartStateExtractStylized() { return ( <SmartProvider client={client}> <Demo /> </SmartProvider> );}Why arrays need a non-empty seed
useSmartState({ actionItems: [] }, …) would throw at mount — an empty array doesn't say what kind of array. Either seed it with one example value (actionItems: ['']) or pass options.shape for the array.
useSmartState(
{ summary: '', actionItems: [] }, // ❌ throws — empty array has no shape
'Structured fields',
);
useSmartState(
{ summary: '', actionItems: [''] }, // ✅ seeded — shape = array of string
'Structured fields',
);Streaming the extraction
useSmartState is currently atomic — setValue fires once on success. For long extractions you may want a progress UI: read status and show a skeleton while status === 'loading'.