Inline assist
AIProThe Cmd+K inline edit for text: select a passage, ask the AI how to change it, then review the proposed change as a diff and accept, reject or try again.
npx prodular-ui add inline-assistPart of Prodular UI Pro. Preview and read the docs freely, install with a license. See pricing
In a document
Select a sentence. Ask AI to edit appears under it, Cmd+K opens the instruction. Try Make it shorter or Fix grammar on the second paragraph.
Q3 close memo
Select text, then press ⌘K"use client"
import * as React from "react"
import { InlineAssist, type InlineAssistRequest } from "@/components/ui/inline-assist"
const initial = `Close for Q3 is basically done. We're still waiting on 3 vendor credits from Halcyon Freight, which means accounts payable is a little higher than it should be. In order to finish on time we need the credits by Friday, otherwise we'll book an accrual and fix it in October.
Revenue came in at $4.82M, up 12% on Q2. Most of the growth came from the Team plan, which grew faster then we expected after the price change in July. Churn stayed flat at 1.9%.`
const rules: Record<string, [RegExp, string][]> = { shorter: [ [/basically /g, ""], [/still /g, ""], [/a little higher than it should be/g, "overstated"], [/In order to/g, "To"], [/, which means/g, ", so"], [/, otherwise we'll/g, " or we'll"], [/Most of the growth came from/g, "Growth came from"], [/ after the price change in July/g, ""], ], formal: [ [/basically done/g, "substantially complete"], [/We're/g, "We are"], [/we'll/g, "we will"], [/a little higher than it should be/g, "temporarily overstated"], [/fix it/g, "reverse it"], [/came in at/g, "was"], ], grammar: [ [/faster then/g, "faster than"], [/We're still waiting/g, "We are still waiting"], ],}
function rewrite({ instruction, selection }: InlineAssistRequest) { const key = /short|concise|trim/i.test(instruction) ? "shorter" : /formal|professional/i.test(instruction) ? "formal" : /grammar|spell|typo/i.test(instruction) ? "grammar" : "formal" let out = selection for (const [pattern, replacement] of rules[key]) out = out.replace(pattern, replacement) if (out === selection && key === "shorter") { const cut = selection.lastIndexOf(", ") if (cut > selection.length / 2) out = selection.slice(0, cut) + "." } return out}
export default function InlineAssistMemo() { const [value, setValue] = React.useState(initial)
return ( <div className="mx-auto w-full max-w-2xl self-start rounded-xl border bg-card shadow-xs"> <div className="flex items-baseline justify-between gap-3 border-b px-5 py-3"> <h3 className="text-sm font-medium">Q3 close memo</h3> <span className="text-[13px] text-muted-foreground">Select text, then press ⌘K</span> </div> <div className="px-5 py-4"> <InlineAssist value={value} onValueChange={setValue} onGenerate={(request) => new Promise((resolve, reject) => { const id = setTimeout(() => resolve(rewrite(request)), 1400) request.signal.addEventListener("abort", () => { clearTimeout(id) reject(new Error("aborted")) }) }) } /> </div> </div> )}In a support reply
Select the whole reply and choose Make it warmer. Enter accepts, Escape rejects.
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"import { InlineAssist } from "@/components/ui/inline-assist"
const initial = `Hi Lena, sorry about this. Looks like you got charged twice on October 14 because the card retry ran after the first payment already went through. I've refunded the second charge of $480.00, you should see it in 5 to 10 business days.`
const warmer = `Hi Lena, I'm really sorry for the trouble. You were charged twice on October 14: our card retry ran after your first payment had already gone through. I've refunded the second charge of $480.00, and it should reach your account within 5 to 10 business days.`
export default function InlineAssistReply() { const [value, setValue] = React.useState(initial)
return ( <div className="flex w-full max-w-xl flex-col rounded-xl border bg-card shadow-xs"> <div className="border-b px-4 py-2.5 text-[13px] text-muted-foreground"> Reply to <span className="font-medium text-foreground">Lena Fischer</span>, ticket #48213 </div> <div className="px-4 py-3"> <InlineAssist value={value} onValueChange={setValue} suggestions={["Make it warmer", "Make it shorter", "Add next steps"]} contentClassName="text-sm leading-6" onGenerate={({ selection, instruction, signal }) => new Promise((resolve, reject) => { const id = setTimeout(() => { if (selection === initial) resolve(warmer) else if (/short/i.test(instruction)) resolve(selection.replace(/, you should see it.*$/, ".")) else if (/next/i.test(instruction)) resolve(`${selection} If it has not arrived by October 31, reply here and I will chase it with our bank.`) else resolve(selection.replace(/sorry about this/, "I'm really sorry for the trouble").replace(/Looks like you got/, "You were")) }, 1200) signal.addEventListener("abort", () => { clearTimeout(id) reject(new Error("aborted")) }) }) } /> </div> <div className="flex items-center justify-end gap-2 border-t px-4 py-2.5"> <Button variant="ghost" size="sm"> Save draft </Button> <Button size="sm">Send reply</Button> </div> </div> )}Anatomy
<InlineAssist value={text} onValueChange={setText} onGenerate={async ({ instruction, selection, signal }) => { const res = await fetch("/api/edit", { method: "POST", body: JSON.stringify({ instruction, selection }), signal }) return (await res.json()).text }}/>Guidance
Use it when
- Use it where people write longer text and want to change part of it: memos, replies, descriptions, release notes.
- Offer two to four quick instructions in suggestions that fit the surface, such as Make it warmer for support replies.
- Let people try again with the same instruction before they reject.
Avoid
- Replacing the text before the person accepts. The proposal is shown as a diff in place.
- Using it for single line inputs. Use AiSuggestion or AiGhostInput there.
- Hiding what will change. The diff shows removed text struck through and added text highlighted.
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.
- Control the text with value and onValueChange. onValueChange is called only when the person accepts a proposal.
- Implement onGenerate to return only the replacement for the selection, not the whole document. Use before and after only as context.
- Respect request.signal: abort the model call when it fires and reject the promise. Stop and Escape trigger it.
- Reject the promise on failure. The component shows an error with Try again. Never resolve with an error message as text.
- Do not add your own Cmd+K, Enter or Escape handlers around InlineAssist. It handles them while a selection or proposal is active.
API
InlineAssist
| Prop | Type | Default | Description |
|---|---|---|---|
| value / onValueChange | string / (value: string) => void | Plain text. Paragraphs separated by blank lines. | |
| onGenerate | (request: { instruction, selection, before, after, signal }) => Promise<string> | Returns the replacement for the selection. | |
| suggestions | string[] | ["Make it shorter", "Make it more formal", "Fix grammar"] | Quick instructions under the prompt input. |
| contentClassName | string | Classes for the text surface, for example a smaller size. |
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 inline-assist# or, with the registry in components.jsonnpx shadcn@latest add @prodular/inline-assist