Inline edits
Make simple, small edits to the document. Select content and ask AI model to re-write it based on your instructions.
Tech stack
- React + Next.js
- AI SDK by Vercel + OpenAI models
- Tiptap AI Toolkit
Installation
Create a Next.js project:
npx create-next-app@latest inline-edits
Install the core Tiptap packages and the Vercel AI SDK for OpenAI:
npm install @tiptap/react @tiptap/starter-kit ai @ai-sdk/react @ai-sdk/openai
Install the Tiptap AI Toolkit and the tool definitions for the Vercel AI SDK.
Pro package
The AI Toolkit is a pro package. Before installation, set up access to the private NPM registry by following the private registry guide.
npm install @tiptap-pro/ai-toolkit @tiptap-pro/ai-toolkit-ai-sdk
API endpoint
Create an API endpoint /api/inline-edits
that uses the Vercel AI SDK to call the OpenAI model. This endpoint receives two parameters:
userRequest
(string
): The user's request to edit the selection. For example: "Add emojis to this text"selection
(string
): The current content of the selection in HTML format
The endpoint calls the AI model, asking it to re-write the content of the selection.
// app/api/inline-edits/route.ts
import { openai } from '@ai-sdk/openai'
import { streamText } from 'ai'
export async function POST(req: Request) {
const { userRequest, selection } = await req.json()
const result = streamText({
model: openai('gpt-5-mini'),
system:
"You are an expert writer that can edit rich text documents. The user has selected part of the document. You will receive the current content of the selection (in HTML format) and the user's request. Re-write the content of the selection to meet the user's request. Generate the HTML code for the new content of the selection. If the user's request is not clear or does not relate to editing the document, generate HTML code where you ask the user to clarify the request. Your response should only contain the HTML code, no other text or explanation, no Markdown, and your HTML response should not be wrapped in backticks, Markdown code blocks, or other extra formatting.",
prompt: `User request:
"""
${userRequest}
"""
Selection:
"""
${selection}
"""`,
})
// Return the text stream directly
return result.toTextStreamResponse()
}
To access the OpenAI API, create an API key in the OpenAI Dashboard and add it as an environment variable. The environment variable will be detected automatically by the Vercel AI SDK.
# .env
OPENAI_API_KEY=your-api-key
Client
Create a client-side React component that renders the Tiptap Editor and a button that, when clicked, calls the AI model to add emojis to the selected text.
// app/page.tsx
'use client'
import { EditorContent, useEditor, useEditorState } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { useState } from 'react'
import { AiToolkit, getAiToolkit } from '@tiptap-pro/ai-toolkit'
export default function Page() {
const editor = useEditor({
immediatelyRender: false,
extensions: [StarterKit, AiToolkit],
content: `<p>Select some text and click the "Add emojis" button to add emojis to your selection.</p>`,
})
if (!editor) return null
return (
<>
<EditorContent editor={editor} />
<button>Add emojis</button>
</>
)
}
Next, create an editSelection
function to call the AI model when the button is clicked.
Inside this function, get the current selection with the getHtmlSelection
method of the AI Toolkit. Then, call the API endpoint with the user's request and the selection. The API endpoint will return a stream of HTML that can be inserted into the editor with the streamHtml
method of the AI Toolkit.
// Show a loading state when the AI is generating content
const [isLoading, setIsLoading] = useState(false)
// Create a function to edit the selection with the AI-generated content
const editSelection = async (userRequest: string) => {
if (!editor) return
const toolkit = getAiToolkit(editor)
// Use the AI Toolkit to get the selection in HTML format
const selection = toolkit.getHtmlSelection()
setIsLoading(true)
// Call the API endpoint to get the edited HTML content
const response = await fetch('/api/inline-edits', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userRequest,
selection,
}),
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
// The response is a stream of HTML content
const readableStream = response.body
if (!readableStream) {
throw new Error('No response body')
}
// Use the AI Toolkit to stream HTML into the selection
await toolkit.streamHtml(readableStream, { position: 'selection' })
setIsLoading(false)
}
Finally, add the editSelection
function to the React component and call it when the button is clicked.
// Call the editSelection function when the button is clicked
<button onClick={() => editSelection('Add emojis to this text')}>Add emojis</button>
This is the full React component:
// app/page.tsx
'use client'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { useState } from 'react'
import { AiToolkit, getAiToolkit } from '@tiptap-pro/ai-toolkit'
export default function Page() {
const editor = useEditor({
immediatelyRender: false,
extensions: [StarterKit, AiToolkit],
content: `<p>Select some text and click the "Add emojis" button to add emojis to your selection.</p>`,
})
// Disable the buttons when the AI is generating content
const [isLoading, setIsLoading] = useState(false)
// Disable the buttons when the selection is empty
const selectionIsEmpty = useEditorState({
editor,
selector: (snapshot) => snapshot.editor?.state.selection.empty ?? true,
})
if (!editor) return null
const editSelection = async (userRequest: string) => {
const toolkit = getAiToolkit(editor)
// Use the AI Toolkit to get the selection in HTML format
const selection = toolkit.getHtmlSelection()
setIsLoading(true)
// Call the API endpoint to get the edited HTML content
const response = await fetch('/api/inline-edits', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userRequest,
selection,
}),
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
// The response is a stream of HTML content
const readableStream = response.body
if (!readableStream) {
throw new Error('No response body')
}
// Use the AI Toolkit to stream HTML into the selection
await toolkit.streamHtml(readableStream, { position: 'selection' })
setIsLoading(false)
}
const disabled = selectionIsEmpty || isLoading
return (
<>
<EditorContent editor={editor} />
<button onClick={() => editSelection('Add emojis to this text')} disabled={disabled}>
{isLoading ? 'Loading...' : 'Add emojis'}
</button>
</>
)
}
End result
With additional CSS styles, the result is a simple but polished text editor with buttons to edit the selection with AI:
See the source code on GitHub.
Next steps
- See the API reference of the
streamHtml
method to learn how to show a review UI after the AI has generated content. - Give the AI more context by, for example, providing it the content that goes before/after the selection. You can do it with the
getHtmlRange
method (see API reference).