Smart State ✦
useState with an ai.generate() action — JSON-typed state any LLM can fill.
Headline hook
useSmartState — typed AI state, in one line.
A drop-in useState that adds an ai.generate(context?) action. The runtime shape is read from your initial value, so the model is constrained to return JSON that matches — no JSON schemas, no parsers, no boilerplate.
const [user, setUser, ai] = useSmartState(
{ name: '', age: 0, bio: '' },
'a fictitious cyberpunk character',
);
ai.generate(); // → { name: 'Kael-9', age: 28, bio: '…' }
setUser({ ... }); // still works — full useState semanticsEvery example below ships in two flavors — a Skeleton (bare API, unstyled) and a Stylized version using Tailwind. The hook is the same; only the wrapping markup changes. More advanced use cases live on their own pages — form autofill, extract from text, and color palette.
Why it's different
Seed an initial value; the prompt + JSON parser are derived automatically.
If the user calls setValue mid-generate, the in-flight request is aborted. Manual intent always wins.
LRU(16) keyed by (shape, context) — repeated generates with the same context are free.
No generics required. The runtime shape comes from the seed, so plain JS callers get the same behavior.
1. Primitive — random number
The simplest case: a number initial value plus a context string.
Skeleton
n = 0
import { SmartProvider, useSmartState } from '@extedcoud/smart-components';import { createMockClient } from '@extedcoud/smart-components/adapters/mock';const client = createMockClient({complete: async () => String(Math.floor(Math.random() * 100) + 1),});function Demo() {const [n, setN, ai] = useSmartState(0, 'a random integer between 1 and 100');return ( <div> <button onClick={() => ai.generate()} disabled={ai.status === 'loading'}> {ai.status === 'loading' ? 'Generating…' : 'Generate'} </button> <button onClick={() => setN(0)}>Reset</button> <pre>n = {n}</pre> </div>);}export default function Example() {return <SmartProvider client={client}><Demo /></SmartProvider>;}Stylized
A lucky-number card with gradient text and a soft glow.
Lucky number
'use client';import { SmartProvider, useSmartState } from '@extedcoud/smart-components';import { makeSmartStateMock } from '@/lib/mock-client';const client = makeSmartStateMock();function Demo() { const [n, setN, ai] = useSmartState(0, 'a random integer between 1 and 100'); const loading = ai.status === 'loading'; return ( <div className="w-full max-w-sm"> <div className="relative overflow-hidden rounded-2xl border border-fd-border bg-gradient-to-br from-indigo-500/10 via-fuchsia-500/10 to-cyan-500/10 p-8 shadow-sm"> <div className="pointer-events-none absolute -right-12 -top-12 h-40 w-40 rounded-full bg-fuchsia-500/20 blur-3xl" /> <div className="pointer-events-none absolute -bottom-16 -left-10 h-40 w-40 rounded-full bg-indigo-500/20 blur-3xl" /> <p className="relative text-xs font-medium uppercase tracking-[0.18em] text-fd-muted-foreground"> Lucky number </p> <div className={`relative mt-3 bg-gradient-to-r from-indigo-500 via-fuchsia-500 to-cyan-500 bg-clip-text text-7xl font-bold leading-none tracking-tight text-transparent transition ${ loading ? 'animate-pulse opacity-60' : '' }`} > {String(n).padStart(2, '0')} </div> <div className="relative mt-6 flex flex-wrap gap-2"> <button onClick={() => ai.generate()} disabled={loading} className="inline-flex h-10 min-h-[44px] items-center rounded-full bg-fd-foreground px-5 text-sm font-medium text-fd-background shadow transition hover:opacity-90 disabled:opacity-50" style={{ touchAction: 'manipulation' }} > {loading ? 'Rolling…' : 'Roll the dice'} </button> <button onClick={() => setN(0)} className="inline-flex h-10 min-h-[44px] items-center rounded-full border border-fd-border bg-fd-background px-5 text-sm font-medium text-fd-foreground transition hover:bg-fd-accent" style={{ touchAction: 'manipulation' }} > Reset </button> </div> </div> </div> );}export default function SmartStateRandomStylized() { return ( <SmartProvider client={client}> <Demo /> </SmartProvider> );}2. Object — generated profile
Pass a fully-seeded object to let the hook introspect the shape from initial.
Skeleton
{
"name": "",
"age": 0,
"bio": ""
}const [user, setUser, ai] = useSmartState({ name: '', age: 0, bio: '' },'a fictitious cyberpunk character',);<button onClick={() => ai.generate()}>Roll a new user</button><pre>{JSON.stringify(user, null, 2)}</pre>Stylized
A profile card with avatar initials, gradient banner, and a loading spinner.
No character yet
—
Click generate to summon a new persona.
'use client';import { SmartProvider, useSmartState } from '@extedcoud/smart-components';import { makeSmartStateMock } from '@/lib/mock-client';const client = makeSmartStateMock();const EMPTY = { name: '', age: 0, bio: '' };function initials(name: string) { return ( name .split(/\s+/) .filter(Boolean) .slice(0, 2) .map((s) => s[0]?.toUpperCase()) .join('') || '??' );}function Demo() { const [user, setUser, ai] = useSmartState(EMPTY, 'a fictitious cyberpunk character'); const loading = ai.status === 'loading'; const empty = !user.name; return ( <div className="w-full max-w-md"> <div className="overflow-hidden rounded-2xl border border-fd-border bg-fd-card shadow-sm"> <div className="relative h-24 bg-gradient-to-r from-indigo-500 via-violet-500 to-fuchsia-500"> <div className="absolute inset-x-0 -bottom-10 flex justify-center"> <div className={`flex h-20 w-20 items-center justify-center rounded-full border-4 border-fd-card bg-fd-background text-xl font-bold text-fd-foreground shadow ${ loading ? 'animate-pulse' : '' }`} > {empty ? '✦' : initials(user.name)} </div> </div> </div> <div className="px-6 pb-6 pt-12 text-center"> <h3 className="text-lg font-semibold text-fd-foreground"> {empty ? 'No character yet' : user.name} </h3> <p className="mt-1 text-xs uppercase tracking-wider text-fd-muted-foreground"> {empty ? '—' : `Age ${user.age}`} </p> <p className="mt-3 min-h-[3rem] text-sm leading-relaxed text-fd-muted-foreground"> {empty ? 'Click generate to summon a new persona.' : user.bio} </p> <div className="mt-5 flex justify-center gap-2"> <button onClick={() => ai.generate()} disabled={loading} className="inline-flex h-10 min-h-[44px] items-center gap-2 rounded-full bg-gradient-to-r from-indigo-500 to-fuchsia-500 px-5 text-sm font-medium text-white shadow transition hover:opacity-90 disabled:opacity-50" style={{ touchAction: 'manipulation' }} > {loading ? ( <> <span className="h-3 w-3 animate-spin rounded-full border-2 border-white/40 border-t-white" /> Summoning… </> ) : ( <>✦ Generate</> )} </button> <button onClick={() => setUser(EMPTY)} className="inline-flex h-10 min-h-[44px] items-center rounded-full border border-fd-border bg-fd-background px-4 text-sm font-medium text-fd-foreground transition hover:bg-fd-accent" style={{ touchAction: 'manipulation' }} > Clear </button> </div> {ai.error && ( <p className="mt-3 text-xs text-red-500">{ai.error.message}</p> )} </div> </div> </div> );}export default function SmartStateUserStylized() { return ( <SmartProvider client={client}> <Demo /> </SmartProvider> );}3. Array — generated tags (with options.shape)
When initial is empty ([], null, undefined), pass options.shape so the hook knows what to ask for.
Skeleton
[]
const [tags, , ai] = useSmartState<string[]>([], 'tags for a sci-fi blog post', {shape: { type: 'array', item: 'string' },});<button onClick={() => ai.generate()}>Generate tags</button><pre>{JSON.stringify(tags)}</pre>Stylized
A category switcher feeding a per-call context override, rendering tags as gradient pills.
No tags yet — generate to see suggestions.
'use client';import { useState } from 'react';import { SmartProvider, useSmartState } from '@extedcoud/smart-components';import { makeSmartStateMock } from '@/lib/mock-client';const client = makeSmartStateMock();const CATEGORIES = [ { key: 'scifi', label: '🛸 Sci-fi blog post', context: 'tags for a sci-fi blog post' }, { key: 'recipe', label: '🍳 Weeknight recipe', context: 'tags for a weeknight recipe post' }, { key: 'travel', label: '🌍 Travel guide', context: 'tags for a travel guide article' },] as const;function Demo() { const [category, setCategory] = useState<(typeof CATEGORIES)[number]>(CATEGORIES[0]); const [tags, , ai] = useSmartState<string[]>([], category.context, { shape: { type: 'array', item: 'string' }, }); const loading = ai.status === 'loading'; return ( <div className="w-full max-w-md"> <div className="rounded-2xl border border-fd-border bg-fd-card p-5 shadow-sm"> <div className="flex flex-wrap gap-2"> {CATEGORIES.map((c) => { const active = c.key === category.key; return ( <button key={c.key} onClick={() => setCategory(c)} 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' }} > {c.label} </button> ); })} </div> <div className="mt-5 min-h-[5.5rem] rounded-lg border border-dashed border-fd-border bg-fd-background p-3"> {tags.length === 0 ? ( <p className="py-3 text-center text-sm text-fd-muted-foreground"> No tags yet — generate to see suggestions. </p> ) : ( <div className="flex flex-wrap gap-2"> {tags.map((t, i) => ( <span key={`${t}-${i}`} className={`inline-flex items-center rounded-full bg-gradient-to-r from-emerald-500/15 to-teal-500/15 px-3 py-1 text-xs font-medium text-emerald-700 ring-1 ring-emerald-500/30 dark:text-emerald-300 ${ loading ? 'animate-pulse' : '' }`} > #{t} </span> ))} </div> )} </div> <button onClick={() => ai.generate(category.context)} disabled={loading} className="mt-4 inline-flex h-10 min-h-[44px] w-full items-center justify-center gap-2 rounded-full bg-gradient-to-r from-emerald-500 to-teal-500 px-4 text-sm font-semibold text-white shadow transition hover:opacity-90 disabled:opacity-50" style={{ touchAction: 'manipulation' }} > {loading ? 'Generating…' : `Generate ${category.label.split(' ').slice(1).join(' ')} tags`} </button> </div> </div> );}export default function SmartStateTagsStylized() { return ( <SmartProvider client={client}> <Demo /> </SmartProvider> );}4. Per-call context override
ai.generate(contextOverride) replaces the default context for one call — useful when the prompt is user-driven.
Skeleton
{
"name": "",
"age": 0,
"bio": ""
}const [user, , ai] = useSmartState({ name: '', age: 0, bio: '' });const [prompt, setPrompt] = useState('a brave wizard');<input value={prompt} onChange={(e) => setPrompt(e.target.value)} /><button onClick={() => ai.generate(prompt)}>Generate</button><pre>{JSON.stringify(user, null, 2)}</pre>Stylized
A character-builder card with quick-pick preset chips and a soft amber/rose result panel.
Pick a prompt or type your own, then press Generate.
'use client';import { useState } from 'react';import { SmartProvider, useSmartState } from '@extedcoud/smart-components';import { makeSmartStateMock } from '@/lib/mock-client';const client = makeSmartStateMock();const PRESETS = [ 'a stoic cyberpunk netrunner', 'a brave wizard from a forgotten realm', 'a retired space pirate now running a noodle shop',];function Demo() { const [user, , ai] = useSmartState({ name: '', age: 0, bio: '' }); const [prompt, setPrompt] = useState(PRESETS[0]); const loading = ai.status === 'loading'; const empty = !user.name; return ( <div className="w-full max-w-lg"> <div className="rounded-2xl border border-fd-border bg-fd-card p-5 shadow-sm"> <label htmlFor="smart-state-context-input" className="block text-xs font-medium uppercase tracking-wider text-fd-muted-foreground"> Describe a character </label> <div className="mt-2 flex gap-2"> <input id="smart-state-context-input" value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="a brave wizard…" className="flex-1 rounded-lg border border-fd-border bg-fd-background px-3 py-2 text-base text-fd-foreground outline-none transition focus:border-fd-foreground" style={{ fontSize: 16 }} /> <button onClick={() => ai.generate(prompt)} disabled={loading || !prompt.trim()} className="inline-flex h-11 min-h-[44px] items-center gap-2 rounded-lg bg-fd-foreground px-4 text-sm font-medium text-fd-background shadow transition hover:opacity-90 disabled:opacity-50" style={{ touchAction: 'manipulation' }} > {loading ? 'Summoning…' : '✦ Generate'} </button> </div> <div className="mt-3 flex flex-wrap gap-2"> {PRESETS.map((p) => ( <button key={p} onClick={() => setPrompt(p)} className="inline-flex items-center rounded-full border border-fd-border bg-fd-background px-3 py-1 text-xs text-fd-muted-foreground transition hover:bg-fd-accent" style={{ touchAction: 'manipulation' }} > {p} </button> ))} </div> <div className={`mt-5 rounded-xl border border-fd-border bg-gradient-to-br from-amber-500/5 to-rose-500/5 p-4 transition ${ loading ? 'animate-pulse' : '' }`} > {empty ? ( <p className="py-6 text-center text-sm text-fd-muted-foreground"> Pick a prompt or type your own, then press Generate. </p> ) : ( <> <div className="flex items-baseline justify-between gap-2"> <h4 className="text-base font-semibold text-fd-foreground">{user.name}</h4> <span className="text-xs uppercase tracking-wider text-fd-muted-foreground"> Age {user.age} </span> </div> <p className="mt-2 text-sm leading-relaxed text-fd-muted-foreground">{user.bio}</p> </> )} </div> </div> </div> );}export default function SmartStateContextStylized() { return ( <SmartProvider client={client}> <Demo /> </SmartProvider> );}Where next
Type a brief → fill a typed event form.
Paste raw notes → structured summary + action items.
Mood brief → 5-color hex palette with live preview.
Full API reference: see useSmartState API.