@blocknote/xl-ai 0.42.2 → 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.
- package/dist/blocknote-xl-ai.cjs +34 -5
- package/dist/blocknote-xl-ai.cjs.map +1 -1
- package/dist/blocknote-xl-ai.js +25678 -1443
- package/dist/blocknote-xl-ai.js.map +1 -1
- package/dist/webpack-stats.json +1 -1
- package/package.json +7 -8
- package/src/AIExtension.ts +366 -409
- package/src/api/formats/json/tools/jsontools.test.ts +2 -2
- package/src/api/formats/tests/sharedTestCases.ts +2 -2
- package/src/components/AIMenu/AIMenu.tsx +17 -8
- package/src/components/AIMenu/AIMenuController.tsx +83 -29
- package/src/components/AIMenu/PromptSuggestionMenu.tsx +4 -2
- package/src/components/AIMenu/getDefaultAIMenuItems.tsx +17 -5
- package/src/components/FormattingToolbar/AIToolbarButton.tsx +8 -4
- package/src/components/SuggestionMenu/getAISlashMenuItems.tsx +5 -2
- package/src/index.ts +0 -1
- package/src/prosemirror/agent.test.ts +2 -2
- package/src/testUtil/cases/editors/blockFormatting.ts +2 -2
- package/src/testUtil/cases/editors/emptyEditor.ts +2 -2
- package/src/testUtil/cases/editors/formattingAndMentions.ts +2 -2
- package/src/testUtil/cases/editors/simpleEditor.ts +3 -3
- package/src/testUtil/cases/editors/tables.ts +2 -2
- package/src/testUtil/cases/updateOperationTestCases.ts +4 -4
- package/types/src/AIExtension.d.ts +34 -49
- package/types/src/components/AIMenu/AIMenuController.d.ts +2 -0
- package/types/src/index.d.ts +0 -1
- package/src/components/AIMenu/BlockPositioner.tsx +0 -86
- package/types/src/components/AIMenu/BlockPositioner.d.ts +0 -10
package/src/AIExtension.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { Chat } from "@ai-sdk/react";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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(
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
414
|
-
|
|
327
|
+
status: status,
|
|
328
|
+
blockId: aiMenuState.blockId,
|
|
415
329
|
},
|
|
416
330
|
});
|
|
331
|
+
}
|
|
332
|
+
},
|
|
417
333
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
334
|
+
/**
|
|
335
|
+
* @deprecated Use {@link invokeAI} instead
|
|
336
|
+
*/
|
|
337
|
+
async callLLM(opts: InvokeAIOptions) {
|
|
338
|
+
return this.invokeAI(opts);
|
|
339
|
+
},
|
|
423
340
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
431
|
+
this.setAIResponseStatus("user-reviewing");
|
|
432
|
+
} catch (e) {
|
|
433
|
+
this.setAIResponseStatus({
|
|
434
|
+
status: "error",
|
|
435
|
+
error: e,
|
|
444
436
|
});
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
+
);
|