@@ -2,6 +2,7 @@ import { Chat } from "@ai-sdk/react";
22import {
33 BlockNoteEditor ,
44 BlockNoteExtension ,
5+ getNodeById ,
56 UnreachableCaseError ,
67} from "@blocknote/core" ;
78import {
@@ -65,6 +66,9 @@ export class AIExtension extends BlockNoteExtension {
6566 }
6667 | undefined ;
6768
69+ private scrollInProgress = false ;
70+ private autoScroll = false ;
71+
6872 public static key ( ) : string {
6973 return "ai" ;
7074 }
@@ -134,6 +138,31 @@ export class AIExtension extends BlockNoteExtension {
134138 options . agentCursor || { name : "AI" , color : "#8bc6ff" } ,
135139 ) ,
136140 ) ;
141+
142+ // Listens for `scroll` and `scrollend` events to see if a new scroll was
143+ // started before an existing one ended. This is the most reliable way we
144+ // have of checking if a scroll event was caused by the user and not by
145+ // `scrollIntoView`, as the events are otherwise indistinguishable. If a
146+ // scroll was started before an existing one finished (meaning the user has
147+ // scrolled), auto scrolling is disabled.
148+ document . addEventListener (
149+ "scroll" ,
150+ ( ) => {
151+ if ( this . scrollInProgress ) {
152+ this . autoScroll = false ;
153+ }
154+
155+ this . scrollInProgress = true ;
156+ } ,
157+ true ,
158+ ) ;
159+ document . addEventListener (
160+ "scrollend" ,
161+ ( ) => {
162+ this . scrollInProgress = false ;
163+ } ,
164+ true ,
165+ ) ;
137166 }
138167
139168 /**
@@ -148,6 +177,12 @@ export class AIExtension extends BlockNoteExtension {
148177 status : "user-input" ,
149178 } ,
150179 } ) ;
180+
181+ // Scrolls to the block when the menu opens.
182+ const blockElement = this . editor . domElement ?. querySelector (
183+ `[data-node-type="blockContainer"][data-id="${ blockID } "]` ,
184+ ) ;
185+ blockElement ?. scrollIntoView ( { block : "center" } ) ;
151186 }
152187
153188 /**
@@ -371,14 +406,42 @@ export class AIExtension extends BlockNoteExtension {
371406 useSelection : opts . useSelection ,
372407 deleteEmptyCursorBlock : opts . deleteEmptyCursorBlock ,
373408 streamToolsProvider : opts . streamToolsProvider ,
374- onBlockUpdated : ( blockId : string ) => {
409+ onBlockUpdated : ( blockId ) => {
375410 // NOTE: does this setState with an anon object trigger unnecessary re-renders?
376411 this . _store . setState ( {
377412 aiMenuState : {
378413 blockId,
379414 status : "ai-writing" ,
380415 } ,
381416 } ) ;
417+
418+ // Scrolls to the block being edited by the AI while auto scrolling is
419+ // enabled.
420+ if ( ! this . autoScroll ) {
421+ return ;
422+ }
423+
424+ const aiMenuState = this . _store . getState ( ) . aiMenuState ;
425+ const aiMenuOpenState =
426+ aiMenuState === "closed" ? undefined : aiMenuState ;
427+ if ( ! aiMenuOpenState || aiMenuOpenState . status !== "ai-writing" ) {
428+ return ;
429+ }
430+
431+ const nodeInfo = getNodeById (
432+ aiMenuOpenState . blockId ,
433+ this . editor . prosemirrorState . doc ,
434+ ) ;
435+ if ( ! nodeInfo ) {
436+ return ;
437+ }
438+
439+ const blockElement = this . editor . prosemirrorView . domAtPos (
440+ nodeInfo . posBeforeNode + 1 ,
441+ ) ;
442+ ( blockElement . node as HTMLElement ) . scrollIntoView ( {
443+ block : "center" ,
444+ } ) ;
382445 } ,
383446 } ) ;
384447
@@ -387,6 +450,7 @@ export class AIExtension extends BlockNoteExtension {
387450 sender,
388451 chatRequestOptions : opts . chatRequestOptions ,
389452 onStart : ( ) => {
453+ this . autoScroll = true ;
390454 this . setAIResponseStatus ( "ai-writing" ) ;
391455 } ,
392456 } ) ;
0 commit comments