diff --git a/examples/09-ai/01-minimal/src/App.tsx b/examples/09-ai/01-minimal/src/App.tsx
index e78d2b0af8..8c0ce912a2 100644
--- a/examples/09-ai/01-minimal/src/App.tsx
+++ b/examples/09-ai/01-minimal/src/App.tsx
@@ -77,6 +77,7 @@ export default function App() {
// We're disabling some default UI elements
formattingToolbar={false}
slashMenu={false}
+ style={{ paddingBottom: "300px" }}
>
{/* Add the AI Command menu to the editor */}
diff --git a/examples/09-ai/02-playground/src/App.tsx b/examples/09-ai/02-playground/src/App.tsx
index a180746de5..17ceacd057 100644
--- a/examples/09-ai/02-playground/src/App.tsx
+++ b/examples/09-ai/02-playground/src/App.tsx
@@ -122,6 +122,7 @@ export default function App() {
editor={editor}
formattingToolbar={false}
slashMenu={false}
+ style={{ paddingBottom: "300px" }}
>
{/* Add the AI Command menu to the editor */}
diff --git a/examples/09-ai/03-custom-ai-menu-items/src/App.tsx b/examples/09-ai/03-custom-ai-menu-items/src/App.tsx
index 19005a17c3..876e371b4d 100644
--- a/examples/09-ai/03-custom-ai-menu-items/src/App.tsx
+++ b/examples/09-ai/03-custom-ai-menu-items/src/App.tsx
@@ -78,6 +78,7 @@ export default function App() {
editor={editor}
formattingToolbar={false}
slashMenu={false}
+ style={{ paddingBottom: "300px" }}
>
{/* Creates a new AIMenu with the default items,
as well as our custom ones. */}
diff --git a/packages/xl-ai/src/AIExtension.ts b/packages/xl-ai/src/AIExtension.ts
index 0e4df22066..6258cdc38f 100644
--- a/packages/xl-ai/src/AIExtension.ts
+++ b/packages/xl-ai/src/AIExtension.ts
@@ -2,6 +2,7 @@ import { Chat } from "@ai-sdk/react";
import {
BlockNoteEditor,
BlockNoteExtension,
+ getNodeById,
UnreachableCaseError,
} from "@blocknote/core";
import {
@@ -65,6 +66,9 @@ export class AIExtension extends BlockNoteExtension {
}
| undefined;
+ private scrollInProgress = false;
+ private autoScroll = false;
+
public static key(): string {
return "ai";
}
@@ -134,6 +138,31 @@ export class AIExtension extends BlockNoteExtension {
options.agentCursor || { name: "AI", color: "#8bc6ff" },
),
);
+
+ // Listens for `scroll` and `scrollend` events to see if a new scroll was
+ // started before an existing one ended. This is the most reliable way we
+ // have of checking if a scroll event was caused by the user and not by
+ // `scrollIntoView`, as the events are otherwise indistinguishable. If a
+ // scroll was started before an existing one finished (meaning the user has
+ // scrolled), auto scrolling is disabled.
+ document.addEventListener(
+ "scroll",
+ () => {
+ if (this.scrollInProgress) {
+ this.autoScroll = false;
+ }
+
+ this.scrollInProgress = true;
+ },
+ true,
+ );
+ document.addEventListener(
+ "scrollend",
+ () => {
+ this.scrollInProgress = false;
+ },
+ true,
+ );
}
/**
@@ -148,6 +177,12 @@ export class AIExtension extends BlockNoteExtension {
status: "user-input",
},
});
+
+ // Scrolls to the block when the menu opens.
+ const blockElement = this.editor.domElement?.querySelector(
+ `[data-node-type="blockContainer"][data-id="${blockID}"]`,
+ );
+ blockElement?.scrollIntoView({ block: "center" });
}
/**
@@ -371,7 +406,7 @@ export class AIExtension extends BlockNoteExtension {
useSelection: opts.useSelection,
deleteEmptyCursorBlock: opts.deleteEmptyCursorBlock,
streamToolsProvider: opts.streamToolsProvider,
- onBlockUpdated: (blockId: string) => {
+ onBlockUpdated: (blockId) => {
// NOTE: does this setState with an anon object trigger unnecessary re-renders?
this._store.setState({
aiMenuState: {
@@ -379,6 +414,34 @@ export class AIExtension extends BlockNoteExtension {
status: "ai-writing",
},
});
+
+ // Scrolls to the block being edited by the AI while auto scrolling is
+ // enabled.
+ if (!this.autoScroll) {
+ return;
+ }
+
+ const aiMenuState = this._store.getState().aiMenuState;
+ const aiMenuOpenState =
+ aiMenuState === "closed" ? undefined : aiMenuState;
+ if (!aiMenuOpenState || aiMenuOpenState.status !== "ai-writing") {
+ return;
+ }
+
+ const nodeInfo = getNodeById(
+ aiMenuOpenState.blockId,
+ this.editor.prosemirrorState.doc,
+ );
+ if (!nodeInfo) {
+ return;
+ }
+
+ const blockElement = this.editor.prosemirrorView.domAtPos(
+ nodeInfo.posBeforeNode + 1,
+ );
+ (blockElement.node as HTMLElement).scrollIntoView({
+ block: "center",
+ });
},
});
@@ -387,6 +450,7 @@ export class AIExtension extends BlockNoteExtension {
sender,
chatRequestOptions: opts.chatRequestOptions,
onStart: () => {
+ this.autoScroll = true;
this.setAIResponseStatus("ai-writing");
},
});