AI agent chatbot

Build a simple AI agent chatbot that can read and edit Tiptap documents.

Tech stack

Installation

Create a Next.js project:

npx create-next-app@latest ai-agent-chatbot

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 that uses the Vercel AI SDK to call the OpenAI model. Include the tool definitions for the Tiptap AI Toolkit.

// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai'
import { toolDefinitions } from '@tiptap-pro/ai-toolkit-ai-sdk'
import { convertToModelMessages, streamText, UIMessage } from 'ai'

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json()

  const result = streamText({
    model: openai('gpt-5-mini'),
    system: 'You are an assistant that can edit rich text documents.',
    messages: convertToModelMessages(messages),
    tools: toolDefinitions(),
  })

  return result.toUIMessageStreamResponse()
}

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 simple chat UI. This component leverages the useChat hook from the Vercel AI SDK to call the API endpoint and manage the chat conversation. When the AI model outputs a tool call, it uses the Tiptap AI Toolkit to execute the tool.

// app/page.tsx
'use client'

import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from 'ai'
import { useChat } from '@ai-sdk/react'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { useRef, useState } from 'react'
import { AiToolkit, getAiToolkit } from '@tiptap-pro/ai-toolkit'

export default function Page() {
  const editor = useEditor({
    immediatelyRender: false,
    extensions: [StarterKit, AiToolkit],
    content: `<h1>AI Agent Demo</h1><p>Ask the AI to improve this.</p>`,
  })

  // Fixes issue: https://github.com/vercel/ai/issues/8148
  const editorRef = useRef(editor)
  editorRef.current = editor

  // The AI Agent reads the document in chunks. This variable keeps track of the current chunk
  // that the AI Agent is reading.
  const currentChunk = useRef(0)

  const { messages, sendMessage, addToolResult } = useChat({
    transport: new DefaultChatTransport({ api: '/api/chat' }),
    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
    async onToolCall({ toolCall }) {
      const editor = editorRef.current
      if (!editor) return

      const { toolName, input, toolCallId } = toolCall

      // Use the AI Toolkit to execute the tool
      const toolkit = getAiToolkit(editor)
      const result = toolkit.executeTool({
        toolName,
        input,
        currentChunk: currentChunk.current,
      })

      currentChunk.current = result.currentChunk

      addToolResult({ tool: toolName, toolCallId, output: result.output })
    },
  })

  const [input, setInput] = useState('Replace the last paragraph with a short story about Tiptap')

  if (!editor) return null

  return (
    <div>
      <EditorContent editor={editor} />
      {messages?.map((message) => (
        <div key={message.id} style={{ whiteSpace: 'pre-wrap' }}>
          <strong>{message.role}</strong>
          <br />
          {message.parts
            .filter((p) => p.type === 'text')
            .map((p) => p.text)
            .join('\n')}
        </div>
      ))}
      <form
        onSubmit={(e) => {
          e.preventDefault()
          if (input.trim()) {
            sendMessage({ text: input })
            setInput('')
          }
        }}
      >
        <input value={input} onChange={(e) => setInput(e.target.value)} />
      </form>
    </div>
  )
}

End result

With additional CSS styles, the result is a simple but polished AI chatbot application:

See the source code on GitHub.

Next steps

Let your users review AI-generated changes with the review changes guide.