@@ -2,6 +2,7 @@ import { Chat } from "@ai-sdk/react";
2
2
import {
3
3
BlockNoteEditor ,
4
4
BlockNoteExtension ,
5
+ getNodeById ,
5
6
UnreachableCaseError ,
6
7
} from "@blocknote/core" ;
7
8
import {
@@ -65,6 +66,9 @@ export class AIExtension extends BlockNoteExtension {
65
66
}
66
67
| undefined ;
67
68
69
+ private scrollInProgress = false ;
70
+ private autoScroll = false ;
71
+
68
72
public static key ( ) : string {
69
73
return "ai" ;
70
74
}
@@ -134,6 +138,31 @@ export class AIExtension extends BlockNoteExtension {
134
138
options . agentCursor || { name : "AI" , color : "#8bc6ff" } ,
135
139
) ,
136
140
) ;
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
+ ) ;
137
166
}
138
167
139
168
/**
@@ -148,6 +177,12 @@ export class AIExtension extends BlockNoteExtension {
148
177
status : "user-input" ,
149
178
} ,
150
179
} ) ;
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" } ) ;
151
186
}
152
187
153
188
/**
@@ -371,14 +406,42 @@ export class AIExtension extends BlockNoteExtension {
371
406
useSelection : opts . useSelection ,
372
407
deleteEmptyCursorBlock : opts . deleteEmptyCursorBlock ,
373
408
streamToolsProvider : opts . streamToolsProvider ,
374
- onBlockUpdated : ( blockId : string ) => {
409
+ onBlockUpdated : ( blockId ) => {
375
410
// NOTE: does this setState with an anon object trigger unnecessary re-renders?
376
411
this . _store . setState ( {
377
412
aiMenuState : {
378
413
blockId,
379
414
status : "ai-writing" ,
380
415
} ,
381
416
} ) ;
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
+ } ) ;
382
445
} ,
383
446
} ) ;
384
447
@@ -387,6 +450,7 @@ export class AIExtension extends BlockNoteExtension {
387
450
sender,
388
451
chatRequestOptions : opts . chatRequestOptions ,
389
452
onStart : ( ) => {
453
+ this . autoScroll = true ;
390
454
this . setAIResponseStatus ( "ai-writing" ) ;
391
455
} ,
392
456
} ) ;
0 commit comments