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.
See the source code on GitHub.
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 rejectAllSuggestions method. This will reset the editor content to how it was before the AI made changes. The method returns AI feedback that you can collect and include in the next user message. Then, call stopComparingDocuments to stop the real-time document comparison.
const result = toolkit.rejectAllSuggestions()
// Collect feedback events to include in next user message
const userFeedback = result.aiFeedback.events
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.
The acceptSuggestion and rejectSuggestion methods return AI feedback that you can collect and send to the AI to improve future suggestions.
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', () => {
const result = toolkit.acceptSuggestion(options.suggestion.id)
// Collect feedback events
setReviewState((prev) => ({
...prev,
userFeedback: [...prev.userFeedback, ...result.aiFeedback.events],
}))
if (toolkit.getSuggestions().length === 0) {
stopComparing()
}
})
return element
}),
// Reject button
Decoration.widget(options.range.to, () => {
const element = document.createElement('button')
element.textContent = 'Reject'
element.addEventListener('click', () => {
const result = toolkit.rejectSuggestion(options.suggestion.id)
// Collect feedback events
setReviewState((prev) => ({
...prev,
userFeedback: [...prev.userFeedback, ...result.aiFeedback.events],
}))
if (toolkit.getSuggestions().length === 0) {
stopComparing()
}
})
return element
}),
]
},
},
})Full demo code
// app/page.tsx
'use client'
import { useChat } from '@ai-sdk/react'
import { Decoration } from '@tiptap/pm/view'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { AiToolkit, getAiToolkit, type SuggestionFeedbackEvent } from '@tiptap-pro/ai-toolkit'
import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from 'ai'
import { useState } from 'react'
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>`,
})
const { messages, sendMessage, addToolOutput, status } = useChat({
transport: new DefaultChatTransport({ api: '/api/chat' }),
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
async onToolCall({ toolCall }) {
if (!editor) return
const { toolName, input, toolCallId } = toolCall
// Reset feedback events when a new tool call starts
setReviewState((prev) => ({ ...prev, userFeedback: [] }))
// 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')
const [reviewState, setReviewState] = useState({
isComparing: false,
userFeedback: [] as SuggestionFeedbackEvent[],
})
if (!editor) return null
const toolkit = getAiToolkit(editor)
function stopComparing() {
toolkit.stopComparingDocuments()
setReviewState((prev) => ({ ...prev, isComparing: false }))
}
const showReviewUI = reviewState.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 (reviewState.isComparing) return
// Build message text with feedback if available
let messageText = input.trim()
if (reviewState.userFeedback.length > 0) {
const feedbackOutput = JSON.stringify(reviewState.userFeedback)
messageText += `\n\n<user_feedback>${feedbackOutput}</user_feedback>`
}
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', () => {
const result = toolkit.acceptSuggestion(options.suggestion.id)
setReviewState((prev) => ({
...prev,
userFeedback: [...prev.userFeedback, ...result.aiFeedback.events],
}))
if (toolkit.getSuggestions().length === 0) {
stopComparing()
}
})
return element
}),
// Reject button
Decoration.widget(options.range.to, () => {
const element = document.createElement('button')
element.textContent = 'Reject'
element.addEventListener('click', () => {
const result = toolkit.rejectSuggestion(options.suggestion.id)
setReviewState((prev) => ({
...prev,
userFeedback: [...prev.userFeedback, ...result.aiFeedback.events],
}))
if (toolkit.getSuggestions().length === 0) {
stopComparing()
}
})
return element
}),
]
},
},
})
setReviewState((prev) => ({ ...prev, isComparing: true }))
if (messageText) {
sendMessage({ text: messageText })
setInput('')
setReviewState((prev) => ({ ...prev, userFeedback: [] }))
}
}}
>
<input
disabled={reviewState.isComparing}
value={input}
onChange={(e) => setInput(e.target.value)}
/>
</form>
)}
{showReviewUI && (
<div>
<h2>Reviewing Changes</h2>
<button
onClick={() => {
const result = toolkit.acceptAllSuggestions()
// Collect all feedback events
const userFeedback = [...reviewState.userFeedback, ...result.aiFeedback.events]
setReviewState({
isComparing: false,
userFeedback,
})
toolkit.stopComparingDocuments()
}}
>
Accept all
</button>
<button
onClick={() => {
const result = toolkit.rejectAllSuggestions()
// Collect all feedback events
const userFeedback = [...reviewState.userFeedback, ...result.aiFeedback.events]
setReviewState({
isComparing: false,
userFeedback,
})
toolkit.stopComparingDocuments()
}}
>
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.