Review changes (as a summary)

Continuation from the AI agent chatbot guide

This guide continues the AI agent chatbot guide. Read it first.

After the AI agent finishes its work, show a summary of all the changes.

Note

This guide is a variation of the Review changes guide where, instead of reviewing changes every time the AI edits the document, the user reviews all of them at the end.

To implement this feature, we'll use the AI Toolkit's capability to compare documents in real time.

When the user sends a message to the AI, call the startComparingDocuments method. This will start comparing the documents before and after the AI made changes.

// inside the onSubmit handler
const toolkit = getAiToolkit(editor)
toolkit.startComparingDocuments()

When the AI modifies the document, the changes are displayed as suggestions inside the editor.

Style suggestions

Create a CSS file (app/suggestions.css) to style suggestions in red/green.

/* Highlight inserted text in green */
.tiptap-ai-suggestion,
.tiptap-ai-suggestion > * {
  background-color: oklch(87.1% 0.15 154.449);
}
.tiptap-ai-suggestion--selected,
.tiptap-ai-suggestion--selected > * {
  background-color: oklch(79.2% 0.209 151.711);
}

/* Highlight deleted text in red */
.tiptap-ai-suggestion-diff,
.tiptap-ai-suggestion-diff > * {
  background-color: oklch(80.8% 0.114 19.571);
}
.tiptap-ai-suggestion-diff--selected,
.tiptap-ai-suggestion-diff--selected > * {
  background-color: oklch(70.4% 0.191 22.216);
}

Accept/reject all changes

To accept all changes, call stopComparingDocuments. This will hide the diff view and stop the real-time document comparison.

const toolkit = getAiToolkit(editor)

toolkit.stopComparingDocuments()

To reject all changes, call the rejectAllChanges method. This will reset the editor content to how it was before the AI made changes. Then, call stopComparingDocuments to stop the real-time document comparison.

toolkit.rejectAllChanges()
toolkit.stopComparingDocuments()

Accept/reject individual changes

You can also display buttons or a popover over a suggestion, with actions to accept or reject its associated change.

To render custom elements in the UI, set the displayOptions parameter. There, you can set the renderDecorations option. It's a function that returns a list of ProseMirror decorations.

toolkit.startComparingDocuments({
  displayOptions: {
    renderDecorations(options) {
      return [
        ...options.defaultRenderDecorations(),

        // Accept button
        Decoration.widget(options.range.to, () => {
          const element = document.createElement('button')
          element.textContent = 'Accept'
          element.addEventListener('click', () => {
            toolkit.acceptChange(options.suggestion.id)
          })
          return element
        }),

        // Reject button
        Decoration.widget(options.range.to, () => {
          const element = document.createElement('button')
          element.textContent = 'Reject'
          element.addEventListener('click', () => {
            toolkit.rejectChange(options.suggestion.id)
          })
          return element
        }),
      ]
    },
  },
})

Full demo code

// 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'
import { Decoration } from '@tiptap/pm/view'
import './suggestions.css'

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, status } = 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')
  const [isComparing, setIsComparing] = useState(false)

  if (!editor) return null

  const toolkit = getAiToolkit(editor)

  const showReviewUI = isComparing && status === 'ready'

  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>
      ))}
      {!showReviewUI && (
        <form
          onSubmit={(e) => {
            e.preventDefault()

            if (isComparing) return

            toolkit.startComparingDocuments({
              displayOptions: {
                renderDecorations(options) {
                  return [
                    ...options.defaultRenderDecorations(),

                    // Accept button
                    Decoration.widget(options.range.to, () => {
                      const element = document.createElement('button')
                      element.textContent = 'Accept'
                      element.addEventListener('click', () => {
                        toolkit.acceptChange(options.suggestion.id)
                      })
                      return element
                    }),

                    // Reject button
                    Decoration.widget(options.range.to, () => {
                      const element = document.createElement('button')
                      element.textContent = 'Reject'
                      element.addEventListener('click', () => {
                        toolkit.rejectChange(options.suggestion.id)
                      })
                      return element
                    }),
                  ]
                },
              },
            })
            setIsComparing(true)

            sendMessage({ text: input })
            setInput('')
          }}
        >
          <input disabled={isComparing} value={input} onChange={(e) => setInput(e.target.value)} />
        </form>
      )}
      {showReviewUI && (
        <div>
          <h2>Reviewing Changes</h2>
          <button
            onClick={() => {
              toolkit.stopComparingDocuments()
              setIsComparing(false)
            }}
          >
            Accept all
          </button>
          <button
            onClick={() => {
              toolkit.rejectAllChanges()
              toolkit.stopComparingDocuments()
              setIsComparing(false)
            }}
          >
            Reject all
          </button>
        </div>
      )}
    </div>
  )
}

End result

With additional CSS styles, the result is a simple but polished AI chatbot application where users can review all AI-generated changes after the AI finishes its work:

See the source code on GitHub.

Next steps

Learn more about the AI Toolkit's capability to compare documents in real time.