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

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).