Creating a Document Editor with AI and delta changes a faster way.
Instead of replacing the entire document, create transactions that apply specific changes:
// Your chatbot returns structured commands
const chatbotResponse = {
message: "I've added a security section and updated the introduction.",
edits: [
{
type: "insertHeading",
position: 150,
level: 2,
text: "API Security Best Practices"
},
{
type: "insertParagraph",
position: 151,
text: "Implement rate limiting and input validation..."
},
{
type: "replaceText",
from: 45,
to: 67,
text: "comprehensive security protocols"
}
]
};
// Apply changes using Tiptap transactions
function applyEdits(editor, edits) {
const tr = editor.state.tr;
edits.forEach(edit => {
switch(edit.type) {
case 'insertHeading':
tr.insert(edit.position, editor.schema.nodes.heading.create(
{ level: edit.level },
editor.schema.text(edit.text)
));
break;
case 'insertParagraph':
tr.insert(edit.position, editor.schema.nodes.paragraph.create(
{},
editor.schema.text(edit.text)
));
break;
case 'replaceText':
tr.replaceWith(edit.from, edit.to, editor.schema.text(edit.text));
break;
}
});
editor.view.dispatch(tr);
}Add IDs to your document structure for reliable targeting:
// Configure Tiptap with node IDs
const editor = new Editor({
extensions: [
StarterKit,
// Add ID extension for trackable nodes
Document.extend({
addGlobalAttributes() {
return [
{
types: ['heading', 'paragraph', 'bulletList'],
attributes: {
id: {
default: null,
parseHTML: element => element.getAttribute('data-id'),
renderHTML: attributes => {
if (!attributes.id) return {};
return { 'data-id': attributes.id };
},
},
},
},
];
},
}),
],
});
// Chatbot references nodes by ID
const edits = [
{
type: "updateNodeById",
nodeId: "intro-paragraph-1",
content: "Updated introduction text..."
},
{
type: "insertAfterNode",
nodeId: "section-2-heading",
nodeType: "paragraph",
content: "New paragraph after section 2..."
}
];Create helper functions to work with document sections:
class DocumentManager {
constructor(editor) {
this.editor = editor;
}
findNodeById(id) {
let foundNode = null;
this.editor.state.doc.descendants((node, pos) => {
if (node.attrs.id === id) {
foundNode = { node, pos };
return false; // stop iteration
}
});
return foundNode;
}
insertAfterNode(nodeId, nodeType, content) {
const found = this.findNodeById(nodeId);
if (!found) return;
const tr = this.editor.state.tr;
const insertPos = found.pos + found.node.nodeSize;
const newNode = this.editor.schema.nodes[nodeType].create(
{ id: `generated-${Date.now()}` },
this.editor.schema.text(content)
);
tr.insert(insertPos, newNode);
this.editor.view.dispatch(tr);
}
updateNodeContent(nodeId, newContent) {
const found = this.findNodeById(nodeId);
if (!found) return;
const tr = this.editor.state.tr;
const newNode = found.node.type.create(
found.node.attrs,
this.editor.schema.text(newContent)
);
tr.replaceWith(found.pos, found.pos + found.node.nodeSize, newNode);
this.editor.view.dispatch(tr);
}
}
// Usage
const docManager = new DocumentManager(editor);
// Apply chatbot edits
chatbotEdits.forEach(edit => {
switch(edit.type) {
case 'updateSection':
docManager.updateNodeContent(edit.sectionId, edit.content);
break;
case 'addAfterSection':
docManager.insertAfterNode(edit.afterId, edit.nodeType, edit.content);
break;
}
});For more complex edits, use Tiptap's range utilities:
import { findChildren } from '@tiptap/core';
function applyRangeEdit(editor, edit) {
const { from, to, content, nodeType = 'paragraph' } = edit;
const tr = editor.state.tr;
// Create new content
const nodes = content.map(text =>
editor.schema.nodes[nodeType].create({}, editor.schema.text(text))
);
// Replace range with new nodes
tr.replaceWith(from, to, nodes);
editor.view.dispatch(tr);
}
// Chatbot can specify ranges to replace
const edit = {
type: "replaceRange",
from: 100,
to: 250,
content: [
"Updated first paragraph...",
"New second paragraph..."
]
};Structure your RAG prompt to return Tiptap-compatible edits:
const prompt = `
You are editing a document using Tiptap editor. Return your response as JSON with:
1. "message": Your conversational response
2. "edits": Array of edit operations
Available edit types:
- insertAfterNode: {type, afterNodeId, nodeType, content, id}
- updateNode: {type, nodeId, content}
- replaceRange: {type, from, to, content}
- insertAtPosition: {type, position, nodeType, content, id}
Current document structure: ${getDocumentStructure()}
User request: ${userMessage}
`;
// Process RAG response
async function handleChatbotResponse(userMessage) {
const response = await callRAG(prompt);
const parsed = JSON.parse(response);
// Show conversational response
displayChatMessage(parsed.message);
// Apply document edits
applyEditsToDocument(parsed.edits);
}Tiptap handles this automatically, but you can add custom tracking:
// Before applying edits
const beforeState = editor.state;
applyEdits(editor, chatbotEdits);
// Add to custom history if needed
addToChangeHistory({
type: 'chatbot-edit',
before: beforeState,
after: editor.state,
timestamp: Date.now()
});This approach lets you make precise, incremental changes to your Tiptap document without regenerating the entire content. The editor's transaction system ensures smooth updates and maintains undo/redo functionality.
Would you like me to elaborate on any specific part or help you implement a particular edit type?