AI field actions
ProAI for one text field: rewrite, shorten, expand, fix, change the tone, translate or follow an instruction. Streams into the field's AI state, shows the change as a before and after diff to accept or reject, and lets people undo.
npx prodular-ui add ai-field-actionsPart of Prodular UI Pro. Preview and read the docs freely, install with a license. See pricing
A reminder email
Open AI next to the label. Try Make shorter, Change tone or Translate to Polish, then Accept and Undo. The model here is simulated and streams word by word.
"use client"
import * as React from "react"
import { AiFieldActions, type AiFieldRequest } from "@/components/ui/ai-field-actions"import { Textarea } from "@/components/ui/textarea"
const original = "Hi Dana, just checking in about invoice INV-2041 for $5,900 which was due on October 3. We havent recieved payment yet. Could you let us know when we can expect it? Thanks, Maya"
// A stand-in for your model: canned answers streamed word by word. Replace with a call to your API route.function answer(r: AiFieldRequest) { switch (r.action) { case "shorten": return "Hi Dana, invoice INV-2041 ($5,900) was due October 3 and is still open. When can we expect payment? Thanks, Maya" case "expand": return `${r.text} If it helps, I can resend the invoice or share our bank details again. You can also pay by card from the link in the original email.` case "fix": return "Hi Dana, just checking in about invoice INV-2041 for $5,900, which was due on October 3. We haven't received payment yet. Could you let us know when we can expect it? Thanks, Maya" case "tone": return r.option === "Formal" ? "Dear Dana, I am writing regarding invoice INV-2041 for $5,900, due on October 3. We have not yet received payment. Could you confirm the expected payment date? Kind regards, Maya Okafor" : "Hi Dana, hope your week is going well! Quick nudge on invoice INV-2041 ($5,900), which was due October 3. Any idea when it might go out? Thanks so much, Maya" case "translate": return r.option === "Polish" ? "Cześć Dana, piszę w sprawie faktury INV-2041 na 5 900 USD z terminem płatności 3 października. Nie otrzymaliśmy jeszcze wpłaty. Czy możesz dać znać, kiedy możemy się jej spodziewać? Dzięki, Maya" : `[${r.option}] ${r.text}` case "custom": return `${r.text.replace("Thanks, Maya", "")}We need to close the books on Friday, so a date this week would really help. Thanks, Maya` default: return "Hi Dana, a quick note on invoice INV-2041 for $5,900, due October 3: we have not seen the payment yet. When do you expect to send it? Thanks, Maya" }}
async function* mockModel(r: AiFieldRequest, signal: AbortSignal) { await new Promise((res) => setTimeout(res, 350)) for (const word of answer(r).split(/(\s+)/)) { if (signal.aborted) return await new Promise((res) => setTimeout(res, 18)) yield word }}
export default function AiFieldActionsReminder() { const [value, setValue] = React.useState(original) return ( <div className="w-full max-w-xl"> <AiFieldActions value={value} onValueChange={setValue} run={mockModel} label={<label htmlFor="reminder-body">Reminder email</label>}> <Textarea id="reminder-body" rows={5} value={value} onChange={(e) => setValue(e.target.value)} /> </AiFieldActions> </div> )}A single line, with an error
Only three actions. Translate fails to show the error and Try again.
"use client"
import * as React from "react"
import { AiFieldActions, type AiFieldRequest } from "@/components/ui/ai-field-actions"import { Input } from "@/components/ui/input"
async function flakyModel(r: AiFieldRequest, signal: AbortSignal) { await new Promise((res) => setTimeout(res, 600)) if (signal.aborted) return "" if (r.action === "translate") throw new Error("Translation is turned off for this workspace. An admin can enable it in AI settings.") return r.action === "shorten" ? "INV-2041 is overdue" : "Payment reminder: INV-2041 is now 19 days overdue"}
export default function AiFieldActionsSubject() { const [value, setValue] = React.useState("Reminder about your invoice INV-2041 which is overdue") return ( <div className="w-full max-w-xl"> <AiFieldActions value={value} onValueChange={setValue} run={flakyModel} actions={["rewrite", "shorten", "translate"]} label={<label htmlFor="reminder-subject">Subject</label>} > <Input id="reminder-subject" value={value} onChange={(e) => setValue(e.target.value)} /> </AiFieldActions> </div> )}Anatomy
<AiFieldActions value={body} onValueChange={setBody} run={rewrite} label={<label htmlFor="body">Message</label>}> <Textarea id="body" value={body} onChange={(e) => setBody(e.target.value)} /></AiFieldActions>Guidance
Use it when
- Long text people write in forms: emails, notes, descriptions, replies.
- actions to offer only what makes sense for the field, like shorten for subjects.
- Alongside a normal Textarea or Input that stays controlled by your state.
Avoid
- Rich documents. Rich text editor has selection-based AI editing built in.
- Generating a whole form. Use AI form fill.
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.
- Implement run(request, signal) on your server: request has action, option (tone or language), instruction and text. Return the new text as a stream of chunks, for example from the AI SDK's streamText textStream.
- Pass the signal to fetch so Stop cancels the request.
- Keep the field controlled: value and onValueChange on AiFieldActions and on the control itself.
- Results are never applied without Accept. Undo is offered until the person edits the field.
- Throw an Error with a message people can act on; it shows under the field with Try again.
API
AiFieldActions
| Prop | Type | Default | Description |
|---|---|---|---|
| value / onValueChange | string / (value) => void | The field's text. | |
| run | (request: { action, option?, instruction?, text }, signal) => AiResult | Calls your model. Returns text, a promise or an async iterable of chunks. | |
| label | ReactNode | The label row; the AI menu sits at its end. | |
| actions | AiFieldAction[] | all | rewrite, shorten, expand, fix, tone, translate, custom. |
| tones / languages | string[] | Options for Change tone and Translate. |
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-field-actions# or, with the registry in components.jsonnpx shadcn@latest add @prodular/ai-field-actions