Build a proofreader with the AI Toolkit
Build a proofreader that detects and corrects grammar and spelling errors in your Tiptap documents using AI.
See the source code on GitHub.
Tech stack
- React + Next.js
- AI SDK by Vercel + OpenAI models
- Tiptap AI Toolkit
Project overview
This demo uses the AI Toolkit's setHtmlSuggestions() method with streaming to provide real-time grammar checking. As the AI finds corrections, they are immediately highlighted in the editor, creating a responsive user experience. Users can then accept or reject suggestions individually or all at once.
Installation
Create a Next.js project:
npx create-next-app@latest proofreaderInstall 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 zodInstall the Tiptap AI Toolkit:
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-toolkitServer setup
Create an API endpoint that uses the Vercel AI SDK to call the OpenAI model for grammar checking. The endpoint streams corrections as they're generated, allowing for real-time display.
// app/api/grammar-check-changes/route.ts
import { openai } from '@ai-sdk/openai'
import { streamObject } from 'ai'
import { z } from 'zod'
export async function POST(req: Request) {
const { html } = await req.json()
const result = streamObject({
model: openai('gpt-4o-mini'),
prompt: `Fix all spelling and grammar errors in the HTML below. Return a list of specific changes that need to be made.
For each error found:
- Provide ONLY the exact text that needs to be changed, not the entire sentence
- Example: If "I didn't knew" needs to become "I didn't know", only provide delete: "knew" and insert: "know"
- Both "insert" and "delete" fields MUST contain non-empty text
- Each change should be minimal and specific
HTML to correct:
${html}`,
schema: z.object({
changes: z.array(
z.object({
insert: z.string().describe('The corrected text to insert'),
delete: z.string().describe('The incorrect text to delete'),
}),
),
}),
})
return result.toTextStreamResponse()
}Client setup
Create a React component that renders the editor and handles grammar checking. The component uses Vercel AI SDK's useObject hook to handle streaming, and the AI Toolkit's setHtmlSuggestions() method to display corrections in real-time.
// app/proofreader/page.tsx
'use client'
import { experimental_useObject as useObject } from '@ai-sdk/react'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { AiToolkit, getAiToolkit } from '@tiptap-pro/ai-toolkit'
import { useEffect, useRef, useState } from 'react'
import { z } from 'zod'
const INITIAL_CONTENT = `<h1>Grammar Check Demo</h1><p>This is a excelent editor for writng documents. It have many feature's that makes it very powerfull.</p>`
// Helper: Filter out invalid partial changes during streaming
function filterValidChanges(changes: Array<{ insert?: string; delete?: string } | undefined>) {
return changes.filter(
(change): change is { insert: string; delete: string } =>
!!change && !!change.insert?.trim() && !!change.delete?.trim(),
)
}
export default function Page() {
const editor = useEditor({
immediatelyRender: false,
extensions: [StarterKit, AiToolkit],
content: INITIAL_CONTENT,
})
const editorRef = useRef(editor)
editorRef.current = editor
const [isReviewing, setIsReviewing] = useState(false)
const { submit, isLoading, object } = useObject({
api: '/api/grammar-check-changes',
schema: z.object({
changes: z.array(
z.object({
insert: z.string(),
delete: z.string(),
}),
),
}),
onFinish: () => {
setIsReviewing(true)
},
})
// Apply streaming suggestions in real-time as they arrive
useEffect(() => {
const currentEditor = editorRef.current
if (!currentEditor || !object?.changes) return
const validChanges = filterValidChanges(object.changes)
if (validChanges.length > 0) {
const toolkit = getAiToolkit(currentEditor)
toolkit.setHtmlSuggestions({ changes: validChanges })
}
}, [object?.changes])
const checkGrammar = () => {
const currentEditor = editorRef.current
if (!currentEditor) return
const htmlToCheck = currentEditor.getHTML()
submit({ html: htmlToCheck })
}
if (!editor) return null
return (
<div>
<EditorContent editor={editor} />
{!isReviewing && (
<button onClick={checkGrammar} disabled={isLoading}>
{isLoading ? 'Checking...' : 'Check Grammar'}
</button>
)}
{isReviewing && (
<div>
<p>Corrections are highlighted in the document above.</p>
<button
onClick={() => {
const toolkit = getAiToolkit(editor)
toolkit.applyAllSuggestions()
setIsReviewing(false)
}}
>
Accept all
</button>
<button
onClick={() => {
const toolkit = getAiToolkit(editor)
toolkit.setSuggestions([])
setIsReviewing(false)
}}
>
Reject all
</button>
</div>
)}
</div>
)
}Related guides
- Display suggestions - Learn more about the
setHtmlSuggestions()method - Review changes - Build a review changes UI
- AI engineering guide - Best practices for AI integration