A user wants a shorter paragraph. Moving that paragraph into a chat window adds a second workflow: copy the text, switch context, explain the task, copy the answer, paste it back, then remove the conversational preamble.
An inline editor can keep the task in one place: select, rewrite, compare, accept. Anky UI supplies the interaction primitives for that flow. This article walks through how to connect them without giving the model direct ownership of your document.
Repository: https://github.com/ErAnkushPatil/anky-ui
Video walkthrough: https://x.com/ErAnkushP/status/2115629939106230425
Separate the suggestion from the saved text
Treat the model’s response as a proposal. Keep the original text, the in-progress rewrite, and the committed document distinct. That lets a user reject a change and makes network failure recoverable.
Start with the diff primitive:
"use client";
import { useState } from "react";
import { InlineDiff } from "@erankushpatil/anky-ui";
export function ReviewEdit() {
const [text, setText] = useState("Our team shipped the new onboarding flow this quarter.");
const [proposal, setProposal] = useState<string | null>(null);
return (
<section>
<p>{text}</p>
<button onClick={() => setProposal("We shipped a new onboarding flow.")}>
Preview an example edit
</button>
{proposal !== null && (
<InlineDiff
originalText={text}
modifiedText={proposal}
onAccept={() => {
setText(proposal);
setProposal(null);
}}
onReject={() => setProposal(null)}
/>
)}
</section>
);
}
This example deliberately uses a fixed proposal so the review and commit behavior can be tried without a backend. The application updates its document only in onAccept. Rendering a diff alone does not replace selected DOM text.
Anchor actions to the selection
SelectionPalette uses floating-ui to position actions next to highlighted text. Its callback gives your application the prompt and selected text:
<SelectionPalette
containerRef={editorRef}
actions={[
{ label: "Shorten", prompt: "Make this shorter. Return only the revised text." },
{ label: "Fix grammar", prompt: "Fix grammar. Return only the revised text." },
]}
onSelectPrompt={(prompt, selectedText) => {
void requestRewrite(prompt, selectedText);
}}
/>
Here editorRef and requestRewrite belong to your application. Scope the palette to the editor so selecting navigation text elsewhere does not open an editing action. Decide what should happen if the document changes while the request is running; a stale range should not overwrite newer user edits.
Stream into state with a rollback boundary
The state hook captures a snapshot before a stream begins. On failure or abort, it can return to that snapshot.
Use useOptimisticAIState to track the streamed proposal. Its mutateWithStream(source, transformFn) API accepts a response or stream factory and folds chunks into state. Pass the supplied abort signal to your fetch request. Initialize each proposal with the selected original text and an empty modified value, then append incoming chunks to that proposal. Connect currentState and isStreaming to InlineDiff. The application still owns applying accepted text to the document.
Your server route calls the model and returns a UTF-8 text stream or text SSE. Keep provider credentials on the server, and handle unsuccessful HTTP responses before treating their bodies as rewrite text. Provider helpers also decode supported OpenAI, Anthropic, and Vercel AI SDK formats into text tokens.
Keep keyboard controls predictable
The diff supports Cmd/Ctrl+Enter to accept and Escape to reject. Accept is disabled during streaming. GhostInput uses Tab or right arrow at the end of the textarea to accept a continuation, while Escape clears it.
Test these controls alongside the rest of your editor. A shortcut that works in isolation may conflict with a parent dialog, an input method, or an editor command. Rich-text adapters are provided, but the repository explicitly recommends checking them against your editor version.
Try the primitives
npm i @erankushpatil/anky-ui
For source ownership:
npx anky-ui add selection-palette
The repository also includes a mock-stream playground:
git clone https://github.com/ErAnkushPatil/anky-ui.git
cd anky-ui
pnpm install
pnpm dev:demo
Anky UI supports React 18/19 and Next.js App Router Client Components. Tailwind presets are optional. Start with the included demo, then test cancellation, rapid successive rewrites, selection changes, and accepting an edit after the document has changed.
Disclosure: I’m the author of Anky UI. This article was prepared with ChatGPT using the repository documentation. The code is an illustrative integration example.
Top comments (0)