From b87a4abdbb83f54e46907c6fa1ee69907e2c8bd4 Mon Sep 17 00:00:00 2001 From: yousefed Date: Fri, 14 Nov 2025 21:32:13 +0100 Subject: [PATCH 01/10] initial autocomplete attempt --- examples/09-ai/01-minimal/src/App.tsx | 2 + .../SuggestionMenu/SuggestionPlugin.ts | 17 +- packages/xl-ai/src/index.ts | 1 + .../xl-ai/src/plugins/AutoCompletePlugin.ts | 437 ++++++++++++++++++ packages/xl-ai/src/style.css | 6 + 5 files changed, 456 insertions(+), 7 deletions(-) create mode 100644 packages/xl-ai/src/plugins/AutoCompletePlugin.ts diff --git a/examples/09-ai/01-minimal/src/App.tsx b/examples/09-ai/01-minimal/src/App.tsx index 8c0ce912a2..8cc25c07db 100644 --- a/examples/09-ai/01-minimal/src/App.tsx +++ b/examples/09-ai/01-minimal/src/App.tsx @@ -14,6 +14,7 @@ import { import { AIMenuController, AIToolbarButton, + createAIAutoCompleteExtension, createAIExtension, getAISlashMenuItems, } from "@blocknote/xl-ai"; @@ -41,6 +42,7 @@ export default function App() { api: `${BASE_URL}/regular/streamText`, }), }), + createAIAutoCompleteExtension(), ], // We set some initial content for demo purposes initialContent: [ diff --git a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts index 0c4bb7f4a6..10b5ee8818 100644 --- a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts +++ b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts @@ -290,7 +290,7 @@ export class SuggestionMenuProseMirrorPlugin< }, props: { - handleTextInput(view, from, to, text) { + handleTextInput(view, from, to, text, deflt) { // only on insert if (from === to) { const doc = view.state.doc; @@ -301,18 +301,21 @@ export class SuggestionMenuProseMirrorPlugin< : text; if (str === snippet) { - view.dispatch(view.state.tr.insertText(text)); view.dispatch( - view.state.tr - .setMeta(suggestionMenuPluginKey, { - triggerCharacter: snippet, - }) - .scrollIntoView(), + deflt().setMeta(suggestionMenuPluginKey, { + triggerCharacter: snippet, + }), ); return true; } } } + if (this.getState(view.state)) { + // when menu is open, we dispatch the default transaction + // and return true so that other event handlers (i.e.: AI AutoComplete) are not triggered + view.dispatch(deflt()); + return true; + } return false; }, diff --git a/packages/xl-ai/src/index.ts b/packages/xl-ai/src/index.ts index 843bb17d03..7db8f0650d 100644 --- a/packages/xl-ai/src/index.ts +++ b/packages/xl-ai/src/index.ts @@ -11,4 +11,5 @@ export * from "./components/AIMenu/PromptSuggestionMenu.js"; export * from "./components/FormattingToolbar/AIToolbarButton.js"; export * from "./components/SuggestionMenu/getAISlashMenuItems.js"; export * from "./i18n/dictionary.js"; +export * from "./plugins/AutoCompletePlugin.js"; export * from "./streamTool/index.js"; diff --git a/packages/xl-ai/src/plugins/AutoCompletePlugin.ts b/packages/xl-ai/src/plugins/AutoCompletePlugin.ts new file mode 100644 index 0000000000..7d11e9642d --- /dev/null +++ b/packages/xl-ai/src/plugins/AutoCompletePlugin.ts @@ -0,0 +1,437 @@ +import { + BlockNoteEditor, + BlockNoteExtension, + BlockSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { EditorState, Plugin, PluginKey } from "prosemirror-state"; +import { Decoration, DecorationSet } from "prosemirror-view"; + +export type AutoCompleteState = + | { + autoCompleteSuggestion: AutoCompleteSuggestion; + } + | undefined; + +// class AutoCompleteView< +// BSchema extends BlockSchema, +// I extends InlineContentSchema, +// S extends StyleSchema, +// > implements PluginView +// { +// public state?: AutoCompleteState; + +// private rootEl?: Document | ShadowRoot; +// // pluginState: AutoCompleteState; + +// constructor( +// private readonly editor: BlockNoteEditor, +// public readonly view: EditorView, +// ) { +// // this.pluginState = undefined; +// } +// } + +const autoCompletePluginKey = new PluginKey<{ isUserInput: boolean }>( + "AutoCompletePlugin", +); + +type AutoCompleteSuggestion = { + position: number; + suggestion: string; +}; + +async function fetchAutoCompleteSuggestions( + state: EditorState, + _signal: AbortSignal, +) { + console.log("fetch"); + return [ + { + position: state.selection.from, + suggestion: "Hello World", + }, + { + position: state.selection.from, + suggestion: "Hello Planet", + }, + ]; +} + +function getMatchingSuggestions( + autoCompleteSuggestions: AutoCompleteSuggestion[], + state: EditorState, +): AutoCompleteSuggestion[] { + return autoCompleteSuggestions + .map((suggestion) => { + if (suggestion.position > state.selection.from) { + return false; + } + + if ( + !state.doc + .resolve(suggestion.position) + .sameParent(state.selection.$from) + ) { + return false; + } + + const text = state.doc.textBetween( + suggestion.position, + state.selection.from, + ); + if ( + suggestion.suggestion.startsWith(text) && + suggestion.suggestion.length > text.length + ) { + return { + position: suggestion.position, + suggestion: suggestion.suggestion.slice(text.length), + }; + } + return false; + }) + .filter((suggestion) => suggestion !== false); +} + +export class AutoCompleteProseMirrorPlugin< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema, +> extends BlockNoteExtension { + public static key() { + return "suggestionMenu"; + } + + public get priority(): number | undefined { + return 1000000; // should be lower (e.g.: -1000 to be below suggestion menu, but that currently breaks Tab) + } + + // private view: AutoCompleteView | undefined; + + // private view: EditorView | undefined; + private autoCompleteSuggestions: AutoCompleteSuggestion[] = []; + + private debounceFetchSuggestions = debounceWithAbort( + async (state: EditorState, signal: AbortSignal) => { + // fetch suggestions + const autoCompleteSuggestions = await fetchAutoCompleteSuggestions( + state, + signal, + ); + + // TODO: map positions? + + if (signal.aborted) { + return; + } + + this.autoCompleteSuggestions = autoCompleteSuggestions; + this.editor.transact((tr) => { + tr.setMeta(autoCompletePluginKey, { + autoCompleteSuggestions, + }); + }); + }, + ); + + constructor(private readonly editor: BlockNoteEditor) { + super(); + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + this.addProsemirrorPlugin( + new Plugin({ + key: autoCompletePluginKey, + + // view: (view) => { + // this.view = new AutoCompleteView(editor, view); + // return this.view; + // }, + + state: { + // Initialize the plugin's internal state. + init(): AutoCompleteState { + return undefined; + }, + + // Apply changes to the plugin state from an editor transaction. + apply: ( + transaction, + prev, + _oldState, + newState, + ): AutoCompleteState => { + // selection is active, no autocomplete + if (newState.selection.from !== newState.selection.to) { + this.debounceFetchSuggestions.cancel(); + return undefined; + } + + // Are there matching suggestions? + const matchingSuggestions = getMatchingSuggestions( + this.autoCompleteSuggestions, + newState, + ); + + if (matchingSuggestions.length > 0) { + this.debounceFetchSuggestions.cancel(); + return { + autoCompleteSuggestion: matchingSuggestions[0], + }; + } + + // No matching suggestions, if isUserInput is true, debounce fetch suggestions + if (transaction.getMeta(autoCompletePluginKey)?.isUserInput) { + this.debounceFetchSuggestions(newState).catch((error) => { + /* eslint-disable-next-line no-console */ + console.error(error); + }); + } else { + // clear suggestions + this.autoCompleteSuggestions = []; + } + return undefined; + + // Ignore transactions in code blocks. + // if (transaction.selection.$from.parent.type.spec.code) { + // return prev; + // } + + // // Either contains the trigger character if the menu should be shown, + // // or null if it should be hidden. + // const suggestionPluginTransactionMeta: { + // triggerCharacter: string; + // deleteTriggerCharacter?: boolean; + // ignoreQueryLength?: boolean; + // } | null = transaction.getMeta(autoCompletePluginKey); + + // if ( + // typeof suggestionPluginTransactionMeta === "object" && + // suggestionPluginTransactionMeta !== null + // ) { + // if (prev) { + // // Close the previous menu if it exists + // this.closeMenu(); + // } + // const trackedPosition = trackPosition( + // editor, + // newState.selection.from - + // // Need to account for the trigger char that was inserted, so we offset the position by the length of the trigger character. + // suggestionPluginTransactionMeta.triggerCharacter.length, + // ); + // return { + // triggerCharacter: + // suggestionPluginTransactionMeta.triggerCharacter, + // deleteTriggerCharacter: + // suggestionPluginTransactionMeta.deleteTriggerCharacter !== + // false, + // // When reading the queryStartPos, we offset the result by the length of the trigger character, to make it easy on the caller + // queryStartPos: () => + // trackedPosition() + + // suggestionPluginTransactionMeta.triggerCharacter.length, + // query: "", + // decorationId: `id_${Math.floor(Math.random() * 0xffffffff)}`, + // ignoreQueryLength: + // suggestionPluginTransactionMeta?.ignoreQueryLength, + // }; + // } + + // // Checks if the menu is hidden, in which case it doesn't need to be hidden or updated. + // if (prev === undefined) { + // return prev; + // } + + // // Checks if the menu should be hidden. + // if ( + // // Highlighting text should hide the menu. + // newState.selection.from !== newState.selection.to || + // // Transactions with plugin metadata should hide the menu. + // suggestionPluginTransactionMeta === null || + // // Certain mouse events should hide the menu. + // // TODO: Change to global mousedown listener. + // transaction.getMeta("focus") || + // transaction.getMeta("blur") || + // transaction.getMeta("pointer") || + // // Moving the caret before the character which triggered the menu should hide it. + // (prev.triggerCharacter !== undefined && + // newState.selection.from < prev.queryStartPos()) || + // // Moving the caret to a new block should hide the menu. + // !newState.selection.$from.sameParent( + // newState.doc.resolve(prev.queryStartPos()), + // ) + // ) { + // return undefined; + // } + + // const next = { ...prev }; + // // here we wi + // // Updates the current query. + // next.query = newState.doc.textBetween( + // prev.queryStartPos(), + // newState.selection.from, + // ); + + // return next; + }, + }, + + props: { + handleKeyDown(view, event) { + if (event.key === "Tab") { + // TODO (discuss with Nick): + // Plugin priority needs to be below suggestion menu, so no auto complete is triggered when the suggestion menu is open + // However, Plugin priority needs to be above other Tab handlers (because now indentation will be wrongly prioritized over auto complete) + const autoCompleteState = this.getState(view.state); + + if (autoCompleteState) { + // insert suggestion + view.dispatch( + view.state.tr + .insertText( + autoCompleteState.autoCompleteSuggestion.suggestion, + ) + .setMeta(autoCompletePluginKey, { isUserInput: true }), // isUserInput true to trigger new fetch + ); + return true; + } + + // if tab to suggest is enabled (TODO: make configurable) + view.dispatch( + view.state.tr.setMeta(autoCompletePluginKey, { + isUserInput: true, + }), + ); + return true; + } + + if (event.key === "Escape") { + self.autoCompleteSuggestions = []; + self.debounceFetchSuggestions.cancel(); + view.dispatch(view.state.tr.setMeta(autoCompletePluginKey, {})); + return true; + } + + return false; + }, + handleTextInput(view, _from, _to, _text, deflt) { + const tr = deflt(); + tr.setMeta(autoCompletePluginKey, { + isUserInput: true, + }); + view.dispatch(tr); + return true; + }, + + // Setup decorator on the currently active suggestion. + decorations(state) { + const autoCompleteState: AutoCompleteState = this.getState(state); + + if (!autoCompleteState) { + return null; + } + + console.log(autoCompleteState); + // Creates an inline decoration around the trigger character. + return DecorationSet.create(state.doc, [ + Decoration.widget( + state.selection.from, + renderAutoCompleteSuggestion( + autoCompleteState.autoCompleteSuggestion.suggestion, + ), + {}, + ), + ]); + }, + }, + }), + ); + } +} + +function renderAutoCompleteSuggestion(suggestion: string) { + const element = document.createElement("span"); + element.classList.add("bn-autocomplete-decorator"); + element.textContent = suggestion; + return element; +} + +export function debounceWithAbort( + fn: (...args: [...T, AbortSignal]) => Promise | R, + delay = 300, // TODO: configurable +) { + let timeoutId: ReturnType | null = null; + let controller: AbortController | null = null; + + const debounced = (...args: T): Promise => { + // Clear pending timeout + if (timeoutId) { + clearTimeout(timeoutId); + } + + // Abort any in-flight execution + if (controller) { + controller.abort(); + } + + controller = new AbortController(); + const signal = controller.signal; + + return new Promise((resolve, reject) => { + timeoutId = setTimeout(async () => { + try { + const result = await fn(...args, signal); + resolve(result); + } catch (err) { + reject(err); + } + }, delay); + }); + }; + + // External cancel method + debounced.cancel = () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = null; + + if (controller) { + controller.abort(); + } + controller = null; + }; + + return debounced; +} + +// Add a type for the cancel method +export interface DebouncedFunction { + (...args: T): Promise; + cancel(): void; +} + +// TODO: more to blocknote API? +// TODO: test with Collaboration edits +// TODO: compare kilocode / cline etc +// TODO: think about advanced scenarios (e.g.: multiple suggestions, etc.) + +/** + * Create a new AIExtension instance, this can be passed to the BlockNote editor via the `extensions` option + */ +export function createAIAutoCompleteExtension() { + // options: ConstructorParameters[1], + return (editor: BlockNoteEditor) => { + return new AutoCompleteProseMirrorPlugin(editor); + }; +} + +/** + * Return the AIExtension instance from the editor + */ +export function getAIAutoCompleteExtension( + editor: BlockNoteEditor, +) { + return editor.extension(AutoCompleteProseMirrorPlugin); +} diff --git a/packages/xl-ai/src/style.css b/packages/xl-ai/src/style.css index 4b7558d518..5547d181b3 100644 --- a/packages/xl-ai/src/style.css +++ b/packages/xl-ai/src/style.css @@ -31,3 +31,9 @@ del, text-decoration: line-through; text-decoration-thickness: 1px; } + +.bn-autocomplete-decorator { + width: 50px; + height: 50px; + color: var(--bn-colors-side-menu); +} From 656d9eb57ad5423bade2a5732f66ca2e2b428851 Mon Sep 17 00:00:00 2001 From: yousefed Date: Fri, 14 Nov 2025 23:05:49 +0100 Subject: [PATCH 02/10] connect api --- packages/xl-ai-server/src/index.ts | 2 + .../xl-ai-server/src/routes/autocomplete.ts | 46 +++++++++++++++++ .../xl-ai/src/plugins/AutoCompletePlugin.ts | 49 +++++++++++++------ 3 files changed, 83 insertions(+), 14 deletions(-) create mode 100644 packages/xl-ai-server/src/routes/autocomplete.ts diff --git a/packages/xl-ai-server/src/index.ts b/packages/xl-ai-server/src/index.ts index f66ef6738e..375252e1e3 100644 --- a/packages/xl-ai-server/src/index.ts +++ b/packages/xl-ai-server/src/index.ts @@ -5,6 +5,7 @@ import { cors } from "hono/cors"; import { existsSync, readFileSync } from "node:fs"; import { createSecureServer } from "node:http2"; import { Agent, setGlobalDispatcher } from "undici"; +import { autocompleteRoute } from "./routes/autocomplete.js"; import { modelPlaygroundRoute } from "./routes/model-playground/index.js"; import { objectGenerationRoute } from "./routes/objectGeneration.js"; import { proxyRoute } from "./routes/proxy.js"; @@ -37,6 +38,7 @@ app.route("/ai/proxy", proxyRoute); app.route("/ai/object-generation", objectGenerationRoute); app.route("/ai/server-promptbuilder", serverPromptbuilderRoute); app.route("/ai/model-playground", modelPlaygroundRoute); +app.route("/ai/autocomplete", autocompleteRoute); const http2 = existsSync("localhost.pem"); serve( diff --git a/packages/xl-ai-server/src/routes/autocomplete.ts b/packages/xl-ai-server/src/routes/autocomplete.ts new file mode 100644 index 0000000000..a7099a5f75 --- /dev/null +++ b/packages/xl-ai-server/src/routes/autocomplete.ts @@ -0,0 +1,46 @@ +import { createGroq } from "@ai-sdk/groq"; +import { generateText } from "ai"; +import { Hono } from "hono"; + +export const autocompleteRoute = new Hono(); + +// Setup your model +// const model = createOpenAI({ +// apiKey: process.env.OPENAI_API_KEY, +// })("gpt-4.1-nano"); + +const model = createGroq({ + apiKey: process.env.GROQ_API_KEY, +})("openai/gpt-oss-20b"); + +// Use `streamText` to stream text responses from the LLM +autocompleteRoute.post("/generateText", async (c) => { + const { text } = await c.req.json(); + + const result = await generateText({ + model, + system: `You are a writing assistant. Predict and generate the most likely next part of the text. +- separate suggestions by newlines +- max 3 suggestions +- keep it short, max 5 words per suggestion +- don't include other text (or explanations) +- ONLY return the text to be appended. Your suggestion will EXACTLY replace [SUGGESTION_HERE]. +- DONT include the original text / characters (prefix) +- add a space (or other relevant punctuation) before the suggestion if starting a new word`, + messages: [ + { + role: "user", + content: `Complete the following text: + ${text}[SUGGESTION_HERE]`, + }, + ], + abortSignal: c.req.raw.signal, + }); + + return c.json({ + suggestions: result.text + .split("\n") + .map((suggestion) => suggestion.trimEnd()) + .filter((suggestion) => suggestion.trim().length > 0), + }); +}); diff --git a/packages/xl-ai/src/plugins/AutoCompletePlugin.ts b/packages/xl-ai/src/plugins/AutoCompletePlugin.ts index 7d11e9642d..be7d767ab5 100644 --- a/packages/xl-ai/src/plugins/AutoCompletePlugin.ts +++ b/packages/xl-ai/src/plugins/AutoCompletePlugin.ts @@ -46,17 +46,34 @@ async function fetchAutoCompleteSuggestions( state: EditorState, _signal: AbortSignal, ) { - console.log("fetch"); - return [ - { - position: state.selection.from, - suggestion: "Hello World", - }, + // TODO: options to get block json until selection + const text = state.doc.textBetween( + state.selection.from - 300, + state.selection.from, + ); + + const response = await fetch( + `https://localhost:3000/ai/autocomplete/generateText`, { - position: state.selection.from, - suggestion: "Hello Planet", + method: "POST", + body: JSON.stringify({ text }), }, - ]; + ); + const data = await response.json(); + return data.suggestions.map((suggestion: string) => ({ + position: state.selection.from, + suggestion: suggestion, + })); + // return [ + // { + // position: state.selection.from, + // suggestion: "Hello World", + // }, + // { + // position: state.selection.from, + // suggestion: "Hello Planet", + // }, + // ]; } function getMatchingSuggestions( @@ -136,7 +153,10 @@ export class AutoCompleteProseMirrorPlugin< }, ); - constructor(private readonly editor: BlockNoteEditor) { + constructor( + private readonly editor: BlockNoteEditor, + options: {}, + ) { super(); // eslint-disable-next-line @typescript-eslint/no-this-alias @@ -416,14 +436,15 @@ export interface DebouncedFunction { // TODO: test with Collaboration edits // TODO: compare kilocode / cline etc // TODO: think about advanced scenarios (e.g.: multiple suggestions, etc.) - +// TODO: double tap -> extra long /** * Create a new AIExtension instance, this can be passed to the BlockNote editor via the `extensions` option */ -export function createAIAutoCompleteExtension() { - // options: ConstructorParameters[1], +export function createAIAutoCompleteExtension( + options: ConstructorParameters[1], +) { return (editor: BlockNoteEditor) => { - return new AutoCompleteProseMirrorPlugin(editor); + return new AutoCompleteProseMirrorPlugin(editor, options); }; } From 643c739df29d1b0917a127934b608f7de197b220 Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 20 Nov 2025 14:39:39 +0100 Subject: [PATCH 03/10] wip --- examples/09-ai/01-minimal/src/App.tsx | 41 ++++- .../xl-ai-server/src/routes/autocomplete.ts | 6 +- .../xl-ai/src/plugins/AutoCompletePlugin.ts | 153 +++--------------- 3 files changed, 68 insertions(+), 132 deletions(-) diff --git a/examples/09-ai/01-minimal/src/App.tsx b/examples/09-ai/01-minimal/src/App.tsx index 8cc25c07db..adc1989fe7 100644 --- a/examples/09-ai/01-minimal/src/App.tsx +++ b/examples/09-ai/01-minimal/src/App.tsx @@ -27,6 +27,45 @@ import { getEnv } from "./getEnv"; const BASE_URL = getEnv("BLOCKNOTE_AI_SERVER_BASE_URL") || "https://localhost:3000/ai"; +async function autoCompleteProvider( + editor: BlockNoteEditor, + signal: AbortSignal, +) { + // TODO: + // - API is very prosemirror-based, make something more BlockNote-native + // - Add simple method to retrieve relevant context (e.g. block content / json until selection) + + const state = editor.prosemirrorState; + const text = state.doc.textBetween( + state.selection.from - 300, + state.selection.from, + ); + + const response = await fetch( + `https://localhost:3000/ai/autocomplete/generateText`, + { + method: "POST", + body: JSON.stringify({ text }), + signal, + }, + ); + const data = await response.json(); + return data.suggestions.map((suggestion: string) => ({ + position: state.selection.from, + suggestion: suggestion, + })); + // return [ + // { + // position: state.selection.from, + // suggestion: "Hello World", + // }, + // { + // position: state.selection.from, + // suggestion: "Hello Planet", + // }, + // ]; +} + export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ @@ -42,7 +81,7 @@ export default function App() { api: `${BASE_URL}/regular/streamText`, }), }), - createAIAutoCompleteExtension(), + createAIAutoCompleteExtension({ autoCompleteProvider }), ], // We set some initial content for demo purposes initialContent: [ diff --git a/packages/xl-ai-server/src/routes/autocomplete.ts b/packages/xl-ai-server/src/routes/autocomplete.ts index a7099a5f75..b5a47a00a1 100644 --- a/packages/xl-ai-server/src/routes/autocomplete.ts +++ b/packages/xl-ai-server/src/routes/autocomplete.ts @@ -24,9 +24,9 @@ autocompleteRoute.post("/generateText", async (c) => { - max 3 suggestions - keep it short, max 5 words per suggestion - don't include other text (or explanations) -- ONLY return the text to be appended. Your suggestion will EXACTLY replace [SUGGESTION_HERE]. -- DONT include the original text / characters (prefix) -- add a space (or other relevant punctuation) before the suggestion if starting a new word`, +- YOU MUST ONLY return the text to be appended. Your suggestion will EXACTLY replace [SUGGESTION_HERE]. +- YOU MUST NOT include the original text / characters (prefix) in your suggestion. +- YOU MUST add a space (or other relevant punctuation) before the suggestion IF starting a new word (the suggestion will be directly concatenated to the text)`, messages: [ { role: "user", diff --git a/packages/xl-ai/src/plugins/AutoCompletePlugin.ts b/packages/xl-ai/src/plugins/AutoCompletePlugin.ts index be7d767ab5..907ff3ce88 100644 --- a/packages/xl-ai/src/plugins/AutoCompletePlugin.ts +++ b/packages/xl-ai/src/plugins/AutoCompletePlugin.ts @@ -42,39 +42,10 @@ type AutoCompleteSuggestion = { suggestion: string; }; -async function fetchAutoCompleteSuggestions( - state: EditorState, - _signal: AbortSignal, -) { - // TODO: options to get block json until selection - const text = state.doc.textBetween( - state.selection.from - 300, - state.selection.from, - ); - - const response = await fetch( - `https://localhost:3000/ai/autocomplete/generateText`, - { - method: "POST", - body: JSON.stringify({ text }), - }, - ); - const data = await response.json(); - return data.suggestions.map((suggestion: string) => ({ - position: state.selection.from, - suggestion: suggestion, - })); - // return [ - // { - // position: state.selection.from, - // suggestion: "Hello World", - // }, - // { - // position: state.selection.from, - // suggestion: "Hello Planet", - // }, - // ]; -} +type AutoCompleteProvider = ( + editor: BlockNoteEditor, + signal: AbortSignal, +) => Promise; function getMatchingSuggestions( autoCompleteSuggestions: AutoCompleteSuggestion[], @@ -131,10 +102,10 @@ export class AutoCompleteProseMirrorPlugin< private autoCompleteSuggestions: AutoCompleteSuggestion[] = []; private debounceFetchSuggestions = debounceWithAbort( - async (state: EditorState, signal: AbortSignal) => { + async (editor: BlockNoteEditor, signal: AbortSignal) => { // fetch suggestions - const autoCompleteSuggestions = await fetchAutoCompleteSuggestions( - state, + const autoCompleteSuggestions = await this.options.autoCompleteProvider( + editor, signal, ); @@ -155,7 +126,9 @@ export class AutoCompleteProseMirrorPlugin< constructor( private readonly editor: BlockNoteEditor, - options: {}, + private readonly options: { + autoCompleteProvider: AutoCompleteProvider; + }, ) { super(); @@ -179,7 +152,7 @@ export class AutoCompleteProseMirrorPlugin< // Apply changes to the plugin state from an editor transaction. apply: ( transaction, - prev, + _prev, _oldState, newState, ): AutoCompleteState => { @@ -204,96 +177,19 @@ export class AutoCompleteProseMirrorPlugin< // No matching suggestions, if isUserInput is true, debounce fetch suggestions if (transaction.getMeta(autoCompletePluginKey)?.isUserInput) { - this.debounceFetchSuggestions(newState).catch((error) => { - /* eslint-disable-next-line no-console */ - console.error(error); + // TODO: this queueMicrotask is a workaround to ensure the transaction is applied before the debounceFetchSuggestions is called + // (discuss with Nick what ideal architecture would be) + queueMicrotask(() => { + this.debounceFetchSuggestions(self.editor).catch((error) => { + /* eslint-disable-next-line no-console */ + console.error(error); + }); }); } else { // clear suggestions this.autoCompleteSuggestions = []; } return undefined; - - // Ignore transactions in code blocks. - // if (transaction.selection.$from.parent.type.spec.code) { - // return prev; - // } - - // // Either contains the trigger character if the menu should be shown, - // // or null if it should be hidden. - // const suggestionPluginTransactionMeta: { - // triggerCharacter: string; - // deleteTriggerCharacter?: boolean; - // ignoreQueryLength?: boolean; - // } | null = transaction.getMeta(autoCompletePluginKey); - - // if ( - // typeof suggestionPluginTransactionMeta === "object" && - // suggestionPluginTransactionMeta !== null - // ) { - // if (prev) { - // // Close the previous menu if it exists - // this.closeMenu(); - // } - // const trackedPosition = trackPosition( - // editor, - // newState.selection.from - - // // Need to account for the trigger char that was inserted, so we offset the position by the length of the trigger character. - // suggestionPluginTransactionMeta.triggerCharacter.length, - // ); - // return { - // triggerCharacter: - // suggestionPluginTransactionMeta.triggerCharacter, - // deleteTriggerCharacter: - // suggestionPluginTransactionMeta.deleteTriggerCharacter !== - // false, - // // When reading the queryStartPos, we offset the result by the length of the trigger character, to make it easy on the caller - // queryStartPos: () => - // trackedPosition() + - // suggestionPluginTransactionMeta.triggerCharacter.length, - // query: "", - // decorationId: `id_${Math.floor(Math.random() * 0xffffffff)}`, - // ignoreQueryLength: - // suggestionPluginTransactionMeta?.ignoreQueryLength, - // }; - // } - - // // Checks if the menu is hidden, in which case it doesn't need to be hidden or updated. - // if (prev === undefined) { - // return prev; - // } - - // // Checks if the menu should be hidden. - // if ( - // // Highlighting text should hide the menu. - // newState.selection.from !== newState.selection.to || - // // Transactions with plugin metadata should hide the menu. - // suggestionPluginTransactionMeta === null || - // // Certain mouse events should hide the menu. - // // TODO: Change to global mousedown listener. - // transaction.getMeta("focus") || - // transaction.getMeta("blur") || - // transaction.getMeta("pointer") || - // // Moving the caret before the character which triggered the menu should hide it. - // (prev.triggerCharacter !== undefined && - // newState.selection.from < prev.queryStartPos()) || - // // Moving the caret to a new block should hide the menu. - // !newState.selection.$from.sameParent( - // newState.doc.resolve(prev.queryStartPos()), - // ) - // ) { - // return undefined; - // } - - // const next = { ...prev }; - // // here we wi - // // Updates the current query. - // next.query = newState.doc.textBetween( - // prev.queryStartPos(), - // newState.selection.from, - // ); - - // return next; }, }, @@ -352,7 +248,7 @@ export class AutoCompleteProseMirrorPlugin< return null; } - console.log(autoCompleteState); + // console.log(autoCompleteState); // Creates an inline decoration around the trigger character. return DecorationSet.create(state.doc, [ Decoration.widget( @@ -432,11 +328,6 @@ export interface DebouncedFunction { cancel(): void; } -// TODO: more to blocknote API? -// TODO: test with Collaboration edits -// TODO: compare kilocode / cline etc -// TODO: think about advanced scenarios (e.g.: multiple suggestions, etc.) -// TODO: double tap -> extra long /** * Create a new AIExtension instance, this can be passed to the BlockNote editor via the `extensions` option */ @@ -456,3 +347,9 @@ export function getAIAutoCompleteExtension( ) { return editor.extension(AutoCompleteProseMirrorPlugin); } + +// TODO: move more to blocknote API? +// TODO: test with Collaboration edits +// TODO: compare kilocode / cline etc +// TODO: think about advanced scenarios (e.g.: multiple suggestions, etc.) +// TODO: double tap -> insert extra long suggestion From 1f7d9a39c77dacdc934fed2ff60d30079ee2fc7e Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 20 Nov 2025 14:46:13 +0100 Subject: [PATCH 04/10] comments --- examples/09-ai/01-minimal/src/App.tsx | 3 +++ .../xl-ai/src/plugins/AutoCompletePlugin.ts | 19 ------------------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/examples/09-ai/01-minimal/src/App.tsx b/examples/09-ai/01-minimal/src/App.tsx index adc1989fe7..c3147a0543 100644 --- a/examples/09-ai/01-minimal/src/App.tsx +++ b/examples/09-ai/01-minimal/src/App.tsx @@ -27,6 +27,9 @@ import { getEnv } from "./getEnv"; const BASE_URL = getEnv("BLOCKNOTE_AI_SERVER_BASE_URL") || "https://localhost:3000/ai"; +/** + * Fetches suggestions for the auto complete plugin from our backend API. + */ async function autoCompleteProvider( editor: BlockNoteEditor, signal: AbortSignal, diff --git a/packages/xl-ai/src/plugins/AutoCompletePlugin.ts b/packages/xl-ai/src/plugins/AutoCompletePlugin.ts index 907ff3ce88..3d332d4a05 100644 --- a/packages/xl-ai/src/plugins/AutoCompletePlugin.ts +++ b/packages/xl-ai/src/plugins/AutoCompletePlugin.ts @@ -14,25 +14,6 @@ export type AutoCompleteState = } | undefined; -// class AutoCompleteView< -// BSchema extends BlockSchema, -// I extends InlineContentSchema, -// S extends StyleSchema, -// > implements PluginView -// { -// public state?: AutoCompleteState; - -// private rootEl?: Document | ShadowRoot; -// // pluginState: AutoCompleteState; - -// constructor( -// private readonly editor: BlockNoteEditor, -// public readonly view: EditorView, -// ) { -// // this.pluginState = undefined; -// } -// } - const autoCompletePluginKey = new PluginKey<{ isUserInput: boolean }>( "AutoCompletePlugin", ); From 5f8a12c1a6c4c5d89fae64e15d5f39897377b65e Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 20 Nov 2025 15:19:28 +0100 Subject: [PATCH 05/10] fix url --- examples/09-ai/01-minimal/src/App.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/09-ai/01-minimal/src/App.tsx b/examples/09-ai/01-minimal/src/App.tsx index c3147a0543..f85818afa3 100644 --- a/examples/09-ai/01-minimal/src/App.tsx +++ b/examples/09-ai/01-minimal/src/App.tsx @@ -45,7 +45,8 @@ async function autoCompleteProvider( ); const response = await fetch( - `https://localhost:3000/ai/autocomplete/generateText`, + "https://blocknote-pr-2191.onrender.com/ai/autocomplete/generateText", + // `https://localhost:3000/ai/autocomplete/generateText`, { method: "POST", body: JSON.stringify({ text }), From 377274e12df92aae3c6f110b40b53c6857eb6948 Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 2 Dec 2025 14:51:56 +0100 Subject: [PATCH 06/10] merge --- .../xl-ai/src/plugins/AutoCompletePlugin.ts | 335 ++++++++---------- 1 file changed, 148 insertions(+), 187 deletions(-) diff --git a/packages/xl-ai/src/plugins/AutoCompletePlugin.ts b/packages/xl-ai/src/plugins/AutoCompletePlugin.ts index 3d332d4a05..c417318bfc 100644 --- a/packages/xl-ai/src/plugins/AutoCompletePlugin.ts +++ b/packages/xl-ai/src/plugins/AutoCompletePlugin.ts @@ -1,9 +1,7 @@ import { BlockNoteEditor, - BlockNoteExtension, - BlockSchema, - InlineContentSchema, - StyleSchema, + createExtension, + ExtensionOptions, } from "@blocknote/core"; import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { Decoration, DecorationSet } from "prosemirror-view"; @@ -64,188 +62,171 @@ function getMatchingSuggestions( .filter((suggestion) => suggestion !== false); } -export class AutoCompleteProseMirrorPlugin< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema, -> extends BlockNoteExtension { - public static key() { - return "suggestionMenu"; - } - - public get priority(): number | undefined { - return 1000000; // should be lower (e.g.: -1000 to be below suggestion menu, but that currently breaks Tab) - } - - // private view: AutoCompleteView | undefined; - - // private view: EditorView | undefined; - private autoCompleteSuggestions: AutoCompleteSuggestion[] = []; - - private debounceFetchSuggestions = debounceWithAbort( - async (editor: BlockNoteEditor, signal: AbortSignal) => { - // fetch suggestions - const autoCompleteSuggestions = await this.options.autoCompleteProvider( - editor, - signal, - ); - - // TODO: map positions? - - if (signal.aborted) { - return; - } +export const AIAutoCompleteExtension = createExtension( + ({ + editor, + options, + }: ExtensionOptions<{ autoCompleteProvider: AutoCompleteProvider }>) => { + let autoCompleteSuggestions: AutoCompleteSuggestion[] = []; + + const debounceFetchSuggestions = debounceWithAbort( + async (editor: BlockNoteEditor, signal: AbortSignal) => { + // fetch suggestions + const newAutoCompleteSuggestions = await options.autoCompleteProvider( + editor, + signal, + ); + + // TODO: map positions? + + if (signal.aborted) { + return; + } - this.autoCompleteSuggestions = autoCompleteSuggestions; - this.editor.transact((tr) => { - tr.setMeta(autoCompletePluginKey, { - autoCompleteSuggestions, + autoCompleteSuggestions = newAutoCompleteSuggestions; + editor.transact((tr) => { + tr.setMeta(autoCompletePluginKey, { + autoCompleteSuggestions, + }); }); - }); - }, - ); - - constructor( - private readonly editor: BlockNoteEditor, - private readonly options: { - autoCompleteProvider: AutoCompleteProvider; - }, - ) { - super(); - - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; - this.addProsemirrorPlugin( - new Plugin({ - key: autoCompletePluginKey, - - // view: (view) => { - // this.view = new AutoCompleteView(editor, view); - // return this.view; - // }, - - state: { - // Initialize the plugin's internal state. - init(): AutoCompleteState { - return undefined; - }, + }, + ); - // Apply changes to the plugin state from an editor transaction. - apply: ( - transaction, - _prev, - _oldState, - newState, - ): AutoCompleteState => { - // selection is active, no autocomplete - if (newState.selection.from !== newState.selection.to) { - this.debounceFetchSuggestions.cancel(); + return { + key: "aiAutoCompleteExtension", + priority: 1000000, // should be lower (e.g.: -1000 to be below suggestion menu, but that currently breaks Tab) + prosemirrorPlugins: [ + new Plugin({ + key: autoCompletePluginKey, + + // view: (view) => { + // this.view = new AutoCompleteView(editor, view); + // return this.view; + // }, + + state: { + // Initialize the plugin's internal state. + init(): AutoCompleteState { return undefined; - } + }, - // Are there matching suggestions? - const matchingSuggestions = getMatchingSuggestions( - this.autoCompleteSuggestions, + // Apply changes to the plugin state from an editor transaction. + apply: ( + transaction, + _prev, + _oldState, newState, - ); - - if (matchingSuggestions.length > 0) { - this.debounceFetchSuggestions.cancel(); - return { - autoCompleteSuggestion: matchingSuggestions[0], - }; - } - - // No matching suggestions, if isUserInput is true, debounce fetch suggestions - if (transaction.getMeta(autoCompletePluginKey)?.isUserInput) { - // TODO: this queueMicrotask is a workaround to ensure the transaction is applied before the debounceFetchSuggestions is called - // (discuss with Nick what ideal architecture would be) - queueMicrotask(() => { - this.debounceFetchSuggestions(self.editor).catch((error) => { - /* eslint-disable-next-line no-console */ - console.error(error); + ): AutoCompleteState => { + // selection is active, no autocomplete + if (newState.selection.from !== newState.selection.to) { + debounceFetchSuggestions.cancel(); + return undefined; + } + + // Are there matching suggestions? + const matchingSuggestions = getMatchingSuggestions( + autoCompleteSuggestions, + newState, + ); + + if (matchingSuggestions.length > 0) { + debounceFetchSuggestions.cancel(); + return { + autoCompleteSuggestion: matchingSuggestions[0], + }; + } + + // No matching suggestions, if isUserInput is true, debounce fetch suggestions + if (transaction.getMeta(autoCompletePluginKey)?.isUserInput) { + // TODO: this queueMicrotask is a workaround to ensure the transaction is applied before the debounceFetchSuggestions is called + // (discuss with Nick what ideal architecture would be) + queueMicrotask(() => { + debounceFetchSuggestions(editor).catch((error) => { + /* eslint-disable-next-line no-console */ + console.error(error); + }); }); - }); - } else { - // clear suggestions - this.autoCompleteSuggestions = []; - } - return undefined; + } else { + // clear suggestions + autoCompleteSuggestions = []; + } + return undefined; + }, }, - }, - - props: { - handleKeyDown(view, event) { - if (event.key === "Tab") { - // TODO (discuss with Nick): - // Plugin priority needs to be below suggestion menu, so no auto complete is triggered when the suggestion menu is open - // However, Plugin priority needs to be above other Tab handlers (because now indentation will be wrongly prioritized over auto complete) - const autoCompleteState = this.getState(view.state); - - if (autoCompleteState) { - // insert suggestion + + props: { + handleKeyDown(view, event) { + if (event.key === "Tab") { + // TODO (discuss with Nick): + // Plugin priority needs to be below suggestion menu, so no auto complete is triggered when the suggestion menu is open + // However, Plugin priority needs to be above other Tab handlers (because now indentation will be wrongly prioritized over auto complete) + const autoCompleteState = this.getState(view.state); + + if (autoCompleteState) { + // insert suggestion + view.dispatch( + view.state.tr + .insertText( + autoCompleteState.autoCompleteSuggestion.suggestion, + ) + .setMeta(autoCompletePluginKey, { isUserInput: true }), // isUserInput true to trigger new fetch + ); + return true; + } + + // if tab to suggest is enabled (TODO: make configurable) view.dispatch( - view.state.tr - .insertText( - autoCompleteState.autoCompleteSuggestion.suggestion, - ) - .setMeta(autoCompletePluginKey, { isUserInput: true }), // isUserInput true to trigger new fetch + view.state.tr.setMeta(autoCompletePluginKey, { + isUserInput: true, + }), ); return true; } - // if tab to suggest is enabled (TODO: make configurable) - view.dispatch( - view.state.tr.setMeta(autoCompletePluginKey, { - isUserInput: true, - }), - ); - return true; - } + if (event.key === "Escape") { + autoCompleteSuggestions = []; + debounceFetchSuggestions.cancel(); + view.dispatch(view.state.tr.setMeta(autoCompletePluginKey, {})); + return true; + } - if (event.key === "Escape") { - self.autoCompleteSuggestions = []; - self.debounceFetchSuggestions.cancel(); - view.dispatch(view.state.tr.setMeta(autoCompletePluginKey, {})); + return false; + }, + handleTextInput(view, _from, _to, _text, deflt) { + const tr = deflt(); + tr.setMeta(autoCompletePluginKey, { + isUserInput: true, + }); + view.dispatch(tr); return true; - } + }, - return false; - }, - handleTextInput(view, _from, _to, _text, deflt) { - const tr = deflt(); - tr.setMeta(autoCompletePluginKey, { - isUserInput: true, - }); - view.dispatch(tr); - return true; - }, + // Setup decorator on the currently active suggestion. + decorations(state) { + const autoCompleteState: AutoCompleteState = this.getState(state); - // Setup decorator on the currently active suggestion. - decorations(state) { - const autoCompleteState: AutoCompleteState = this.getState(state); - - if (!autoCompleteState) { - return null; - } - - // console.log(autoCompleteState); - // Creates an inline decoration around the trigger character. - return DecorationSet.create(state.doc, [ - Decoration.widget( - state.selection.from, - renderAutoCompleteSuggestion( - autoCompleteState.autoCompleteSuggestion.suggestion, + if (!autoCompleteState) { + return null; + } + + // console.log(autoCompleteState); + // Creates an inline decoration around the trigger character. + return DecorationSet.create(state.doc, [ + Decoration.widget( + state.selection.from, + renderAutoCompleteSuggestion( + autoCompleteState.autoCompleteSuggestion.suggestion, + ), + {}, ), - {}, - ), - ]); + ]); + }, }, - }, - }), - ); - } -} + }), + ], + }; + }, +); function renderAutoCompleteSuggestion(suggestion: string) { const element = document.createElement("span"); @@ -309,26 +290,6 @@ export interface DebouncedFunction { cancel(): void; } -/** - * Create a new AIExtension instance, this can be passed to the BlockNote editor via the `extensions` option - */ -export function createAIAutoCompleteExtension( - options: ConstructorParameters[1], -) { - return (editor: BlockNoteEditor) => { - return new AutoCompleteProseMirrorPlugin(editor, options); - }; -} - -/** - * Return the AIExtension instance from the editor - */ -export function getAIAutoCompleteExtension( - editor: BlockNoteEditor, -) { - return editor.extension(AutoCompleteProseMirrorPlugin); -} - // TODO: move more to blocknote API? // TODO: test with Collaboration edits // TODO: compare kilocode / cline etc From 959ca8a612d46a6ceed30ee651897691bd101dbd Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 2 Dec 2025 15:10:00 +0100 Subject: [PATCH 07/10] mistral autocomplete model --- packages/xl-ai-server/src/routes/autocomplete.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/xl-ai-server/src/routes/autocomplete.ts b/packages/xl-ai-server/src/routes/autocomplete.ts index b5a47a00a1..88b90dfdac 100644 --- a/packages/xl-ai-server/src/routes/autocomplete.ts +++ b/packages/xl-ai-server/src/routes/autocomplete.ts @@ -1,4 +1,4 @@ -import { createGroq } from "@ai-sdk/groq"; +import { createMistral } from "@ai-sdk/mistral"; import { generateText } from "ai"; import { Hono } from "hono"; @@ -9,9 +9,13 @@ export const autocompleteRoute = new Hono(); // apiKey: process.env.OPENAI_API_KEY, // })("gpt-4.1-nano"); -const model = createGroq({ - apiKey: process.env.GROQ_API_KEY, -})("openai/gpt-oss-20b"); +// const model = createGroq({ +// apiKey: process.env.GROQ_API_KEY, +// })("openai/gpt-oss-20b"); + +const model = createMistral({ + apiKey: process.env.MISTRAL_API_KEY, +})("codestral-latest"); // Use `streamText` to stream text responses from the LLM autocompleteRoute.post("/generateText", async (c) => { @@ -19,10 +23,10 @@ autocompleteRoute.post("/generateText", async (c) => { const result = await generateText({ model, - system: `You are a writing assistant. Predict and generate the most likely next part of the text. + system: `You are a writing assistant, helping the user write text (NOT CODE). Predict and generate the most likely next part of the text. - separate suggestions by newlines - max 3 suggestions -- keep it short, max 5 words per suggestion +- YOU MUST keep it short, USE MAXIMUM 5 (FIVE) WORDS per suggestion - don't include other text (or explanations) - YOU MUST ONLY return the text to be appended. Your suggestion will EXACTLY replace [SUGGESTION_HERE]. - YOU MUST NOT include the original text / characters (prefix) in your suggestion. From 63ab9386525dd46244deb396605ce0e8b8715bc6 Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 2 Dec 2025 15:10:54 +0100 Subject: [PATCH 08/10] add comments --- packages/xl-ai-server/src/routes/autocomplete.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/xl-ai-server/src/routes/autocomplete.ts b/packages/xl-ai-server/src/routes/autocomplete.ts index 88b90dfdac..03bc0ded56 100644 --- a/packages/xl-ai-server/src/routes/autocomplete.ts +++ b/packages/xl-ai-server/src/routes/autocomplete.ts @@ -13,6 +13,11 @@ export const autocompleteRoute = new Hono(); // apiKey: process.env.GROQ_API_KEY, // })("openai/gpt-oss-20b"); +/** + * For this demo, we use `codestral-latest` from Mistral. + * It's originally designed for code completion, but it's + * performance make it a good candidate for fast text completions as well + */ const model = createMistral({ apiKey: process.env.MISTRAL_API_KEY, })("codestral-latest"); From 6d83a08f445b8454d44e59170fb4dce293c084ff Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 2 Dec 2025 15:27:03 +0100 Subject: [PATCH 09/10] fix --- examples/09-ai/01-minimal/src/App.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/09-ai/01-minimal/src/App.tsx b/examples/09-ai/01-minimal/src/App.tsx index f39e89a23c..307ab245cf 100644 --- a/examples/09-ai/01-minimal/src/App.tsx +++ b/examples/09-ai/01-minimal/src/App.tsx @@ -43,6 +43,7 @@ async function autoCompleteProvider( const text = state.doc.textBetween( state.selection.from - 300, state.selection.from, + "\n", ); const response = await fetch( From 77dec5d2d60ac7f0ce7b121dd2064f907f31a9b8 Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 3 Dec 2025 19:02:47 +0100 Subject: [PATCH 10/10] wip --- .../content/docs/features/ai/autocomplete.mdx | 168 + docs/content/docs/features/ai/meta.json | 1 + examples/09-ai/01-minimal/README.md | 4 +- examples/09-ai/01-minimal/index.html | 2 +- examples/09-ai/01-minimal/src/App.tsx | 46 - .../09-ai/08-autocomplete/.bnexample.json | 11 + examples/09-ai/08-autocomplete/README.md | 10 + examples/09-ai/08-autocomplete/index.html | 14 + examples/09-ai/08-autocomplete/main.tsx | 11 + examples/09-ai/08-autocomplete/package.json | 33 + examples/09-ai/08-autocomplete/src/App.tsx | 68 + examples/09-ai/08-autocomplete/src/getEnv.ts | 20 + examples/09-ai/08-autocomplete/tsconfig.json | 36 + examples/09-ai/08-autocomplete/vite.config.ts | 32 + ...tePlugin.ts => AIAutoCompleteExtension.ts} | 215 +- packages/xl-ai/src/index.ts | 3 +- packages/xl-ai/src/style.css | 5 +- playground/src/examples.gen.tsx | 3155 +++++++++-------- pnpm-lock.yaml | 52 + 19 files changed, 2256 insertions(+), 1630 deletions(-) create mode 100644 docs/content/docs/features/ai/autocomplete.mdx create mode 100644 examples/09-ai/08-autocomplete/.bnexample.json create mode 100644 examples/09-ai/08-autocomplete/README.md create mode 100644 examples/09-ai/08-autocomplete/index.html create mode 100644 examples/09-ai/08-autocomplete/main.tsx create mode 100644 examples/09-ai/08-autocomplete/package.json create mode 100644 examples/09-ai/08-autocomplete/src/App.tsx create mode 100644 examples/09-ai/08-autocomplete/src/getEnv.ts create mode 100644 examples/09-ai/08-autocomplete/tsconfig.json create mode 100644 examples/09-ai/08-autocomplete/vite.config.ts rename packages/xl-ai/src/{plugins/AutoCompletePlugin.ts => AIAutoCompleteExtension.ts} (59%) diff --git a/docs/content/docs/features/ai/autocomplete.mdx b/docs/content/docs/features/ai/autocomplete.mdx new file mode 100644 index 0000000000..7e42e4ffba --- /dev/null +++ b/docs/content/docs/features/ai/autocomplete.mdx @@ -0,0 +1,168 @@ +--- +title: AI Autocomplete +description: Add AI-powered autocomplete suggestions to your BlockNote editor +imageTitle: BlockNote AI Autocomplete +--- + +# AI Autocomplete + +The AI Autocomplete extension provides real-time, AI-powered text completion suggestions as users type. When enabled, the editor will display inline suggestions that can be accepted with the **Tab** key or dismissed with **Escape**. + +## Installation + +First, ensure you have the `@blocknote/xl-ai` package installed: + +```bash +npm install @blocknote/xl-ai +``` + +## Basic Setup + +To enable AI autocomplete in your editor, add the `AIAutoCompleteExtension` to your editor configuration: + +```tsx +import { useCreateBlockNote } from "@blocknote/react"; +import { AIAutoCompleteExtension } from "@blocknote/xl-ai"; +import { en as aiEn } from "@blocknote/xl-ai/locales"; +import "@blocknote/xl-ai/style.css"; + +const editor = useCreateBlockNote({ + dictionary: { + ...en, + ai: aiEn, // add translations for the AI extension + }, + extensions: [ + AIAutoCompleteExtension({ + // Fetch suggestions from this URL + provider: "/api/autocomplete", + }), + ], +}); +``` + +## Configuration + +The `AIAutoCompleteExtension` accepts the following options: + +| Option | Type | Default | Description | +| :--- | :--- | :--- | :--- | +| `provider` | `string` \| `function` | Required | URL to fetch suggestions from, or a custom provider function. | +| `contextLength` | `number` | `300` | Number of characters of context to send when using a URL provider. | +| `acceptKey` | `string` | `"Tab"` | Key to accept the current suggestion. | +| `cancelKey` | `string` | `"Escape"` | Key to discard suggestions. | +| `debounceDelay` | `number` | `300` | Delay in ms before fetching suggestions. | + +### Using a Custom Provider + +If you need more control than the default URL provider offers, you can pass a function to `provider`. + +```typescript +AIAutoCompleteExtension({ + provider: async (editor, signal) => { + // Get context + const state = editor.prosemirrorState; + const text = state.doc.textBetween( + state.selection.from - 500, + state.selection.from, + "\n" + ); + + // Fetch suggestions + const response = await fetch("/api/autocomplete", { + method: "POST", + body: JSON.stringify({ text }), + signal, + }); + const data = await response.json(); + + // Return suggestions + return data.suggestions.map(suggestion => ({ + suggestion: suggestion, + // Optional: specify position (defaults to cursor) + // position: state.selection.from + })); + }, +}) +``` + +## User Interaction + +When autocomplete suggestions are available: + +- **Tab** (or configured `acceptKey`): Accept the current suggestion +- **Escape** (or configured `cancelKey`): Dismiss the suggestion +- **Continue typing**: The suggestion updates as you type if it still matches + +## Backend Integration + +The default URL provider sends a `POST` request with the following body: + +```json +{ + "text": "The last 300 characters of text before the cursor..." +} +``` + +It expects a JSON response with an array of suggestion strings: + +```json +{ + "suggestions": ["completion text"] +} +``` + +Here's an example API route using the Vercel AI SDK: + +```typescript +// app/api/autocomplete/route.ts +import { openai } from "@ai-sdk/openai"; +import { generateText } from "ai"; + +export async function POST(req: Request) { + const { text } = await req.json(); + + const result = await generateText({ + model: openai("gpt-4"), + prompt: `Complete the following text:\n\n${text}`, + }); + + return Response.json({ + suggestions: [result.text], + }); +} +``` + +## Programmatic Control + +You can also control the autocomplete extension programmatically: + +```typescript +const ext = editor.extensions.aiAutoCompleteExtension; + +// Accept the current suggestion +ext.acceptAutoCompleteSuggestion(); + +// Discard suggestions +ext.discardAutoCompleteSuggestions(); +``` + +## Combining with Other AI Features + +The `AIAutoCompleteExtension` works seamlessly alongside the main `AIExtension`: + +```tsx +import { AIExtension, AIAutoCompleteExtension } from "@blocknote/xl-ai"; + +const editor = useCreateBlockNote({ + extensions: [ + AIExtension({ + transport: new DefaultChatTransport({ api: "/api/chat" }), + }), + AIAutoCompleteExtension({ + provider: "/api/autocomplete", + }), + ], +}); +``` + +See the [AI Getting Started](/docs/features/ai/getting-started) guide for more information on the main AI extension. diff --git a/docs/content/docs/features/ai/meta.json b/docs/content/docs/features/ai/meta.json index 18e7406d17..af99e16643 100644 --- a/docs/content/docs/features/ai/meta.json +++ b/docs/content/docs/features/ai/meta.json @@ -4,6 +4,7 @@ "getting-started", "backend-integration", "custom-commands", + "autocomplete", "reference" ] } diff --git a/examples/09-ai/01-minimal/README.md b/examples/09-ai/01-minimal/README.md index 5ffcb1af89..11ee9d5982 100644 --- a/examples/09-ai/01-minimal/README.md +++ b/examples/09-ai/01-minimal/README.md @@ -1,4 +1,4 @@ -# Rich Text editor AI integration +# Rich Text Editor AI Integration This example shows the minimal setup to add AI integration to your BlockNote rich text editor. @@ -6,6 +6,6 @@ Select some text and click the AI (stars) button, or type `/ai` anywhere in the **Relevant Docs:** -- [Getting Stared with BlockNote AI](/docs/features/ai/getting-started) +- [Getting Started with BlockNote AI](/docs/features/ai/getting-started) - [Changing the Formatting Toolbar](/docs/react/components/formatting-toolbar) - [Changing Slash Menu Items](/docs/react/components/suggestion-menus) diff --git a/examples/09-ai/01-minimal/index.html b/examples/09-ai/01-minimal/index.html index b31f43f604..d547ed7f78 100644 --- a/examples/09-ai/01-minimal/index.html +++ b/examples/09-ai/01-minimal/index.html @@ -2,7 +2,7 @@ - Rich Text editor AI integration + Rich Text Editor AI Integration diff --git a/examples/09-ai/01-minimal/src/App.tsx b/examples/09-ai/01-minimal/src/App.tsx index 307ab245cf..03e4caa90b 100644 --- a/examples/09-ai/01-minimal/src/App.tsx +++ b/examples/09-ai/01-minimal/src/App.tsx @@ -13,7 +13,6 @@ import { useCreateBlockNote, } from "@blocknote/react"; import { - AIAutoCompleteExtension, AIExtension, AIMenuController, AIToolbarButton, @@ -28,50 +27,6 @@ import { getEnv } from "./getEnv"; const BASE_URL = getEnv("BLOCKNOTE_AI_SERVER_BASE_URL") || "https://localhost:3000/ai"; -/** - * Fetches suggestions for the auto complete plugin from our backend API. - */ -async function autoCompleteProvider( - editor: BlockNoteEditor, - signal: AbortSignal, -) { - // TODO: - // - API is very prosemirror-based, make something more BlockNote-native - // - Add simple method to retrieve relevant context (e.g. block content / json until selection) - - const state = editor.prosemirrorState; - const text = state.doc.textBetween( - state.selection.from - 300, - state.selection.from, - "\n", - ); - - const response = await fetch( - // "https://blocknote-pr-2191.onrender.com/ai/autocomplete/generateText", - `https://localhost:3000/ai/autocomplete/generateText`, - { - method: "POST", - body: JSON.stringify({ text }), - signal, - }, - ); - const data = await response.json(); - return data.suggestions.map((suggestion: string) => ({ - position: state.selection.from, - suggestion: suggestion, - })); - // return [ - // { - // position: state.selection.from, - // suggestion: "Hello World", - // }, - // { - // position: state.selection.from, - // suggestion: "Hello Planet", - // }, - // ]; -} - export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ @@ -87,7 +42,6 @@ export default function App() { api: `${BASE_URL}/regular/streamText`, }), }), - AIAutoCompleteExtension({ autoCompleteProvider }), ], // We set some initial content for demo purposes initialContent: [ diff --git a/examples/09-ai/08-autocomplete/.bnexample.json b/examples/09-ai/08-autocomplete/.bnexample.json new file mode 100644 index 0000000000..2a0da0969a --- /dev/null +++ b/examples/09-ai/08-autocomplete/.bnexample.json @@ -0,0 +1,11 @@ +{ + "playground": true, + "docs": true, + "author": "yousefed", + "tags": ["AI", "autocomplete"], + "dependencies": { + "@blocknote/xl-ai": "latest", + "@mantine/core": "^8.3.4", + "ai": "^5.0.102" + } +} diff --git a/examples/09-ai/08-autocomplete/README.md b/examples/09-ai/08-autocomplete/README.md new file mode 100644 index 0000000000..8fb6f0c986 --- /dev/null +++ b/examples/09-ai/08-autocomplete/README.md @@ -0,0 +1,10 @@ +# AI Autocomplete + +This example demonstrates the AI autocomplete feature in BlockNote. As you type, the editor will automatically suggest completions using AI. + +Press **Tab** to accept a suggestion or **Escape** to dismiss it. + +**Relevant Docs:** + +- [AI Autocomplete](/docs/features/ai/autocomplete) +- [AI Getting Started](/docs/features/ai/getting-started) diff --git a/examples/09-ai/08-autocomplete/index.html b/examples/09-ai/08-autocomplete/index.html new file mode 100644 index 0000000000..cd02311000 --- /dev/null +++ b/examples/09-ai/08-autocomplete/index.html @@ -0,0 +1,14 @@ + + + + + AI Autocomplete + + + +
+ + + diff --git a/examples/09-ai/08-autocomplete/main.tsx b/examples/09-ai/08-autocomplete/main.tsx new file mode 100644 index 0000000000..677c7f7eed --- /dev/null +++ b/examples/09-ai/08-autocomplete/main.tsx @@ -0,0 +1,11 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./src/App.jsx"; + +const root = createRoot(document.getElementById("root")!); +root.render( + + + +); diff --git a/examples/09-ai/08-autocomplete/package.json b/examples/09-ai/08-autocomplete/package.json new file mode 100644 index 0000000000..e09522ab24 --- /dev/null +++ b/examples/09-ai/08-autocomplete/package.json @@ -0,0 +1,33 @@ +{ + "name": "@blocknote/example-ai-autocomplete", + "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "type": "module", + "private": true, + "version": "0.12.4", + "scripts": { + "start": "vite", + "dev": "vite", + "build:prod": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@blocknote/ariakit": "latest", + "@blocknote/core": "latest", + "@blocknote/mantine": "latest", + "@blocknote/react": "latest", + "@blocknote/shadcn": "latest", + "@mantine/core": "^8.3.4", + "@mantine/hooks": "^8.3.4", + "@mantine/utils": "^6.0.22", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "@blocknote/xl-ai": "latest", + "ai": "^5.0.102" + }, + "devDependencies": { + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.1", + "@vitejs/plugin-react": "^4.7.0", + "vite": "^5.4.20" + } +} \ No newline at end of file diff --git a/examples/09-ai/08-autocomplete/src/App.tsx b/examples/09-ai/08-autocomplete/src/App.tsx new file mode 100644 index 0000000000..7a81901b45 --- /dev/null +++ b/examples/09-ai/08-autocomplete/src/App.tsx @@ -0,0 +1,68 @@ +import "@blocknote/core/fonts/inter.css"; +import { en } from "@blocknote/core/locales"; +import { BlockNoteView } from "@blocknote/mantine"; +import "@blocknote/mantine/style.css"; +import { useCreateBlockNote } from "@blocknote/react"; +import { + AIAutoCompleteExtension +} from "@blocknote/xl-ai"; +import { en as aiEn } from "@blocknote/xl-ai/locales"; +import "@blocknote/xl-ai/style.css"; + + +import { getEnv } from "./getEnv"; + +const BASE_URL = + getEnv("BLOCKNOTE_AI_SERVER_BASE_URL") || "https://localhost:3000/ai"; + + + +export default function App() { + // Creates a new editor instance with AI autocomplete enabled + const editor = useCreateBlockNote({ + dictionary: { + ...en, + ai: aiEn, + }, + // Register the AI Autocomplete extension + extensions: [ + AIAutoCompleteExtension({ + provider: `${BASE_URL}/autocomplete/generateText`, + }), + ], + // We set some initial content for demo purposes + initialContent: [ + { + type: "heading", + props: { + level: 1, + }, + content: "AI Autocomplete Demo", + }, + { + type: "paragraph", + content: + "Start typing and press Tab to trigger AI autocomplete suggestions. The AI will suggest completions based on your current context.", + }, + { + type: "paragraph", + content: + "For example, try typing 'Open source software is' and then press Tab to see autocomplete suggestions appear.", + }, + { + type: "paragraph", + content: "", + }, + ], + }); + + // Renders the editor instance using a React component. + return ( +
+ +
+ ); +} diff --git a/examples/09-ai/08-autocomplete/src/getEnv.ts b/examples/09-ai/08-autocomplete/src/getEnv.ts new file mode 100644 index 0000000000..b225fc462e --- /dev/null +++ b/examples/09-ai/08-autocomplete/src/getEnv.ts @@ -0,0 +1,20 @@ +// helper function to get env variables across next / vite +// only needed so this example works in BlockNote demos and docs +export function getEnv(key: string) { + const env = (import.meta as any).env + ? { + BLOCKNOTE_AI_SERVER_API_KEY: (import.meta as any).env + .VITE_BLOCKNOTE_AI_SERVER_API_KEY, + BLOCKNOTE_AI_SERVER_BASE_URL: (import.meta as any).env + .VITE_BLOCKNOTE_AI_SERVER_BASE_URL, + } + : { + BLOCKNOTE_AI_SERVER_API_KEY: + process.env.NEXT_PUBLIC_BLOCKNOTE_AI_SERVER_API_KEY, + BLOCKNOTE_AI_SERVER_BASE_URL: + process.env.NEXT_PUBLIC_BLOCKNOTE_AI_SERVER_BASE_URL, + }; + + const value = env[key as keyof typeof env]; + return value; +} diff --git a/examples/09-ai/08-autocomplete/tsconfig.json b/examples/09-ai/08-autocomplete/tsconfig.json new file mode 100644 index 0000000000..dbe3e6f62d --- /dev/null +++ b/examples/09-ai/08-autocomplete/tsconfig.json @@ -0,0 +1,36 @@ +{ + "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "composite": true + }, + "include": [ + "." + ], + "__ADD_FOR_LOCAL_DEV_references": [ + { + "path": "../../../packages/core/" + }, + { + "path": "../../../packages/react/" + } + ] +} \ No newline at end of file diff --git a/examples/09-ai/08-autocomplete/vite.config.ts b/examples/09-ai/08-autocomplete/vite.config.ts new file mode 100644 index 0000000000..f62ab20bc2 --- /dev/null +++ b/examples/09-ai/08-autocomplete/vite.config.ts @@ -0,0 +1,32 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import react from "@vitejs/plugin-react"; +import * as fs from "fs"; +import * as path from "path"; +import { defineConfig } from "vite"; +// import eslintPlugin from "vite-plugin-eslint"; +// https://vitejs.dev/config/ +export default defineConfig((conf) => ({ + plugins: [react()], + optimizeDeps: {}, + build: { + sourcemap: true, + }, + resolve: { + alias: + conf.command === "build" || + !fs.existsSync(path.resolve(__dirname, "../../packages/core/src")) + ? {} + : ({ + // Comment out the lines below to load a built version of blocknote + // or, keep as is to load live from sources with live reload working + "@blocknote/core": path.resolve( + __dirname, + "../../packages/core/src/" + ), + "@blocknote/react": path.resolve( + __dirname, + "../../packages/react/src/" + ), + } as any), + }, +})); diff --git a/packages/xl-ai/src/plugins/AutoCompletePlugin.ts b/packages/xl-ai/src/AIAutoCompleteExtension.ts similarity index 59% rename from packages/xl-ai/src/plugins/AutoCompletePlugin.ts rename to packages/xl-ai/src/AIAutoCompleteExtension.ts index c417318bfc..fff6f98e70 100644 --- a/packages/xl-ai/src/plugins/AutoCompletePlugin.ts +++ b/packages/xl-ai/src/AIAutoCompleteExtension.ts @@ -12,70 +12,149 @@ export type AutoCompleteState = } | undefined; -const autoCompletePluginKey = new PluginKey<{ isUserInput: boolean }>( +const autoCompletePluginKey = new PluginKey( "AutoCompletePlugin", ); -type AutoCompleteSuggestion = { +export type AutoCompleteSuggestion = { + position?: number; + suggestion: string; +}; + +type InternalAutoCompleteSuggestion = { position: number; suggestion: string; }; -type AutoCompleteProvider = ( +export type AutoCompleteProvider = ( editor: BlockNoteEditor, signal: AbortSignal, ) => Promise; +export type AutoCompleteOptions = { + /** + * The provider to fetch autocomplete suggestions from. + * Can be a URL string (uses default provider) or a custom function. + */ + provider: AutoCompleteProvider | string; + /** + * Number of characters of context to send to the API when using the default provider (string URL). + * Default: 300 + */ + contextLength?: number; + /** Key to accept autocomplete suggestion. Default: "Tab" */ + acceptKey?: string; + /** Key to cancel/discard autocomplete suggestion. Default: "Escape" */ + cancelKey?: string; + /** Debounce delay in milliseconds before fetching suggestions. Default: 300 */ + debounceDelay?: number; +}; + function getMatchingSuggestions( - autoCompleteSuggestions: AutoCompleteSuggestion[], + autoCompleteSuggestions: InternalAutoCompleteSuggestion[], state: EditorState, -): AutoCompleteSuggestion[] { - return autoCompleteSuggestions - .map((suggestion) => { - if (suggestion.position > state.selection.from) { - return false; - } +): InternalAutoCompleteSuggestion[] { + return autoCompleteSuggestions.flatMap((suggestion) => { + // Suggestion must be before or at current cursor position + if (suggestion.position > state.selection.from) { + return []; + } - if ( - !state.doc - .resolve(suggestion.position) - .sameParent(state.selection.$from) - ) { - return false; - } + // Suggestion must be in the same parent block + if ( + !state.doc + .resolve(suggestion.position) + .sameParent(state.selection.$from) + ) { + return []; + } - const text = state.doc.textBetween( - suggestion.position, - state.selection.from, - ); - if ( - suggestion.suggestion.startsWith(text) && - suggestion.suggestion.length > text.length - ) { - return { + // Get text that has been typed since the suggestion + // start position + const text = state.doc.textBetween( + suggestion.position, + state.selection.from, + ); + + // User's typed text must be a prefix of the suggestion + if ( + suggestion.suggestion.startsWith(text) && + suggestion.suggestion.length > text.length + ) { + return [ + { position: suggestion.position, suggestion: suggestion.suggestion.slice(text.length), - }; - } - return false; - }) - .filter((suggestion) => suggestion !== false); + }, + ]; + } + + return []; + }); +} + +export function createDefaultAutoCompleteProvider(args: { + url: string; + contextLength?: number; +}): AutoCompleteProvider { + const { url, contextLength = 300 } = args; + return async (editor, signal) => { + const state = editor.prosemirrorState; + // Get last N chars of context + const text = state.doc.textBetween( + Math.max(0, state.selection.from - contextLength), + state.selection.from, + "\n", + ); + + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ text }), + signal, + }); + + if (!response.ok) { + throw new Error(`AutoComplete request failed: ${response.statusText}`); + } + + const data = await response.json(); + return data.suggestions.map((suggestion: string) => ({ + suggestion: suggestion, + })); + }; } export const AIAutoCompleteExtension = createExtension( ({ editor, options, - }: ExtensionOptions<{ autoCompleteProvider: AutoCompleteProvider }>) => { - let autoCompleteSuggestions: AutoCompleteSuggestion[] = []; + }: ExtensionOptions) => { + let autoCompleteSuggestions: InternalAutoCompleteSuggestion[] = []; + + const acceptKey = options.acceptKey || "Tab"; + const cancelKey = options.cancelKey || "Escape"; + const debounceDelay = options.debounceDelay ?? 300; + + // Determine the provider to use: + // 1. If a string is provided, create a default provider that fetches from that URL. + // 2. If a function is provided, use it directly. + const provider = + typeof options.provider === "string" + ? createDefaultAutoCompleteProvider({ + url: options.provider, + contextLength: options.contextLength, + }) + : options.provider; const debounceFetchSuggestions = debounceWithAbort( async (editor: BlockNoteEditor, signal: AbortSignal) => { // fetch suggestions - const newAutoCompleteSuggestions = await options.autoCompleteProvider( - editor, - signal, - ); + const newAutoCompleteSuggestions = await provider(editor, signal); + + // TODO: map positions? @@ -83,27 +162,61 @@ export const AIAutoCompleteExtension = createExtension( return; } - autoCompleteSuggestions = newAutoCompleteSuggestions; + // Fill in missing positions with current cursor position + const processedSuggestions = newAutoCompleteSuggestions.map( + (suggestion) => ({ + position: editor.prosemirrorState.selection.from, + ...suggestion, + }), + ); + + autoCompleteSuggestions = processedSuggestions; + // Force plugin state update to trigger decorations refresh editor.transact((tr) => { - tr.setMeta(autoCompletePluginKey, { - autoCompleteSuggestions, - }); + tr.setMeta(autoCompletePluginKey, {}); }); }, + debounceDelay, ); + /** + * Accepts the current autocomplete suggestion and inserts it into the editor. + * @returns true if a suggestion was accepted, false otherwise + */ + const acceptAutoCompleteSuggestion = (): boolean => { + const state = autoCompletePluginKey.getState(editor.prosemirrorState); + if (state) { + editor.transact((tr) => { + tr.insertText(state.autoCompleteSuggestion.suggestion).setMeta( + autoCompletePluginKey, + { isUserInput: true }, + ); + }); + return true; + } + return false; + }; + + /** + * Discards all current autocomplete suggestions. + */ + const discardAutoCompleteSuggestions = (): void => { + autoCompleteSuggestions = []; + debounceFetchSuggestions.cancel(); + editor.transact((tr) => { + tr.setMeta(autoCompletePluginKey, {}); + }); + }; + return { + acceptAutoCompleteSuggestion, + discardAutoCompleteSuggestions, key: "aiAutoCompleteExtension", priority: 1000000, // should be lower (e.g.: -1000 to be below suggestion menu, but that currently breaks Tab) prosemirrorPlugins: [ new Plugin({ key: autoCompletePluginKey, - // view: (view) => { - // this.view = new AutoCompleteView(editor, view); - // return this.view; - // }, - state: { // Initialize the plugin's internal state. init(): AutoCompleteState { @@ -156,7 +269,7 @@ export const AIAutoCompleteExtension = createExtension( props: { handleKeyDown(view, event) { - if (event.key === "Tab") { + if (event.key === acceptKey) { // TODO (discuss with Nick): // Plugin priority needs to be below suggestion menu, so no auto complete is triggered when the suggestion menu is open // However, Plugin priority needs to be above other Tab handlers (because now indentation will be wrongly prioritized over auto complete) @@ -183,10 +296,8 @@ export const AIAutoCompleteExtension = createExtension( return true; } - if (event.key === "Escape") { - autoCompleteSuggestions = []; - debounceFetchSuggestions.cancel(); - view.dispatch(view.state.tr.setMeta(autoCompletePluginKey, {})); + if (event.key === cancelKey) { + discardAutoCompleteSuggestions(); return true; } @@ -237,7 +348,7 @@ function renderAutoCompleteSuggestion(suggestion: string) { export function debounceWithAbort( fn: (...args: [...T, AbortSignal]) => Promise | R, - delay = 300, // TODO: configurable + delay = 300, ) { let timeoutId: ReturnType | null = null; let controller: AbortController | null = null; diff --git a/packages/xl-ai/src/index.ts b/packages/xl-ai/src/index.ts index 6433e5212c..8055a88196 100644 --- a/packages/xl-ai/src/index.ts +++ b/packages/xl-ai/src/index.ts @@ -1,5 +1,6 @@ import "./style.css"; +export * from "./AIAutoCompleteExtension.js"; export * from "./AIExtension.js"; export * from "./components/AIMenu/AIMenu.js"; export * from "./components/AIMenu/AIMenuController.js"; @@ -8,6 +9,6 @@ export * from "./components/AIMenu/PromptSuggestionMenu.js"; export * from "./components/FormattingToolbar/AIToolbarButton.js"; export * from "./components/SuggestionMenu/getAISlashMenuItems.js"; export * from "./hooks/useAIDictionary.js"; -export * from "./plugins/AutoCompletePlugin.js"; export * from "./server.js"; export * from "./streamTool/index.js"; + diff --git a/packages/xl-ai/src/style.css b/packages/xl-ai/src/style.css index 5547d181b3..5ebcf45d53 100644 --- a/packages/xl-ai/src/style.css +++ b/packages/xl-ai/src/style.css @@ -33,7 +33,6 @@ del, } .bn-autocomplete-decorator { - width: 50px; - height: 50px; - color: var(--bn-colors-side-menu); + display: inline; + color: var(--bn-colors-side-menu); /* Hacky, shoyuld review variable setup */ } diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index 8b2688dfe0..70325d9607 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -1,588 +1,615 @@ // generated by dev-scripts/examples/gen.ts -export const examples = { - basic: { - pathFromRoot: "examples/01-basic", - slug: "basic", - projects: [ - { - projectSlug: "minimal", - fullSlug: "basic/minimal", - pathFromRoot: "examples/01-basic/01-minimal", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic"], - }, - title: "Basic Setup", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - readme: - "This example shows the minimal code required to set up a BlockNote editor in React.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)", - }, - { - projectSlug: "block-objects", - fullSlug: "basic/block-objects", - pathFromRoot: "examples/01-basic/02-block-objects", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Basic", "Blocks", "Inline Content"], - }, - title: "Displaying Document JSON", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - readme: - "In this example, the document's JSON representation is displayed below the editor.\n\n**Try it out:** Try typing in the editor and see the JSON update!\n\n**Relevant Docs:**\n\n- [Document Structure](/docs/foundations/document-structure)\n- [Getting the Document](/docs/reference/editor/manipulating-content)", - }, - { - projectSlug: "multi-column", - fullSlug: "basic/multi-column", - pathFromRoot: "examples/01-basic/03-multi-column", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Basic", "Blocks"], - dependencies: { - "@blocknote/xl-multi-column": "latest", + export const examples = { + "basic": { + "pathFromRoot": "examples/01-basic", + "slug": "basic", + "projects": [ + { + "projectSlug": "minimal", + "fullSlug": "basic/minimal", + "pathFromRoot": "examples/01-basic/01-minimal", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic" + ] + }, + "title": "Basic Setup", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + }, + "readme": "This example shows the minimal code required to set up a BlockNote editor in React.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)" + }, + { + "projectSlug": "block-objects", + "fullSlug": "basic/block-objects", + "pathFromRoot": "examples/01-basic/02-block-objects", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Basic", + "Blocks", + "Inline Content" + ] + }, + "title": "Displaying Document JSON", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + }, + "readme": "In this example, the document's JSON representation is displayed below the editor.\n\n**Try it out:** Try typing in the editor and see the JSON update!\n\n**Relevant Docs:**\n\n- [Document Structure](/docs/foundations/document-structure)\n- [Getting the Document](/docs/reference/editor/manipulating-content)" + }, + { + "projectSlug": "multi-column", + "fullSlug": "basic/multi-column", + "pathFromRoot": "examples/01-basic/03-multi-column", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Basic", + "Blocks" + ], + "dependencies": { + "@blocknote/xl-multi-column": "latest" } as any, - pro: true, - }, - title: "Multi-Column Blocks", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - readme: - "This example showcases multi-column blocks, allowing you to stack blocks next to each other. These come as part of the `@blocknote/xl-multi-column` package.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Document Structure](/docs/foundations/document-structure)", - }, - { - projectSlug: "default-blocks", - fullSlug: "basic/default-blocks", - pathFromRoot: "examples/01-basic/04-default-blocks", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Basic", "Blocks", "Inline Content"], - }, - title: "Default Schema Showcase", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - readme: - "This example showcases each block and inline content type in BlockNote's default schema.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Document Structure](/docs/foundations/document-structure)\n- [Default Schema](/docs/foundations/schemas)", - }, - { - projectSlug: "removing-default-blocks", - fullSlug: "basic/removing-default-blocks", - pathFromRoot: "examples/01-basic/05-removing-default-blocks", - config: { - playground: true, - docs: true, - author: "hunxjunedo", - tags: ["Basic", "removing", "blocks"], - }, - title: "Removing Default Blocks from Schema", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - readme: - "This example shows how to change the default schema and disable the Audio and Image blocks. To do this, we pass in a custom schema based on the built-in, default schema, with two specific blocks removed.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Custom Schemas](/docs/features/custom-schemas)\n- [Default Schema](/docs/foundations/schemas)", - }, - { - projectSlug: "block-manipulation", - fullSlug: "basic/block-manipulation", - pathFromRoot: "examples/01-basic/06-block-manipulation", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic", "Blocks"], - }, - title: "Manipulating Blocks", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - readme: - "This example shows 4 buttons to manipulate the first block using the `insertBlocks`, `updateBlock`, `removeBlocks` and `replaceBlocks` methods.\n\n**Relevant Docs:**\n\n- [Block Manipulation](/docs/reference/editor/manipulating-content)", - }, - { - projectSlug: "selection-blocks", - fullSlug: "basic/selection-blocks", - pathFromRoot: "examples/01-basic/07-selection-blocks", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic", "Blocks"], - }, - title: "Displaying Selected Blocks", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - readme: - "In this example, the JSON representation of blocks spanned by the user's selection, is displayed below the editor.\n\n**Try it out:** Select different blocks in the editor and see the JSON update!\n\n**Relevant Docs:**\n\n- [Cursor Selections](/docs/reference/editor/cursor-selections)", - }, - { - projectSlug: "ariakit", - fullSlug: "basic/ariakit", - pathFromRoot: "examples/01-basic/08-ariakit", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic"], - }, - title: "Use with Ariakit", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - readme: - "This example shows how you can use BlockNote with Ariakit (instead of Mantine).\n\n**Relevant Docs:**\n\n- [Ariakit Docs](/docs/getting-started/ariakit)\n- [Editor Setup](/docs/getting-started/editor-setup)", - }, - { - projectSlug: "shadcn", - fullSlug: "basic/shadcn", - pathFromRoot: "examples/01-basic/09-shadcn", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic"], - tailwind: true, - stackBlitz: false, - }, - title: "Use with ShadCN", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - readme: - "This example shows how you can use BlockNote with ShadCN (instead of Mantine).\n\n**Relevant Docs:**\n\n- [Getting Started with ShadCN](/docs/getting-started/shadcn)", - }, - { - projectSlug: "localization", - fullSlug: "basic/localization", - pathFromRoot: "examples/01-basic/10-localization", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Basic"], - }, - title: "Localization (i18n)", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - readme: - "In this example, we pass in a custom dictionary to change the interface of the editor to use Dutch (NL) strings.\n\nYou can also provide your own dictionary to customize the strings used in the editor, or submit a Pull Request to add support for your language of your choice.\n\n**Relevant Docs:**\n\n- [Localization](/docs/features/localization)", - }, - { - projectSlug: "custom-placeholder", - fullSlug: "basic/custom-placeholder", - pathFromRoot: "examples/01-basic/11-custom-placeholder", - config: { - playground: true, - docs: true, - author: "ezhil56x", - tags: ["Basic"], - }, - title: "Change placeholder text", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - readme: - "In this example, we show how to change the placeholders:\n\n- For an empty document, we show a placeholder `Start typing..` (by default, this is not set)\n- the default placeholder in this editor shows `Custom default placeholder` instead of the default (`Enter text or type '/' for commands`)\n- for Headings, the placeholder shows `Custom heading placeholder` instead of the default (`Heading`). Try adding a Heading to see the change\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Localization (i18n)](/examples/basic/localization)", - }, - { - projectSlug: "multi-editor", - fullSlug: "basic/multi-editor", - pathFromRoot: "examples/01-basic/12-multi-editor", - config: { - playground: true, - docs: true, - author: "areknawo", - tags: ["Basic"], - }, - title: "Multi-Editor Setup", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - readme: - "This example showcases use of multiple editors in a single page - you can even drag blocks between them.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)", - }, - { - projectSlug: "custom-paste-handler", - fullSlug: "basic/custom-paste-handler", - pathFromRoot: "examples/01-basic/13-custom-paste-handler", - config: { - playground: true, - docs: true, - author: "nperez0111", - tags: ["Basic"], - }, - title: "Custom Paste Handler", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - readme: - "In this example, we change the default paste handler to append some text to the pasted content when the content is plain text.\n\n**Try it out:** Use the buttons to copy some content to the clipboard and paste it in the editor to trigger our custom paste handler.\n\n**Relevant Docs:**\n\n- [Paste Handling](/docs/reference/editor/paste-handling)", - }, - { - projectSlug: "testing", - fullSlug: "basic/testing", - pathFromRoot: "examples/01-basic/testing", - config: { - playground: true, - docs: false, - }, - title: "Test Editor", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - readme: "This example is meant for use in end-to-end tests.", - }, - ], + "pro": true + }, + "title": "Multi-Column Blocks", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + }, + "readme": "This example showcases multi-column blocks, allowing you to stack blocks next to each other. These come as part of the `@blocknote/xl-multi-column` package.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Document Structure](/docs/foundations/document-structure)" + }, + { + "projectSlug": "default-blocks", + "fullSlug": "basic/default-blocks", + "pathFromRoot": "examples/01-basic/04-default-blocks", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Basic", + "Blocks", + "Inline Content" + ] + }, + "title": "Default Schema Showcase", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + }, + "readme": "This example showcases each block and inline content type in BlockNote's default schema.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Document Structure](/docs/foundations/document-structure)\n- [Default Schema](/docs/foundations/schemas)" + }, + { + "projectSlug": "removing-default-blocks", + "fullSlug": "basic/removing-default-blocks", + "pathFromRoot": "examples/01-basic/05-removing-default-blocks", + "config": { + "playground": true, + "docs": true, + "author": "hunxjunedo", + "tags": [ + "Basic", + "removing", + "blocks" + ] + }, + "title": "Removing Default Blocks from Schema", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + }, + "readme": "This example shows how to change the default schema and disable the Audio and Image blocks. To do this, we pass in a custom schema based on the built-in, default schema, with two specific blocks removed.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Custom Schemas](/docs/features/custom-schemas)\n- [Default Schema](/docs/foundations/schemas)" + }, + { + "projectSlug": "block-manipulation", + "fullSlug": "basic/block-manipulation", + "pathFromRoot": "examples/01-basic/06-block-manipulation", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic", + "Blocks" + ] + }, + "title": "Manipulating Blocks", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + }, + "readme": "This example shows 4 buttons to manipulate the first block using the `insertBlocks`, `updateBlock`, `removeBlocks` and `replaceBlocks` methods.\n\n**Relevant Docs:**\n\n- [Block Manipulation](/docs/reference/editor/manipulating-content)" + }, + { + "projectSlug": "selection-blocks", + "fullSlug": "basic/selection-blocks", + "pathFromRoot": "examples/01-basic/07-selection-blocks", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic", + "Blocks" + ] + }, + "title": "Displaying Selected Blocks", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + }, + "readme": "In this example, the JSON representation of blocks spanned by the user's selection, is displayed below the editor.\n\n**Try it out:** Select different blocks in the editor and see the JSON update!\n\n**Relevant Docs:**\n\n- [Cursor Selections](/docs/reference/editor/cursor-selections)" + }, + { + "projectSlug": "ariakit", + "fullSlug": "basic/ariakit", + "pathFromRoot": "examples/01-basic/08-ariakit", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic" + ] + }, + "title": "Use with Ariakit", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + }, + "readme": "This example shows how you can use BlockNote with Ariakit (instead of Mantine).\n\n**Relevant Docs:**\n\n- [Ariakit Docs](/docs/getting-started/ariakit)\n- [Editor Setup](/docs/getting-started/editor-setup)" + }, + { + "projectSlug": "shadcn", + "fullSlug": "basic/shadcn", + "pathFromRoot": "examples/01-basic/09-shadcn", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic" + ], + "tailwind": true, + "stackBlitz": false + }, + "title": "Use with ShadCN", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + }, + "readme": "This example shows how you can use BlockNote with ShadCN (instead of Mantine).\n\n**Relevant Docs:**\n\n- [Getting Started with ShadCN](/docs/getting-started/shadcn)" + }, + { + "projectSlug": "localization", + "fullSlug": "basic/localization", + "pathFromRoot": "examples/01-basic/10-localization", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Basic" + ] + }, + "title": "Localization (i18n)", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + }, + "readme": "In this example, we pass in a custom dictionary to change the interface of the editor to use Dutch (NL) strings.\n\nYou can also provide your own dictionary to customize the strings used in the editor, or submit a Pull Request to add support for your language of your choice.\n\n**Relevant Docs:**\n\n- [Localization](/docs/features/localization)" + }, + { + "projectSlug": "custom-placeholder", + "fullSlug": "basic/custom-placeholder", + "pathFromRoot": "examples/01-basic/11-custom-placeholder", + "config": { + "playground": true, + "docs": true, + "author": "ezhil56x", + "tags": [ + "Basic" + ] + }, + "title": "Change placeholder text", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + }, + "readme": "In this example, we show how to change the placeholders:\n\n- For an empty document, we show a placeholder `Start typing..` (by default, this is not set)\n- the default placeholder in this editor shows `Custom default placeholder` instead of the default (`Enter text or type '/' for commands`)\n- for Headings, the placeholder shows `Custom heading placeholder` instead of the default (`Heading`). Try adding a Heading to see the change\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Localization (i18n)](/examples/basic/localization)" + }, + { + "projectSlug": "multi-editor", + "fullSlug": "basic/multi-editor", + "pathFromRoot": "examples/01-basic/12-multi-editor", + "config": { + "playground": true, + "docs": true, + "author": "areknawo", + "tags": [ + "Basic" + ] + }, + "title": "Multi-Editor Setup", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + }, + "readme": "This example showcases use of multiple editors in a single page - you can even drag blocks between them.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)" + }, + { + "projectSlug": "custom-paste-handler", + "fullSlug": "basic/custom-paste-handler", + "pathFromRoot": "examples/01-basic/13-custom-paste-handler", + "config": { + "playground": true, + "docs": true, + "author": "nperez0111", + "tags": [ + "Basic" + ] + }, + "title": "Custom Paste Handler", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + }, + "readme": "In this example, we change the default paste handler to append some text to the pasted content when the content is plain text.\n\n**Try it out:** Use the buttons to copy some content to the clipboard and paste it in the editor to trigger our custom paste handler.\n\n**Relevant Docs:**\n\n- [Paste Handling](/docs/reference/editor/paste-handling)" + }, + { + "projectSlug": "testing", + "fullSlug": "basic/testing", + "pathFromRoot": "examples/01-basic/testing", + "config": { + "playground": true, + "docs": false + }, + "title": "Test Editor", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + }, + "readme": "This example is meant for use in end-to-end tests." + } + ] }, - backend: { - pathFromRoot: "examples/02-backend", - slug: "backend", - projects: [ - { - projectSlug: "file-uploading", - fullSlug: "backend/file-uploading", - pathFromRoot: "examples/02-backend/01-file-uploading", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Intermediate", "Saving/Loading"], - }, - title: "Upload Files", - group: { - pathFromRoot: "examples/02-backend", - slug: "backend", - }, - readme: - 'This example allows users to upload files and use them in the editor. The files are uploaded to [/TMP/Files](https://tmpfiles.org/), and can be used for File, Image, Video, and Audio blocks.\n\n**Try it out:** Click the "Add Image" button and see there\'s now an "Upload" tab in the toolbar!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [File Block](/docs/features/blocks/embeds#file)', - }, - { - projectSlug: "saving-loading", - fullSlug: "backend/saving-loading", - pathFromRoot: "examples/02-backend/02-saving-loading", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Intermediate", "Blocks", "Saving/Loading"], - }, - title: "Saving & Loading", - group: { - pathFromRoot: "examples/02-backend", - slug: "backend", - }, - readme: - "This example shows how to save the editor contents to local storage whenever a change is made, and load the saved contents when the editor is created.\n\nYou can replace the `saveToStorage` and `loadFromStorage` functions with calls to your backend or database.\n\n**Try it out:** Try typing in the editor and reloading the page!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Getting the Document](/docs/foundations/manipulating-content#reading-blocks)", - }, - { - projectSlug: "s3", - fullSlug: "backend/s3", - pathFromRoot: "examples/02-backend/03-s3", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Intermediate", "Saving/Loading"], - dependencies: { + "backend": { + "pathFromRoot": "examples/02-backend", + "slug": "backend", + "projects": [ + { + "projectSlug": "file-uploading", + "fullSlug": "backend/file-uploading", + "pathFromRoot": "examples/02-backend/01-file-uploading", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Intermediate", + "Saving/Loading" + ] + }, + "title": "Upload Files", + "group": { + "pathFromRoot": "examples/02-backend", + "slug": "backend" + }, + "readme": "This example allows users to upload files and use them in the editor. The files are uploaded to [/TMP/Files](https://tmpfiles.org/), and can be used for File, Image, Video, and Audio blocks.\n\n**Try it out:** Click the \"Add Image\" button and see there's now an \"Upload\" tab in the toolbar!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [File Block](/docs/features/blocks/embeds#file)" + }, + { + "projectSlug": "saving-loading", + "fullSlug": "backend/saving-loading", + "pathFromRoot": "examples/02-backend/02-saving-loading", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Intermediate", + "Blocks", + "Saving/Loading" + ] + }, + "title": "Saving & Loading", + "group": { + "pathFromRoot": "examples/02-backend", + "slug": "backend" + }, + "readme": "This example shows how to save the editor contents to local storage whenever a change is made, and load the saved contents when the editor is created.\n\nYou can replace the `saveToStorage` and `loadFromStorage` functions with calls to your backend or database.\n\n**Try it out:** Try typing in the editor and reloading the page!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Getting the Document](/docs/foundations/manipulating-content#reading-blocks)" + }, + { + "projectSlug": "s3", + "fullSlug": "backend/s3", + "pathFromRoot": "examples/02-backend/03-s3", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Intermediate", + "Saving/Loading" + ], + "dependencies": { "@aws-sdk/client-s3": "^3.609.0", - "@aws-sdk/s3-request-presigner": "^3.609.0", - } as any, - pro: true, - }, - title: "Upload Files to AWS S3", - group: { - pathFromRoot: "examples/02-backend", - slug: "backend", - }, - readme: - 'This example allows users to upload files to an AWS S3 bucket and use them in the editor. The files can be used for File, Image, Video, and Audio blocks.\n\n**Try it out:** Click the "Add Image" button and see there\'s now an "Upload" tab in the toolbar!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [File Block](/docs/features/blocks/embeds#file)', - }, - { - projectSlug: "rendering-static-documents", - fullSlug: "backend/rendering-static-documents", - pathFromRoot: "examples/02-backend/04-rendering-static-documents", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["server"], - dependencies: { - "@blocknote/server-util": "latest", + "@aws-sdk/s3-request-presigner": "^3.609.0" } as any, + "pro": true }, - title: "Rendering static documents", - group: { - pathFromRoot: "examples/02-backend", - slug: "backend", + "title": "Upload Files to AWS S3", + "group": { + "pathFromRoot": "examples/02-backend", + "slug": "backend" }, - readme: - "This example shows how you can use HTML exported using the `blocksToFullHTML` and render it as a static document (a view-only document, without the editor). You can use this for example if you use BlockNote to edit blog posts in a CMS, but want to display non-editable static, published pages to end-users.\n\n**Relevant Docs:**\n\n- [Server-side processing](/docs/features/server-processing)", + "readme": "This example allows users to upload files to an AWS S3 bucket and use them in the editor. The files can be used for File, Image, Video, and Audio blocks.\n\n**Try it out:** Click the \"Add Image\" button and see there's now an \"Upload\" tab in the toolbar!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [File Block](/docs/features/blocks/embeds#file)" }, - ], + { + "projectSlug": "rendering-static-documents", + "fullSlug": "backend/rendering-static-documents", + "pathFromRoot": "examples/02-backend/04-rendering-static-documents", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "server" + ], + "dependencies": { + "@blocknote/server-util": "latest" + } as any + }, + "title": "Rendering static documents", + "group": { + "pathFromRoot": "examples/02-backend", + "slug": "backend" + }, + "readme": "This example shows how you can use HTML exported using the `blocksToFullHTML` and render it as a static document (a view-only document, without the editor). You can use this for example if you use BlockNote to edit blog posts in a CMS, but want to display non-editable static, published pages to end-users.\n\n**Relevant Docs:**\n\n- [Server-side processing](/docs/features/server-processing)" + } + ] }, "ui-components": { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - projects: [ - { - projectSlug: "formatting-toolbar-buttons", - fullSlug: "ui-components/formatting-toolbar-buttons", - pathFromRoot: "examples/03-ui-components/02-formatting-toolbar-buttons", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: [ + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components", + "projects": [ + { + "projectSlug": "formatting-toolbar-buttons", + "fullSlug": "ui-components/formatting-toolbar-buttons", + "pathFromRoot": "examples/03-ui-components/02-formatting-toolbar-buttons", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ "Intermediate", "Inline Content", "UI Components", - "Formatting Toolbar", - ], + "Formatting Toolbar" + ] }, - title: "Adding Formatting Toolbar Buttons", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", + "title": "Adding Formatting Toolbar Buttons", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" }, - readme: - "In this example, we add a blue text/background color and code style button to the Formatting Toolbar. We also make sure it only shows up when some text is selected.\n\n**Try it out:** Select some text to open the Formatting Toolbar, and click one of the new buttons!\n\n**Relevant Docs:**\n\n- [Changing the Formatting Toolbar](/docs/react/components/formatting-toolbar)\n- [Manipulating Inline Content](/docs/reference/editor/manipulating-content)\n- [Editor Setup](/docs/getting-started/editor-setup)", + "readme": "In this example, we add a blue text/background color and code style button to the Formatting Toolbar. We also make sure it only shows up when some text is selected.\n\n**Try it out:** Select some text to open the Formatting Toolbar, and click one of the new buttons!\n\n**Relevant Docs:**\n\n- [Changing the Formatting Toolbar](/docs/react/components/formatting-toolbar)\n- [Manipulating Inline Content](/docs/reference/editor/manipulating-content)\n- [Editor Setup](/docs/getting-started/editor-setup)" }, { - projectSlug: "formatting-toolbar-block-type-items", - fullSlug: "ui-components/formatting-toolbar-block-type-items", - pathFromRoot: - "examples/03-ui-components/03-formatting-toolbar-block-type-items", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: [ + "projectSlug": "formatting-toolbar-block-type-items", + "fullSlug": "ui-components/formatting-toolbar-block-type-items", + "pathFromRoot": "examples/03-ui-components/03-formatting-toolbar-block-type-items", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ "Intermediate", "Blocks", "UI Components", "Formatting Toolbar", - "Custom Schemas", + "Custom Schemas" ], - dependencies: { + "dependencies": { "@mantine/core": "^8.3.4", - "react-icons": "^5.2.1", - } as any, + "react-icons": "^5.2.1" + } as any }, - title: "Adding Block Type Select Items", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - }, - readme: - 'In this example, we add an item to the Block Type Select, so that it works for a custom alert block we create.\n\n**Try it out:** Select some text to open the Formatting Toolbar, and click "Alert" in the Block Type Select to change the selected block!\n\n**Relevant Docs:**\n\n- [Changing Block Type Select Items](/docs/react/components/formatting-toolbar)\n- [Custom Block Types](/docs/features/custom-schemas/custom-blocks)\n- [Editor Setup](/docs/getting-started/editor-setup)', - }, - { - projectSlug: "side-menu-buttons", - fullSlug: "ui-components/side-menu-buttons", - pathFromRoot: "examples/03-ui-components/04-side-menu-buttons", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Intermediate", "Blocks", "UI Components", "Block Side Menu"], - dependencies: { - "react-icons": "^5.2.1", - } as any, - }, - title: "Adding Block Side Menu Buttons", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - }, - readme: - "In this example, we replace the button to add a block in the Block Side Menu, with a button to remove the hovered block.\n\n**Try it out:** Hover a block to open the Block Side Menu, and click the new button!\n\n**Relevant Docs:**\n\n- [Changing the Block Side Menu](/docs/react/components/side-menu)\n- [Removing Blocks](/docs/reference/editor/manipulating-content)\n- [Editor Setup](/docs/getting-started/editor-setup)", - }, - { - projectSlug: "side-menu-drag-handle-items", - fullSlug: "ui-components/side-menu-drag-handle-items", - pathFromRoot: - "examples/03-ui-components/05-side-menu-drag-handle-items", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Intermediate", "Blocks", "UI Components", "Block Side Menu"], - dependencies: { - "react-icons": "^5.2.1", - } as any, - }, - title: "Adding Drag Handle Menu Items", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", + "title": "Adding Block Type Select Items", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" }, - readme: - 'In this example, we add an item to the Drag Handle Menu, which resets the hovered block to a paragraph.\n\n**Try it out:** Hover a block to open the Block Side Menu, and click "Reset Type" in the Drag Handle Menu to reset the selected block!\n\n**Relevant Docs:**\n\n- [Changing Drag Handle Menu Items](/docs/react/components/side-menu)\n- [Updating Blocks](/docs/reference/editor/manipulating-content)\n- [Editor Setup](/docs/getting-started/editor-setup)', + "readme": "In this example, we add an item to the Block Type Select, so that it works for a custom alert block we create.\n\n**Try it out:** Select some text to open the Formatting Toolbar, and click \"Alert\" in the Block Type Select to change the selected block!\n\n**Relevant Docs:**\n\n- [Changing Block Type Select Items](/docs/react/components/formatting-toolbar)\n- [Custom Block Types](/docs/features/custom-schemas/custom-blocks)\n- [Editor Setup](/docs/getting-started/editor-setup)" }, { - projectSlug: "suggestion-menus-slash-menu-items", - fullSlug: "ui-components/suggestion-menus-slash-menu-items", - pathFromRoot: - "examples/03-ui-components/06-suggestion-menus-slash-menu-items", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: [ + "projectSlug": "side-menu-buttons", + "fullSlug": "ui-components/side-menu-buttons", + "pathFromRoot": "examples/03-ui-components/04-side-menu-buttons", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Intermediate", + "Blocks", + "UI Components", + "Block Side Menu" + ], + "dependencies": { + "react-icons": "^5.2.1" + } as any + }, + "title": "Adding Block Side Menu Buttons", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + }, + "readme": "In this example, we replace the button to add a block in the Block Side Menu, with a button to remove the hovered block.\n\n**Try it out:** Hover a block to open the Block Side Menu, and click the new button!\n\n**Relevant Docs:**\n\n- [Changing the Block Side Menu](/docs/react/components/side-menu)\n- [Removing Blocks](/docs/reference/editor/manipulating-content)\n- [Editor Setup](/docs/getting-started/editor-setup)" + }, + { + "projectSlug": "side-menu-drag-handle-items", + "fullSlug": "ui-components/side-menu-drag-handle-items", + "pathFromRoot": "examples/03-ui-components/05-side-menu-drag-handle-items", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Intermediate", + "Blocks", + "UI Components", + "Block Side Menu" + ], + "dependencies": { + "react-icons": "^5.2.1" + } as any + }, + "title": "Adding Drag Handle Menu Items", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + }, + "readme": "In this example, we add an item to the Drag Handle Menu, which resets the hovered block to a paragraph.\n\n**Try it out:** Hover a block to open the Block Side Menu, and click \"Reset Type\" in the Drag Handle Menu to reset the selected block!\n\n**Relevant Docs:**\n\n- [Changing Drag Handle Menu Items](/docs/react/components/side-menu)\n- [Updating Blocks](/docs/reference/editor/manipulating-content)\n- [Editor Setup](/docs/getting-started/editor-setup)" + }, + { + "projectSlug": "suggestion-menus-slash-menu-items", + "fullSlug": "ui-components/suggestion-menus-slash-menu-items", + "pathFromRoot": "examples/03-ui-components/06-suggestion-menus-slash-menu-items", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ "Intermediate", "Blocks", "UI Components", "Suggestion Menus", - "Slash Menu", + "Slash Menu" ], - dependencies: { - "react-icons": "^5.2.1", - } as any, - }, - title: "Adding Slash Menu Items", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - }, - readme: - 'In this example, we add an item to the Slash Menu, which adds a new block below with a bold "Hello World" string.\n\n**Try it out:** Press the "/" key to open the Slash Menu and select the new item!\n\n**Relevant Docs:**\n\n- [Changing Slash Menu Items](/docs/react/components/suggestion-menus)\n- [Getting Text Cursor Position](/docs/reference/editor/cursor-selections)\n- [Inserting New Blocks](/docs/reference/editor/manipulating-content)\n- [Editor Setup](/docs/getting-started/editor-setup)', - }, - { - projectSlug: "suggestion-menus-slash-menu-component", - fullSlug: "ui-components/suggestion-menus-slash-menu-component", - pathFromRoot: - "examples/03-ui-components/07-suggestion-menus-slash-menu-component", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: [ + "dependencies": { + "react-icons": "^5.2.1" + } as any + }, + "title": "Adding Slash Menu Items", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + }, + "readme": "In this example, we add an item to the Slash Menu, which adds a new block below with a bold \"Hello World\" string.\n\n**Try it out:** Press the \"/\" key to open the Slash Menu and select the new item!\n\n**Relevant Docs:**\n\n- [Changing Slash Menu Items](/docs/react/components/suggestion-menus)\n- [Getting Text Cursor Position](/docs/reference/editor/cursor-selections)\n- [Inserting New Blocks](/docs/reference/editor/manipulating-content)\n- [Editor Setup](/docs/getting-started/editor-setup)" + }, + { + "projectSlug": "suggestion-menus-slash-menu-component", + "fullSlug": "ui-components/suggestion-menus-slash-menu-component", + "pathFromRoot": "examples/03-ui-components/07-suggestion-menus-slash-menu-component", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ "Intermediate", "UI Components", "Suggestion Menus", "Slash Menu", - "Appearance & Styling", - ], + "Appearance & Styling" + ] }, - title: "Replacing Slash Menu Component", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", + "title": "Replacing Slash Menu Component", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" }, - readme: - 'In this example, we replace the default Slash Menu component with a basic custom one.\n\n**Try it out:** Press the "/" key to see the new Slash Menu!\n\n**Relevant Docs:**\n\n- [Replacing the Slash Menu Component](/docs/react/components/suggestion-menus)\n- [Editor Setup](/docs/getting-started/editor-setup)', + "readme": "In this example, we replace the default Slash Menu component with a basic custom one.\n\n**Try it out:** Press the \"/\" key to see the new Slash Menu!\n\n**Relevant Docs:**\n\n- [Replacing the Slash Menu Component](/docs/react/components/suggestion-menus)\n- [Editor Setup](/docs/getting-started/editor-setup)" }, { - projectSlug: "suggestion-menus-emoji-picker-columns", - fullSlug: "ui-components/suggestion-menus-emoji-picker-columns", - pathFromRoot: - "examples/03-ui-components/08-suggestion-menus-emoji-picker-columns", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: [ + "projectSlug": "suggestion-menus-emoji-picker-columns", + "fullSlug": "ui-components/suggestion-menus-emoji-picker-columns", + "pathFromRoot": "examples/03-ui-components/08-suggestion-menus-emoji-picker-columns", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ "Intermediate", "Blocks", "UI Components", "Suggestion Menus", - "Emoji Picker", - ], + "Emoji Picker" + ] }, - title: "Changing Emoji Picker Columns", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", + "title": "Changing Emoji Picker Columns", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" }, - readme: - 'In this example, we change the Emoji Picker to display 5 columns instead of 10.\n\n**Try it out:** Press the ":" key to open the Emoji Picker!\n\n**Relevant Docs:**\n\n- [Changing Emoji Picker Columns](/docs/react/components/suggestion-menus)\n- [Editor Setup](/docs/getting-started/editor-setup)', + "readme": "In this example, we change the Emoji Picker to display 5 columns instead of 10.\n\n**Try it out:** Press the \":\" key to open the Emoji Picker!\n\n**Relevant Docs:**\n\n- [Changing Emoji Picker Columns](/docs/react/components/suggestion-menus)\n- [Editor Setup](/docs/getting-started/editor-setup)" }, { - projectSlug: "suggestion-menus-emoji-picker-component", - fullSlug: "ui-components/suggestion-menus-emoji-picker-component", - pathFromRoot: - "examples/03-ui-components/09-suggestion-menus-emoji-picker-component", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: [ + "projectSlug": "suggestion-menus-emoji-picker-component", + "fullSlug": "ui-components/suggestion-menus-emoji-picker-component", + "pathFromRoot": "examples/03-ui-components/09-suggestion-menus-emoji-picker-component", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ "Intermediate", "UI Components", "Suggestion Menus", "Emoji Picker", - "Appearance & Styling", - ], + "Appearance & Styling" + ] }, - title: "Replacing Emoji Picker Component", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", + "title": "Replacing Emoji Picker Component", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" }, - readme: - 'In this example, we replace the default Emoji Picker component with a basic custom one.\n\n**Try it out:** Press the ":" key to see the new Emoji Picker!\n\n**Relevant Docs:**\n\n- [Replacing the Emoji Picker Component](/docs/react/components/suggestion-menus)\n- [Editor Setup](/docs/getting-started/editor-setup)', + "readme": "In this example, we replace the default Emoji Picker component with a basic custom one.\n\n**Try it out:** Press the \":\" key to see the new Emoji Picker!\n\n**Relevant Docs:**\n\n- [Replacing the Emoji Picker Component](/docs/react/components/suggestion-menus)\n- [Editor Setup](/docs/getting-started/editor-setup)" }, { - projectSlug: "suggestion-menus-grid-mentions", - fullSlug: "ui-components/suggestion-menus-grid-mentions", - pathFromRoot: - "examples/03-ui-components/10-suggestion-menus-grid-mentions", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: [ + "projectSlug": "suggestion-menus-grid-mentions", + "fullSlug": "ui-components/suggestion-menus-grid-mentions", + "pathFromRoot": "examples/03-ui-components/10-suggestion-menus-grid-mentions", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ "Intermediate", "Inline Content", "Custom Schemas", - "Suggestion Menus", - ], + "Suggestion Menus" + ] }, - title: "Grid Mentions Menu", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", + "title": "Grid Mentions Menu", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" }, - readme: - 'In this example, we create a custom `Mention` inline content type which is used to tag people. In addition, we create a new Suggestion Menu for mentions which opens with the "@" character. This Suggestion Menu is displayed as a grid of 2 columns, where each item is the first letter of the person\'s name.\n\n**Try it out:** Press the "@" key to open the mentions menu and insert a mention!\n\n**Relevant Docs:**\n\n- [Custom Inline Content Types](/docs/features/custom-schemas/custom-inline-content)\n- [Creating Suggestion Menus](/docs/react/components/suggestion-menus)\n- [Editor Setup](/docs/getting-started/editor-setup)', + "readme": "In this example, we create a custom `Mention` inline content type which is used to tag people. In addition, we create a new Suggestion Menu for mentions which opens with the \"@\" character. This Suggestion Menu is displayed as a grid of 2 columns, where each item is the first letter of the person's name.\n\n**Try it out:** Press the \"@\" key to open the mentions menu and insert a mention!\n\n**Relevant Docs:**\n\n- [Custom Inline Content Types](/docs/features/custom-schemas/custom-inline-content)\n- [Creating Suggestion Menus](/docs/react/components/suggestion-menus)\n- [Editor Setup](/docs/getting-started/editor-setup)" }, { - projectSlug: "uppy-file-panel", - fullSlug: "ui-components/uppy-file-panel", - pathFromRoot: "examples/03-ui-components/11-uppy-file-panel", - config: { - playground: true, - docs: true, - author: "ezhil56x", - tags: ["Intermediate", "Files"], - dependencies: { + "projectSlug": "uppy-file-panel", + "fullSlug": "ui-components/uppy-file-panel", + "pathFromRoot": "examples/03-ui-components/11-uppy-file-panel", + "config": { + "playground": true, + "docs": true, + "author": "ezhil56x", + "tags": [ + "Intermediate", + "Files" + ], + "dependencies": { "@uppy/core": "^3.13.1", "@uppy/dashboard": "^3.9.1", "@uppy/drag-drop": "^3.1.1", @@ -594,50 +621,48 @@ export const examples = { "@uppy/status-bar": "^3.1.1", "@uppy/webcam": "^3.4.2", "@uppy/xhr-upload": "^3.4.0", - "react-icons": "^5.2.1", + "react-icons": "^5.2.1" } as any, - pro: true, + "pro": true }, - title: "Uppy File Panel", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", + "title": "Uppy File Panel", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" }, - readme: - 'This example allows users to upload files using [Uppy](https://uppy.io/) by replacing the default File Panel with an Uppy Dashboard.\n\nUppy is highly extensible and has an extensive ecosystem of plugins. For example, you can:\n\n- Record audio, screen or webcam\n- Import files from Box / Dropbox / Facebook / Google Drive / Google Photos / Instagram / OneDrive / Zoom\n- Select files from Unsplash\n- Show an image editor (crop, rotate, etc)\n\nIn this example, we\'ve enabled the Webcam, ScreenCapture and Image Editor plugins.\n\n**Try it out:** Click the "Add Image" button and you can either drop files or click "browse files" to upload them.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Image](/docs/foundations/schemas)', + "readme": "This example allows users to upload files using [Uppy](https://uppy.io/) by replacing the default File Panel with an Uppy Dashboard.\n\nUppy is highly extensible and has an extensive ecosystem of plugins. For example, you can:\n\n- Record audio, screen or webcam\n- Import files from Box / Dropbox / Facebook / Google Drive / Google Photos / Instagram / OneDrive / Zoom\n- Select files from Unsplash\n- Show an image editor (crop, rotate, etc)\n\nIn this example, we've enabled the Webcam, ScreenCapture and Image Editor plugins.\n\n**Try it out:** Click the \"Add Image\" button and you can either drop files or click \"browse files\" to upload them.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Image](/docs/foundations/schemas)" }, { - projectSlug: "static-formatting-toolbar", - fullSlug: "ui-components/static-formatting-toolbar", - pathFromRoot: "examples/03-ui-components/12-static-formatting-toolbar", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: [ + "projectSlug": "static-formatting-toolbar", + "fullSlug": "ui-components/static-formatting-toolbar", + "pathFromRoot": "examples/03-ui-components/12-static-formatting-toolbar", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ "Basic", "UI Components", "Formatting Toolbar", - "Appearance & Styling", - ], + "Appearance & Styling" + ] }, - title: "Static Formatting Toolbar", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", + "title": "Static Formatting Toolbar", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" }, - readme: - "This example shows how to make the formatting toolbar always visible and static\nabove the editor.\n\n**Relevant Docs:**\n\n- [Changing the Formatting Toolbar](/docs/react/components/formatting-toolbar)\n- [Editor Setup](/docs/getting-started/editor-setup)", + "readme": "This example shows how to make the formatting toolbar always visible and static\nabove the editor.\n\n**Relevant Docs:**\n\n- [Changing the Formatting Toolbar](/docs/react/components/formatting-toolbar)\n- [Editor Setup](/docs/getting-started/editor-setup)" }, { - projectSlug: "custom-ui", - fullSlug: "ui-components/custom-ui", - pathFromRoot: "examples/03-ui-components/13-custom-ui", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: [ + "projectSlug": "custom-ui", + "fullSlug": "ui-components/custom-ui", + "pathFromRoot": "examples/03-ui-components/13-custom-ui", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ "Advanced", "Inline Content", "UI Components", @@ -645,1124 +670,1204 @@ export const examples = { "Formatting Toolbar", "Suggestion Menus", "Slash Menu", - "Appearance & Styling", + "Appearance & Styling" ], - dependencies: { + "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.16.1", - "@mui/material": "^5.16.1", + "@mui/material": "^5.16.1" } as any, - pro: true, - }, - title: "UI With Third-Party Components", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - }, - readme: - "In this example, we implement a basic editor interface using components from Material UI. We replace the Formatting Toolbar, Slash Menu, and Block Side Menu while disabling the other default elements. Additionally, the Formatting Toolbar is made static and always visible above the editor.\n\n**Relevant Docs:**\n\n- [Formatting Toolbar](/docs/react/components/formatting-toolbar)\n- [Manipulating Inline Content](/docs/reference/editor/manipulating-content)\n- [Slash Menu](/docs/react/components/suggestion-menus)\n- [Side Menu](/docs/react/components/side-menu)\n- [Editor Setup](/docs/getting-started/editor-setup)", - }, - { - projectSlug: "experimental-mobile-formatting-toolbar", - fullSlug: "ui-components/experimental-mobile-formatting-toolbar", - pathFromRoot: - "examples/03-ui-components/14-experimental-mobile-formatting-toolbar", - config: { - playground: true, - docs: true, - author: "areknawo", - tags: [ + "pro": true + }, + "title": "UI With Third-Party Components", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + }, + "readme": "In this example, we implement a basic editor interface using components from Material UI. We replace the Formatting Toolbar, Slash Menu, and Block Side Menu while disabling the other default elements. Additionally, the Formatting Toolbar is made static and always visible above the editor.\n\n**Relevant Docs:**\n\n- [Formatting Toolbar](/docs/react/components/formatting-toolbar)\n- [Manipulating Inline Content](/docs/reference/editor/manipulating-content)\n- [Slash Menu](/docs/react/components/suggestion-menus)\n- [Side Menu](/docs/react/components/side-menu)\n- [Editor Setup](/docs/getting-started/editor-setup)" + }, + { + "projectSlug": "experimental-mobile-formatting-toolbar", + "fullSlug": "ui-components/experimental-mobile-formatting-toolbar", + "pathFromRoot": "examples/03-ui-components/14-experimental-mobile-formatting-toolbar", + "config": { + "playground": true, + "docs": true, + "author": "areknawo", + "tags": [ "Intermediate", "UI Components", "Formatting Toolbar", - "Appearance & Styling", - ], + "Appearance & Styling" + ] }, - title: "Experimental Mobile Formatting Toolbar", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", + "title": "Experimental Mobile Formatting Toolbar", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" }, - readme: - "This example shows how to use the experimental mobile formatting toolbar, which uses [Visual Viewport API](https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API) to position the toolbar right above the virtual keyboard on mobile devices.\n\nController is currently marked **experimental** due to the flickering issue with positioning (caused by delays of the Visual Viewport API)\n\n**Relevant Docs:**\n\n- [Changing the Formatting Toolbar](/docs/react/components/formatting-toolbar)\n- [Editor Setup](/docs/getting-started/editor-setup)", + "readme": "This example shows how to use the experimental mobile formatting toolbar, which uses [Visual Viewport API](https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API) to position the toolbar right above the virtual keyboard on mobile devices.\n\nController is currently marked **experimental** due to the flickering issue with positioning (caused by delays of the Visual Viewport API)\n\n**Relevant Docs:**\n\n- [Changing the Formatting Toolbar](/docs/react/components/formatting-toolbar)\n- [Editor Setup](/docs/getting-started/editor-setup)" }, { - projectSlug: "advanced-tables", - fullSlug: "ui-components/advanced-tables", - pathFromRoot: "examples/03-ui-components/15-advanced-tables", - config: { - playground: true, - docs: true, - author: "nperez0111", - tags: [ + "projectSlug": "advanced-tables", + "fullSlug": "ui-components/advanced-tables", + "pathFromRoot": "examples/03-ui-components/15-advanced-tables", + "config": { + "playground": true, + "docs": true, + "author": "nperez0111", + "tags": [ "Intermediate", "UI Components", "Tables", - "Appearance & Styling", - ], + "Appearance & Styling" + ] }, - title: "Advanced Tables", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", + "title": "Advanced Tables", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" }, - readme: - "This example enables the following features in tables:\n\n- Split cells\n- Cell background color\n- Cell text color\n- Table row and column headers\n\n**Relevant Docs:**\n\n- [Tables](/docs/features/blocks/tables)\n- [Editor Setup](/docs/getting-started/editor-setup)", + "readme": "This example enables the following features in tables:\n\n- Split cells\n- Cell background color\n- Cell text color\n- Table row and column headers\n\n**Relevant Docs:**\n\n- [Tables](/docs/features/blocks/tables)\n- [Editor Setup](/docs/getting-started/editor-setup)" }, { - projectSlug: "link-toolbar-buttons", - fullSlug: "ui-components/link-toolbar-buttons", - pathFromRoot: "examples/03-ui-components/16-link-toolbar-buttons", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: [ + "projectSlug": "link-toolbar-buttons", + "fullSlug": "ui-components/link-toolbar-buttons", + "pathFromRoot": "examples/03-ui-components/16-link-toolbar-buttons", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ "Intermediate", "Inline Content", "UI Components", - "Link Toolbar", - ], + "Link Toolbar" + ] }, - title: "Adding Link Toolbar Buttons", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", + "title": "Adding Link Toolbar Buttons", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" }, - readme: - 'In this example, we add a button to the Link Toolbar which opens a browser alert.\n\n**Try it out:** Hover the link open the Link Toolbar, and click the new "Open Alert" button!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)', + "readme": "In this example, we add a button to the Link Toolbar which opens a browser alert.\n\n**Try it out:** Hover the link open the Link Toolbar, and click the new \"Open Alert\" button!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)" }, { - projectSlug: "advanced-tables-2", - fullSlug: "ui-components/advanced-tables-2", - pathFromRoot: "examples/03-ui-components/17-advanced-tables-2", - config: { - playground: true, - docs: true, - author: "must", - tags: [ + "projectSlug": "advanced-tables-2", + "fullSlug": "ui-components/advanced-tables-2", + "pathFromRoot": "examples/03-ui-components/17-advanced-tables-2", + "config": { + "playground": true, + "docs": true, + "author": "must", + "tags": [ "Intermediate", "UI Components", "Tables", + "Appearance & Styling" + ] + }, + "title": "Advanced Tables with Calculated Columns", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + }, + "readme": "This example demonstrates advanced table features including automatic calculations. It shows how to create a table with calculated columns that automatically update when values change.\n\n## Features\n\n- **Automatic Calculations**: Quantity × Price = Total for each row\n- **Grand Total**: Automatically calculated sum of all totals\n- **Real-time Updates**: Calculations update immediately when you change quantity or price values\n- **Split cells**: Merge and split table cells\n- **Cell background color**: Color individual cells\n- **Cell text color**: Change text color in cells\n- **Table row and column headers**: Use headers for better organization\n\n## How It Works\n\nThe example uses the `onChange` event listener to detect when table content changes. When a table is updated, it automatically:\n\n1. Extracts quantity and price values from each data row\n2. Calculates the total (quantity × price) for each row\n3. Updates the total column with the calculated values\n4. Calculates and updates the grand total\n\n## Code Highlights\n\n```tsx\n {\n const changes = getChanges();\n\n changes.forEach((change) => {\n if (change.type === \"update\" && change.block.type === \"table\") {\n const updatedRows = calculateTableTotals(change.block);\n if (updatedRows) {\n editor.updateBlock(change.block, {\n type: \"table\",\n content: {\n ...change.block.content,\n rows: updatedRows as any,\n } as any,\n });\n }\n }\n });\n }}\n>\n```\n\n**Relevant Docs:**\n\n- [Tables](/docs/features/blocks/tables)\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Events](/docs/reference/editor/events)" + } + ] + }, + "theming": { + "pathFromRoot": "examples/04-theming", + "slug": "theming", + "projects": [ + { + "projectSlug": "theming-dom-attributes", + "fullSlug": "theming/theming-dom-attributes", + "pathFromRoot": "examples/04-theming/01-theming-dom-attributes", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic", + "Blocks", + "Appearance & Styling" + ] + }, + "title": "Adding CSS Class to Blocks", + "group": { + "pathFromRoot": "examples/04-theming", + "slug": "theming" + }, + "readme": "In this example, we add a `hello-world-block` class to each block in the editor. We also create a CSS rule to add a border to all elements with that class.\n\n**Relevant Docs:**\n\n- [Adding DOM Attributes](/docs/react/styling-theming/adding-dom-attributes)" + }, + { + "projectSlug": "changing-font", + "fullSlug": "theming/changing-font", + "pathFromRoot": "examples/04-theming/02-changing-font", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic", + "Appearance & Styling" + ] + }, + "title": "Changing Editor Font", + "group": { + "pathFromRoot": "examples/04-theming", + "slug": "theming" + }, + "readme": "In this example, we override some of the default editor CSS to change font within the editor.\n\n**Relevant Docs:**\n\n- [Overriding CSS](/docs/react/styling-theming/overriding-css)" + }, + { + "projectSlug": "theming-css", + "fullSlug": "theming/theming-css", + "pathFromRoot": "examples/04-theming/03-theming-css", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic", "Appearance & Styling", - ], + "UI Components" + ] }, - title: "Advanced Tables with Calculated Columns", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", + "title": "Overriding CSS Styles", + "group": { + "pathFromRoot": "examples/04-theming", + "slug": "theming" }, - readme: - 'This example demonstrates advanced table features including automatic calculations. It shows how to create a table with calculated columns that automatically update when values change.\n\n## Features\n\n- **Automatic Calculations**: Quantity × Price = Total for each row\n- **Grand Total**: Automatically calculated sum of all totals\n- **Real-time Updates**: Calculations update immediately when you change quantity or price values\n- **Split cells**: Merge and split table cells\n- **Cell background color**: Color individual cells\n- **Cell text color**: Change text color in cells\n- **Table row and column headers**: Use headers for better organization\n\n## How It Works\n\nThe example uses the `onChange` event listener to detect when table content changes. When a table is updated, it automatically:\n\n1. Extracts quantity and price values from each data row\n2. Calculates the total (quantity × price) for each row\n3. Updates the total column with the calculated values\n4. Calculates and updates the grand total\n\n## Code Highlights\n\n```tsx\n {\n const changes = getChanges();\n\n changes.forEach((change) => {\n if (change.type === "update" && change.block.type === "table") {\n const updatedRows = calculateTableTotals(change.block);\n if (updatedRows) {\n editor.updateBlock(change.block, {\n type: "table",\n content: {\n ...change.block.content,\n rows: updatedRows as any,\n } as any,\n });\n }\n }\n });\n }}\n>\n```\n\n**Relevant Docs:**\n\n- [Tables](/docs/features/blocks/tables)\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Events](/docs/reference/editor/events)', + "readme": "In this example, we override some of the default editor CSS to make the editor text and hovered Slash Menu items blue.\n\n**Relevant Docs:**\n\n- [Overriding CSS](/docs/react/styling-theming/overriding-css)" }, - ], - }, - theming: { - pathFromRoot: "examples/04-theming", - slug: "theming", - projects: [ - { - projectSlug: "theming-dom-attributes", - fullSlug: "theming/theming-dom-attributes", - pathFromRoot: "examples/04-theming/01-theming-dom-attributes", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic", "Blocks", "Appearance & Styling"], - }, - title: "Adding CSS Class to Blocks", - group: { - pathFromRoot: "examples/04-theming", - slug: "theming", - }, - readme: - "In this example, we add a `hello-world-block` class to each block in the editor. We also create a CSS rule to add a border to all elements with that class.\n\n**Relevant Docs:**\n\n- [Adding DOM Attributes](/docs/react/styling-theming/adding-dom-attributes)", - }, - { - projectSlug: "changing-font", - fullSlug: "theming/changing-font", - pathFromRoot: "examples/04-theming/02-changing-font", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic", "Appearance & Styling"], - }, - title: "Changing Editor Font", - group: { - pathFromRoot: "examples/04-theming", - slug: "theming", - }, - readme: - "In this example, we override some of the default editor CSS to change font within the editor.\n\n**Relevant Docs:**\n\n- [Overriding CSS](/docs/react/styling-theming/overriding-css)", - }, - { - projectSlug: "theming-css", - fullSlug: "theming/theming-css", - pathFromRoot: "examples/04-theming/03-theming-css", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic", "Appearance & Styling", "UI Components"], - }, - title: "Overriding CSS Styles", - group: { - pathFromRoot: "examples/04-theming", - slug: "theming", - }, - readme: - "In this example, we override some of the default editor CSS to make the editor text and hovered Slash Menu items blue.\n\n**Relevant Docs:**\n\n- [Overriding CSS](/docs/react/styling-theming/overriding-css)", - }, - { - projectSlug: "theming-css-variables", - fullSlug: "theming/theming-css-variables", - pathFromRoot: "examples/04-theming/04-theming-css-variables", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic", "Appearance & Styling", "UI Components"], - }, - title: "Overriding Theme CSS Variables", - group: { - pathFromRoot: "examples/04-theming", - slug: "theming", - }, - readme: - "In this example, we override the editor's default theme CSS variables to create a red theme for both light and dark modes.\n\n**Relevant Docs:**\n\n- [Theme CSS Variables](/docs/react/styling-theming/themes#css-variables)", - }, - { - projectSlug: "theming-css-variables-code", - fullSlug: "theming/theming-css-variables-code", - pathFromRoot: "examples/04-theming/05-theming-css-variables-code", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic", "Appearance & Styling", "UI Components"], - }, - title: "Changing Themes Through Code", - group: { - pathFromRoot: "examples/04-theming", - slug: "theming", - }, - readme: - "In this example, we use the `BlockNoteView` component's `theme` props to create a red theme for both light and dark modes.\n\n**Relevant Docs:**\n\n- [Changing CSS Variables Through Code](/docs/react/styling-theming/themes#programmatic-configuration)", - }, - { - projectSlug: "code-block", - fullSlug: "theming/code-block", - pathFromRoot: "examples/04-theming/06-code-block", - config: { - playground: true, - docs: true, - author: "nperez0111", - tags: ["Basic"], - dependencies: { - "@blocknote/code-block": "latest", - } as any, + { + "projectSlug": "theming-css-variables", + "fullSlug": "theming/theming-css-variables", + "pathFromRoot": "examples/04-theming/04-theming-css-variables", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic", + "Appearance & Styling", + "UI Components" + ] }, - title: "Code Block Syntax Highlighting", - group: { - pathFromRoot: "examples/04-theming", - slug: "theming", + "title": "Overriding Theme CSS Variables", + "group": { + "pathFromRoot": "examples/04-theming", + "slug": "theming" }, - readme: - "To enable code block syntax highlighting, you can extend the editor's default schema with a new `codeBlock`, which you can pass options into when creating. By passing the default options from `@blocknote/code-block`, you can enable syntax highlighting. This is excluded by default to reduce bundle size.\n\n**Relevant Docs:**\n\n- [Code Block Syntax Highlighting](/docs/features/blocks/code-blocks)\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Custom Schema](/docs/features/custom-schemas)", + "readme": "In this example, we override the editor's default theme CSS variables to create a red theme for both light and dark modes.\n\n**Relevant Docs:**\n\n- [Theme CSS Variables](/docs/react/styling-theming/themes#css-variables)" }, { - projectSlug: "custom-code-block", - fullSlug: "theming/custom-code-block", - pathFromRoot: "examples/04-theming/07-custom-code-block", - config: { - playground: true, - docs: true, - author: "nperez0111", - tags: ["Basic"], - dependencies: { + "projectSlug": "theming-css-variables-code", + "fullSlug": "theming/theming-css-variables-code", + "pathFromRoot": "examples/04-theming/05-theming-css-variables-code", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic", + "Appearance & Styling", + "UI Components" + ] + }, + "title": "Changing Themes Through Code", + "group": { + "pathFromRoot": "examples/04-theming", + "slug": "theming" + }, + "readme": "In this example, we use the `BlockNoteView` component's `theme` props to create a red theme for both light and dark modes.\n\n**Relevant Docs:**\n\n- [Changing CSS Variables Through Code](/docs/react/styling-theming/themes#programmatic-configuration)" + }, + { + "projectSlug": "code-block", + "fullSlug": "theming/code-block", + "pathFromRoot": "examples/04-theming/06-code-block", + "config": { + "playground": true, + "docs": true, + "author": "nperez0111", + "tags": [ + "Basic" + ], + "dependencies": { + "@blocknote/code-block": "latest" + } as any + }, + "title": "Code Block Syntax Highlighting", + "group": { + "pathFromRoot": "examples/04-theming", + "slug": "theming" + }, + "readme": "To enable code block syntax highlighting, you can extend the editor's default schema with a new `codeBlock`, which you can pass options into when creating. By passing the default options from `@blocknote/code-block`, you can enable syntax highlighting. This is excluded by default to reduce bundle size.\n\n**Relevant Docs:**\n\n- [Code Block Syntax Highlighting](/docs/features/blocks/code-blocks)\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Custom Schema](/docs/features/custom-schemas)" + }, + { + "projectSlug": "custom-code-block", + "fullSlug": "theming/custom-code-block", + "pathFromRoot": "examples/04-theming/07-custom-code-block", + "config": { + "playground": true, + "docs": true, + "author": "nperez0111", + "tags": [ + "Basic" + ], + "dependencies": { "@blocknote/code-block": "latest", "@shikijs/types": "^3.2.1", "@shikijs/core": "^3.2.1", "@shikijs/engine-javascript": "^3.2.1", "@shikijs/langs-precompiled": "^3.2.1", - "@shikijs/themes": "^3.2.1", - } as any, + "@shikijs/themes": "^3.2.1" + } as any }, - title: "Custom Code Block Theme & Language", - group: { - pathFromRoot: "examples/04-theming", - slug: "theming", + "title": "Custom Code Block Theme & Language", + "group": { + "pathFromRoot": "examples/04-theming", + "slug": "theming" }, - readme: - "To configure a code block highlighting theme and language, you can extend the editor's default schema with a new `codeBlock`, which you can pass options into when creating. You can then use a shiki highlighter to add custom syntax highlighting.\n\nFirst use the [shiki-codegen](https://shiki.style/packages/codegen) CLI to create a `shiki.bundle.ts` file. You can then pass this file into the `codeBlock` options when creating it.\n\n**Relevant Docs:**\n\n- [Code Blocks](/docs/features/blocks/code-blocks)\n- [shiki-codegen](https://shiki.style/packages/codegen)\n- [Custom Schema](/docs/features/custom-schemas)", - }, - ], + "readme": "To configure a code block highlighting theme and language, you can extend the editor's default schema with a new `codeBlock`, which you can pass options into when creating. You can then use a shiki highlighter to add custom syntax highlighting.\n\nFirst use the [shiki-codegen](https://shiki.style/packages/codegen) CLI to create a `shiki.bundle.ts` file. You can then pass this file into the `codeBlock` options when creating it.\n\n**Relevant Docs:**\n\n- [Code Blocks](/docs/features/blocks/code-blocks)\n- [shiki-codegen](https://shiki.style/packages/codegen)\n- [Custom Schema](/docs/features/custom-schemas)" + } + ] }, - interoperability: { - pathFromRoot: "examples/05-interoperability", - slug: "interoperability", - projects: [ - { - projectSlug: "converting-blocks-to-html", - fullSlug: "interoperability/converting-blocks-to-html", - pathFromRoot: - "examples/05-interoperability/01-converting-blocks-to-html", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic", "Blocks", "Import/Export"], - }, - title: "Converting Blocks to HTML", - group: { - pathFromRoot: "examples/05-interoperability", - slug: "interoperability", - }, - readme: - "This example exports the current document (all blocks) as HTML and displays it below the editor.\n\n**Try it out:** Edit the document to see the HTML representation!\n\n**Relevant Docs:**\n\n- [Converting Blocks to HTML](/docs/features/export/html)", - }, - { - projectSlug: "converting-blocks-from-html", - fullSlug: "interoperability/converting-blocks-from-html", - pathFromRoot: - "examples/05-interoperability/02-converting-blocks-from-html", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic", "Blocks", "Import/Export"], - }, - title: "Parsing HTML to Blocks", - group: { - pathFromRoot: "examples/05-interoperability", - slug: "interoperability", - }, - readme: - "This example shows how you can convert HTML content to a BlockNote document.\n\nNote that the editor itself is locked for editing by setting `editable` to `false`.\n\n**Try it out:** Edit the HTML in the textarea to see the BlockNote document update!\n\n**Relevant Docs:**\n\n- [Parsing HTML to Blocks](/docs/features/import/html)", - }, - { - projectSlug: "converting-blocks-to-md", - fullSlug: "interoperability/converting-blocks-to-md", - pathFromRoot: "examples/05-interoperability/03-converting-blocks-to-md", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Basic", "Blocks", "Import/Export"], - }, - title: "Converting Blocks to Markdown", - group: { - pathFromRoot: "examples/05-interoperability", - slug: "interoperability", - }, - readme: - "This example exports the current document (all blocks) as Markdown and displays it below the editor.\n\n**Try it out:** Edit the document to see the Markdown representation!\n\n**Relevant Docs:**\n\n- [Converting Blocks to Markdown](/docs/features/export/markdown)", - }, - { - projectSlug: "converting-blocks-from-md", - fullSlug: "interoperability/converting-blocks-from-md", - pathFromRoot: - "examples/05-interoperability/04-converting-blocks-from-md", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Basic", "Blocks", "Import/Export"], - }, - title: "Parsing Markdown to Blocks", - group: { - pathFromRoot: "examples/05-interoperability", - slug: "interoperability", - }, - readme: - "This example shows how you can convert HTML content to a BlockNote document.\n\nNote that the editor itself is locked for editing by setting `editable` to `false`.\n\n**Try it out:** Edit the Markdown in the textarea to see the BlockNote document update!\n\n**Relevant Docs:**\n\n- [Parsing Markdown to Blocks](/docs/features/import/markdown)", - }, - { - projectSlug: "converting-blocks-to-pdf", - fullSlug: "interoperability/converting-blocks-to-pdf", - pathFromRoot: - "examples/05-interoperability/05-converting-blocks-to-pdf", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Interoperability"], - dependencies: { + "interoperability": { + "pathFromRoot": "examples/05-interoperability", + "slug": "interoperability", + "projects": [ + { + "projectSlug": "converting-blocks-to-html", + "fullSlug": "interoperability/converting-blocks-to-html", + "pathFromRoot": "examples/05-interoperability/01-converting-blocks-to-html", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic", + "Blocks", + "Import/Export" + ] + }, + "title": "Converting Blocks to HTML", + "group": { + "pathFromRoot": "examples/05-interoperability", + "slug": "interoperability" + }, + "readme": "This example exports the current document (all blocks) as HTML and displays it below the editor.\n\n**Try it out:** Edit the document to see the HTML representation!\n\n**Relevant Docs:**\n\n- [Converting Blocks to HTML](/docs/features/export/html)" + }, + { + "projectSlug": "converting-blocks-from-html", + "fullSlug": "interoperability/converting-blocks-from-html", + "pathFromRoot": "examples/05-interoperability/02-converting-blocks-from-html", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic", + "Blocks", + "Import/Export" + ] + }, + "title": "Parsing HTML to Blocks", + "group": { + "pathFromRoot": "examples/05-interoperability", + "slug": "interoperability" + }, + "readme": "This example shows how you can convert HTML content to a BlockNote document.\n\nNote that the editor itself is locked for editing by setting `editable` to `false`.\n\n**Try it out:** Edit the HTML in the textarea to see the BlockNote document update!\n\n**Relevant Docs:**\n\n- [Parsing HTML to Blocks](/docs/features/import/html)" + }, + { + "projectSlug": "converting-blocks-to-md", + "fullSlug": "interoperability/converting-blocks-to-md", + "pathFromRoot": "examples/05-interoperability/03-converting-blocks-to-md", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Basic", + "Blocks", + "Import/Export" + ] + }, + "title": "Converting Blocks to Markdown", + "group": { + "pathFromRoot": "examples/05-interoperability", + "slug": "interoperability" + }, + "readme": "This example exports the current document (all blocks) as Markdown and displays it below the editor.\n\n**Try it out:** Edit the document to see the Markdown representation!\n\n**Relevant Docs:**\n\n- [Converting Blocks to Markdown](/docs/features/export/markdown)" + }, + { + "projectSlug": "converting-blocks-from-md", + "fullSlug": "interoperability/converting-blocks-from-md", + "pathFromRoot": "examples/05-interoperability/04-converting-blocks-from-md", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Basic", + "Blocks", + "Import/Export" + ] + }, + "title": "Parsing Markdown to Blocks", + "group": { + "pathFromRoot": "examples/05-interoperability", + "slug": "interoperability" + }, + "readme": "This example shows how you can convert HTML content to a BlockNote document.\n\nNote that the editor itself is locked for editing by setting `editable` to `false`.\n\n**Try it out:** Edit the Markdown in the textarea to see the BlockNote document update!\n\n**Relevant Docs:**\n\n- [Parsing Markdown to Blocks](/docs/features/import/markdown)" + }, + { + "projectSlug": "converting-blocks-to-pdf", + "fullSlug": "interoperability/converting-blocks-to-pdf", + "pathFromRoot": "examples/05-interoperability/05-converting-blocks-to-pdf", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Interoperability" + ], + "dependencies": { "@blocknote/xl-pdf-exporter": "latest", "@blocknote/xl-multi-column": "latest", - "@react-pdf/renderer": "^4.3.0", + "@react-pdf/renderer": "^4.3.0" } as any, - pro: true, - }, - title: "Exporting documents to PDF", - group: { - pathFromRoot: "examples/05-interoperability", - slug: "interoperability", - }, - readme: - 'This example exports the current document (all blocks) as an PDF file and downloads it to your computer.\n\n**Try it out:** Edit the document and click "Download .pdf" at the top to download the PDF file.', - }, - { - projectSlug: "converting-blocks-to-docx", - fullSlug: "interoperability/converting-blocks-to-docx", - pathFromRoot: - "examples/05-interoperability/06-converting-blocks-to-docx", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: [""], - dependencies: { + "pro": true + }, + "title": "Exporting documents to PDF", + "group": { + "pathFromRoot": "examples/05-interoperability", + "slug": "interoperability" + }, + "readme": "This example exports the current document (all blocks) as an PDF file and downloads it to your computer.\n\n**Try it out:** Edit the document and click \"Download .pdf\" at the top to download the PDF file." + }, + { + "projectSlug": "converting-blocks-to-docx", + "fullSlug": "interoperability/converting-blocks-to-docx", + "pathFromRoot": "examples/05-interoperability/06-converting-blocks-to-docx", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "" + ], + "dependencies": { "@blocknote/xl-docx-exporter": "latest", "@blocknote/xl-multi-column": "latest", - docx: "^9.5.1", + "docx": "^9.5.1" } as any, - pro: true, - }, - title: "Exporting documents to DOCX (Office Open XML)", - group: { - pathFromRoot: "examples/05-interoperability", - slug: "interoperability", - }, - readme: - 'This example exports the current document (all blocks) as an Microsoft Word Document (DOCX) file and downloads it to your computer.\n\n**Try it out:** Edit the document and click "Download .docx" at the top to download the DOCX file.', - }, - { - projectSlug: "converting-blocks-to-odt", - fullSlug: "interoperability/converting-blocks-to-odt", - pathFromRoot: - "examples/05-interoperability/07-converting-blocks-to-odt", - config: { - playground: true, - docs: true, - author: "areknawo", - tags: [""], - dependencies: { + "pro": true + }, + "title": "Exporting documents to DOCX (Office Open XML)", + "group": { + "pathFromRoot": "examples/05-interoperability", + "slug": "interoperability" + }, + "readme": "This example exports the current document (all blocks) as an Microsoft Word Document (DOCX) file and downloads it to your computer.\n\n**Try it out:** Edit the document and click \"Download .docx\" at the top to download the DOCX file." + }, + { + "projectSlug": "converting-blocks-to-odt", + "fullSlug": "interoperability/converting-blocks-to-odt", + "pathFromRoot": "examples/05-interoperability/07-converting-blocks-to-odt", + "config": { + "playground": true, + "docs": true, + "author": "areknawo", + "tags": [ + "" + ], + "dependencies": { "@blocknote/xl-odt-exporter": "latest", - "@blocknote/xl-multi-column": "latest", + "@blocknote/xl-multi-column": "latest" } as any, - pro: true, - }, - title: "Exporting documents to ODT (Open Document Text)", - group: { - pathFromRoot: "examples/05-interoperability", - slug: "interoperability", - }, - readme: - 'This example exports the current document (all blocks) as an Open Document Text (ODT) file and downloads it to your computer.\n\n**Try it out:** Edit the document and click "Download .odt" at the top to download the ODT file.', - }, - { - projectSlug: "converting-blocks-to-react-email", - fullSlug: "interoperability/converting-blocks-to-react-email", - pathFromRoot: - "examples/05-interoperability/08-converting-blocks-to-react-email", - config: { - playground: true, - docs: true, - author: "jmarbutt", - tags: [""], - dependencies: { + "pro": true + }, + "title": "Exporting documents to ODT (Open Document Text)", + "group": { + "pathFromRoot": "examples/05-interoperability", + "slug": "interoperability" + }, + "readme": "This example exports the current document (all blocks) as an Open Document Text (ODT) file and downloads it to your computer.\n\n**Try it out:** Edit the document and click \"Download .odt\" at the top to download the ODT file." + }, + { + "projectSlug": "converting-blocks-to-react-email", + "fullSlug": "interoperability/converting-blocks-to-react-email", + "pathFromRoot": "examples/05-interoperability/08-converting-blocks-to-react-email", + "config": { + "playground": true, + "docs": true, + "author": "jmarbutt", + "tags": [ + "" + ], + "dependencies": { "@blocknote/xl-email-exporter": "latest", - "@react-email/render": "^1.1.2", + "@react-email/render": "^1.1.2" } as any, - pro: true, + "pro": true }, - title: "Exporting documents to Email (HTML)", - group: { - pathFromRoot: "examples/05-interoperability", - slug: "interoperability", + "title": "Exporting documents to Email (HTML)", + "group": { + "pathFromRoot": "examples/05-interoperability", + "slug": "interoperability" }, - readme: - 'This example exports the current document (all blocks) as an HTML file for use in emails, and downloads it to your computer.\n\n**Try it out:** Edit the document and click "Download email .html" at the top to download the HTML file.', - }, - ], + "readme": "This example exports the current document (all blocks) as an HTML file for use in emails, and downloads it to your computer.\n\n**Try it out:** Edit the document and click \"Download email .html\" at the top to download the HTML file." + } + ] }, "custom-schema": { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", - projects: [ - { - projectSlug: "alert-block", - fullSlug: "custom-schema/alert-block", - pathFromRoot: "examples/06-custom-schema/01-alert-block", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: [ + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema", + "projects": [ + { + "projectSlug": "alert-block", + "fullSlug": "custom-schema/alert-block", + "pathFromRoot": "examples/06-custom-schema/01-alert-block", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ "Intermediate", "Blocks", "Custom Schemas", "Suggestion Menus", - "Slash Menu", + "Slash Menu" ], - dependencies: { + "dependencies": { "@mantine/core": "^8.3.4", - "react-icons": "^5.2.1", - } as any, + "react-icons": "^5.2.1" + } as any }, - title: "Alert Block", - group: { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", + "title": "Alert Block", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" }, - readme: - 'In this example, we create a custom `Alert` block which is used to emphasize text.\n\n**Try it out:** Click the "!" icon to change the alert type!\n\n**Relevant Docs:**\n\n- [Custom Blocks](/docs/features/custom-schemas/custom-blocks)\n- [Editor Setup](/docs/getting-started/editor-setup)', + "readme": "In this example, we create a custom `Alert` block which is used to emphasize text.\n\n**Try it out:** Click the \"!\" icon to change the alert type!\n\n**Relevant Docs:**\n\n- [Custom Blocks](/docs/features/custom-schemas/custom-blocks)\n- [Editor Setup](/docs/getting-started/editor-setup)" }, { - projectSlug: "suggestion-menus-mentions", - fullSlug: "custom-schema/suggestion-menus-mentions", - pathFromRoot: "examples/06-custom-schema/02-suggestion-menus-mentions", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: [ + "projectSlug": "suggestion-menus-mentions", + "fullSlug": "custom-schema/suggestion-menus-mentions", + "pathFromRoot": "examples/06-custom-schema/02-suggestion-menus-mentions", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ "Intermediate", "Inline Content", "Custom Schemas", - "Suggestion Menus", - ], + "Suggestion Menus" + ] }, - title: "Mentions Menu", - group: { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", + "title": "Mentions Menu", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" }, - readme: - 'In this example, we create a custom `Mention` inline content type which is used to tag people. In addition, we create a new Suggestion Menu for mentions which opens with the "@" character.\n\n**Try it out:** Press the "@" key to open the mentions menu and insert a mention!\n\n**Relevant Docs:**\n\n- [Custom Inline Content Types](/docs/features/custom-schemas/custom-inline-content)\n- [Creating Suggestion Menus](/docs/react/components/suggestion-menus#creating-additional-suggestion-menus)\n- [Editor Setup](/docs/getting-started/editor-setup)', + "readme": "In this example, we create a custom `Mention` inline content type which is used to tag people. In addition, we create a new Suggestion Menu for mentions which opens with the \"@\" character.\n\n**Try it out:** Press the \"@\" key to open the mentions menu and insert a mention!\n\n**Relevant Docs:**\n\n- [Custom Inline Content Types](/docs/features/custom-schemas/custom-inline-content)\n- [Creating Suggestion Menus](/docs/react/components/suggestion-menus#creating-additional-suggestion-menus)\n- [Editor Setup](/docs/getting-started/editor-setup)" }, { - projectSlug: "font-style", - fullSlug: "custom-schema/font-style", - pathFromRoot: "examples/06-custom-schema/03-font-style", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: [ + "projectSlug": "font-style", + "fullSlug": "custom-schema/font-style", + "pathFromRoot": "examples/06-custom-schema/03-font-style", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ "Intermediate", "Inline Content", "Custom Schemas", - "Formatting Toolbar", + "Formatting Toolbar" ], - dependencies: { - "react-icons": "^5.2.1", - } as any, - }, - title: "Font Style", - group: { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", - }, - readme: - "In this example, we create a custom `Font` style which is used to set the `fontFamily` style. In addition, we create a Formatting Toolbar button which sets the `Font` style on the selected text.\n\n**Try it out:** Highlight some text to open the Formatting Toolbar and set the `Font` style!\n\n**Relevant Docs:**\n\n- [Custom Styles](/docs/features/custom-schemas/custom-styles)\n- [Changing the Formatting Toolbar](/docs/react/components/formatting-toolbar)\n- [Editor Setup](/docs/getting-started/editor-setup)", - }, - { - projectSlug: "pdf-file-block", - fullSlug: "custom-schema/pdf-file-block", - pathFromRoot: "examples/06-custom-schema/04-pdf-file-block", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: [ + "dependencies": { + "react-icons": "^5.2.1" + } as any + }, + "title": "Font Style", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" + }, + "readme": "In this example, we create a custom `Font` style which is used to set the `fontFamily` style. In addition, we create a Formatting Toolbar button which sets the `Font` style on the selected text.\n\n**Try it out:** Highlight some text to open the Formatting Toolbar and set the `Font` style!\n\n**Relevant Docs:**\n\n- [Custom Styles](/docs/features/custom-schemas/custom-styles)\n- [Changing the Formatting Toolbar](/docs/react/components/formatting-toolbar)\n- [Editor Setup](/docs/getting-started/editor-setup)" + }, + { + "projectSlug": "pdf-file-block", + "fullSlug": "custom-schema/pdf-file-block", + "pathFromRoot": "examples/06-custom-schema/04-pdf-file-block", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ "Intermediate", "Blocks", "Custom Schemas", "Suggestion Menus", - "Slash Menu", + "Slash Menu" ], - dependencies: { + "dependencies": { "@mantine/core": "^8.3.4", - "react-icons": "^5.2.1", + "react-icons": "^5.2.1" } as any, - pro: true, + "pro": true }, - title: "PDF Block", - group: { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", + "title": "PDF Block", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" }, - readme: - 'In this example, we create a custom `PDF` block which extends the built-in `File` block. In addition, we create a Slash Menu item which inserts a `PDF` block.\n\n**Try it out:** Press the "/" key to open the Slash Menu and insert an `PDF` block!\n\n**Relevant Docs:**\n\n- [Custom Blocks](/docs/features/custom-schemas/custom-blocks)\n- [Changing Slash Menu Items](/docs/react/components/suggestion-menus)\n- [Editor Setup](/docs/getting-started/editor-setup)', + "readme": "In this example, we create a custom `PDF` block which extends the built-in `File` block. In addition, we create a Slash Menu item which inserts a `PDF` block.\n\n**Try it out:** Press the \"/\" key to open the Slash Menu and insert an `PDF` block!\n\n**Relevant Docs:**\n\n- [Custom Blocks](/docs/features/custom-schemas/custom-blocks)\n- [Changing Slash Menu Items](/docs/react/components/suggestion-menus)\n- [Editor Setup](/docs/getting-started/editor-setup)" }, { - projectSlug: "alert-block-full-ux", - fullSlug: "custom-schema/alert-block-full-ux", - pathFromRoot: "examples/06-custom-schema/05-alert-block-full-ux", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: [ + "projectSlug": "alert-block-full-ux", + "fullSlug": "custom-schema/alert-block-full-ux", + "pathFromRoot": "examples/06-custom-schema/05-alert-block-full-ux", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ "Intermediate", "Blocks", "Custom Schemas", "Formatting Toolbar", "Suggestion Menus", - "Slash Menu", + "Slash Menu" ], - dependencies: { + "dependencies": { "@mantine/core": "^8.3.4", - "react-icons": "^5.2.1", - } as any, - }, - title: "Alert Block with Full UX", - group: { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", - }, - readme: - 'In this example, we create a custom `Alert` block which is used to emphasize text, same as in the [minimal `Alert` block example](/examples/custom-schema/alert-block). However, in this example, we also add a command to insert the block via the Slash Menu, and an entry in the Formatting Toolbar\'s Block Type Select to change the current block to an `Alert`.\n\n**Try it out:** Press the "/" key to open the Slash Menu and insert an `Alert` block! Or highlight text in a paragraph, then change the block type to an `Alert` using the Block Type Select in the Formatting Toolbar!\n\n**Relevant Docs:**\n\n- [Minimal Alert Block Example](/examples/custom-schema/alert-block)\n- [Changing Slash Menu Items](/docs/react/components/suggestion-menus)\n- [Changing Block Type Select Items](/docs/react/components/formatting-toolbar)\n- [Custom Blocks](/docs/features/custom-schemas/custom-blocks)\n- [Editor Setup](/docs/getting-started/editor-setup)', - }, - { - projectSlug: "toggleable-blocks", - fullSlug: "custom-schema/toggleable-blocks", - pathFromRoot: "examples/06-custom-schema/06-toggleable-blocks", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic"], - }, - title: "Toggleable Custom Blocks", - group: { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", - }, - readme: - "This example shows how to create custom blocks with a toggle button to show/hide their children, like with the default toggle heading and list item blocks. This is done using the use the `ToggleWrapper` component from `@blocknote/react`.\n\n**Relevant Docs:**\n\n- [Custom Blocks](/docs/features/custom-schemas/custom-blocks)\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Default Schema](/docs/features/blocks)", - }, - { - projectSlug: "configuring-blocks", - fullSlug: "custom-schema/configuring-blocks", - pathFromRoot: "examples/06-custom-schema/07-configuring-blocks", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic"], - }, - title: "Configuring Default Blocks", - group: { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", - }, - readme: - "This example shows how you can configure the editor's default blocks. Specifically, heading blocks are made to only support levels 1-3, and cannot be toggleable.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Default Schema](/docs/foundations/schemas)\n- [Custom Schemas](/docs/features/custom-schemas)", - }, - { - projectSlug: "draggable-inline-content", - fullSlug: "custom-schema/draggable-inline-content", - pathFromRoot: "examples/06-custom-schema/draggable-inline-content", - config: { - playground: true, - docs: false, - author: "hectorzhuang", - tags: [], - }, - title: "Draggable Inline Content", - group: { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", - }, - readme: "", - }, - { - projectSlug: "react-custom-blocks", - fullSlug: "custom-schema/react-custom-blocks", - pathFromRoot: "examples/06-custom-schema/react-custom-blocks", - config: { - playground: true, - docs: false, - author: "matthewlipski", - tags: [], - }, - title: "Custom Blocks - React API", - group: { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", - }, - readme: "", - }, - { - projectSlug: "react-custom-inline-content", - fullSlug: "custom-schema/react-custom-inline-content", - pathFromRoot: "examples/06-custom-schema/react-custom-inline-content", - config: { - playground: true, - docs: false, - author: "matthewlipski", - tags: [], - }, - title: "Custom Inline Content - React API", - group: { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", - }, - readme: "", - }, - { - projectSlug: "react-custom-styles", - fullSlug: "custom-schema/react-custom-styles", - pathFromRoot: "examples/06-custom-schema/react-custom-styles", - config: { - playground: true, - docs: false, - author: "matthewlipski", - tags: [], - }, - title: "Custom Styles - React API", - group: { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", - }, - readme: "", - }, - ], + "react-icons": "^5.2.1" + } as any + }, + "title": "Alert Block with Full UX", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" + }, + "readme": "In this example, we create a custom `Alert` block which is used to emphasize text, same as in the [minimal `Alert` block example](/examples/custom-schema/alert-block). However, in this example, we also add a command to insert the block via the Slash Menu, and an entry in the Formatting Toolbar's Block Type Select to change the current block to an `Alert`.\n\n**Try it out:** Press the \"/\" key to open the Slash Menu and insert an `Alert` block! Or highlight text in a paragraph, then change the block type to an `Alert` using the Block Type Select in the Formatting Toolbar!\n\n**Relevant Docs:**\n\n- [Minimal Alert Block Example](/examples/custom-schema/alert-block)\n- [Changing Slash Menu Items](/docs/react/components/suggestion-menus)\n- [Changing Block Type Select Items](/docs/react/components/formatting-toolbar)\n- [Custom Blocks](/docs/features/custom-schemas/custom-blocks)\n- [Editor Setup](/docs/getting-started/editor-setup)" + }, + { + "projectSlug": "toggleable-blocks", + "fullSlug": "custom-schema/toggleable-blocks", + "pathFromRoot": "examples/06-custom-schema/06-toggleable-blocks", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic" + ] + }, + "title": "Toggleable Custom Blocks", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" + }, + "readme": "This example shows how to create custom blocks with a toggle button to show/hide their children, like with the default toggle heading and list item blocks. This is done using the use the `ToggleWrapper` component from `@blocknote/react`.\n\n**Relevant Docs:**\n\n- [Custom Blocks](/docs/features/custom-schemas/custom-blocks)\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Default Schema](/docs/features/blocks)" + }, + { + "projectSlug": "configuring-blocks", + "fullSlug": "custom-schema/configuring-blocks", + "pathFromRoot": "examples/06-custom-schema/07-configuring-blocks", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic" + ] + }, + "title": "Configuring Default Blocks", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" + }, + "readme": "This example shows how you can configure the editor's default blocks. Specifically, heading blocks are made to only support levels 1-3, and cannot be toggleable.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Default Schema](/docs/foundations/schemas)\n- [Custom Schemas](/docs/features/custom-schemas)" + }, + { + "projectSlug": "draggable-inline-content", + "fullSlug": "custom-schema/draggable-inline-content", + "pathFromRoot": "examples/06-custom-schema/draggable-inline-content", + "config": { + "playground": true, + "docs": false, + "author": "hectorzhuang", + "tags": [] + }, + "title": "Draggable Inline Content", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" + }, + "readme": "" + }, + { + "projectSlug": "react-custom-blocks", + "fullSlug": "custom-schema/react-custom-blocks", + "pathFromRoot": "examples/06-custom-schema/react-custom-blocks", + "config": { + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": [] + }, + "title": "Custom Blocks - React API", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" + }, + "readme": "" + }, + { + "projectSlug": "react-custom-inline-content", + "fullSlug": "custom-schema/react-custom-inline-content", + "pathFromRoot": "examples/06-custom-schema/react-custom-inline-content", + "config": { + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": [] + }, + "title": "Custom Inline Content - React API", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" + }, + "readme": "" + }, + { + "projectSlug": "react-custom-styles", + "fullSlug": "custom-schema/react-custom-styles", + "pathFromRoot": "examples/06-custom-schema/react-custom-styles", + "config": { + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": [] + }, + "title": "Custom Styles - React API", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" + }, + "readme": "" + } + ] }, - collaboration: { - pathFromRoot: "examples/07-collaboration", - slug: "collaboration", - projects: [ - { - projectSlug: "partykit", - fullSlug: "collaboration/partykit", - pathFromRoot: "examples/07-collaboration/01-partykit", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Advanced", "Saving/Loading", "Collaboration"], - dependencies: { + "collaboration": { + "pathFromRoot": "examples/07-collaboration", + "slug": "collaboration", + "projects": [ + { + "projectSlug": "partykit", + "fullSlug": "collaboration/partykit", + "pathFromRoot": "examples/07-collaboration/01-partykit", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Advanced", + "Saving/Loading", + "Collaboration" + ], + "dependencies": { "y-partykit": "^0.0.25", - yjs: "^13.6.27", - } as any, + "yjs": "^13.6.27" + } as any }, - title: "Collaborative Editing with PartyKit", - group: { - pathFromRoot: "examples/07-collaboration", - slug: "collaboration", + "title": "Collaborative Editing with PartyKit", + "group": { + "pathFromRoot": "examples/07-collaboration", + "slug": "collaboration" }, - readme: - "In this example, we use PartyKit to let multiple users collaborate on a single BlockNote document in real-time.\n\n**Try it out:** Open this page in a new browser tab or window to see it in action!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [PartyKit](/docs/features/collaboration#partykit)", + "readme": "In this example, we use PartyKit to let multiple users collaborate on a single BlockNote document in real-time.\n\n**Try it out:** Open this page in a new browser tab or window to see it in action!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [PartyKit](/docs/features/collaboration#partykit)" }, { - projectSlug: "liveblocks", - fullSlug: "collaboration/liveblocks", - pathFromRoot: "examples/07-collaboration/02-liveblocks", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Advanced", "Saving/Loading", "Collaboration"], - dependencies: { + "projectSlug": "liveblocks", + "fullSlug": "collaboration/liveblocks", + "pathFromRoot": "examples/07-collaboration/02-liveblocks", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Advanced", + "Saving/Loading", + "Collaboration" + ], + "dependencies": { "@liveblocks/client": "3.7.1-tiptap3", "@liveblocks/react": "3.7.1-tiptap3", "@liveblocks/react-blocknote": "3.7.1-tiptap3", "@liveblocks/react-tiptap": "3.7.1-tiptap3", "@liveblocks/react-ui": "3.7.1-tiptap3", - yjs: "^13.6.27", - } as any, + "yjs": "^13.6.27" + } as any }, - title: "Collaborative Editing with Liveblocks", - group: { - pathFromRoot: "examples/07-collaboration", - slug: "collaboration", + "title": "Collaborative Editing with Liveblocks", + "group": { + "pathFromRoot": "examples/07-collaboration", + "slug": "collaboration" }, - readme: - "In this example, we use\nthe [Liveblocks + BlockNote setup guide](https://liveblocks.io/docs/get-started/react-blocknote)\nto create a collaborative BlockNote editor, where multiple people can work on\nthe same document in real-time.\n\nUsers can also add comments to the documents to start threads, which are\ndisplayed next to the editor. As well as that, they can react to, reply to, and\nresolve existing comments.\n\n**Try it out:** Open this page in a new browser tab or window to see it in\naction!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Liveblocks](/docs/features/collaboration#liveblocks)\n\n**From Liveblocks Website:**\n\n- [Get Started with BlockNote](https://liveblocks.io/docs/get-started/react-blocknote)\n- [Ready Made Features](https://liveblocks.io/docs/ready-made-features/text-editor/blocknote)\n- [API Reference](https://liveblocks.io/docs/api-reference/liveblocks-react-blocknote)\n- [Advanced Example](https://liveblocks.io/examples/collaborative-text-editor/nextjs-blocknote)", + "readme": "In this example, we use\nthe [Liveblocks + BlockNote setup guide](https://liveblocks.io/docs/get-started/react-blocknote)\nto create a collaborative BlockNote editor, where multiple people can work on\nthe same document in real-time.\n\nUsers can also add comments to the documents to start threads, which are\ndisplayed next to the editor. As well as that, they can react to, reply to, and\nresolve existing comments.\n\n**Try it out:** Open this page in a new browser tab or window to see it in\naction!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Liveblocks](/docs/features/collaboration#liveblocks)\n\n**From Liveblocks Website:**\n\n- [Get Started with BlockNote](https://liveblocks.io/docs/get-started/react-blocknote)\n- [Ready Made Features](https://liveblocks.io/docs/ready-made-features/text-editor/blocknote)\n- [API Reference](https://liveblocks.io/docs/api-reference/liveblocks-react-blocknote)\n- [Advanced Example](https://liveblocks.io/examples/collaborative-text-editor/nextjs-blocknote)" }, { - projectSlug: "y-sweet", - fullSlug: "collaboration/y-sweet", - pathFromRoot: "examples/07-collaboration/03-y-sweet", - config: { - playground: true, - docs: true, - author: "jakelazaroff", - tags: ["Advanced", "Saving/Loading", "Collaboration"], - dependencies: { - "@y-sweet/react": "^0.6.3", - } as any, - }, - title: "Collaborative Editing with Y-Sweet", - group: { - pathFromRoot: "examples/07-collaboration", - slug: "collaboration", - }, - readme: - "In this example, we use Y-Sweet to let multiple users collaborate on a single BlockNote document in real-time.\n\n**Try it out:** Open the [Y-Sweet BlockNote demo](https://demos.y-sweet.dev/blocknote) in multiple browser tabs to see it in action!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Real-time collaboration](/docs/features/collaboration)\n- [Y-Sweet on Jamsocket](https://docs.jamsocket.com/y-sweet/tutorials/blocknote)", - }, - { - projectSlug: "electric-sql", - fullSlug: "collaboration/electric-sql", - pathFromRoot: "examples/07-collaboration/04-electric-sql", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Advanced", "Saving/Loading", "Collaboration"], - }, - title: "Collaborative Editing with ElectricSQL", - group: { - pathFromRoot: "examples/07-collaboration", - slug: "collaboration", - }, - readme: - "In this example, we use ElectricSQL to let multiple users collaborate on a single BlockNote document in real-time. The setup for this demo is more involved than the other collaboration examples, as it requires a running server and has a more fully-fledged UI. Therefore, the demo just uses an iframe element to show a hosted instance of the full ElectricSQL + BlockNote setup, which you can find the code for [here](https://github.com/TypeCellOS/blocknote-electric-example).\n\n**Try it out:** Open this page (or the [iframe url](https://blocknote-electric-example.vercel.app/)) in a new browser tab or window to see it in action!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Real-time collaboration](/docs/features/collaboration)\n- [ElectricSQL](https://electric-sql.com/)", - }, - { - projectSlug: "comments", - fullSlug: "collaboration/comments", - pathFromRoot: "examples/07-collaboration/05-comments", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Advanced", "Comments", "Collaboration"], - dependencies: { + "projectSlug": "y-sweet", + "fullSlug": "collaboration/y-sweet", + "pathFromRoot": "examples/07-collaboration/03-y-sweet", + "config": { + "playground": true, + "docs": true, + "author": "jakelazaroff", + "tags": [ + "Advanced", + "Saving/Loading", + "Collaboration" + ], + "dependencies": { + "@y-sweet/react": "^0.6.3" + } as any + }, + "title": "Collaborative Editing with Y-Sweet", + "group": { + "pathFromRoot": "examples/07-collaboration", + "slug": "collaboration" + }, + "readme": "In this example, we use Y-Sweet to let multiple users collaborate on a single BlockNote document in real-time.\n\n**Try it out:** Open the [Y-Sweet BlockNote demo](https://demos.y-sweet.dev/blocknote) in multiple browser tabs to see it in action!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Real-time collaboration](/docs/features/collaboration)\n- [Y-Sweet on Jamsocket](https://docs.jamsocket.com/y-sweet/tutorials/blocknote)" + }, + { + "projectSlug": "electric-sql", + "fullSlug": "collaboration/electric-sql", + "pathFromRoot": "examples/07-collaboration/04-electric-sql", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Advanced", + "Saving/Loading", + "Collaboration" + ] + }, + "title": "Collaborative Editing with ElectricSQL", + "group": { + "pathFromRoot": "examples/07-collaboration", + "slug": "collaboration" + }, + "readme": "In this example, we use ElectricSQL to let multiple users collaborate on a single BlockNote document in real-time. The setup for this demo is more involved than the other collaboration examples, as it requires a running server and has a more fully-fledged UI. Therefore, the demo just uses an iframe element to show a hosted instance of the full ElectricSQL + BlockNote setup, which you can find the code for [here](https://github.com/TypeCellOS/blocknote-electric-example).\n\n**Try it out:** Open this page (or the [iframe url](https://blocknote-electric-example.vercel.app/)) in a new browser tab or window to see it in action!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Real-time collaboration](/docs/features/collaboration)\n- [ElectricSQL](https://electric-sql.com/)" + }, + { + "projectSlug": "comments", + "fullSlug": "collaboration/comments", + "pathFromRoot": "examples/07-collaboration/05-comments", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Advanced", + "Comments", + "Collaboration" + ], + "dependencies": { "@y-sweet/react": "^0.6.3", - "@mantine/core": "^8.3.4", - } as any, + "@mantine/core": "^8.3.4" + } as any }, - title: "Comments & Threads", - group: { - pathFromRoot: "examples/07-collaboration", - slug: "collaboration", + "title": "Comments & Threads", + "group": { + "pathFromRoot": "examples/07-collaboration", + "slug": "collaboration" }, - readme: - 'In this example, you can add comments to the document while collaborating with others. You can also pick user accounts with different permissions, as well as react to, reply to, and resolve existing comments. The comments are displayed floating next to the text they refer to, and appear when selecting said text.\n\n**Try it out:** Click the "Add comment" button in the [Formatting Toolbar](/docs/react/components/formatting-toolbar) to add a comment!\n\n**Relevant Docs:**\n\n- [Comments](/docs/features/collaboration/comments)\n- [Real-time collaboration](/docs/features/collaboration)\n- [Y-Sweet on Jamsocket](https://docs.jamsocket.com/y-sweet/tutorials/blocknote)\n- [Editor Setup](/docs/getting-started/editor-setup)', + "readme": "In this example, you can add comments to the document while collaborating with others. You can also pick user accounts with different permissions, as well as react to, reply to, and resolve existing comments. The comments are displayed floating next to the text they refer to, and appear when selecting said text.\n\n**Try it out:** Click the \"Add comment\" button in the [Formatting Toolbar](/docs/react/components/formatting-toolbar) to add a comment!\n\n**Relevant Docs:**\n\n- [Comments](/docs/features/collaboration/comments)\n- [Real-time collaboration](/docs/features/collaboration)\n- [Y-Sweet on Jamsocket](https://docs.jamsocket.com/y-sweet/tutorials/blocknote)\n- [Editor Setup](/docs/getting-started/editor-setup)" }, { - projectSlug: "comments-with-sidebar", - fullSlug: "collaboration/comments-with-sidebar", - pathFromRoot: "examples/07-collaboration/06-comments-with-sidebar", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Advanced", "Comments", "Collaboration"], - dependencies: { + "projectSlug": "comments-with-sidebar", + "fullSlug": "collaboration/comments-with-sidebar", + "pathFromRoot": "examples/07-collaboration/06-comments-with-sidebar", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Advanced", + "Comments", + "Collaboration" + ], + "dependencies": { "y-partykit": "^0.0.25", - yjs: "^13.6.27", - "@mantine/core": "^8.3.4", - } as any, - }, - title: "Threads Sidebar", - group: { - pathFromRoot: "examples/07-collaboration", - slug: "collaboration", - }, - readme: - 'In this example, you can add comments to the document while collaborating with others. You can also pick user accounts with different permissions, as well as react to, reply to, and resolve existing comments. The comments are displayed floating next to the text they refer to, and appear when selecting said text. The comments are shown in a separate sidebar using the `ThreadsSidebar` component.\n\n**Try it out:** Click the "Add comment" button in\nthe [Formatting Toolbar](/docs/react/components/formatting-toolbar) to add a\ncomment!\n\n**Relevant Docs:**\n\n- [Comments Sidebar](/docs/features/collaboration/comments#sidebar-view)\n- [Real-time collaboration](/docs/features/collaboration)\n- [Y-Sweet on Jamsocket](https://docs.jamsocket.com/y-sweet/tutorials/blocknote)\n- [Editor Setup](/docs/getting-started/editor-setup)', - }, - { - projectSlug: "ghost-writer", - fullSlug: "collaboration/ghost-writer", - pathFromRoot: "examples/07-collaboration/07-ghost-writer", - config: { - playground: true, - docs: false, - author: "nperez0111", - tags: ["Advanced", "Development", "Collaboration"], - dependencies: { + "yjs": "^13.6.27", + "@mantine/core": "^8.3.4" + } as any + }, + "title": "Threads Sidebar", + "group": { + "pathFromRoot": "examples/07-collaboration", + "slug": "collaboration" + }, + "readme": "In this example, you can add comments to the document while collaborating with others. You can also pick user accounts with different permissions, as well as react to, reply to, and resolve existing comments. The comments are displayed floating next to the text they refer to, and appear when selecting said text. The comments are shown in a separate sidebar using the `ThreadsSidebar` component.\n\n**Try it out:** Click the \"Add comment\" button in\nthe [Formatting Toolbar](/docs/react/components/formatting-toolbar) to add a\ncomment!\n\n**Relevant Docs:**\n\n- [Comments Sidebar](/docs/features/collaboration/comments#sidebar-view)\n- [Real-time collaboration](/docs/features/collaboration)\n- [Y-Sweet on Jamsocket](https://docs.jamsocket.com/y-sweet/tutorials/blocknote)\n- [Editor Setup](/docs/getting-started/editor-setup)" + }, + { + "projectSlug": "ghost-writer", + "fullSlug": "collaboration/ghost-writer", + "pathFromRoot": "examples/07-collaboration/07-ghost-writer", + "config": { + "playground": true, + "docs": false, + "author": "nperez0111", + "tags": [ + "Advanced", + "Development", + "Collaboration" + ], + "dependencies": { "y-partykit": "^0.0.25", - yjs: "^13.6.27", - } as any, + "yjs": "^13.6.27" + } as any }, - title: "Ghost Writer", - group: { - pathFromRoot: "examples/07-collaboration", - slug: "collaboration", + "title": "Ghost Writer", + "group": { + "pathFromRoot": "examples/07-collaboration", + "slug": "collaboration" }, - readme: - "In this example, we use a local Yjs document to store the document state, and have a ghost writer that edits the document in real-time.\n\n**Try it out:** Open this page in a new browser tab or window to see it in action!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)", + "readme": "In this example, we use a local Yjs document to store the document state, and have a ghost writer that edits the document in real-time.\n\n**Try it out:** Open this page in a new browser tab or window to see it in action!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)" }, { - projectSlug: "forking", - fullSlug: "collaboration/forking", - pathFromRoot: "examples/07-collaboration/08-forking", - config: { - playground: true, - docs: false, - author: "nperez0111", - tags: ["Advanced", "Development", "Collaboration"], - dependencies: { + "projectSlug": "forking", + "fullSlug": "collaboration/forking", + "pathFromRoot": "examples/07-collaboration/08-forking", + "config": { + "playground": true, + "docs": false, + "author": "nperez0111", + "tags": [ + "Advanced", + "Development", + "Collaboration" + ], + "dependencies": { "y-partykit": "^0.0.25", - yjs: "^13.6.27", - } as any, + "yjs": "^13.6.27" + } as any }, - title: "Collaborative Editing with Forking", - group: { - pathFromRoot: "examples/07-collaboration", - slug: "collaboration", + "title": "Collaborative Editing with Forking", + "group": { + "pathFromRoot": "examples/07-collaboration", + "slug": "collaboration" }, - readme: - "In this example, we can fork a document and edit it independently of other collaborators. Then, we can choose to merge the changes back into the original document, or discard the changes.\n\n**Try it out:** Open this page in a new browser tab or window to see it in action!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)", - }, - ], + "readme": "In this example, we can fork a document and edit it independently of other collaborators. Then, we can choose to merge the changes back into the original document, or discard the changes.\n\n**Try it out:** Open this page in a new browser tab or window to see it in action!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)" + } + ] }, - extensions: { - pathFromRoot: "examples/08-extensions", - slug: "extensions", - projects: [ - { - projectSlug: "tiptap-arrow-conversion", - fullSlug: "extensions/tiptap-arrow-conversion", - pathFromRoot: "examples/08-extensions/01-tiptap-arrow-conversion", - config: { - playground: true, - docs: true, - author: "komsenapati", - tags: ["Extension"], - pro: true, - dependencies: { - "@tiptap/core": "^3.11.0", - } as any, - }, - title: "TipTap extension (arrow InputRule)", - group: { - pathFromRoot: "examples/08-extensions", - slug: "extensions", - }, - readme: - "This example shows how to set up a BlockNote editor with a TipTap extension that registers an InputRule to convert `->` into `→`.\n\n**Try it out:** Type `->` anywhere in the editor and see how it's automatically converted to a single arrow unicode character.", - }, - ], + "extensions": { + "pathFromRoot": "examples/08-extensions", + "slug": "extensions", + "projects": [ + { + "projectSlug": "tiptap-arrow-conversion", + "fullSlug": "extensions/tiptap-arrow-conversion", + "pathFromRoot": "examples/08-extensions/01-tiptap-arrow-conversion", + "config": { + "playground": true, + "docs": true, + "author": "komsenapati", + "tags": [ + "Extension" + ], + "pro": true, + "dependencies": { + "@tiptap/core": "^3.11.0" + } as any + }, + "title": "TipTap extension (arrow InputRule)", + "group": { + "pathFromRoot": "examples/08-extensions", + "slug": "extensions" + }, + "readme": "This example shows how to set up a BlockNote editor with a TipTap extension that registers an InputRule to convert `->` into `→`.\n\n**Try it out:** Type `->` anywhere in the editor and see how it's automatically converted to a single arrow unicode character." + } + ] }, - ai: { - pathFromRoot: "examples/09-ai", - slug: "ai", - projects: [ - { - projectSlug: "minimal", - fullSlug: "ai/minimal", - pathFromRoot: "examples/09-ai/01-minimal", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["AI", "llm"], - dependencies: { + "ai": { + "pathFromRoot": "examples/09-ai", + "slug": "ai", + "projects": [ + { + "projectSlug": "minimal", + "fullSlug": "ai/minimal", + "pathFromRoot": "examples/09-ai/01-minimal", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "AI", + "llm" + ], + "dependencies": { "@blocknote/xl-ai": "latest", "@mantine/core": "^8.3.4", - ai: "^5.0.102", - } as any, - }, - title: "Rich Text editor AI integration", - group: { - pathFromRoot: "examples/09-ai", - slug: "ai", - }, - readme: - "This example shows the minimal setup to add AI integration to your BlockNote rich text editor.\n\nSelect some text and click the AI (stars) button, or type `/ai` anywhere in the editor to access AI functionality.\n\n**Relevant Docs:**\n\n- [Getting Stared with BlockNote AI](/docs/features/ai/getting-started)\n- [Changing the Formatting Toolbar](/docs/react/components/formatting-toolbar)\n- [Changing Slash Menu Items](/docs/react/components/suggestion-menus)", - }, - { - projectSlug: "playground", - fullSlug: "ai/playground", - pathFromRoot: "examples/09-ai/02-playground", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["AI", "llm"], - dependencies: { + "ai": "^5.0.102" + } as any + }, + "title": "Rich Text Editor AI Integration", + "group": { + "pathFromRoot": "examples/09-ai", + "slug": "ai" + }, + "readme": "This example shows the minimal setup to add AI integration to your BlockNote rich text editor.\n\nSelect some text and click the AI (stars) button, or type `/ai` anywhere in the editor to access AI functionality.\n\n**Relevant Docs:**\n\n- [Getting Started with BlockNote AI](/docs/features/ai/getting-started)\n- [Changing the Formatting Toolbar](/docs/react/components/formatting-toolbar)\n- [Changing Slash Menu Items](/docs/react/components/suggestion-menus)" + }, + { + "projectSlug": "playground", + "fullSlug": "ai/playground", + "pathFromRoot": "examples/09-ai/02-playground", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "AI", + "llm" + ], + "dependencies": { "@blocknote/xl-ai": "latest", "@mantine/core": "^8.3.4", - ai: "^5.0.102", - } as any, - }, - title: "AI Playground", - group: { - pathFromRoot: "examples/09-ai", - slug: "ai", - }, - readme: - "Explore different LLM models integrated with BlockNote in the AI Playground.\n\nChange the configuration, then highlight some text to access the AI menu, or type `/ai` anywhere in the editor.\n\n**Relevant Docs:**\n\n- [Getting Stared with BlockNote AI](/docs/features/ai/getting-started)\n- [BlockNote AI Reference](/docs/features/ai/reference)\n- [Changing the Formatting Toolbar](/docs/react/components/formatting-toolbar)\n- [Changing Slash Menu Items](/docs/react/components/suggestion-menus)", - }, - { - projectSlug: "custom-ai-menu-items", - fullSlug: "ai/custom-ai-menu-items", - pathFromRoot: "examples/09-ai/03-custom-ai-menu-items", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["AI", "llm"], - dependencies: { + "ai": "^5.0.102" + } as any + }, + "title": "AI Playground", + "group": { + "pathFromRoot": "examples/09-ai", + "slug": "ai" + }, + "readme": "Explore different LLM models integrated with BlockNote in the AI Playground.\n\nChange the configuration, then highlight some text to access the AI menu, or type `/ai` anywhere in the editor.\n\n**Relevant Docs:**\n\n- [Getting Stared with BlockNote AI](/docs/features/ai/getting-started)\n- [BlockNote AI Reference](/docs/features/ai/reference)\n- [Changing the Formatting Toolbar](/docs/react/components/formatting-toolbar)\n- [Changing Slash Menu Items](/docs/react/components/suggestion-menus)" + }, + { + "projectSlug": "custom-ai-menu-items", + "fullSlug": "ai/custom-ai-menu-items", + "pathFromRoot": "examples/09-ai/03-custom-ai-menu-items", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "AI", + "llm" + ], + "dependencies": { "@blocknote/xl-ai": "latest", "@mantine/core": "^8.3.4", - ai: "^5.0.102", - "react-icons": "^5.2.1", - } as any, - }, - title: "Adding AI Menu Items", - group: { - pathFromRoot: "examples/09-ai", - slug: "ai", - }, - readme: - 'In this example, we add two items to the AI Menu. The first prompts the AI to make the selected text more casual, and can be found by selecting some text and click the AI (stars) button. The second prompts the AI to give ideas on related topics to extend the document with, and can be found by clicking the "Ask AI" Slash Menu item.\n\nSelect some text and click the AI (stars) button, or type `/ai` anywhere in the editor to access AI functionality.\n\n**Relevant Docs:**\n\n- [Getting Stared with BlockNote AI](/docs/features/ai/getting-started)\n- [Custom AI Menu Items](/docs/features/ai/custom-commands)', - }, - { - projectSlug: "with-collaboration", - fullSlug: "ai/with-collaboration", - pathFromRoot: "examples/09-ai/04-with-collaboration", - config: { - playground: true, - docs: false, - author: "nperez0111", - tags: ["AI", "llm"], - dependencies: { + "ai": "^5.0.102", + "react-icons": "^5.2.1" + } as any + }, + "title": "Adding AI Menu Items", + "group": { + "pathFromRoot": "examples/09-ai", + "slug": "ai" + }, + "readme": "In this example, we add two items to the AI Menu. The first prompts the AI to make the selected text more casual, and can be found by selecting some text and click the AI (stars) button. The second prompts the AI to give ideas on related topics to extend the document with, and can be found by clicking the \"Ask AI\" Slash Menu item.\n\nSelect some text and click the AI (stars) button, or type `/ai` anywhere in the editor to access AI functionality.\n\n**Relevant Docs:**\n\n- [Getting Stared with BlockNote AI](/docs/features/ai/getting-started)\n- [Custom AI Menu Items](/docs/features/ai/custom-commands)" + }, + { + "projectSlug": "with-collaboration", + "fullSlug": "ai/with-collaboration", + "pathFromRoot": "examples/09-ai/04-with-collaboration", + "config": { + "playground": true, + "docs": false, + "author": "nperez0111", + "tags": [ + "AI", + "llm" + ], + "dependencies": { "@blocknote/xl-ai": "latest", "@mantine/core": "^8.3.4", - ai: "^5.0.102", + "ai": "^5.0.102", "y-partykit": "^0.0.25", - yjs: "^13.6.27", - } as any, - }, - title: "AI + Ghost Writer", - group: { - pathFromRoot: "examples/09-ai", - slug: "ai", - }, - readme: - "This example combines the AI extension with the ghost writer example to show how to use the AI extension in a collaborative environment.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Changing the Formatting Toolbar](/docs/react/components/formatting-toolbar#changing-the-formatting-toolbar)\n- [Changing Slash Menu Items](/docs/react/components/suggestion-menus#changing-slash-menu-items)\n- [Getting Stared with BlockNote AI](/docs/features/ai/setup)", - }, - { - projectSlug: "manual-execution", - fullSlug: "ai/manual-execution", - pathFromRoot: "examples/09-ai/05-manual-execution", - config: { - playground: true, - docs: false, - author: "yousefed", - tags: ["AI", "llm"], - dependencies: { + "yjs": "^13.6.27" + } as any + }, + "title": "AI + Ghost Writer", + "group": { + "pathFromRoot": "examples/09-ai", + "slug": "ai" + }, + "readme": "This example combines the AI extension with the ghost writer example to show how to use the AI extension in a collaborative environment.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Changing the Formatting Toolbar](/docs/react/components/formatting-toolbar#changing-the-formatting-toolbar)\n- [Changing Slash Menu Items](/docs/react/components/suggestion-menus#changing-slash-menu-items)\n- [Getting Stared with BlockNote AI](/docs/features/ai/setup)" + }, + { + "projectSlug": "manual-execution", + "fullSlug": "ai/manual-execution", + "pathFromRoot": "examples/09-ai/05-manual-execution", + "config": { + "playground": true, + "docs": false, + "author": "yousefed", + "tags": [ + "AI", + "llm" + ], + "dependencies": { "@blocknote/xl-ai": "latest", "@mantine/core": "^8.3.4", - ai: "^5.0.102", + "ai": "^5.0.102", "y-partykit": "^0.0.25", - yjs: "^13.6.27", - } as any, - }, - title: "AI manual execution", - group: { - pathFromRoot: "examples/09-ai", - slug: "ai", - }, - readme: - "Instead of calling AI models directly, this example shows how you can use an existing stream of responses and apply them to the editor.", - }, - { - projectSlug: "client-side-transport", - fullSlug: "ai/client-side-transport", - pathFromRoot: "examples/09-ai/06-client-side-transport", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["AI", "llm"], - dependencies: { + "yjs": "^13.6.27" + } as any + }, + "title": "AI manual execution", + "group": { + "pathFromRoot": "examples/09-ai", + "slug": "ai" + }, + "readme": "Instead of calling AI models directly, this example shows how you can use an existing stream of responses and apply them to the editor." + }, + { + "projectSlug": "client-side-transport", + "fullSlug": "ai/client-side-transport", + "pathFromRoot": "examples/09-ai/06-client-side-transport", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "AI", + "llm" + ], + "dependencies": { "@ai-sdk/groq": "^2.0.16", "@blocknote/xl-ai": "latest", "@mantine/core": "^8.3.4", - ai: "^5.0.102", - } as any, - }, - title: "AI Integration with ClientSideTransport", - group: { - pathFromRoot: "examples/09-ai", - slug: "ai", - }, - readme: - "The standard setup is to have BlockNote AI call your server, which then calls an LLM of your choice. In this example, we show how you can use the `ClientSideTransport` to make calls directly to your LLM provider.\n\nTo hide API keys of our LLM provider, we do still route calls through a proxy server using `fetchViaProxy` (this is optional).", - }, - { - projectSlug: "server-persistence", - fullSlug: "ai/server-persistence", - pathFromRoot: "examples/09-ai/07-server-persistence", - config: { - playground: true, - docs: false, - author: "yousefed", - tags: ["AI", "llm"], - dependencies: { + "ai": "^5.0.102" + } as any + }, + "title": "AI Integration with ClientSideTransport", + "group": { + "pathFromRoot": "examples/09-ai", + "slug": "ai" + }, + "readme": "The standard setup is to have BlockNote AI call your server, which then calls an LLM of your choice. In this example, we show how you can use the `ClientSideTransport` to make calls directly to your LLM provider.\n\nTo hide API keys of our LLM provider, we do still route calls through a proxy server using `fetchViaProxy` (this is optional)." + }, + { + "projectSlug": "server-persistence", + "fullSlug": "ai/server-persistence", + "pathFromRoot": "examples/09-ai/07-server-persistence", + "config": { + "playground": true, + "docs": false, + "author": "yousefed", + "tags": [ + "AI", + "llm" + ], + "dependencies": { "@blocknote/xl-ai": "latest", "@mantine/core": "^8.3.4", - ai: "^5.0.102", - } as any, + "ai": "^5.0.102" + } as any + }, + "title": "AI Integration with server LLM message persistence", + "group": { + "pathFromRoot": "examples/09-ai", + "slug": "ai" + }, + "readme": "This example shows how to setup to add AI integration while handling the LLM calls (in this case, using the Vercel AI SDK) on your server, using a custom executor.\n\nInstead of sending all messages, these are kept server-side and we only submit the latest message." + }, + { + "projectSlug": "autocomplete", + "fullSlug": "ai/autocomplete", + "pathFromRoot": "examples/09-ai/08-autocomplete", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "AI", + "autocomplete" + ], + "dependencies": { + "@blocknote/xl-ai": "latest", + "@mantine/core": "^8.3.4", + "ai": "^5.0.102" + } as any }, - title: "AI Integration with server LLM message persistence", - group: { - pathFromRoot: "examples/09-ai", - slug: "ai", + "title": "AI Autocomplete", + "group": { + "pathFromRoot": "examples/09-ai", + "slug": "ai" }, - readme: - "This example shows how to setup to add AI integration while handling the LLM calls (in this case, using the Vercel AI SDK) on your server, using a custom executor.\n\nInstead of sending all messages, these are kept server-side and we only submit the latest message.", - }, - ], + "readme": "This example demonstrates the AI autocomplete feature in BlockNote. As you type, the editor will automatically suggest completions using AI.\n\nPress **Tab** to accept a suggestion or **Escape** to dismiss it.\n\n**Relevant Docs:**\n\n- [AI Autocomplete](/docs/features/ai/autocomplete)\n- [AI Getting Started](/docs/features/ai/getting-started)" + } + ] }, "vanilla-js": { - pathFromRoot: "examples/vanilla-js", - slug: "vanilla-js", - projects: [ - { - projectSlug: "react-vanilla-custom-blocks", - fullSlug: "vanilla-js/react-vanilla-custom-blocks", - pathFromRoot: "examples/vanilla-js/react-vanilla-custom-blocks", - config: { - playground: true, - docs: false, - author: "matthewlipski", - tags: [], - }, - title: "Custom Blocks - Vanilla JS API", - group: { - pathFromRoot: "examples/vanilla-js", - slug: "vanilla-js", - }, - readme: "", - }, - { - projectSlug: "react-vanilla-custom-inline-content", - fullSlug: "vanilla-js/react-vanilla-custom-inline-content", - pathFromRoot: "examples/vanilla-js/react-vanilla-custom-inline-content", - config: { - playground: true, - docs: false, - author: "matthewlipski", - tags: [], - }, - title: "Custom Inline Content - Vanilla JS API", - group: { - pathFromRoot: "examples/vanilla-js", - slug: "vanilla-js", - }, - readme: "", - }, - { - projectSlug: "react-vanilla-custom-styles", - fullSlug: "vanilla-js/react-vanilla-custom-styles", - pathFromRoot: "examples/vanilla-js/react-vanilla-custom-styles", - config: { - playground: true, - docs: false, - author: "matthewlipski", - tags: [], - }, - title: "Custom Styles - Vanilla JS API", - group: { - pathFromRoot: "examples/vanilla-js", - slug: "vanilla-js", - }, - readme: "", - }, - ], - }, -}; + "pathFromRoot": "examples/vanilla-js", + "slug": "vanilla-js", + "projects": [ + { + "projectSlug": "react-vanilla-custom-blocks", + "fullSlug": "vanilla-js/react-vanilla-custom-blocks", + "pathFromRoot": "examples/vanilla-js/react-vanilla-custom-blocks", + "config": { + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": [] + }, + "title": "Custom Blocks - Vanilla JS API", + "group": { + "pathFromRoot": "examples/vanilla-js", + "slug": "vanilla-js" + }, + "readme": "" + }, + { + "projectSlug": "react-vanilla-custom-inline-content", + "fullSlug": "vanilla-js/react-vanilla-custom-inline-content", + "pathFromRoot": "examples/vanilla-js/react-vanilla-custom-inline-content", + "config": { + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": [] + }, + "title": "Custom Inline Content - Vanilla JS API", + "group": { + "pathFromRoot": "examples/vanilla-js", + "slug": "vanilla-js" + }, + "readme": "" + }, + { + "projectSlug": "react-vanilla-custom-styles", + "fullSlug": "vanilla-js/react-vanilla-custom-styles", + "pathFromRoot": "examples/vanilla-js/react-vanilla-custom-styles", + "config": { + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": [] + }, + "title": "Custom Styles - Vanilla JS API", + "group": { + "pathFromRoot": "examples/vanilla-js", + "slug": "vanilla-js" + }, + "readme": "" + } + ] + } +}; \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aec701f239..7587ed0f71 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4193,6 +4193,58 @@ importers: specifier: ^5.4.20 version: 5.4.20(@types/node@24.8.1)(lightningcss@1.30.1)(terser@5.44.1) + examples/09-ai/08-autocomplete: + dependencies: + '@blocknote/ariakit': + specifier: latest + version: link:../../../packages/ariakit + '@blocknote/core': + specifier: latest + version: link:../../../packages/core + '@blocknote/mantine': + specifier: latest + version: link:../../../packages/mantine + '@blocknote/react': + specifier: latest + version: link:../../../packages/react + '@blocknote/shadcn': + specifier: latest + version: link:../../../packages/shadcn + '@blocknote/xl-ai': + specifier: latest + version: link:../../../packages/xl-ai + '@mantine/core': + specifier: ^8.3.4 + version: 8.3.4(@mantine/hooks@8.3.4(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mantine/hooks': + specifier: ^8.3.4 + version: 8.3.4(react@19.2.0) + '@mantine/utils': + specifier: ^6.0.22 + version: 6.0.22(react@19.2.0) + ai: + specifier: ^5.0.102 + version: 5.0.102(zod@4.1.12) + react: + specifier: ^19.2.0 + version: 19.2.0 + react-dom: + specifier: ^19.2.0 + version: 19.2.0(react@19.2.0) + devDependencies: + '@types/react': + specifier: ^19.2.2 + version: 19.2.2 + '@types/react-dom': + specifier: ^19.2.1 + version: 19.2.2(@types/react@19.2.2) + '@vitejs/plugin-react': + specifier: ^4.7.0 + version: 4.7.0(vite@5.4.20(@types/node@24.8.1)(lightningcss@1.30.1)(terser@5.44.1)) + vite: + specifier: ^5.4.20 + version: 5.4.20(@types/node@24.8.1)(lightningcss@1.30.1)(terser@5.44.1) + examples/vanilla-js/react-vanilla-custom-blocks: dependencies: '@blocknote/ariakit':