Skip to content
1 change: 1 addition & 0 deletions examples/09-ai/01-minimal/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 */}
<AIMenuController />
Expand Down
1 change: 1 addition & 0 deletions examples/09-ai/02-playground/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 */}
<AIMenuController />
Expand Down
1 change: 1 addition & 0 deletions examples/09-ai/03-custom-ai-menu-items/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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. */}
Expand Down
55 changes: 55 additions & 0 deletions packages/xl-ai/src/AIExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ export class AIExtension extends BlockNoteExtension {
}
| undefined;

private scrollInProgress = false;
private autoScroll = false;

public static key(): string {
return "ai";
}
Expand Down Expand Up @@ -134,6 +137,51 @@ export class AIExtension extends BlockNoteExtension {
options.agentCursor || { name: "AI", color: "#8bc6ff" },
),
);

// Scrolls to the block being edited by the AI while auto scrolling is
// enabled.
this.editor.onCreate(() => {
this.editor.onChange(() => {
if (!this.autoScroll) {
return;
}

const aiMenuState = this._store.getState().aiMenuState;
const aiMenuNonErrorState =
aiMenuState === "closed" ? undefined : aiMenuState;
if (aiMenuNonErrorState?.status === "ai-writing") {
const blockElement = this.editor.domElement?.querySelector(
`[data-node-type="blockContainer"][data-id="${aiMenuNonErrorState.blockId}"]`,
);
blockElement?.scrollIntoView({ block: "center" });
}
});
});
Comment on lines 17 to 29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why wrap in an editor.onCreate? I'm actually uncertain that onCreate gets invoked right now, does it? Is it on mount?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The underlying TipTap editor is undefined when the extension constructor is run, and so an error gets thrown attempting to call tiptapEditor.on("update", ...) (within editor.onChange(...)). Wrapping it in editor.onCreate fixes this. Maybe onMount would be better than onCreate? Any way that that we can ensure the TipTap editor is already initialized.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to fix this then, because I don't think this is a great pattern


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

/**
Expand All @@ -148,6 +196,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" });
}

/**
Expand Down Expand Up @@ -387,6 +441,7 @@ export class AIExtension extends BlockNoteExtension {
sender,
chatRequestOptions: opts.chatRequestOptions,
onStart: () => {
this.autoScroll = true;
this.setAIResponseStatus("ai-writing");
},
});
Expand Down
Loading