Suggestions (Experimental)
In this example, we have 4 editors (2 clients) & 1 in suggestion-view mode & 1 in suggestion-edit mode. To show the experimental support for suggesting content in (@y/y v14)
import "./style.css";import "@blocknote/core/fonts/inter.css";import "@blocknote/mantine/style.css";import { BlockNoteView } from "@blocknote/mantine";import { useCreateBlockNote } from "@blocknote/react";import { Awareness } from "@y/protocols/awareness";import { withCollaboration } from "@blocknote/core/y";import * as Y from "@y/y";const doc = new Y.Doc();const provider = { awareness: new Awareness(doc),};provider.awareness.setLocalStateField("user", { name: "Alice", color: "#30bced",});const doc2 = new Y.Doc();const provider2 = { awareness: new Awareness(doc2),};provider2.awareness.setLocalStateField("user", { name: "Bob", color: "#6eeb83",});const attrs = new Y.Attributions();// Batch timestamps: reuse the same timestamp for edits from the same user// within a 10-second window of inactivity.const BATCH_INTERVAL_MS = 10_000;const batchTimestamps = new Map<string, number>();const batchTimers = new Map<string, ReturnType<typeof setTimeout>>();function getBatchedTimestamp(userName: string): number { const existing = batchTimestamps.get(userName); const now = Date.now(); // Clear any pending reset timer const timer = batchTimers.get(userName); if (timer) clearTimeout(timer); // Start a new batch if none exists or the previous one expired if (existing == null) { batchTimestamps.set(userName, now); } // Reset the batch after 10s of inactivity batchTimers.set( userName, setTimeout(() => { batchTimestamps.delete(userName); batchTimers.delete(userName); }, BATCH_INTERVAL_MS), ); return batchTimestamps.get(userName)!;}// Track attributions per user for each docfunction trackAttributions( trackedDoc: Y.Doc, userName: string, attributions: Y.Attributions,) { trackedDoc.on( "update", ( update: Uint8Array, _origin: unknown, _ydoc: Y.Doc, tr: { local: boolean }, ) => { if (!tr.local) return; const contentIds = Y.createContentIdsFromUpdate(update); const timestamp = getBatchedTimestamp(userName); Y.insertIntoIdMap( attributions.inserts, Y.createIdMapFromIdSet(contentIds.inserts, [ Y.createContentAttribute("insert", userName), Y.createContentAttribute("insertAt", timestamp), ]), ); Y.insertIntoIdMap( attributions.deletes, Y.createIdMapFromIdSet(contentIds.deletes, [ Y.createContentAttribute("delete", userName), Y.createContentAttribute("deleteAt", timestamp), ]), ); }, );}// Track local changes on each doc with a distinct user nametrackAttributions(doc, "Alice", attrs);trackAttributions(doc2, "Bob", attrs);const suggestingDoc = new Y.Doc({ isSuggestionDoc: true });const suggestingProvider = { awareness: new Awareness(suggestingDoc),};suggestingProvider.awareness.setLocalStateField("user", { name: "Charlie", color: "#ffbc42",});const suggestingAttributionManager = Y.createAttributionManagerFromDiff( doc, suggestingDoc, { attrs },);suggestingAttributionManager.suggestionMode = false;const suggestionModeDoc = new Y.Doc({ isSuggestionDoc: true });const suggestionModeProvider = { awareness: new Awareness(suggestionModeDoc),};suggestionModeProvider.awareness.setLocalStateField("user", { name: "Debbie", color: "#ee6352",});const suggestionModeAttributionManager = Y.createAttributionManagerFromDiff( doc, suggestionModeDoc, { attrs },);suggestionModeAttributionManager.suggestionMode = true;// Track local changes on suggestion docs with distinct user namestrackAttributions(suggestingDoc, "Charlie", attrs);trackAttributions(suggestionModeDoc, "Debbie", attrs);// Function to sync two documentsfunction syncDocs(sourceDoc: Y.Doc, targetDoc: Y.Doc) { const update = Y.encodeStateAsUpdate(sourceDoc); Y.applyUpdate(targetDoc, update);}// Set up two-way syncfunction setupTwoWaySync(doc1: Y.Doc, doc2: Y.Doc) { syncDocs(doc1, doc2); syncDocs(doc2, doc1); doc1.on("update", (update) => { Y.applyUpdate(doc2, update); }); doc2.on("update", (update) => { Y.applyUpdate(doc1, update); });}setupTwoWaySync(doc, doc2);setupTwoWaySync(suggestingDoc, suggestionModeDoc);function Editor({ fragment, provider, attributionManager, userName, userColor,}: { fragment: Y.Type; provider: { awareness?: Awareness }; attributionManager?: Y.DiffAttributionManager; userName: string; userColor: string;}) { const editor = useCreateBlockNote( withCollaboration({ collaboration: { fragment, provider, attributionManager, user: { name: userName, color: userColor }, }, }), ); return <BlockNoteView editor={editor} />;}export default function App() { // Renders the editor instance using a React component. return ( <div> <div style={{ display: "flex", flexDirection: "row", gap: "10px", margin: "10px", }} > <div style={{ flex: 1 }}> Client A (Alice) <Editor fragment={doc.get("doc")} provider={provider} userName="Alice" userColor="#30bced" /> </div> <div style={{ flex: 1 }}> Client B (Bob) <Editor fragment={doc2.get("doc")} provider={provider2} userName="Bob" userColor="#6eeb83" /> </div> </div> <div style={{ display: "flex", flexDirection: "row", gap: "10px", margin: "10px", }} > <div style={{ flex: 1 }}> View Suggestions (Charlie) <Editor fragment={suggestingDoc.get("doc")} provider={suggestingProvider} attributionManager={suggestingAttributionManager} userName="Charlie" userColor="#ffbc42" /> </div> <div style={{ flex: 1 }}> Suggestion Mode (Debbie) <Editor fragment={suggestionModeDoc.get("doc")} provider={suggestionModeProvider} attributionManager={suggestionModeAttributionManager} userName="Debbie" userColor="#ee6352" /> </div> </div> </div> );}