@blocknote/xl-ai 0.42.3 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,15 @@
1
1
  import { Chat } from "@ai-sdk/react";
2
2
  import {
3
- BlockNoteEditor,
4
- BlockNoteExtension,
3
+ createExtension,
4
+ createStore,
5
+ ExtensionOptions,
5
6
  getNodeById,
6
7
  UnreachableCaseError,
7
8
  } from "@blocknote/core";
9
+ import {
10
+ ForkYDocExtension,
11
+ ShowSelectionExtension,
12
+ } from "@blocknote/core/extensions";
8
13
  import {
9
14
  applySuggestions,
10
15
  revertSuggestions,
@@ -14,7 +19,6 @@ import { UIMessage } from "ai";
14
19
  import { Fragment, Slice } from "prosemirror-model";
15
20
  import { Plugin, PluginKey } from "prosemirror-state";
16
21
  import { fixTablesKey } from "prosemirror-tables";
17
- import { createStore, StoreApi } from "zustand/vanilla";
18
22
 
19
23
  import {
20
24
  aiDocumentFormats,
@@ -25,18 +29,7 @@ import {
25
29
  import { createAgentCursorPlugin } from "./plugins/AgentCursorPlugin.js";
26
30
  import { AIRequestHelpers, InvokeAIOptions } from "./types.js";
27
31
 
28
- type ReadonlyStoreApi<T> = Pick<
29
- StoreApi<T>,
30
- "getState" | "getInitialState" | "subscribe"
31
- >;
32
-
33
32
  type AIPluginState = {
34
- // zustand design considerations:
35
- // - moved this to a nested object to have better typescript typing
36
- // - if we'd do this without a nested object, then we could easily set "wrong" values,
37
- // because "setState" takes a partial object (unless the second parameter "replace" = true),
38
- // and thus we'd lose typescript's typing help
39
-
40
33
  aiMenuState:
41
34
  | ({
42
35
  /**
@@ -58,228 +51,211 @@ type AIPluginState = {
58
51
 
59
52
  const PLUGIN_KEY = new PluginKey(`blocknote-ai-plugin`);
60
53
 
61
- export class AIExtension extends BlockNoteExtension {
62
- private chatSession:
63
- | {
64
- previousRequestOptions: InvokeAIOptions;
65
- chat: Chat<UIMessage>;
66
- }
67
- | undefined;
68
-
69
- private scrollInProgress = false;
70
- private autoScroll = false;
71
-
72
- public static key(): string {
73
- return "ai";
74
- }
75
-
76
- // internal store including setters
77
- private readonly _store = createStore<AIPluginState>()((_set) => ({
78
- aiMenuState: "closed",
79
- }));
80
-
81
- /**
82
- * Returns a read-only zustand store with the state of the AI Extension
83
- */
84
- public get store() {
85
- // externally, don't expose setters (if we want to do so, expose `set` methods seperately)
86
- return this._store as ReadonlyStoreApi<AIPluginState>;
87
- }
88
-
89
- /**
90
- * Returns a zustand store with the global configuration of the AI Extension.
91
- * These options are used by default across all LLM calls when calling {@link executeLLMRequest}
92
- */
93
- public readonly options: ReturnType<
94
- ReturnType<typeof createStore<AIRequestHelpers>>
95
- >;
96
-
97
- /**
98
- * @internal use `createAIExtension` instead
99
- */
100
- constructor(
101
- public readonly editor: BlockNoteEditor<any, any, any>,
102
- options: AIRequestHelpers & {
103
- /**
104
- * The name and color of the agent cursor
105
- *
106
- * @default { name: "AI", color: "#8bc6ff" }
107
- */
108
- agentCursor?: { name: string; color: string };
109
- },
110
- ) {
111
- super();
112
-
113
- this.options = createStore<AIRequestHelpers>()((_set) => ({
114
- ...options,
115
- }));
116
-
117
- this.addProsemirrorPlugin(
118
- new Plugin({
119
- key: PLUGIN_KEY,
120
- filterTransaction: (tr) => {
121
- const menuState = this.store.getState().aiMenuState;
122
-
123
- if (menuState !== "closed" && menuState.status === "ai-writing") {
124
- if (tr.getMeta(fixTablesKey)?.fixTables) {
125
- // the fixtables plugin causes the steps between of the AI Agent to become invalid
126
- // so we need to prevent it from running
127
- // (we might need to filter out other / or maybe any transactions during the writing phase)
128
- return false;
129
- }
130
- }
131
- return true;
132
- },
133
- }),
134
- );
135
- this.addProsemirrorPlugin(suggestChanges());
136
- this.addProsemirrorPlugin(
137
- createAgentCursorPlugin(
138
- options.agentCursor || { name: "AI", color: "#8bc6ff" },
139
- ),
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;
54
+ export const AIExtension = createExtension(
55
+ ({
56
+ editor,
57
+ options: editorOptions,
58
+ }: ExtensionOptions<
59
+ | (AIRequestHelpers & {
60
+ /**
61
+ * The name and color of the agent cursor
62
+ *
63
+ * @default { name: "AI", color: "#8bc6ff" }
64
+ */
65
+ agentCursor?: { name: string; color: string };
66
+ })
67
+ | undefined
68
+ >) => {
69
+ // TODO should we really expose it like this?
70
+ const options = createStore<AIRequestHelpers>(editorOptions ?? {});
71
+ const store = createStore<AIPluginState>({
72
+ aiMenuState: "closed",
73
+ });
74
+ let chatSession:
75
+ | {
76
+ previousRequestOptions: InvokeAIOptions;
77
+ chat: Chat<UIMessage>;
153
78
  }
79
+ | undefined;
80
+ let autoScroll = false;
81
+
82
+ return {
83
+ key: "ai",
84
+ options,
85
+ store,
86
+ mount({ signal }: { signal: AbortSignal }) {
87
+ let scrollInProgress = false;
88
+ // Listens for `scroll` and `scrollend` events to see if a new scroll was
89
+ // started before an existing one ended. This is the most reliable way we
90
+ // have of checking if a scroll event was caused by the user and not by
91
+ // `scrollIntoView`, as the events are otherwise indistinguishable. If a
92
+ // scroll was started before an existing one finished (meaning the user has
93
+ // scrolled), auto scrolling is disabled.
94
+ document.addEventListener(
95
+ "scroll",
96
+ () => {
97
+ if (scrollInProgress) {
98
+ autoScroll = false;
99
+ }
154
100
 
155
- this.scrollInProgress = true;
101
+ scrollInProgress = true;
102
+ },
103
+ {
104
+ capture: true,
105
+ signal,
106
+ },
107
+ );
108
+ document.addEventListener(
109
+ "scrollend",
110
+ () => {
111
+ scrollInProgress = false;
112
+ },
113
+ {
114
+ capture: true,
115
+ signal,
116
+ },
117
+ );
156
118
  },
157
- true,
158
- );
159
- document.addEventListener(
160
- "scrollend",
161
- () => {
162
- this.scrollInProgress = false;
119
+ prosemirrorPlugins: [
120
+ new Plugin({
121
+ key: PLUGIN_KEY,
122
+ filterTransaction: (tr) => {
123
+ const menuState = store.state.aiMenuState;
124
+
125
+ if (menuState !== "closed" && menuState.status === "ai-writing") {
126
+ if (tr.getMeta(fixTablesKey)?.fixTables) {
127
+ // the fixtables plugin causes the steps between of the AI Agent to become invalid
128
+ // so we need to prevent it from running
129
+ // (we might need to filter out other / or maybe any transactions during the writing phase)
130
+ return false;
131
+ }
132
+ }
133
+ return true;
134
+ },
135
+ }),
136
+ suggestChanges(),
137
+ createAgentCursorPlugin(
138
+ editorOptions?.agentCursor || { name: "AI", color: "#8bc6ff" },
139
+ ),
140
+ ],
141
+
142
+ /**
143
+ * Open the AI menu at a specific block
144
+ */
145
+ openAIMenuAtBlock(blockID: string) {
146
+ editor.getExtension(ShowSelectionExtension)?.showSelection(true);
147
+ editor.isEditable = false;
148
+ store.setState({
149
+ aiMenuState: {
150
+ blockId: blockID,
151
+ status: "user-input",
152
+ },
153
+ });
154
+
155
+ // Scrolls to the block when the menu opens.
156
+ const blockElement = editor.domElement?.querySelector(
157
+ `[data-node-type="blockContainer"][data-id="${blockID}"]`,
158
+ );
159
+ blockElement?.scrollIntoView({ block: "center" });
163
160
  },
164
- true,
165
- );
166
- }
167
-
168
- /**
169
- * Open the AI menu at a specific block
170
- */
171
- public openAIMenuAtBlock(blockID: string) {
172
- this.editor.setForceSelectionVisible(true);
173
- this.editor.isEditable = false;
174
- this._store.setState({
175
- aiMenuState: {
176
- blockId: blockID,
177
- status: "user-input",
161
+
162
+ /**
163
+ * Close the AI menu
164
+ */
165
+ closeAIMenu() {
166
+ store.setState({
167
+ aiMenuState: "closed",
168
+ });
169
+ chatSession = undefined;
170
+ editor.getExtension(ShowSelectionExtension)?.showSelection(false);
171
+ editor.isEditable = true;
172
+ editor.focus();
178
173
  },
179
- });
180
174
 
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" });
186
- }
187
-
188
- /**
189
- * Close the AI menu
190
- */
191
- public closeAIMenu() {
192
- this._store.setState({
193
- aiMenuState: "closed",
194
- });
195
- this.chatSession = undefined;
196
- this.editor.setForceSelectionVisible(false);
197
- this.editor.isEditable = true;
198
- this.editor.focus();
199
- }
200
-
201
- /**
202
- * Accept the changes made by the LLM
203
- */
204
- public acceptChanges() {
205
- // This is slightly convoluted, to try to maintain the undo history as much as possible
206
- // The idea is that the LLM call has appended a number of updates to the document, moving the document from state `A` to state `C`
207
- // But we want the undo history to skip all of the intermediate states and go straight from `C` to `A`
208
- // To do this, we capture the document state `C` (post-LLM call), and then reject the suggestions to recover the original document state `A`
209
- // Then we create an intermediate state `B` that captures the diff between `A` and `C`
210
- // Then we apply the suggestions to `B` to get the final state `C`
211
- // This causes the undo history to skip `B` and go straight from `C` back to `A`
212
-
213
- // Capture the document state `C'` (post-LLM call with all suggestions still in the document)
214
- const markedUpDocument = this.editor.prosemirrorState.doc;
215
-
216
- // revert the suggestions to get back to the original document state `A`
217
- this.editor.exec((state, dispatch) => {
218
- return revertSuggestions(state, (tr) => {
219
- dispatch?.(tr.setMeta("addToHistory", false));
220
- });
221
- });
175
+ /**
176
+ * Accept the changes made by the LLM
177
+ */
178
+ acceptChanges() {
179
+ // This is slightly convoluted, to try to maintain the undo history as much as possible
180
+ // The idea is that the LLM call has appended a number of updates to the document, moving the document from state `A` to state `C`
181
+ // But we want the undo history to skip all of the intermediate states and go straight from `C` to `A`
182
+ // To do this, we capture the document state `C` (post-LLM call), and then reject the suggestions to recover the original document state `A`
183
+ // Then we create an intermediate state `B` that captures the diff between `A` and `C`
184
+ // Then we apply the suggestions to `B` to get the final state `C`
185
+ // This causes the undo history to skip `B` and go straight from `C` back to `A`
186
+
187
+ // Capture the document state `C'` (post-LLM call with all suggestions still in the document)
188
+ const markedUpDocument = editor.prosemirrorState.doc;
189
+
190
+ // revert the suggestions to get back to the original document state `A`
191
+ editor.exec((state, dispatch) => {
192
+ return revertSuggestions(state, (tr) => {
193
+ dispatch?.(tr.setMeta("addToHistory", false));
194
+ });
195
+ });
222
196
 
223
- // Create an intermediate state `B` that captures the diff between the original document and the marked up document
224
- this.editor.exec((state, dispatch) => {
225
- const tr = state.tr;
226
- tr.replace(
227
- 0,
228
- tr.doc.content.size,
229
- new Slice(Fragment.from(markedUpDocument), 0, 0),
230
- );
231
- const nextState = state.apply(tr);
232
- // Apply the suggestions to the intermediate state `B` to get the final state `C`
233
- return applySuggestions(nextState, (resultTr) => {
234
- dispatch?.(
197
+ // Create an intermediate state `B` that captures the diff between the original document and the marked up document
198
+ editor.exec((state, dispatch) => {
199
+ const tr = state.tr;
235
200
  tr.replace(
236
201
  0,
237
202
  tr.doc.content.size,
238
- new Slice(Fragment.from(resultTr.doc), 0, 0),
239
- ),
240
- );
241
- });
242
- });
203
+ new Slice(Fragment.from(markedUpDocument), 0, 0),
204
+ );
205
+ const nextState = state.apply(tr);
206
+ // Apply the suggestions to the intermediate state `B` to get the final state `C`
207
+ return applySuggestions(nextState, (resultTr) => {
208
+ dispatch?.(
209
+ tr.replace(
210
+ 0,
211
+ tr.doc.content.size,
212
+ new Slice(Fragment.from(resultTr.doc), 0, 0),
213
+ ),
214
+ );
215
+ });
216
+ });
243
217
 
244
- // If in collaboration mode, merge the changes back into the original yDoc
245
- this.editor.forkYDocPlugin?.merge({ keepChanges: true });
246
-
247
- this.closeAIMenu();
248
- }
249
-
250
- /**
251
- * Reject the changes made by the LLM
252
- */
253
- public rejectChanges() {
254
- // Revert the suggestions to get back to the original document
255
- this.editor.exec((state, dispatch) => {
256
- return revertSuggestions(state, (tr) => {
257
- // Do so without adding to history (so the last undo step is just prior to the LLM call)
258
- dispatch?.(tr.setMeta("addToHistory", false));
259
- });
260
- });
218
+ // If in collaboration mode, merge the changes back into the original yDoc
219
+ editor.getExtension(ForkYDocExtension)?.merge({ keepChanges: true });
220
+
221
+ this.closeAIMenu();
222
+ },
261
223
 
262
- // If in collaboration mode, discard the changes and revert to the original yDoc
263
- this.editor.forkYDocPlugin?.merge({ keepChanges: false });
264
- this.closeAIMenu();
265
- }
266
-
267
- /**
268
- * Retry the previous LLM call.
269
- *
270
- * Only valid if the current status is "error"
271
- */
272
- public async retry() {
273
- const { aiMenuState } = this.store.getState();
274
- if (
275
- aiMenuState === "closed" ||
276
- aiMenuState.status !== "error" ||
277
- !this.chatSession
278
- ) {
279
- throw new Error("retry() is only valid when a previous response failed");
280
- }
281
-
282
- /*
224
+ /**
225
+ * Reject the changes made by the LLM
226
+ */
227
+ rejectChanges() {
228
+ // Revert the suggestions to get back to the original document
229
+ editor.exec((state, dispatch) => {
230
+ return revertSuggestions(state, (tr) => {
231
+ // Do so without adding to history (so the last undo step is just prior to the LLM call)
232
+ dispatch?.(tr.setMeta("addToHistory", false));
233
+ });
234
+ });
235
+
236
+ // If in collaboration mode, discard the changes and revert to the original yDoc
237
+ editor.getExtension(ForkYDocExtension)?.merge({ keepChanges: false });
238
+ this.closeAIMenu();
239
+ },
240
+
241
+ /**
242
+ * Retry the previous LLM call.
243
+ *
244
+ * Only valid if the current status is "error"
245
+ */
246
+ async retry() {
247
+ const { aiMenuState } = store.state;
248
+ if (
249
+ aiMenuState === "closed" ||
250
+ aiMenuState.status !== "error" ||
251
+ !chatSession
252
+ ) {
253
+ throw new Error(
254
+ "retry() is only valid when a previous response failed",
255
+ );
256
+ }
257
+
258
+ /*
283
259
  Design decisions:
284
260
  - we cannot use chat.regenerate() because the document might have been updated already by toolcalls that failed mid-way
285
261
  - we also cannot revert the document changes and use chat.regenerate(),
@@ -289,198 +265,179 @@ export class AIExtension extends BlockNoteExtension {
289
265
  when a call fails
290
266
  */
291
267
 
292
- if (this.chatSession?.chat.status === "error") {
293
- // the LLM call failed (i.e. a network error)
294
- // console.log("retry failed LLM call", this.chatSession.chat.error);
295
- return this.invokeAI({
296
- ...this.chatSession.previousRequestOptions,
297
- userPrompt: `An error occured in the previous request. Please retry to accomplish the last user prompt.`,
298
- });
299
- } else {
300
- // an error occurred while parsing / executing the previous LLM call
301
- // give the LLM a chance to fix the error
302
- // console.log("retry failed tool execution");
303
- return this.invokeAI({
304
- ...this.chatSession.previousRequestOptions,
305
- userPrompt: `An error occured while executing the previous tool call. Please retry to accomplish the last user prompt.`,
306
- });
307
- }
308
- }
309
-
310
- /**
311
- * Update the status of a call to an LLM
312
- *
313
- * @warning This method should usually only be used for advanced use-cases
314
- * if you want to implement how an LLM call is executed. Usually, you should
315
- * use {@link executeLLMRequest} instead which will handle the status updates for you.
316
- */
317
- public setAIResponseStatus(
318
- status:
319
- | "user-input"
320
- | "thinking"
321
- | "ai-writing"
322
- | "user-reviewing"
323
- | {
324
- status: "error";
325
- error: any;
326
- },
327
- ) {
328
- const aiMenuState = this.store.getState().aiMenuState;
329
- if (aiMenuState === "closed") {
330
- return;
331
- }
332
-
333
- if (status === "ai-writing") {
334
- this.editor.setForceSelectionVisible(false);
335
- }
336
-
337
- if (typeof status === "object") {
338
- if (status.status !== "error") {
339
- throw new UnreachableCaseError(status.status);
340
- }
341
- this._store.setState({
342
- aiMenuState: {
343
- status: status.status,
344
- error: status.error,
345
- blockId: aiMenuState.blockId,
346
- },
347
- });
348
- } else {
349
- this._store.setState({
350
- aiMenuState: {
351
- status: status,
352
- blockId: aiMenuState.blockId,
353
- },
354
- });
355
- }
356
- }
357
-
358
- /**
359
- * @deprecated Use {@link invokeAI} instead
360
- */
361
- public async callLLM(opts: InvokeAIOptions) {
362
- return this.invokeAI(opts);
363
- }
364
-
365
- /**
366
- * Execute a call to an LLM and apply the result to the editor
367
- */
368
- public async invokeAI(opts: InvokeAIOptions) {
369
- this.setAIResponseStatus("thinking");
370
- this.editor.forkYDocPlugin?.fork();
371
-
372
- try {
373
- if (!this.chatSession) {
374
- // note: in the current implementation opts.transport is only used when creating a new chat
375
- // (so changing transport for a subsequent call in the same chat-session is not supported)
376
- this.chatSession = {
377
- previousRequestOptions: opts,
378
- chat: new Chat<UIMessage>({
379
- sendAutomaticallyWhen: () => false,
380
- transport: opts.transport || this.options.getState().transport,
381
- }),
382
- };
383
- } else {
384
- this.chatSession.previousRequestOptions = opts;
385
- }
386
- const chat = this.chatSession.chat;
387
-
388
- // merge the global options with the local options
389
- const globalOpts = this.options.getState();
390
- opts = {
391
- ...globalOpts,
392
- ...opts,
393
- } as InvokeAIOptions;
394
-
395
- const sender =
396
- opts.aiRequestSender ??
397
- defaultAIRequestSender(
398
- aiDocumentFormats.html.defaultPromptBuilder,
399
- aiDocumentFormats.html.defaultPromptInputDataBuilder,
400
- );
268
+ if (chatSession?.chat.status === "error") {
269
+ // the LLM call failed (i.e. a network error)
270
+ // console.log("retry failed LLM call", this.chatSession.chat.error);
271
+ return this.invokeAI({
272
+ ...chatSession.previousRequestOptions,
273
+ userPrompt: `An error occured in the previous request. Please retry to accomplish the last user prompt.`,
274
+ });
275
+ } else {
276
+ // an error occurred while parsing / executing the previous LLM call
277
+ // give the LLM a chance to fix the error
278
+ // console.log("retry failed tool execution");
279
+ return this.invokeAI({
280
+ ...chatSession.previousRequestOptions,
281
+ userPrompt: `An error occured while executing the previous tool call. Please retry to accomplish the last user prompt.`,
282
+ });
283
+ }
284
+ },
285
+
286
+ /**
287
+ * Update the status of a call to an LLM
288
+ *
289
+ * @warning This method should usually only be used for advanced use-cases
290
+ * if you want to implement how an LLM call is executed. Usually, you should
291
+ * use {@link executeLLMRequest} instead which will handle the status updates for you.
292
+ */
293
+ setAIResponseStatus(
294
+ status:
295
+ | "user-input"
296
+ | "thinking"
297
+ | "ai-writing"
298
+ | "user-reviewing"
299
+ | {
300
+ status: "error";
301
+ error: any;
302
+ },
303
+ ) {
304
+ const aiMenuState = store.state.aiMenuState;
305
+ if (aiMenuState === "closed") {
306
+ return;
307
+ }
308
+
309
+ if (status === "ai-writing") {
310
+ editor.getExtension(ShowSelectionExtension)?.showSelection(false);
311
+ }
401
312
 
402
- const aiRequest = buildAIRequest({
403
- editor: this.editor,
404
- chat,
405
- userPrompt: opts.userPrompt,
406
- useSelection: opts.useSelection,
407
- deleteEmptyCursorBlock: opts.deleteEmptyCursorBlock,
408
- streamToolsProvider: opts.streamToolsProvider,
409
- onBlockUpdated: (blockId) => {
410
- // NOTE: does this setState with an anon object trigger unnecessary re-renders?
411
- this._store.setState({
313
+ if (typeof status === "object") {
314
+ if (status.status !== "error") {
315
+ throw new UnreachableCaseError(status.status);
316
+ }
317
+ store.setState({
318
+ aiMenuState: {
319
+ status: status.status,
320
+ error: status.error,
321
+ blockId: aiMenuState.blockId,
322
+ },
323
+ });
324
+ } else {
325
+ store.setState({
412
326
  aiMenuState: {
413
- blockId,
414
- status: "ai-writing",
327
+ status: status,
328
+ blockId: aiMenuState.blockId,
415
329
  },
416
330
  });
331
+ }
332
+ },
417
333
 
418
- // Scrolls to the block being edited by the AI while auto scrolling is
419
- // enabled.
420
- if (!this.autoScroll) {
421
- return;
422
- }
334
+ /**
335
+ * @deprecated Use {@link invokeAI} instead
336
+ */
337
+ async callLLM(opts: InvokeAIOptions) {
338
+ return this.invokeAI(opts);
339
+ },
423
340
 
424
- const aiMenuState = this._store.getState().aiMenuState;
425
- const aiMenuOpenState =
426
- aiMenuState === "closed" ? undefined : aiMenuState;
427
- if (!aiMenuOpenState || aiMenuOpenState.status !== "ai-writing") {
428
- return;
341
+ /**
342
+ * Execute a call to an LLM and apply the result to the editor
343
+ */
344
+ async invokeAI(opts: InvokeAIOptions) {
345
+ this.setAIResponseStatus("thinking");
346
+ editor.getExtension(ForkYDocExtension)?.fork();
347
+
348
+ try {
349
+ if (!chatSession) {
350
+ // note: in the current implementation opts.transport is only used when creating a new chat
351
+ // (so changing transport for a subsequent call in the same chat-session is not supported)
352
+ chatSession = {
353
+ previousRequestOptions: opts,
354
+ chat: new Chat<UIMessage>({
355
+ sendAutomaticallyWhen: () => false,
356
+ transport: opts.transport || options.state.transport,
357
+ }),
358
+ };
359
+ } else {
360
+ chatSession.previousRequestOptions = opts;
429
361
  }
362
+ const chat = chatSession.chat;
363
+
364
+ // merge the global options with the local options
365
+ const globalOpts = options.state;
366
+ opts = {
367
+ ...globalOpts,
368
+ ...opts,
369
+ } as InvokeAIOptions;
370
+
371
+ const sender =
372
+ opts.aiRequestSender ??
373
+ defaultAIRequestSender(
374
+ aiDocumentFormats.html.defaultPromptBuilder,
375
+ aiDocumentFormats.html.defaultPromptInputDataBuilder,
376
+ );
377
+
378
+ const aiRequest = buildAIRequest({
379
+ editor,
380
+ chat,
381
+ userPrompt: opts.userPrompt,
382
+ useSelection: opts.useSelection,
383
+ deleteEmptyCursorBlock: opts.deleteEmptyCursorBlock,
384
+ streamToolsProvider: opts.streamToolsProvider,
385
+ onBlockUpdated: (blockId) => {
386
+ const aiMenuState = store.state.aiMenuState;
387
+ const aiMenuOpenState =
388
+ aiMenuState === "closed" ? undefined : aiMenuState;
389
+ if (!aiMenuOpenState || aiMenuOpenState.status !== "ai-writing") {
390
+ return;
391
+ }
392
+
393
+ // TODO: Sometimes, the updated block doesn't actually exist in
394
+ // the editor. I don't know why this happens, seems like a bug?
395
+ const nodeInfo = getNodeById(
396
+ blockId,
397
+ editor.prosemirrorState.doc,
398
+ );
399
+ if (!nodeInfo) {
400
+ return;
401
+ }
402
+
403
+ store.setState({
404
+ aiMenuState: {
405
+ blockId,
406
+ status: "ai-writing",
407
+ },
408
+ });
409
+
410
+ if (autoScroll) {
411
+ const blockElement = editor.prosemirrorView.domAtPos(
412
+ nodeInfo.posBeforeNode + 1,
413
+ );
414
+ (blockElement.node as HTMLElement).scrollIntoView({
415
+ block: "center",
416
+ });
417
+ }
418
+ },
419
+ });
430
420
 
431
- const nodeInfo = getNodeById(
432
- aiMenuOpenState.blockId,
433
- this.editor.prosemirrorState.doc,
434
- );
435
- if (!nodeInfo) {
436
- return;
437
- }
421
+ await executeAIRequest({
422
+ aiRequest,
423
+ sender,
424
+ chatRequestOptions: opts.chatRequestOptions,
425
+ onStart: () => {
426
+ autoScroll = true;
427
+ this.setAIResponseStatus("ai-writing");
428
+ },
429
+ });
438
430
 
439
- const blockElement = this.editor.prosemirrorView.domAtPos(
440
- nodeInfo.posBeforeNode + 1,
441
- );
442
- (blockElement.node as HTMLElement).scrollIntoView({
443
- block: "center",
431
+ this.setAIResponseStatus("user-reviewing");
432
+ } catch (e) {
433
+ this.setAIResponseStatus({
434
+ status: "error",
435
+ error: e,
444
436
  });
445
- },
446
- });
447
-
448
- await executeAIRequest({
449
- aiRequest,
450
- sender,
451
- chatRequestOptions: opts.chatRequestOptions,
452
- onStart: () => {
453
- this.autoScroll = true;
454
- this.setAIResponseStatus("ai-writing");
455
- },
456
- });
457
-
458
- this.setAIResponseStatus("user-reviewing");
459
- } catch (e) {
460
- this.setAIResponseStatus({
461
- status: "error",
462
- error: e,
463
- });
464
- // eslint-disable-next-line no-console
465
- console.warn("Error calling LLM", e, this.chatSession?.chat.messages);
466
- }
467
- }
468
- }
469
-
470
- /**
471
- * Create a new AIExtension instance, this can be passed to the BlockNote editor via the `extensions` option
472
- */
473
- export function createAIExtension(
474
- options: ConstructorParameters<typeof AIExtension>[1],
475
- ) {
476
- return (editor: BlockNoteEditor<any, any, any>) => {
477
- return new AIExtension(editor, options);
478
- };
479
- }
480
-
481
- /**
482
- * Return the AIExtension instance from the editor
483
- */
484
- export function getAIExtension(editor: BlockNoteEditor<any, any, any>) {
485
- return editor.extension(AIExtension);
486
- }
437
+ // eslint-disable-next-line no-console
438
+ console.warn("Error calling LLM", e, chatSession?.chat.messages);
439
+ }
440
+ },
441
+ } as const;
442
+ },
443
+ );