From 538a78990960ad82166df2ffdddf63d3a547df92 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Wed, 7 May 2025 16:59:21 +0200 Subject: [PATCH 1/4] feat: add a ghost writer demo --- .../06-ghost-writer/.bnexample.json | 10 ++ .../07-collaboration/06-ghost-writer/App.tsx | 95 +++++++++++++++++++ .../06-ghost-writer/README.md | 9 ++ .../06-ghost-writer/index.html | 14 +++ .../07-collaboration/06-ghost-writer/main.tsx | 11 +++ .../06-ghost-writer/package.json | 29 ++++++ .../06-ghost-writer/styles.css | 9 ++ .../06-ghost-writer/tsconfig.json | 36 +++++++ .../06-ghost-writer/vite.config.ts | 32 +++++++ packages/core/src/editor/BlockNoteEditor.ts | 10 +- playground/src/examples.gen.tsx | 24 +++++ pnpm-lock.yaml | 53 ++++++++++- 12 files changed, 325 insertions(+), 7 deletions(-) create mode 100644 examples/07-collaboration/06-ghost-writer/.bnexample.json create mode 100644 examples/07-collaboration/06-ghost-writer/App.tsx create mode 100644 examples/07-collaboration/06-ghost-writer/README.md create mode 100644 examples/07-collaboration/06-ghost-writer/index.html create mode 100644 examples/07-collaboration/06-ghost-writer/main.tsx create mode 100644 examples/07-collaboration/06-ghost-writer/package.json create mode 100644 examples/07-collaboration/06-ghost-writer/styles.css create mode 100644 examples/07-collaboration/06-ghost-writer/tsconfig.json create mode 100644 examples/07-collaboration/06-ghost-writer/vite.config.ts diff --git a/examples/07-collaboration/06-ghost-writer/.bnexample.json b/examples/07-collaboration/06-ghost-writer/.bnexample.json new file mode 100644 index 0000000000..f0028aaf5e --- /dev/null +++ b/examples/07-collaboration/06-ghost-writer/.bnexample.json @@ -0,0 +1,10 @@ +{ + "playground": true, + "docs": true, + "author": "nperez0111", + "tags": ["Advanced", "Development", "Collaboration"], + "dependencies": { + "y-partykit": "^0.0.25", + "yjs": "^13.6.15" + } +} diff --git a/examples/07-collaboration/06-ghost-writer/App.tsx b/examples/07-collaboration/06-ghost-writer/App.tsx new file mode 100644 index 0000000000..37cd71ed17 --- /dev/null +++ b/examples/07-collaboration/06-ghost-writer/App.tsx @@ -0,0 +1,95 @@ +import "@blocknote/core/fonts/inter.css"; +import { BlockNoteView } from "@blocknote/mantine"; +import { useCreateBlockNote } from "@blocknote/react"; + +import YPartyKitProvider from "y-partykit/provider"; +import * as Y from "yjs"; +import "./styles.css"; +import { useEffect, useState } from "react"; + +const params = new URLSearchParams(window.location.search); +const ghostWritingRoom = params.get("room"); +const ghostWriterIndex = parseInt(params.get("index") || "1"); +const isGhostWriting = Boolean(ghostWritingRoom); +const roomName = ghostWritingRoom || `ghost-writer-${Date.now()}`; +// Sets up Yjs document and PartyKit Yjs provider. +const doc = new Y.Doc(); +const provider = new YPartyKitProvider( + "blocknote-dev.yousefed.partykit.dev", + // Use a unique name as a "room" for your application. + roomName, + doc +); + +const ghostContent = + "This demo shows a two-way sync of documents. It allows you to test collaboration features, and see how stable the editor is. "; + +export default function App() { + const [numGhostWriters, setNumGhostWriters] = useState(1); + const editor = useCreateBlockNote({ + collaboration: { + // The Yjs Provider responsible for transporting updates: + provider, + // Where to store BlockNote data in the Y.Doc: + fragment: doc.getXmlFragment("document-store"), + // Information (name and color) for this user: + user: { + name: isGhostWriting + ? `Ghost Writer #${ghostWriterIndex}` + : "My Username", + color: isGhostWriting ? "#CCCCCC" : "#00ff00", + }, + }, + }); + + useEffect(() => { + if (!isGhostWriting) { + return; + } + let index = 0; + let timeout: NodeJS.Timeout; + + const scheduleNextChar = () => { + const jitter = Math.random() * 200; // Random delay between 0-200ms + timeout = setTimeout(() => { + const firstBlock = editor.document?.[0]; + if (firstBlock) { + editor.insertInlineContent(ghostContent[index], { + updateSelection: true, + }); + index = (index + 1) % ghostContent.length; + } + scheduleNextChar(); + }, 50 + jitter); + }; + + scheduleNextChar(); + + return () => clearTimeout(timeout); + }, [editor]); + + // Renders the editor instance. + return ( + <> + {!isGhostWriting && ( + + )} + + + {!isGhostWriting && ( +
+ {Array.from({ length: numGhostWriters }).map((_, index) => ( +