@platejs/ai 49.0.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/README.md +7 -0
- package/dist/BaseAIPlugin-DyivsvyQ.d.mts +36 -0
- package/dist/BaseAIPlugin-DyivsvyQ.d.ts +36 -0
- package/dist/index.d.mts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +992 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +953 -0
- package/dist/index.mjs.map +1 -0
- package/dist/react/index.d.mts +435 -0
- package/dist/react/index.d.ts +435 -0
- package/dist/react/index.js +1215 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/index.mjs +1174 -0
- package/dist/react/index.mjs.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,1174 @@
|
|
|
1
|
+
// src/react/ai/AIPlugin.ts
|
|
2
|
+
import { toPlatePlugin } from "platejs/react";
|
|
3
|
+
|
|
4
|
+
// src/lib/BaseAIPlugin.ts
|
|
5
|
+
import {
|
|
6
|
+
bindFirst,
|
|
7
|
+
createTSlatePlugin,
|
|
8
|
+
KEYS
|
|
9
|
+
} from "platejs";
|
|
10
|
+
|
|
11
|
+
// src/lib/transforms/insertAINodes.ts
|
|
12
|
+
var insertAINodes = (editor, nodes, {
|
|
13
|
+
target
|
|
14
|
+
} = {}) => {
|
|
15
|
+
if (!target && !editor.selection?.focus.path) return;
|
|
16
|
+
const aiNodes = nodes.map((node) => ({
|
|
17
|
+
...node,
|
|
18
|
+
ai: true
|
|
19
|
+
}));
|
|
20
|
+
editor.tf.withoutNormalizing(() => {
|
|
21
|
+
editor.tf.insertNodes(aiNodes, {
|
|
22
|
+
at: editor.api.end(target || editor.selection.focus.path),
|
|
23
|
+
select: true
|
|
24
|
+
});
|
|
25
|
+
editor.tf.collapse({ edge: "end" });
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// src/lib/transforms/removeAIMarks.ts
|
|
30
|
+
var removeAIMarks = (editor, { at = [] } = {}) => {
|
|
31
|
+
editor.tf.unsetNodes("ai", {
|
|
32
|
+
at,
|
|
33
|
+
match: (n) => n.ai
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// src/lib/transforms/removeAINodes.ts
|
|
38
|
+
import { TextApi } from "platejs";
|
|
39
|
+
var removeAINodes = (editor, { at = [] } = {}) => {
|
|
40
|
+
editor.tf.removeNodes({
|
|
41
|
+
at,
|
|
42
|
+
match: (n) => TextApi.isText(n) && !!n.ai
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/lib/transforms/undoAI.ts
|
|
47
|
+
var undoAI = (editor) => {
|
|
48
|
+
if (editor.history.undos.at(-1)?.ai && editor.api.some({
|
|
49
|
+
at: [],
|
|
50
|
+
match: (n) => !!n.ai
|
|
51
|
+
})) {
|
|
52
|
+
editor.undo();
|
|
53
|
+
editor.history.redos.pop();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/lib/transforms/withAIBatch.ts
|
|
58
|
+
var withAIBatch = (editor, fn, {
|
|
59
|
+
split
|
|
60
|
+
} = {}) => {
|
|
61
|
+
if (split) {
|
|
62
|
+
editor.tf.withNewBatch(fn);
|
|
63
|
+
} else {
|
|
64
|
+
editor.tf.withMerging(fn);
|
|
65
|
+
}
|
|
66
|
+
const lastBatch = editor.history.undos?.at(-1);
|
|
67
|
+
if (lastBatch) {
|
|
68
|
+
lastBatch.ai = true;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// src/lib/BaseAIPlugin.ts
|
|
73
|
+
var BaseAIPlugin = createTSlatePlugin({
|
|
74
|
+
key: KEYS.ai,
|
|
75
|
+
node: { isDecoration: false, isLeaf: true }
|
|
76
|
+
}).extendTransforms(({ editor }) => ({
|
|
77
|
+
insertNodes: bindFirst(insertAINodes, editor),
|
|
78
|
+
removeMarks: bindFirst(removeAIMarks, editor),
|
|
79
|
+
removeNodes: bindFirst(removeAINodes, editor),
|
|
80
|
+
undo: bindFirst(undoAI, editor)
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
// src/react/ai/AIPlugin.ts
|
|
84
|
+
var AIPlugin = toPlatePlugin(BaseAIPlugin);
|
|
85
|
+
|
|
86
|
+
// src/react/ai-chat/AIChatPlugin.ts
|
|
87
|
+
import { BlockSelectionPlugin as BlockSelectionPlugin5 } from "@platejs/selection/react";
|
|
88
|
+
import {
|
|
89
|
+
bindFirst as bindFirst2,
|
|
90
|
+
ElementApi as ElementApi3,
|
|
91
|
+
KEYS as KEYS10
|
|
92
|
+
} from "platejs";
|
|
93
|
+
import { createTPlatePlugin } from "platejs/react";
|
|
94
|
+
|
|
95
|
+
// src/react/ai-chat/transforms/acceptAIChat.ts
|
|
96
|
+
import { KEYS as KEYS2 } from "platejs";
|
|
97
|
+
import { getEditorPlugin } from "platejs/react";
|
|
98
|
+
var acceptAIChat = (editor) => {
|
|
99
|
+
const { tf } = getEditorPlugin(editor, AIPlugin);
|
|
100
|
+
const api = editor.getApi({ key: KEYS2.ai });
|
|
101
|
+
const lastAINodePath = api.aiChat.node({ at: [], reverse: true })[1];
|
|
102
|
+
withAIBatch(editor, () => {
|
|
103
|
+
tf.ai.removeMarks();
|
|
104
|
+
editor.getTransforms(AIChatPlugin).aiChat.removeAnchor();
|
|
105
|
+
});
|
|
106
|
+
api.aiChat.hide();
|
|
107
|
+
editor.tf.focus();
|
|
108
|
+
const focusPoint = editor.api.end(lastAINodePath);
|
|
109
|
+
editor.tf.setSelection({
|
|
110
|
+
anchor: focusPoint,
|
|
111
|
+
focus: focusPoint
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// src/react/ai-chat/transforms/insertBelowAIChat.ts
|
|
116
|
+
import { BlockSelectionPlugin as BlockSelectionPlugin2 } from "@platejs/selection/react";
|
|
117
|
+
import cloneDeep2 from "lodash/cloneDeep.js";
|
|
118
|
+
import { KEYS as KEYS4, PathApi, RangeApi } from "platejs";
|
|
119
|
+
|
|
120
|
+
// src/react/ai-chat/transforms/replaceSelectionAIChat.ts
|
|
121
|
+
import {
|
|
122
|
+
BlockSelectionPlugin,
|
|
123
|
+
removeBlockSelectionNodes
|
|
124
|
+
} from "@platejs/selection/react";
|
|
125
|
+
import cloneDeep from "lodash/cloneDeep.js";
|
|
126
|
+
import {
|
|
127
|
+
KEYS as KEYS3,
|
|
128
|
+
NodeApi,
|
|
129
|
+
TextApi as TextApi2
|
|
130
|
+
} from "platejs";
|
|
131
|
+
var createFormattedBlocks = ({
|
|
132
|
+
blocks,
|
|
133
|
+
format,
|
|
134
|
+
sourceBlock
|
|
135
|
+
}) => {
|
|
136
|
+
if (format === "none") return cloneDeep(blocks);
|
|
137
|
+
const [sourceNode] = sourceBlock;
|
|
138
|
+
const firstTextEntry = NodeApi.firstText(sourceNode);
|
|
139
|
+
if (!firstTextEntry) return null;
|
|
140
|
+
const blockProps = NodeApi.extractProps(sourceNode);
|
|
141
|
+
const textProps = NodeApi.extractProps(firstTextEntry[0]);
|
|
142
|
+
const applyTextFormatting = (node) => {
|
|
143
|
+
if (TextApi2.isText(node)) {
|
|
144
|
+
return { ...textProps, ...node };
|
|
145
|
+
}
|
|
146
|
+
if (node.children) {
|
|
147
|
+
return {
|
|
148
|
+
...node,
|
|
149
|
+
children: node.children.map(applyTextFormatting)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return node;
|
|
153
|
+
};
|
|
154
|
+
return blocks.map((block, index) => {
|
|
155
|
+
if (format === "single" && index > 0) {
|
|
156
|
+
return block;
|
|
157
|
+
}
|
|
158
|
+
return applyTextFormatting({
|
|
159
|
+
...block,
|
|
160
|
+
...blockProps
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
var replaceSelectionAIChat = (editor, sourceEditor, { format = "single" } = {}) => {
|
|
165
|
+
if (!sourceEditor || sourceEditor.api.isEmpty()) return;
|
|
166
|
+
const isBlockSelecting = editor.getOption(
|
|
167
|
+
BlockSelectionPlugin,
|
|
168
|
+
"isSelectingSome"
|
|
169
|
+
);
|
|
170
|
+
editor.getApi({ key: KEYS3.ai }).aiChat.hide();
|
|
171
|
+
if (!isBlockSelecting) {
|
|
172
|
+
const firstBlock = editor.api.node({
|
|
173
|
+
block: true,
|
|
174
|
+
mode: "lowest"
|
|
175
|
+
});
|
|
176
|
+
if (firstBlock && editor.api.isSelected(firstBlock[1], { contains: true }) && format !== "none") {
|
|
177
|
+
const formattedBlocks2 = createFormattedBlocks({
|
|
178
|
+
blocks: cloneDeep(sourceEditor.children),
|
|
179
|
+
format,
|
|
180
|
+
sourceBlock: firstBlock
|
|
181
|
+
});
|
|
182
|
+
if (!formattedBlocks2) return;
|
|
183
|
+
editor.tf.insertFragment(formattedBlocks2);
|
|
184
|
+
editor.tf.focus();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
editor.tf.insertFragment(sourceEditor.children);
|
|
188
|
+
editor.tf.focus();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const blockSelectionApi = editor.getApi(BlockSelectionPlugin).blockSelection;
|
|
192
|
+
const selectedBlocks = blockSelectionApi.getNodes();
|
|
193
|
+
if (selectedBlocks.length === 0) return;
|
|
194
|
+
if (format === "none" || format === "single" && selectedBlocks.length > 1) {
|
|
195
|
+
editor.tf.withoutNormalizing(() => {
|
|
196
|
+
removeBlockSelectionNodes(editor);
|
|
197
|
+
editor.tf.withNewBatch(() => {
|
|
198
|
+
editor.getTransforms(BlockSelectionPlugin).blockSelection.insertBlocksAndSelect(
|
|
199
|
+
cloneDeep(sourceEditor.children),
|
|
200
|
+
{
|
|
201
|
+
at: selectedBlocks[0][1]
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
editor.getApi(BlockSelectionPlugin).blockSelection.focus();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const [, firstBlockPath] = selectedBlocks[0];
|
|
210
|
+
const formattedBlocks = createFormattedBlocks({
|
|
211
|
+
blocks: cloneDeep(sourceEditor.children),
|
|
212
|
+
format,
|
|
213
|
+
sourceBlock: selectedBlocks[0]
|
|
214
|
+
});
|
|
215
|
+
if (!formattedBlocks) return;
|
|
216
|
+
editor.tf.withoutNormalizing(() => {
|
|
217
|
+
removeBlockSelectionNodes(editor);
|
|
218
|
+
editor.tf.withNewBatch(() => {
|
|
219
|
+
editor.getTransforms(BlockSelectionPlugin).blockSelection.insertBlocksAndSelect(formattedBlocks, {
|
|
220
|
+
at: firstBlockPath
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
editor.getApi(BlockSelectionPlugin).blockSelection.focus();
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// src/react/ai-chat/transforms/insertBelowAIChat.ts
|
|
228
|
+
var insertBelowAIChat = (editor, sourceEditor, { format = "single" } = {}) => {
|
|
229
|
+
if (!sourceEditor || sourceEditor.api.isEmpty()) return;
|
|
230
|
+
const isBlockSelecting = editor.getOption(
|
|
231
|
+
BlockSelectionPlugin2,
|
|
232
|
+
"isSelectingSome"
|
|
233
|
+
);
|
|
234
|
+
editor.getApi({ key: KEYS4.ai }).aiChat.hide();
|
|
235
|
+
const insertBlocksAndSelect = editor.getTransforms(BlockSelectionPlugin2).blockSelection.insertBlocksAndSelect;
|
|
236
|
+
if (isBlockSelecting) {
|
|
237
|
+
const selectedBlocks = editor.getApi(BlockSelectionPlugin2).blockSelection.getNodes();
|
|
238
|
+
const selectedIds = editor.getOptions(BlockSelectionPlugin2).selectedIds;
|
|
239
|
+
if (!selectedIds || selectedIds.size === 0) return;
|
|
240
|
+
const lastBlock = selectedBlocks.at(-1);
|
|
241
|
+
if (!lastBlock) return;
|
|
242
|
+
const nextPath = PathApi.next(lastBlock[1]);
|
|
243
|
+
if (format === "none") {
|
|
244
|
+
insertBlocksAndSelect(cloneDeep2(sourceEditor.children), {
|
|
245
|
+
at: nextPath
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const formattedBlocks = createFormattedBlocks({
|
|
250
|
+
blocks: cloneDeep2(sourceEditor.children),
|
|
251
|
+
format,
|
|
252
|
+
sourceBlock: lastBlock
|
|
253
|
+
});
|
|
254
|
+
if (!formattedBlocks) return;
|
|
255
|
+
insertBlocksAndSelect(formattedBlocks, {
|
|
256
|
+
at: nextPath
|
|
257
|
+
});
|
|
258
|
+
} else {
|
|
259
|
+
const [, end] = RangeApi.edges(editor.selection);
|
|
260
|
+
const endPath = [end.path[0]];
|
|
261
|
+
const currentBlock = editor.api.node({
|
|
262
|
+
at: endPath,
|
|
263
|
+
block: true,
|
|
264
|
+
mode: "lowest"
|
|
265
|
+
});
|
|
266
|
+
if (!currentBlock) return;
|
|
267
|
+
if (format === "none") {
|
|
268
|
+
insertBlocksAndSelect(cloneDeep2(sourceEditor.children), {
|
|
269
|
+
at: PathApi.next(endPath)
|
|
270
|
+
});
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const formattedBlocks = createFormattedBlocks({
|
|
274
|
+
blocks: cloneDeep2(sourceEditor.children),
|
|
275
|
+
format,
|
|
276
|
+
sourceBlock: currentBlock
|
|
277
|
+
});
|
|
278
|
+
if (!formattedBlocks) return;
|
|
279
|
+
insertBlocksAndSelect(formattedBlocks, {
|
|
280
|
+
at: PathApi.next(endPath)
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// src/react/ai-chat/transforms/removeAnchorAIChat.ts
|
|
286
|
+
import { ElementApi, KEYS as KEYS5 } from "platejs";
|
|
287
|
+
var removeAnchorAIChat = (editor, options) => {
|
|
288
|
+
editor.tf.withoutSaving(() => {
|
|
289
|
+
editor.tf.removeNodes({
|
|
290
|
+
at: [],
|
|
291
|
+
match: (n) => ElementApi.isElement(n) && n.type === KEYS5.aiChat,
|
|
292
|
+
...options
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// src/react/ai-chat/utils/getEditorPrompt.ts
|
|
298
|
+
import { isSelecting } from "@platejs/selection";
|
|
299
|
+
import { BlockSelectionPlugin as BlockSelectionPlugin4 } from "@platejs/selection/react";
|
|
300
|
+
|
|
301
|
+
// src/react/ai-chat/utils/getMarkdown.ts
|
|
302
|
+
import { serializeMd } from "@platejs/markdown";
|
|
303
|
+
import { BlockSelectionPlugin as BlockSelectionPlugin3 } from "@platejs/selection/react";
|
|
304
|
+
import { KEYS as KEYS6 } from "platejs";
|
|
305
|
+
var getMarkdown = (editor, type) => {
|
|
306
|
+
if (type === "editor") {
|
|
307
|
+
return serializeMd(editor);
|
|
308
|
+
}
|
|
309
|
+
if (type === "block") {
|
|
310
|
+
const blocks = editor.getOption(BlockSelectionPlugin3, "isSelectingSome") ? editor.getApi(BlockSelectionPlugin3).blockSelection.getNodes() : editor.api.nodes({
|
|
311
|
+
mode: "highest",
|
|
312
|
+
match: (n) => editor.api.isBlock(n)
|
|
313
|
+
});
|
|
314
|
+
const nodes = Array.from(blocks, (entry) => entry[0]);
|
|
315
|
+
return serializeMd(editor, { value: nodes });
|
|
316
|
+
}
|
|
317
|
+
if (type === "selection") {
|
|
318
|
+
const fragment = editor.api.fragment();
|
|
319
|
+
if (fragment.length === 1) {
|
|
320
|
+
const modifiedFragment = [
|
|
321
|
+
{
|
|
322
|
+
children: fragment[0].children,
|
|
323
|
+
type: KEYS6.p
|
|
324
|
+
}
|
|
325
|
+
];
|
|
326
|
+
return serializeMd(editor, { value: modifiedFragment });
|
|
327
|
+
}
|
|
328
|
+
return serializeMd(editor, { value: fragment });
|
|
329
|
+
}
|
|
330
|
+
return "";
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// src/react/ai-chat/utils/getEditorPrompt.ts
|
|
334
|
+
var replacePlaceholders = (editor, text, {
|
|
335
|
+
prompt
|
|
336
|
+
}) => {
|
|
337
|
+
let result = text.replace("{prompt}", prompt || "");
|
|
338
|
+
const placeholders = {
|
|
339
|
+
"{block}": "block",
|
|
340
|
+
"{editor}": "editor",
|
|
341
|
+
"{selection}": "selection"
|
|
342
|
+
};
|
|
343
|
+
Object.entries(placeholders).forEach(([placeholder, type]) => {
|
|
344
|
+
if (result.includes(placeholder)) {
|
|
345
|
+
result = result.replace(placeholder, getMarkdown(editor, type));
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
return result;
|
|
349
|
+
};
|
|
350
|
+
var createPromptFromConfig = (config, params) => {
|
|
351
|
+
const { isBlockSelecting, isSelecting: isSelecting3 } = params;
|
|
352
|
+
if (isBlockSelecting && config.blockSelecting) {
|
|
353
|
+
return config.blockSelecting ?? config.default;
|
|
354
|
+
} else if (isSelecting3 && config.selecting) {
|
|
355
|
+
return config.selecting ?? config.default;
|
|
356
|
+
} else {
|
|
357
|
+
return config.default;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
var getEditorPrompt = (editor, {
|
|
361
|
+
prompt = "",
|
|
362
|
+
promptTemplate = () => "{prompt}"
|
|
363
|
+
} = {}) => {
|
|
364
|
+
const params = {
|
|
365
|
+
editor,
|
|
366
|
+
isBlockSelecting: editor.getOption(BlockSelectionPlugin4, "isSelectingSome"),
|
|
367
|
+
isSelecting: isSelecting(editor)
|
|
368
|
+
};
|
|
369
|
+
const template = promptTemplate(params);
|
|
370
|
+
if (!template) return;
|
|
371
|
+
let promptText = "";
|
|
372
|
+
if (typeof prompt === "function") {
|
|
373
|
+
promptText = prompt(params);
|
|
374
|
+
} else if (typeof prompt === "object") {
|
|
375
|
+
promptText = createPromptFromConfig(prompt, params);
|
|
376
|
+
} else {
|
|
377
|
+
promptText = prompt;
|
|
378
|
+
}
|
|
379
|
+
return replacePlaceholders(editor, template, {
|
|
380
|
+
prompt: promptText
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// src/react/ai-chat/utils/resetAIChat.ts
|
|
385
|
+
import { KEYS as KEYS7 } from "platejs";
|
|
386
|
+
import { getEditorPlugin as getEditorPlugin2 } from "platejs/react";
|
|
387
|
+
var resetAIChat = (editor) => {
|
|
388
|
+
const { api, getOptions } = getEditorPlugin2(editor, {
|
|
389
|
+
key: KEYS7.aiChat
|
|
390
|
+
});
|
|
391
|
+
api.aiChat.stop();
|
|
392
|
+
const chat = getOptions().chat;
|
|
393
|
+
if (chat.messages && chat.messages.length > 0) {
|
|
394
|
+
chat.setMessages?.([]);
|
|
395
|
+
}
|
|
396
|
+
editor.getTransforms(AIPlugin).ai.undo();
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// src/react/ai-chat/utils/submitAIChat.ts
|
|
400
|
+
import { isSelecting as isSelecting2 } from "@platejs/selection";
|
|
401
|
+
import { KEYS as KEYS8 } from "platejs";
|
|
402
|
+
import { getEditorPlugin as getEditorPlugin3 } from "platejs/react";
|
|
403
|
+
var submitAIChat = (editor, {
|
|
404
|
+
mode,
|
|
405
|
+
options,
|
|
406
|
+
prompt,
|
|
407
|
+
system
|
|
408
|
+
} = {}) => {
|
|
409
|
+
const { getOptions, setOption } = getEditorPlugin3(
|
|
410
|
+
editor,
|
|
411
|
+
{
|
|
412
|
+
key: KEYS8.aiChat
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
const { chat, promptTemplate, systemTemplate } = getOptions();
|
|
416
|
+
if (!prompt && chat.input?.length === 0) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (!prompt) {
|
|
420
|
+
prompt = chat.input;
|
|
421
|
+
}
|
|
422
|
+
if (!mode) {
|
|
423
|
+
mode = isSelecting2(editor) ? "chat" : "insert";
|
|
424
|
+
}
|
|
425
|
+
if (mode === "insert") {
|
|
426
|
+
editor.getTransforms(AIPlugin).ai.undo();
|
|
427
|
+
}
|
|
428
|
+
setOption("mode", mode);
|
|
429
|
+
chat.setInput?.("");
|
|
430
|
+
void chat.append?.(
|
|
431
|
+
{
|
|
432
|
+
content: getEditorPrompt(editor, {
|
|
433
|
+
prompt,
|
|
434
|
+
promptTemplate
|
|
435
|
+
}) ?? "",
|
|
436
|
+
role: "user"
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
body: {
|
|
440
|
+
system: getEditorPrompt(editor, {
|
|
441
|
+
prompt: system,
|
|
442
|
+
promptTemplate: systemTemplate
|
|
443
|
+
})
|
|
444
|
+
},
|
|
445
|
+
...options
|
|
446
|
+
}
|
|
447
|
+
);
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// src/react/ai-chat/withAIChat.ts
|
|
451
|
+
import { ElementApi as ElementApi2, KEYS as KEYS9 } from "platejs";
|
|
452
|
+
var withAIChat = ({
|
|
453
|
+
api,
|
|
454
|
+
editor,
|
|
455
|
+
getOptions,
|
|
456
|
+
tf: { insertText, normalizeNode, setSelection },
|
|
457
|
+
type
|
|
458
|
+
}) => {
|
|
459
|
+
const tf = editor.getTransforms(AIPlugin);
|
|
460
|
+
const matchesTrigger = (text) => {
|
|
461
|
+
const { trigger } = getOptions();
|
|
462
|
+
if (trigger instanceof RegExp) {
|
|
463
|
+
return trigger.test(text);
|
|
464
|
+
}
|
|
465
|
+
if (Array.isArray(trigger)) {
|
|
466
|
+
return trigger.includes(text);
|
|
467
|
+
}
|
|
468
|
+
return text === trigger;
|
|
469
|
+
};
|
|
470
|
+
return {
|
|
471
|
+
transforms: {
|
|
472
|
+
insertText(text, options) {
|
|
473
|
+
const { triggerPreviousCharPattern, triggerQuery } = getOptions();
|
|
474
|
+
const fn = () => {
|
|
475
|
+
if (!editor.selection || !matchesTrigger(text) || triggerQuery && !triggerQuery(editor)) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const previousChar = editor.api.string(
|
|
479
|
+
editor.api.range("before", editor.selection)
|
|
480
|
+
);
|
|
481
|
+
const matchesPreviousCharPattern = triggerPreviousCharPattern?.test(previousChar);
|
|
482
|
+
if (!matchesPreviousCharPattern) return;
|
|
483
|
+
const nodeEntry = editor.api.block({ highest: true });
|
|
484
|
+
if (!nodeEntry || !editor.api.isEmpty(nodeEntry[0])) return;
|
|
485
|
+
api.aiChat.show();
|
|
486
|
+
return true;
|
|
487
|
+
};
|
|
488
|
+
if (fn()) return;
|
|
489
|
+
return insertText(text, options);
|
|
490
|
+
},
|
|
491
|
+
normalizeNode(entry) {
|
|
492
|
+
const [node, path] = entry;
|
|
493
|
+
if (node[KEYS9.ai] && !getOptions().open) {
|
|
494
|
+
tf.ai.removeMarks({ at: path });
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (ElementApi2.isElement(node) && node.type === type && !getOptions().open) {
|
|
498
|
+
editor.getTransforms(AIChatPlugin).aiChat.removeAnchor({ at: path });
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
return normalizeNode(entry);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// src/react/ai-chat/AIChatPlugin.ts
|
|
508
|
+
var AIChatPlugin = createTPlatePlugin({
|
|
509
|
+
key: KEYS10.aiChat,
|
|
510
|
+
dependencies: ["ai"],
|
|
511
|
+
node: {
|
|
512
|
+
isElement: true
|
|
513
|
+
},
|
|
514
|
+
options: {
|
|
515
|
+
_blockChunks: "",
|
|
516
|
+
_blockPath: null,
|
|
517
|
+
aiEditor: null,
|
|
518
|
+
chat: { messages: [] },
|
|
519
|
+
experimental_lastTextId: null,
|
|
520
|
+
mode: "chat",
|
|
521
|
+
open: false,
|
|
522
|
+
streaming: false,
|
|
523
|
+
trigger: " ",
|
|
524
|
+
triggerPreviousCharPattern: /^\s?$/,
|
|
525
|
+
promptTemplate: () => "{prompt}",
|
|
526
|
+
systemTemplate: () => {
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}).overrideEditor(withAIChat).extendApi(({ editor, getOption, getOptions, setOption, type }) => {
|
|
530
|
+
return {
|
|
531
|
+
reset: bindFirst2(resetAIChat, editor),
|
|
532
|
+
submit: bindFirst2(submitAIChat, editor),
|
|
533
|
+
node: (options = {}) => {
|
|
534
|
+
const { anchor = false, streaming = false, ...rest } = options;
|
|
535
|
+
if (anchor) {
|
|
536
|
+
return editor.api.node({
|
|
537
|
+
at: [],
|
|
538
|
+
match: (n) => ElementApi3.isElement(n) && n.type === type,
|
|
539
|
+
...rest
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
if (streaming) {
|
|
543
|
+
if (!getOption("streaming")) return;
|
|
544
|
+
const path = getOption("_blockPath");
|
|
545
|
+
if (!path) return;
|
|
546
|
+
return editor.api.node({
|
|
547
|
+
at: path,
|
|
548
|
+
mode: "lowest",
|
|
549
|
+
reverse: true,
|
|
550
|
+
match: (t) => !!t.ai,
|
|
551
|
+
...rest
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
return editor.api.node({
|
|
555
|
+
match: (n) => n.ai,
|
|
556
|
+
...rest
|
|
557
|
+
});
|
|
558
|
+
},
|
|
559
|
+
reload: () => {
|
|
560
|
+
const { chat, mode } = getOptions();
|
|
561
|
+
if (mode === "insert") {
|
|
562
|
+
editor.getTransforms(AIPlugin).ai.undo();
|
|
563
|
+
}
|
|
564
|
+
void chat.reload?.({
|
|
565
|
+
body: {
|
|
566
|
+
system: getEditorPrompt(editor, {
|
|
567
|
+
promptTemplate: getOptions().systemTemplate
|
|
568
|
+
})
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
},
|
|
572
|
+
stop: () => {
|
|
573
|
+
setOption("streaming", false);
|
|
574
|
+
getOptions().chat.stop?.();
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
}).extendApi(({ api, editor, getOptions, setOption, tf, type }) => ({
|
|
578
|
+
hide: () => {
|
|
579
|
+
api.aiChat.reset();
|
|
580
|
+
setOption("open", false);
|
|
581
|
+
if (editor.getOption(BlockSelectionPlugin5, "isSelectingSome")) {
|
|
582
|
+
} else {
|
|
583
|
+
editor.tf.focus();
|
|
584
|
+
}
|
|
585
|
+
const lastBatch = editor.history.undos.at(-1);
|
|
586
|
+
if (lastBatch?.ai) {
|
|
587
|
+
delete lastBatch.ai;
|
|
588
|
+
}
|
|
589
|
+
tf.aiChat.removeAnchor();
|
|
590
|
+
},
|
|
591
|
+
show: () => {
|
|
592
|
+
api.aiChat.reset();
|
|
593
|
+
getOptions().chat.setMessages?.([]);
|
|
594
|
+
setOption("open", true);
|
|
595
|
+
}
|
|
596
|
+
})).extendTransforms(({ editor }) => ({
|
|
597
|
+
accept: bindFirst2(acceptAIChat, editor),
|
|
598
|
+
insertBelow: bindFirst2(insertBelowAIChat, editor),
|
|
599
|
+
removeAnchor: bindFirst2(removeAnchorAIChat, editor),
|
|
600
|
+
replaceSelection: bindFirst2(replaceSelectionAIChat, editor)
|
|
601
|
+
}));
|
|
602
|
+
|
|
603
|
+
// src/react/ai-chat/hooks/useAIChatEditor.ts
|
|
604
|
+
import { useEffect, useMemo } from "react";
|
|
605
|
+
import { MarkdownPlugin } from "@platejs/markdown";
|
|
606
|
+
import { useEditorPlugin } from "platejs/react";
|
|
607
|
+
var useAIChatEditor = (editor, content, { parser } = {}) => {
|
|
608
|
+
const { setOption } = useEditorPlugin(AIChatPlugin);
|
|
609
|
+
editor.children = useMemo(
|
|
610
|
+
() => {
|
|
611
|
+
return editor.getApi(MarkdownPlugin).markdown.deserialize(content, {
|
|
612
|
+
memoize: true,
|
|
613
|
+
parser
|
|
614
|
+
});
|
|
615
|
+
},
|
|
616
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
617
|
+
[content]
|
|
618
|
+
);
|
|
619
|
+
useEffect(() => {
|
|
620
|
+
setOption("aiEditor", editor);
|
|
621
|
+
}, [editor, setOption]);
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
// src/react/ai-chat/hooks/useChatChunk.ts
|
|
625
|
+
import { useEffect as useEffect2, useRef } from "react";
|
|
626
|
+
import { KEYS as KEYS11 } from "platejs";
|
|
627
|
+
import { usePluginOption as usePluginOption2 } from "platejs/react";
|
|
628
|
+
|
|
629
|
+
// src/react/ai-chat/utils/getLastAssistantMessage.ts
|
|
630
|
+
import { usePluginOption } from "platejs/react";
|
|
631
|
+
function getLastAssistantMessage(editor) {
|
|
632
|
+
const messages = editor.getOptions(AIChatPlugin).chat.messages;
|
|
633
|
+
return messages?.findLast((message) => message.role === "assistant");
|
|
634
|
+
}
|
|
635
|
+
function useLastAssistantMessage() {
|
|
636
|
+
const chat = usePluginOption(AIChatPlugin, "chat");
|
|
637
|
+
return chat.messages?.findLast((message) => message.role === "assistant");
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// src/react/ai-chat/hooks/useChatChunk.ts
|
|
641
|
+
var useChatChunk = ({
|
|
642
|
+
onChunk,
|
|
643
|
+
onFinish
|
|
644
|
+
}) => {
|
|
645
|
+
const { isLoading } = usePluginOption2(
|
|
646
|
+
{ key: KEYS11.aiChat },
|
|
647
|
+
"chat"
|
|
648
|
+
);
|
|
649
|
+
const content = useLastAssistantMessage()?.content;
|
|
650
|
+
const insertedTextRef = useRef("");
|
|
651
|
+
const prevIsLoadingRef = useRef(isLoading);
|
|
652
|
+
useEffect2(() => {
|
|
653
|
+
if (!isLoading) {
|
|
654
|
+
insertedTextRef.current = "";
|
|
655
|
+
}
|
|
656
|
+
if (prevIsLoadingRef.current && !isLoading) {
|
|
657
|
+
onFinish?.({ content: content ?? "" });
|
|
658
|
+
}
|
|
659
|
+
prevIsLoadingRef.current = isLoading;
|
|
660
|
+
}, [isLoading]);
|
|
661
|
+
useEffect2(() => {
|
|
662
|
+
if (!content) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const chunk = content.slice(insertedTextRef.current.length);
|
|
666
|
+
const nodes = [];
|
|
667
|
+
if (chunk) {
|
|
668
|
+
const isFirst = insertedTextRef.current === "";
|
|
669
|
+
nodes.push({ text: chunk });
|
|
670
|
+
onChunk({
|
|
671
|
+
chunk,
|
|
672
|
+
isFirst,
|
|
673
|
+
nodes,
|
|
674
|
+
text: content
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
insertedTextRef.current = content;
|
|
678
|
+
}, [content]);
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
// src/react/ai-chat/hooks/useEditorChat.ts
|
|
682
|
+
import { useEffect as useEffect3 } from "react";
|
|
683
|
+
import { BlockSelectionPlugin as BlockSelectionPlugin6 } from "@platejs/selection/react";
|
|
684
|
+
import { useEditorPlugin as useEditorPlugin2, usePluginOption as usePluginOption3 } from "platejs/react";
|
|
685
|
+
var useEditorChat = ({
|
|
686
|
+
chat,
|
|
687
|
+
onOpenBlockSelection,
|
|
688
|
+
onOpenChange,
|
|
689
|
+
onOpenCursor,
|
|
690
|
+
onOpenSelection
|
|
691
|
+
}) => {
|
|
692
|
+
const { editor, setOption } = useEditorPlugin2(AIChatPlugin);
|
|
693
|
+
const open = usePluginOption3(AIChatPlugin, "open");
|
|
694
|
+
useEffect3(() => {
|
|
695
|
+
setOption("chat", chat);
|
|
696
|
+
}, [chat.input, chat.messages, chat.isLoading, chat.data, chat.error]);
|
|
697
|
+
useEffect3(() => {
|
|
698
|
+
onOpenChange?.(open);
|
|
699
|
+
if (open) {
|
|
700
|
+
if (onOpenBlockSelection) {
|
|
701
|
+
const blockSelectionApi = editor.getApi(BlockSelectionPlugin6).blockSelection;
|
|
702
|
+
const isBlockSelecting = editor.getOption(
|
|
703
|
+
BlockSelectionPlugin6,
|
|
704
|
+
"isSelectingSome"
|
|
705
|
+
);
|
|
706
|
+
if (isBlockSelecting) {
|
|
707
|
+
onOpenBlockSelection(blockSelectionApi.getNodes());
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (onOpenCursor && editor.api.isCollapsed()) {
|
|
712
|
+
onOpenCursor();
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (onOpenSelection && editor.api.isExpanded()) {
|
|
716
|
+
onOpenSelection();
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}, [open]);
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
// src/react/copilot/CopilotPlugin.tsx
|
|
724
|
+
import { serializeMd as serializeMd2 } from "@platejs/markdown";
|
|
725
|
+
import debounce from "lodash/debounce.js";
|
|
726
|
+
import {
|
|
727
|
+
bindFirst as bindFirst3,
|
|
728
|
+
KEYS as KEYS16,
|
|
729
|
+
NodeApi as NodeApi2
|
|
730
|
+
} from "platejs";
|
|
731
|
+
import { createTPlatePlugin as createTPlatePlugin2 } from "platejs/react";
|
|
732
|
+
|
|
733
|
+
// src/react/copilot/renderCopilotBelowNodes.tsx
|
|
734
|
+
import React from "react";
|
|
735
|
+
import { KEYS as KEYS12 } from "platejs";
|
|
736
|
+
import { getEditorPlugin as getEditorPlugin4 } from "platejs/react";
|
|
737
|
+
var renderCopilotBelowNodes = ({
|
|
738
|
+
editor
|
|
739
|
+
}) => {
|
|
740
|
+
const copilot = getEditorPlugin4(editor, {
|
|
741
|
+
key: KEYS12.copilot
|
|
742
|
+
});
|
|
743
|
+
const { renderGhostText: GhostText } = copilot.getOptions();
|
|
744
|
+
if (!GhostText) return;
|
|
745
|
+
return ({ children }) => {
|
|
746
|
+
return /* @__PURE__ */ React.createElement(React.Fragment, null, children, /* @__PURE__ */ React.createElement(GhostText, null));
|
|
747
|
+
};
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
// src/react/copilot/transforms/acceptCopilot.ts
|
|
751
|
+
import { deserializeInlineMd } from "@platejs/markdown";
|
|
752
|
+
import { KEYS as KEYS13 } from "platejs";
|
|
753
|
+
var acceptCopilot = (editor) => {
|
|
754
|
+
const { suggestionText } = editor.getOptions({
|
|
755
|
+
key: KEYS13.copilot
|
|
756
|
+
});
|
|
757
|
+
if (!suggestionText?.length) return false;
|
|
758
|
+
editor.tf.insertFragment(deserializeInlineMd(editor, suggestionText));
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
// src/react/copilot/transforms/acceptCopilotNextWord.ts
|
|
762
|
+
import { deserializeInlineMd as deserializeInlineMd2 } from "@platejs/markdown";
|
|
763
|
+
import { KEYS as KEYS15 } from "platejs";
|
|
764
|
+
import { getEditorPlugin as getEditorPlugin6 } from "platejs/react";
|
|
765
|
+
|
|
766
|
+
// src/react/copilot/utils/callCompletionApi.ts
|
|
767
|
+
var getOriginalFetch = () => fetch;
|
|
768
|
+
async function callCompletionApi({
|
|
769
|
+
api = "/api/completion",
|
|
770
|
+
body,
|
|
771
|
+
credentials,
|
|
772
|
+
fetch: fetch2 = getOriginalFetch(),
|
|
773
|
+
headers,
|
|
774
|
+
prompt,
|
|
775
|
+
setAbortController = () => {
|
|
776
|
+
},
|
|
777
|
+
setCompletion = () => {
|
|
778
|
+
},
|
|
779
|
+
setError = () => {
|
|
780
|
+
},
|
|
781
|
+
setLoading = () => {
|
|
782
|
+
},
|
|
783
|
+
onError,
|
|
784
|
+
onFinish,
|
|
785
|
+
onResponse
|
|
786
|
+
}) {
|
|
787
|
+
try {
|
|
788
|
+
setLoading(true);
|
|
789
|
+
setError(null);
|
|
790
|
+
const abortController = new AbortController();
|
|
791
|
+
setAbortController(abortController);
|
|
792
|
+
setCompletion("");
|
|
793
|
+
const res = await fetch2(api, {
|
|
794
|
+
body: JSON.stringify({
|
|
795
|
+
prompt,
|
|
796
|
+
...body
|
|
797
|
+
}),
|
|
798
|
+
credentials,
|
|
799
|
+
headers: {
|
|
800
|
+
"Content-Type": "application/json",
|
|
801
|
+
...headers
|
|
802
|
+
},
|
|
803
|
+
method: "POST",
|
|
804
|
+
signal: abortController.signal
|
|
805
|
+
}).catch((error) => {
|
|
806
|
+
throw error;
|
|
807
|
+
});
|
|
808
|
+
if (onResponse) {
|
|
809
|
+
await onResponse(res);
|
|
810
|
+
}
|
|
811
|
+
if (!res.ok) {
|
|
812
|
+
throw new Error(
|
|
813
|
+
await res.text() || "Failed to fetch the chat response."
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
if (!res.body) {
|
|
817
|
+
throw new Error("The response body is empty.");
|
|
818
|
+
}
|
|
819
|
+
const { text } = await res.json();
|
|
820
|
+
if (!text) {
|
|
821
|
+
throw new Error("The response does not contain a text field.");
|
|
822
|
+
}
|
|
823
|
+
setCompletion(text);
|
|
824
|
+
if (onFinish) {
|
|
825
|
+
onFinish(prompt, text);
|
|
826
|
+
}
|
|
827
|
+
setAbortController(null);
|
|
828
|
+
return text;
|
|
829
|
+
} catch (error) {
|
|
830
|
+
if (error.name === "AbortError") {
|
|
831
|
+
setAbortController(null);
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
if (error instanceof Error && onError) {
|
|
835
|
+
onError(error);
|
|
836
|
+
}
|
|
837
|
+
setError(error);
|
|
838
|
+
} finally {
|
|
839
|
+
setLoading(false);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// src/react/copilot/utils/getNextWord.ts
|
|
844
|
+
var getNextWord = ({ text }) => {
|
|
845
|
+
if (!text) return { firstWord: "", remainingText: "" };
|
|
846
|
+
const nonSpaceMatch = /^\s*(\S)/.exec(text);
|
|
847
|
+
if (!nonSpaceMatch) return { firstWord: "", remainingText: "" };
|
|
848
|
+
const firstNonSpaceChar = nonSpaceMatch[1];
|
|
849
|
+
const isCJKChar = /[\u1100-\u11FF\u3040-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF]/.test(
|
|
850
|
+
firstNonSpaceChar
|
|
851
|
+
);
|
|
852
|
+
let firstWord, remainingText;
|
|
853
|
+
if (isCJKChar) {
|
|
854
|
+
const match = /^(\s*)([\u1100-\u11FF\u3040-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF])([\u3000-\u303F\uFF00-\uFFEF])?/.exec(
|
|
855
|
+
text
|
|
856
|
+
);
|
|
857
|
+
if (match) {
|
|
858
|
+
const [fullMatch, spaces = "", char = "", punctuation = ""] = match;
|
|
859
|
+
firstWord = spaces + char + punctuation;
|
|
860
|
+
remainingText = text.slice(firstWord.length);
|
|
861
|
+
} else {
|
|
862
|
+
firstWord = "";
|
|
863
|
+
remainingText = text;
|
|
864
|
+
}
|
|
865
|
+
} else {
|
|
866
|
+
const match = /^(\s*\S+?)(?=[\s\u1100-\u11FF\u3040-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF]|$)/.exec(
|
|
867
|
+
text
|
|
868
|
+
);
|
|
869
|
+
if (match) {
|
|
870
|
+
firstWord = match[0];
|
|
871
|
+
remainingText = text.slice(firstWord.length);
|
|
872
|
+
} else {
|
|
873
|
+
firstWord = text;
|
|
874
|
+
remainingText = "";
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return { firstWord, remainingText };
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
// src/react/copilot/utils/triggerCopilotSuggestion.ts
|
|
881
|
+
import { KEYS as KEYS14 } from "platejs";
|
|
882
|
+
import { getEditorPlugin as getEditorPlugin5 } from "platejs/react";
|
|
883
|
+
var triggerCopilotSuggestion = async (editor) => {
|
|
884
|
+
const { api, getOptions, setOption } = getEditorPlugin5(
|
|
885
|
+
editor,
|
|
886
|
+
{
|
|
887
|
+
key: KEYS14.copilot
|
|
888
|
+
}
|
|
889
|
+
);
|
|
890
|
+
const { completeOptions, getPrompt, isLoading, triggerQuery } = getOptions();
|
|
891
|
+
if (isLoading || editor.getOptions({ key: KEYS14.aiChat }).chat?.isLoading) {
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
if (!triggerQuery({ editor })) return false;
|
|
895
|
+
const prompt = getPrompt({ editor });
|
|
896
|
+
if (prompt.length === 0) return false;
|
|
897
|
+
api.copilot.stop();
|
|
898
|
+
await callCompletionApi({
|
|
899
|
+
prompt,
|
|
900
|
+
onFinish: (_, completion) => {
|
|
901
|
+
api.copilot.setBlockSuggestion({ text: completion });
|
|
902
|
+
},
|
|
903
|
+
...completeOptions,
|
|
904
|
+
setAbortController: (controller) => setOption("abortController", controller),
|
|
905
|
+
setCompletion: (completion) => setOption("completion", completion),
|
|
906
|
+
setError: (error) => setOption("error", error),
|
|
907
|
+
setLoading: (loading) => setOption("isLoading", loading),
|
|
908
|
+
onError: (error) => {
|
|
909
|
+
setOption("error", error);
|
|
910
|
+
completeOptions?.onError?.(error);
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
// src/react/copilot/utils/withoutAbort.ts
|
|
916
|
+
var withoutAbort = (editor, fn) => {
|
|
917
|
+
editor.setOption(CopilotPlugin, "shouldAbort", false);
|
|
918
|
+
fn();
|
|
919
|
+
editor.setOption(CopilotPlugin, "shouldAbort", true);
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
// src/react/copilot/transforms/acceptCopilotNextWord.ts
|
|
923
|
+
var acceptCopilotNextWord = (editor) => {
|
|
924
|
+
const { api, getOptions } = getEditorPlugin6(editor, {
|
|
925
|
+
key: KEYS15.copilot
|
|
926
|
+
});
|
|
927
|
+
const { getNextWord: getNextWord2, suggestionText } = getOptions();
|
|
928
|
+
if (!suggestionText?.length) {
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
const { firstWord, remainingText } = getNextWord2({ text: suggestionText });
|
|
932
|
+
api.copilot.setBlockSuggestion({
|
|
933
|
+
text: remainingText
|
|
934
|
+
});
|
|
935
|
+
withoutAbort(editor, () => {
|
|
936
|
+
editor.tf.insertFragment(deserializeInlineMd2(editor, firstWord));
|
|
937
|
+
});
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
// src/react/copilot/withCopilot.ts
|
|
941
|
+
import { serializeInlineMd } from "@platejs/markdown";
|
|
942
|
+
import {
|
|
943
|
+
RangeApi as RangeApi2
|
|
944
|
+
} from "platejs";
|
|
945
|
+
var getPatchString = (editor, operations) => {
|
|
946
|
+
let string = "";
|
|
947
|
+
for (const operation of operations) {
|
|
948
|
+
if (operation.type === "insert_node") {
|
|
949
|
+
const node = operation.node;
|
|
950
|
+
const text = serializeInlineMd(editor, { value: [node] });
|
|
951
|
+
string += text;
|
|
952
|
+
} else if (operation.type === "insert_text") {
|
|
953
|
+
string += operation.text;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return string;
|
|
957
|
+
};
|
|
958
|
+
var withCopilot = ({
|
|
959
|
+
api,
|
|
960
|
+
editor,
|
|
961
|
+
getOptions,
|
|
962
|
+
setOption,
|
|
963
|
+
tf: { apply, insertText, redo, setSelection, undo, writeHistory }
|
|
964
|
+
}) => {
|
|
965
|
+
let prevSelection = null;
|
|
966
|
+
return {
|
|
967
|
+
transforms: {
|
|
968
|
+
apply(operation) {
|
|
969
|
+
const { shouldAbort } = getOptions();
|
|
970
|
+
if (shouldAbort) {
|
|
971
|
+
api.copilot.reject();
|
|
972
|
+
}
|
|
973
|
+
apply(operation);
|
|
974
|
+
},
|
|
975
|
+
insertText(text, options) {
|
|
976
|
+
const suggestionText = getOptions().suggestionText;
|
|
977
|
+
if (suggestionText?.startsWith(text)) {
|
|
978
|
+
withoutAbort(editor, () => {
|
|
979
|
+
editor.tf.withoutMerging(() => {
|
|
980
|
+
const newText = suggestionText?.slice(text.length);
|
|
981
|
+
setOption("suggestionText", newText);
|
|
982
|
+
insertText(text);
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
insertText(text, options);
|
|
988
|
+
},
|
|
989
|
+
redo() {
|
|
990
|
+
if (!getOptions().suggestionText) return redo();
|
|
991
|
+
const topRedo = editor.history.redos.at(-1);
|
|
992
|
+
const prevSuggestion = getOptions().suggestionText;
|
|
993
|
+
if (topRedo && topRedo.shouldAbort === false && prevSuggestion) {
|
|
994
|
+
withoutAbort(editor, () => {
|
|
995
|
+
const shouldRemoveText = getPatchString(editor, topRedo.operations);
|
|
996
|
+
const newText = prevSuggestion.slice(shouldRemoveText.length);
|
|
997
|
+
setOption("suggestionText", newText);
|
|
998
|
+
redo();
|
|
999
|
+
});
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
return redo();
|
|
1003
|
+
},
|
|
1004
|
+
setSelection(props) {
|
|
1005
|
+
setSelection(props);
|
|
1006
|
+
if (editor.selection && (!prevSelection || !RangeApi2.equals(prevSelection, editor.selection)) && getOptions().autoTriggerQuery({ editor }) && editor.api.isFocused()) {
|
|
1007
|
+
void api.copilot.triggerSuggestion();
|
|
1008
|
+
}
|
|
1009
|
+
prevSelection = editor.selection;
|
|
1010
|
+
},
|
|
1011
|
+
undo() {
|
|
1012
|
+
if (!getOptions().suggestionText) return undo();
|
|
1013
|
+
const lastUndos = editor.history.undos.at(-1);
|
|
1014
|
+
const oldText = getOptions().suggestionText;
|
|
1015
|
+
if (lastUndos && lastUndos.shouldAbort === false && oldText) {
|
|
1016
|
+
withoutAbort(editor, () => {
|
|
1017
|
+
const shouldInsertText = getPatchString(
|
|
1018
|
+
editor,
|
|
1019
|
+
lastUndos.operations
|
|
1020
|
+
);
|
|
1021
|
+
const newText = shouldInsertText + oldText;
|
|
1022
|
+
setOption("suggestionText", newText);
|
|
1023
|
+
undo();
|
|
1024
|
+
});
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
return undo();
|
|
1028
|
+
},
|
|
1029
|
+
writeHistory(stacks, batch) {
|
|
1030
|
+
if (!getOptions().isLoading) {
|
|
1031
|
+
batch.shouldAbort = getOptions().shouldAbort;
|
|
1032
|
+
}
|
|
1033
|
+
return writeHistory(stacks, batch);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
// src/react/copilot/CopilotPlugin.tsx
|
|
1040
|
+
var CopilotPlugin = createTPlatePlugin2({
|
|
1041
|
+
key: KEYS16.copilot,
|
|
1042
|
+
handlers: {
|
|
1043
|
+
onBlur: ({ api }) => {
|
|
1044
|
+
api.copilot.reject();
|
|
1045
|
+
},
|
|
1046
|
+
onMouseDown: ({ api }) => {
|
|
1047
|
+
api.copilot.reject();
|
|
1048
|
+
}
|
|
1049
|
+
},
|
|
1050
|
+
options: {
|
|
1051
|
+
abortController: null,
|
|
1052
|
+
completeOptions: {},
|
|
1053
|
+
completion: "",
|
|
1054
|
+
debounceDelay: 0,
|
|
1055
|
+
error: null,
|
|
1056
|
+
getNextWord,
|
|
1057
|
+
isLoading: false,
|
|
1058
|
+
renderGhostText: null,
|
|
1059
|
+
shouldAbort: true,
|
|
1060
|
+
suggestionNodeId: null,
|
|
1061
|
+
suggestionText: null,
|
|
1062
|
+
autoTriggerQuery: ({ editor }) => {
|
|
1063
|
+
if (editor.getOptions({ key: KEYS16.copilot }).suggestionText) {
|
|
1064
|
+
return false;
|
|
1065
|
+
}
|
|
1066
|
+
const isEmpty = editor.api.isEmpty(editor.selection, { block: true });
|
|
1067
|
+
if (isEmpty) return false;
|
|
1068
|
+
const blockAbove = editor.api.block();
|
|
1069
|
+
if (!blockAbove) return false;
|
|
1070
|
+
const blockString = NodeApi2.string(blockAbove[0]);
|
|
1071
|
+
return blockString.at(-1) === " ";
|
|
1072
|
+
},
|
|
1073
|
+
getPrompt: ({ editor }) => {
|
|
1074
|
+
const contextEntry = editor.api.block({ highest: true });
|
|
1075
|
+
if (!contextEntry) return "";
|
|
1076
|
+
return serializeMd2(editor, {
|
|
1077
|
+
value: [contextEntry[0]]
|
|
1078
|
+
});
|
|
1079
|
+
},
|
|
1080
|
+
triggerQuery: ({ editor }) => {
|
|
1081
|
+
if (editor.api.isExpanded()) return false;
|
|
1082
|
+
const isEnd = editor.api.isAt({ end: true });
|
|
1083
|
+
if (!isEnd) return false;
|
|
1084
|
+
return true;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}).overrideEditor(withCopilot).extendSelectors(({ getOptions }) => ({
|
|
1088
|
+
isSuggested: (id) => getOptions().suggestionNodeId === id
|
|
1089
|
+
})).extendTransforms(({ editor }) => ({
|
|
1090
|
+
accept: bindFirst3(acceptCopilot, editor),
|
|
1091
|
+
acceptNextWord: bindFirst3(acceptCopilotNextWord, editor)
|
|
1092
|
+
})).extendApi(
|
|
1093
|
+
({ api, editor, getOptions, setOption, setOptions }) => {
|
|
1094
|
+
const debounceDelay = getOptions().debounceDelay;
|
|
1095
|
+
let triggerSuggestion = bindFirst3(triggerCopilotSuggestion, editor);
|
|
1096
|
+
if (debounceDelay) {
|
|
1097
|
+
triggerSuggestion = debounce(
|
|
1098
|
+
bindFirst3(triggerCopilotSuggestion, editor),
|
|
1099
|
+
debounceDelay
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
return {
|
|
1103
|
+
triggerSuggestion,
|
|
1104
|
+
setBlockSuggestion: ({ id = getOptions().suggestionNodeId, text }) => {
|
|
1105
|
+
if (!id) {
|
|
1106
|
+
id = editor.api.block()[0].id;
|
|
1107
|
+
}
|
|
1108
|
+
setOptions({
|
|
1109
|
+
suggestionNodeId: id,
|
|
1110
|
+
suggestionText: text
|
|
1111
|
+
});
|
|
1112
|
+
},
|
|
1113
|
+
stop: () => {
|
|
1114
|
+
const { abortController } = getOptions();
|
|
1115
|
+
api.copilot.triggerSuggestion?.cancel();
|
|
1116
|
+
if (abortController) {
|
|
1117
|
+
abortController.abort();
|
|
1118
|
+
setOption("abortController", null);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
).extendApi(({ api, getOptions, setOptions }) => ({
|
|
1124
|
+
reject: () => {
|
|
1125
|
+
if (!getOptions().suggestionText?.length) return false;
|
|
1126
|
+
api.copilot.stop();
|
|
1127
|
+
setOptions({
|
|
1128
|
+
completion: null,
|
|
1129
|
+
suggestionNodeId: null,
|
|
1130
|
+
suggestionText: null
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
})).extend({
|
|
1134
|
+
render: {
|
|
1135
|
+
belowNodes: renderCopilotBelowNodes
|
|
1136
|
+
},
|
|
1137
|
+
shortcuts: {
|
|
1138
|
+
accept: {
|
|
1139
|
+
keys: "tab"
|
|
1140
|
+
},
|
|
1141
|
+
reject: {
|
|
1142
|
+
keys: "escape"
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
export {
|
|
1147
|
+
AIChatPlugin,
|
|
1148
|
+
AIPlugin,
|
|
1149
|
+
CopilotPlugin,
|
|
1150
|
+
acceptAIChat,
|
|
1151
|
+
acceptCopilot,
|
|
1152
|
+
acceptCopilotNextWord,
|
|
1153
|
+
callCompletionApi,
|
|
1154
|
+
createFormattedBlocks,
|
|
1155
|
+
getEditorPrompt,
|
|
1156
|
+
getLastAssistantMessage,
|
|
1157
|
+
getMarkdown,
|
|
1158
|
+
getNextWord,
|
|
1159
|
+
insertBelowAIChat,
|
|
1160
|
+
removeAnchorAIChat,
|
|
1161
|
+
renderCopilotBelowNodes,
|
|
1162
|
+
replaceSelectionAIChat,
|
|
1163
|
+
resetAIChat,
|
|
1164
|
+
submitAIChat,
|
|
1165
|
+
triggerCopilotSuggestion,
|
|
1166
|
+
useAIChatEditor,
|
|
1167
|
+
useChatChunk,
|
|
1168
|
+
useEditorChat,
|
|
1169
|
+
useLastAssistantMessage,
|
|
1170
|
+
withAIChat,
|
|
1171
|
+
withCopilot,
|
|
1172
|
+
withoutAbort
|
|
1173
|
+
};
|
|
1174
|
+
//# sourceMappingURL=index.mjs.map
|