AI action
ProThe shared state model for AI actions: start, stream, stop, retry, accept, reject and undo, with any provider. useAiAction powers the field actions and form fill, and builds your own AI features in a few lines.
npx prodular-ui add ai-actionPart of Prodular UI Pro. Preview and read the docs freely, install with a license. See pricing
Your own AI feature
A summary written by a simulated model: it streams, can be stopped, saved or discarded, and the save can be undone.
"use client"
import * as React from "react"
import { AiActionStatusLine, useAiAction } from "@/components/ui/ai-action"import { Button } from "@/components/ui/button"
const summary = "Receivables are $412,800, up 6% on September. Two customers, Ironclad Security and Northwind Logistics, account for 59% of what is overdue. Kestrel Health paid early for the third month running."
async function* summarize(_: { period: string }, signal: AbortSignal) { await new Promise((r) => setTimeout(r, 400)) for (const word of summary.split(/(\s+)/)) { if (signal.aborted) return await new Promise((r) => setTimeout(r, 22)) yield word }}
export default function AiActionCustom() { const [saved, setSaved] = React.useState<string | null>(null) const ai = useAiAction({ run: summarize, onAccept: (text) => { const before = saved setSaved(text) return () => setSaved(before) }, })
return ( <div className="flex w-full max-w-xl flex-col gap-4 rounded-xl border bg-card p-5 shadow-xs"> <div className="flex items-center justify-between gap-3"> <div className="flex flex-col"> <span className="text-sm font-semibold">October summary</span> <span className="text-[13px] text-muted-foreground">{saved ? "Saved to the report" : "Not written yet"}</span> </div> <Button size="sm" variant="outline" disabled={ai.busy} onClick={() => ai.start({ period: "October" })}> {saved ? "Rewrite" : "Write summary"} </Button> </div> {(ai.status === "streaming" || ai.status === "proposed") && <p className="text-sm leading-6">{ai.output}</p>} {!ai.output && saved && ai.status === "idle" && <p className="text-sm leading-6 text-muted-foreground">{saved}</p>} <AiActionStatusLine status={ai.status} error={ai.error} label="Reading October" onCancel={ai.cancel} onRetry={ai.retry} /> {ai.status === "proposed" && ( <div className="flex gap-2"> <Button size="sm" onClick={ai.accept}> Save to report </Button> <Button size="sm" variant="ghost" onClick={ai.reject}> Discard </Button> </div> )} {ai.canUndo && ai.status === "idle" && ( <Button size="xs" variant="link" className="self-start px-0" onClick={ai.undo}> Undo last save </Button> )} </div> )}Anatomy
const ai = useAiAction({ run: summarize, onAccept: (text) => save(text) })
<Button onClick={() => ai.start({ period })}>Write summary</Button><AiActionStatusLine status={ai.status} error={ai.error} onCancel={ai.cancel} onRetry={ai.retry} />Guidance
Use it when
- Any button that asks a model for text and lets people review it before it is used.
- AiActionStatusLine under the feature for the working and error states.
- readAiResult when you only need to read a stream or a promise to the end.
Avoid
- Chat. Use AI chat, which keeps a conversation.
- Applying results without review for anything people will send or publish.
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.
- Call useAiAction({ run, onAccept }). run(input, signal) returns a string, a promise or an async iterable of text chunks; honor the signal.
- In onAccept apply the result and return a function that reverts it; that is what undo calls.
- Render output while status is streaming or proposed, and offer accept and reject when proposed.
- busy is true while running or streaming. Disable the trigger then, and show AiActionStatusLine with onCancel.
- Errors thrown by run show their message; give people a next step in it.
API
useAiAction
| Prop | Type | Default | Description |
|---|---|---|---|
| run | (input, signal) => AiResult | Calls the model. | |
| onAccept | (output, input) => void | (() => void) | Applies the result; return a revert function for undo. | |
| onReject | (input) => void | Called on reject and cancel. | |
| returns | { status, output, error, busy, start, cancel, retry, accept, reject, undo, canUndo, clearUndo } | State and controls. |
AiActionStatusLine
| Prop | Type | Default | Description |
|---|---|---|---|
| status / error / label / onCancel / onRetry | AiActionStatus / string / ReactNode / () => void | Working with Stop, or the error with Try again. |
readAiResult
| Prop | Type | Default | Description |
|---|---|---|---|
| (result, signal, onChunk?) | Promise<string | null> | Reads any result to the end; null when aborted. |
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-action# or, with the registry in components.jsonnpx shadcn@latest add @prodular/ai-action