Skip to content

Instantly share code, notes, and snippets.

@kabeer11000
Created September 25, 2025 10:30
Show Gist options
  • Select an option

  • Save kabeer11000/1f1b3f3d6a8d2f11b74504c179794daa to your computer and use it in GitHub Desktop.

Select an option

Save kabeer11000/1f1b3f3d6a8d2f11b74504c179794daa to your computer and use it in GitHub Desktop.

Creating a Document Editor with AI and delta changes a faster way.

1. Use Tiptap's Transaction API

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);
}

2. Position-Based Updates with Node IDs

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..."
  }
];

3. Semantic Section Management

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;
  }
});

4. Range-Based Updates

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..."
  ]
};

5. Integration with RAG System

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);
}

6. Track Changes for Undo/Redo

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment