smart-components
Smart State ✦

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 semantics

Every 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

Shape-first

Seed an initial value; the prompt + JSON parser are derived automatically.

Cancel-on-edit

If the user calls setValue mid-generate, the in-flight request is aborted. Manual intent always wins.

Built-in cache

LRU(16) keyed by (shape, context) — repeated generates with the same context are free.

JS-compatible

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

00
'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

Full API reference: see useSmartState API.