AI agent chatbot

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

See the source code on GitHub.

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 { createAgentUIStreamResponse, ToolLoopAgent, UIMessage } from 'ai'

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

  const agent = new ToolLoopAgent({
    model: openai('gpt-5-mini'),
    instructions: 'You are an assistant that can edit rich text documents.',
    tools: toolDefinitions(),
    providerOptions: {
      openai: {
        reasoningEffort: 'minimal',
      },
    },
  })

  return createAgentUIStreamResponse({
    agent,
    uiMessages: messages,
  })
}

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

When the AI model receives a request, it will decide to call the available tools to read the document and edit it. See the list of available tools for more details.

Client-side setup

When the AI model decides to call a tool, it needs to be executed on the client. This is done with the executeTool method:

import { Editor } from '@tiptap/react'
import { getAiToolkit } from '@tiptap-pro/ai-toolkit'

// Create a Tiptap Editor instance
const editor = new Editor()

// Get the Tiptap AI Toolkit instance
const toolkit = getAiToolkit(editor)

// The AI decides to call the `tiptapRead` tool to read the document
// Use the `executeTool` method to execute the tool and get the result
const result = toolkit.executeTool({
  // The name of the tool to execute
  toolName: 'tiptapRead',
  // AI-generated input
  input: {},
})
// Send the result back to the AI model

To implement this in your app, 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.

// 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 { 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>`,
  })

  const { messages, sendMessage, addtoolOutput } = useChat({
    transport: new DefaultChatTransport({ api: '/api/chat' }),
    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
    async onToolCall({ toolCall }) {
      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,
      })

      addtoolOutput({ 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()
          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