@lofcz/platejs-ai 52.3.2

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.
@@ -0,0 +1,1409 @@
1
+ import { n as BaseAIPlugin, r as withAIBatch, t as getEditorPrompt } from "../getEditorPrompt-BnVrgUl3.js";
2
+ import { ElementApi, KEYS, NodeApi, PathApi, RangeApi, TextApi, bindFirst, getPluginKey, getPluginType, isDefined, nanoid } from "platejs";
3
+ import { SkipSuggestionDeletes, acceptSuggestion, diffToSuggestions, getSuggestionKey, getTransientSuggestionKey, rejectSuggestion } from "@platejs/suggestion";
4
+ import { isSelecting } from "@platejs/selection";
5
+ import { MarkdownPlugin, deserializeInlineMd, deserializeMd, serializeInlineMd, serializeMd } from "@platejs/markdown";
6
+ import { createTPlatePlugin, getEditorPlugin, toPlatePlugin, useEditorPlugin, usePluginOption } from "platejs/react";
7
+ import { distance } from "fastest-levenshtein";
8
+ import { BlockSelectionPlugin, removeBlockSelectionNodes } from "@platejs/selection/react";
9
+ import { SuggestionPlugin } from "@platejs/suggestion/react";
10
+ import cloneDeep from "lodash/cloneDeep.js";
11
+ import { c } from "react-compiler-runtime";
12
+ import React, { useEffect, useMemo, useRef } from "react";
13
+ import debounce from "lodash/debounce.js";
14
+
15
+ //#region src/react/ai/AIPlugin.ts
16
+ const AIPlugin = toPlatePlugin(BaseAIPlugin);
17
+
18
+ //#endregion
19
+ //#region src/react/ai/utils/findTextRangeInBlock.tsx
20
+ function maxAllowedDistance(len) {
21
+ if (len <= 2) return 0;
22
+ if (len <= 5) return 1;
23
+ if (len <= 10) return 2;
24
+ if (len <= 20) return 3;
25
+ return 5;
26
+ }
27
+ /** Find text within a block, supporting fuzzy matching. */
28
+ function findTextRangeInBlock({ block, findText }) {
29
+ const [blockNode, blockPath] = block;
30
+ const textSegments = [];
31
+ let fullText = "";
32
+ for (const [textNode, textPath] of NodeApi.texts(blockNode)) {
33
+ const startOffset = fullText.length;
34
+ const absolutePath = [...blockPath, ...textPath];
35
+ textSegments.push({
36
+ offset: startOffset,
37
+ path: absolutePath,
38
+ text: textNode.text
39
+ });
40
+ fullText += textNode.text;
41
+ }
42
+ if (!fullText) return null;
43
+ let matchStart = fullText.indexOf(findText);
44
+ let matchEnd = matchStart >= 0 ? matchStart + findText.length : -1;
45
+ if (matchStart === -1) {
46
+ const maxDist = maxAllowedDistance(findText.length);
47
+ let bestMatch = {
48
+ distance: Number.POSITIVE_INFINITY,
49
+ end: -1,
50
+ start: -1
51
+ };
52
+ for (let i = 0; i <= fullText.length - findText.length; i++) for (let lenOffset = -maxDist; lenOffset <= maxDist; lenOffset++) {
53
+ const len = findText.length + lenOffset;
54
+ if (len <= 0 || i + len > fullText.length) continue;
55
+ const dist = distance(fullText.slice(i, i + len), findText);
56
+ if (dist <= maxDist && dist < bestMatch.distance) bestMatch = {
57
+ distance: dist,
58
+ end: i + len,
59
+ start: i
60
+ };
61
+ }
62
+ if (bestMatch.start !== -1) {
63
+ matchStart = bestMatch.start;
64
+ matchEnd = bestMatch.end;
65
+ }
66
+ }
67
+ if (matchStart === -1) for (let prefixLen = findText.length - 1; prefixLen > 0; prefixLen--) {
68
+ const prefix = findText.slice(0, Math.max(0, prefixLen));
69
+ const idx = fullText.indexOf(prefix);
70
+ if (idx !== -1) {
71
+ matchStart = idx;
72
+ matchEnd = idx + prefixLen;
73
+ break;
74
+ }
75
+ }
76
+ if (matchStart === -1) return null;
77
+ const findPoint = (charOffset, isEnd = false) => {
78
+ if (!isEnd) {
79
+ for (const segment of textSegments) if (charOffset === segment.offset) return {
80
+ offset: 0,
81
+ path: segment.path
82
+ };
83
+ }
84
+ for (const segment of textSegments) {
85
+ const segmentEnd = segment.offset + segment.text.length;
86
+ if (charOffset >= segment.offset && charOffset <= segmentEnd) return {
87
+ offset: charOffset - segment.offset,
88
+ path: segment.path
89
+ };
90
+ }
91
+ const lastSegment = textSegments.at(-1);
92
+ if (!lastSegment) return {
93
+ offset: 0,
94
+ path: blockPath
95
+ };
96
+ return {
97
+ offset: lastSegment.text.length,
98
+ path: lastSegment.path
99
+ };
100
+ };
101
+ return {
102
+ anchor: findPoint(matchStart, false),
103
+ focus: findPoint(matchEnd, true)
104
+ };
105
+ }
106
+
107
+ //#endregion
108
+ //#region src/react/ai/utils/aiCommentToRange.ts
109
+ const aiCommentToRange = (editor, aiComment) => {
110
+ const { blockId, content } = aiComment;
111
+ const contentNodes = deserializeMd(editor, content);
112
+ let firstBlock;
113
+ const ranges = [];
114
+ contentNodes.forEach((node, index) => {
115
+ let currentBlock;
116
+ if (index === 0) {
117
+ firstBlock = editor.api.node({
118
+ id: blockId,
119
+ at: []
120
+ });
121
+ currentBlock = firstBlock;
122
+ } else {
123
+ if (!firstBlock) return;
124
+ const [_, firstBlockPath] = firstBlock;
125
+ const blockPath = [firstBlockPath[0] + index];
126
+ currentBlock = editor.api.node(blockPath);
127
+ }
128
+ if (!currentBlock) return;
129
+ const range = findTextRangeInBlock({
130
+ block: currentBlock,
131
+ findText: NodeApi.string(node)
132
+ });
133
+ if (!range) return;
134
+ ranges.push(range);
135
+ });
136
+ if (ranges.length === 0) return;
137
+ if (ranges.length > 1) {
138
+ const startRange = ranges[0];
139
+ const endRange = ranges.at(-1);
140
+ return {
141
+ anchor: startRange.anchor,
142
+ focus: endRange.focus
143
+ };
144
+ }
145
+ if (ranges.length === 1) return ranges[0];
146
+ };
147
+
148
+ //#endregion
149
+ //#region src/react/ai-chat/utils/acceptAISuggestions.ts
150
+ const acceptAISuggestions = (editor) => {
151
+ editor.getApi(SuggestionPlugin).suggestion.nodes({ transient: true }).forEach(([suggestionNode]) => {
152
+ const suggestionData = editor.getApi(SuggestionPlugin).suggestion.suggestionData(suggestionNode);
153
+ if (!suggestionData) return;
154
+ acceptSuggestion(editor, {
155
+ createdAt: new Date(suggestionData.createdAt),
156
+ keyId: getSuggestionKey(suggestionData.id),
157
+ suggestionId: suggestionData.id,
158
+ type: suggestionData.type,
159
+ userId: suggestionData.userId
160
+ });
161
+ });
162
+ editor.tf.unsetNodes([getTransientSuggestionKey()], {
163
+ at: [],
164
+ mode: "all",
165
+ match: (n) => !!n[getTransientSuggestionKey()]
166
+ });
167
+ };
168
+
169
+ //#endregion
170
+ //#region src/react/ai-chat/transforms/acceptAIChat.ts
171
+ const acceptAIChat = (editor) => {
172
+ const mode = editor.getOption(AIChatPlugin, "mode");
173
+ if (mode === "insert") {
174
+ const { tf } = getEditorPlugin(editor, AIPlugin);
175
+ const api = editor.getApi({ key: KEYS.ai });
176
+ const lastAINodePath = api.aiChat.node({
177
+ at: [],
178
+ reverse: true
179
+ })[1];
180
+ withAIBatch(editor, () => {
181
+ tf.ai.removeMarks();
182
+ editor.getTransforms(AIChatPlugin).aiChat.removeAnchor();
183
+ });
184
+ api.aiChat.hide();
185
+ editor.tf.focus();
186
+ const focusPoint = editor.api.end(lastAINodePath);
187
+ editor.tf.setSelection({
188
+ anchor: focusPoint,
189
+ focus: focusPoint
190
+ });
191
+ }
192
+ if (mode === "chat") {
193
+ withAIBatch(editor, () => {
194
+ acceptAISuggestions(editor);
195
+ });
196
+ editor.getApi(AIChatPlugin).aiChat.hide();
197
+ }
198
+ };
199
+
200
+ //#endregion
201
+ //#region src/react/ai-chat/utils/nestedContainerUtils.ts
202
+ /** Check if nodes is a single table with single cell */
203
+ const isSingleCellTable = (nodes) => {
204
+ if (nodes.length !== 1) return false;
205
+ const table = nodes[0];
206
+ if (table.type !== KEYS.table) return false;
207
+ const rows = table.children;
208
+ if (rows.length !== 1) return false;
209
+ const row = rows[0];
210
+ if (row.type !== KEYS.tr) return false;
211
+ const cells = row.children;
212
+ if (cells.length !== 1) return false;
213
+ return cells[0].type === KEYS.td;
214
+ };
215
+ /** Extract td children from single-cell table */
216
+ const getTableCellChildren = (table) => {
217
+ return table.children[0].children[0].children;
218
+ };
219
+
220
+ //#endregion
221
+ //#region src/react/ai-chat/utils/applyAISuggestions.ts
222
+ const applyAISuggestions = (editor, content) => {
223
+ /** Conflict with block selection */
224
+ editor.getApi({ key: KEYS.cursorOverlay })?.cursorOverlay?.removeCursor("selection");
225
+ const { chatNodes } = editor.getOptions(AIChatPlugin);
226
+ if (chatNodes.length > 1) {
227
+ const setReplaceIds = (ids) => {
228
+ editor.setOption(AIChatPlugin, "_replaceIds", ids);
229
+ };
230
+ if (editor.getOption(AIChatPlugin, "_replaceIds").length === 0) setReplaceIds(chatNodes.map((node) => node.id));
231
+ const diffNodes = getDiffNodes(editor, content);
232
+ const replaceNodes = Array.from(editor.api.nodes({
233
+ at: [],
234
+ match: (n) => ElementApi.isElement(n) && editor.getOption(AIChatPlugin, "_replaceIds").includes(n.id)
235
+ }));
236
+ replaceNodes.forEach(([node, path], index) => {
237
+ const diffNode = diffNodes[index];
238
+ if (!diffNode) return;
239
+ if (index === replaceNodes.length - 1 && diffNodes.length > replaceNodes.length) {
240
+ editor.tf.replaceNodes(diffNodes.slice(index), { at: path });
241
+ return;
242
+ }
243
+ const replaceNode = node;
244
+ const diffSuggestion = diffNode;
245
+ const isSameString = SkipSuggestionDeletes(editor, replaceNode) === SkipSuggestionDeletes(editor, diffSuggestion);
246
+ const isSameSuggestion = replaceNode.suggestion?.type === diffSuggestion.suggestion?.type;
247
+ if (isSameString && isSameSuggestion && node.id === diffNode.id) return;
248
+ editor.tf.replaceNodes(diffNode, { at: path });
249
+ });
250
+ editor.getApi(BlockSelectionPlugin).blockSelection.set(diffNodes.map((node) => node.id));
251
+ setReplaceIds(diffNodes.map((node) => node.id));
252
+ } else {
253
+ const diffNodes = getDiffNodes(editor, content);
254
+ editor.tf.insertFragment(diffNodes);
255
+ const nodes = Array.from(editor.api.nodes({
256
+ at: [],
257
+ mode: "lowest",
258
+ match: (n) => TextApi.isText(n) && !!n[getTransientSuggestionKey()]
259
+ }));
260
+ const range = editor.api.nodesRange(nodes);
261
+ editor.tf.setSelection(range);
262
+ return;
263
+ }
264
+ };
265
+ const withProps = (diffNodes, chatNodes) => diffNodes.map((node, index) => {
266
+ if (!ElementApi.isElement(node)) return node;
267
+ const originalNode = chatNodes?.[index];
268
+ return {
269
+ ...node,
270
+ ...originalNode ?? { id: nanoid() },
271
+ children: node.children
272
+ };
273
+ });
274
+ const withTransient = (diffNodes) => diffNodes.map((node) => {
275
+ if (TextApi.isText(node)) return {
276
+ ...node,
277
+ [getTransientSuggestionKey()]: true
278
+ };
279
+ return {
280
+ ...node,
281
+ children: withTransient(node.children),
282
+ [getTransientSuggestionKey()]: true
283
+ };
284
+ });
285
+ const withoutSuggestionAndComments = (nodes) => nodes.map((node) => {
286
+ if (TextApi.isText(node)) {
287
+ if (node[KEYS.suggestion] || node[KEYS.comment]) return { text: node.text };
288
+ return node;
289
+ }
290
+ if (ElementApi.isElement(node)) {
291
+ if (node[KEYS.suggestion]) {
292
+ const nodeWithoutSuggestion = {};
293
+ Object.keys(node).forEach((key) => {
294
+ if (key !== KEYS.suggestion && !key.startsWith(KEYS.suggestion)) nodeWithoutSuggestion[key] = node[key];
295
+ });
296
+ return {
297
+ ...nodeWithoutSuggestion,
298
+ children: withoutSuggestionAndComments(node.children)
299
+ };
300
+ }
301
+ return {
302
+ ...node,
303
+ children: withoutSuggestionAndComments(node.children)
304
+ };
305
+ }
306
+ return node;
307
+ });
308
+ const getDiffNodes = (editor, aiContent) => {
309
+ let chatNodes = withoutSuggestionAndComments(editor.getOption(AIChatPlugin, "chatNodes"));
310
+ /**If selecting one single cell table, we just need to compare it's children to get diff nodes */
311
+ if (isSingleCellTable(chatNodes)) chatNodes = getTableCellChildren(chatNodes[0]);
312
+ const aiNodes = withProps(deserializeMd(editor, aiContent), chatNodes);
313
+ return withTransient(diffToSuggestions(editor, chatNodes, aiNodes, { ignoreProps: ["id", "listStart"] }));
314
+ };
315
+
316
+ //#endregion
317
+ //#region src/react/ai-chat/utils/applyTableCellSuggestion.ts
318
+ /**
319
+ * Apply AI-generated content to a table cell as diff suggestions. Finds the
320
+ * cell by ID, deserializes the markdown content, computes diff, and replaces
321
+ * the cell's children with suggestion-marked nodes.
322
+ */
323
+ const applyTableCellSuggestion = (editor, cellUpdate) => {
324
+ const { content, id } = cellUpdate;
325
+ const cellEntry = editor.api.node({
326
+ at: [],
327
+ match: (n) => ElementApi.isElement(n) && n.id === id
328
+ });
329
+ if (!cellEntry) {
330
+ console.warn(`Table cell with id "${id}" not found`);
331
+ return;
332
+ }
333
+ const [cell, cellPath] = cellEntry;
334
+ const transientDiffNodes = withTransient(diffToSuggestions(editor, withoutSuggestionAndComments(cell.children), deserializeMd(editor, content), { ignoreProps: ["id"] }));
335
+ editor.tf.replaceNodes(transientDiffNodes, {
336
+ at: cellPath,
337
+ children: true
338
+ });
339
+ };
340
+
341
+ //#endregion
342
+ //#region src/react/ai-chat/utils/getLastAssistantMessage.ts
343
+ function getLastAssistantMessage(editor) {
344
+ return editor.getOptions(AIChatPlugin).chat.messages?.findLast((message) => message.role === "assistant");
345
+ }
346
+ function useLastAssistantMessage() {
347
+ const $ = c(2);
348
+ const toolName = usePluginOption(AIChatPlugin, "toolName");
349
+ const chat = usePluginOption(AIChatPlugin, "chat");
350
+ if (toolName === "comment") return;
351
+ let t0;
352
+ if ($[0] !== chat.messages) {
353
+ t0 = chat.messages?.findLast(_temp);
354
+ $[0] = chat.messages;
355
+ $[1] = t0;
356
+ } else t0 = $[1];
357
+ return t0;
358
+ }
359
+ function _temp(message) {
360
+ return message.role === "assistant";
361
+ }
362
+
363
+ //#endregion
364
+ //#region src/react/ai-chat/utils/rejectAISuggestions.ts
365
+ const rejectAISuggestions = (editor) => {
366
+ editor.getApi(SuggestionPlugin).suggestion.nodes({ transient: true }).forEach(([suggestionNode]) => {
367
+ const suggestionData = editor.getApi(SuggestionPlugin).suggestion.suggestionData(suggestionNode);
368
+ if (!suggestionData) return;
369
+ rejectSuggestion(editor, {
370
+ createdAt: new Date(suggestionData.createdAt),
371
+ keyId: getSuggestionKey(suggestionData.id),
372
+ suggestionId: suggestionData.id,
373
+ type: suggestionData.type,
374
+ userId: suggestionData.userId
375
+ });
376
+ });
377
+ editor.tf.unsetNodes([getTransientSuggestionKey()], {
378
+ at: [],
379
+ mode: "all",
380
+ match: (n) => !!n[getTransientSuggestionKey()]
381
+ });
382
+ };
383
+
384
+ //#endregion
385
+ //#region src/react/ai-chat/utils/resetAIChat.ts
386
+ const resetAIChat = (editor, { undo = true } = {}) => {
387
+ const { api, getOptions, setOptions } = getEditorPlugin(editor, { key: KEYS.aiChat });
388
+ api.aiChat.stop();
389
+ const chat = getOptions().chat;
390
+ if (chat.messages && chat.messages.length > 0) chat.setMessages?.([]);
391
+ setOptions({
392
+ _replaceIds: [],
393
+ chatNodes: [],
394
+ mode: "insert",
395
+ toolName: null
396
+ });
397
+ if (undo) editor.getTransforms(AIPlugin).ai.undo();
398
+ };
399
+
400
+ //#endregion
401
+ //#region src/react/ai-chat/utils/submitAIChat.ts
402
+ const submitAIChat = (editor, input, { mode, options, prompt, toolName: toolNameProps } = {}) => {
403
+ const { getOptions, setOption } = getEditorPlugin(editor, { key: KEYS.aiChat });
404
+ const { chat, toolName: toolNameOption } = getOptions();
405
+ const toolName = toolNameProps ?? toolNameOption ?? null;
406
+ if (!prompt && input?.length === 0) return;
407
+ if (!prompt) prompt = input;
408
+ if (!mode) mode = isSelecting(editor) ? "chat" : "insert";
409
+ if (mode === "insert") editor.getTransforms(AIPlugin).ai.undo();
410
+ setOption("mode", mode);
411
+ setOption("toolName", toolName);
412
+ const blocks = editor.getApi(BlockSelectionPlugin).blockSelection.getNodes();
413
+ const blocksRange = editor.api.nodesRange(blocks);
414
+ const promptText = getEditorPrompt(editor, { prompt });
415
+ const selection = blocks.length > 0 ? blocksRange : editor.selection;
416
+ let chatNodes;
417
+ if (blocks.length > 0) chatNodes = blocks.map((block) => block[0]);
418
+ else {
419
+ const selectionBlocks = editor.api.blocks({ mode: "highest" });
420
+ if (selectionBlocks.length > 1) chatNodes = selectionBlocks.map((block) => block[0]);
421
+ else chatNodes = editor.api.fragment();
422
+ }
423
+ setOption("chatNodes", chatNodes);
424
+ setOption("chatSelection", blocks.length > 0 ? null : editor.selection);
425
+ const ctx = {
426
+ children: editor.children,
427
+ selection: selection ?? null,
428
+ toolName
429
+ };
430
+ chat.sendMessage?.({ text: promptText }, {
431
+ body: { ctx },
432
+ ...options
433
+ });
434
+ };
435
+
436
+ //#endregion
437
+ //#region src/react/ai-chat/transforms/replaceSelectionAIChat.ts
438
+ const createFormattedBlocks = ({ blocks, format, sourceBlock }) => {
439
+ if (format === "none") return cloneDeep(blocks);
440
+ const [sourceNode] = sourceBlock;
441
+ const firstTextEntry = NodeApi.firstText(sourceNode);
442
+ if (!firstTextEntry) return null;
443
+ const blockProps = NodeApi.extractProps(sourceNode);
444
+ const textProps = NodeApi.extractProps(firstTextEntry[0]);
445
+ const applyTextFormatting = (node) => {
446
+ if (TextApi.isText(node)) return {
447
+ ...textProps,
448
+ ...node
449
+ };
450
+ if (node.children) return {
451
+ ...node,
452
+ children: node.children.map(applyTextFormatting)
453
+ };
454
+ return node;
455
+ };
456
+ return blocks.map((block, index) => {
457
+ if (format === "single" && index > 0) return block;
458
+ return applyTextFormatting({
459
+ ...block,
460
+ ...blockProps
461
+ });
462
+ });
463
+ };
464
+ const replaceSelectionAIChat = (editor, sourceEditor, { format = "single" } = {}) => {
465
+ if (!sourceEditor || sourceEditor.api.isEmpty()) return;
466
+ const isBlockSelecting = editor.getOption(BlockSelectionPlugin, "isSelectingSome");
467
+ editor.getApi({ key: KEYS.ai }).aiChat.hide();
468
+ if (!isBlockSelecting) {
469
+ const firstBlock = editor.api.node({
470
+ block: true,
471
+ mode: "lowest"
472
+ });
473
+ if (firstBlock && editor.api.isSelected(firstBlock[1], { contains: true }) && format !== "none") {
474
+ const formattedBlocks$1 = createFormattedBlocks({
475
+ blocks: cloneDeep(sourceEditor.children),
476
+ format,
477
+ sourceBlock: firstBlock
478
+ });
479
+ if (!formattedBlocks$1) return;
480
+ /** When user selection is cover the whole code block */
481
+ if (firstBlock[0].type === KEYS.codeLine && sourceEditor.children[0].type === KEYS.codeBlock && sourceEditor.children.length === 1) editor.tf.insertFragment(formattedBlocks$1[0].children);
482
+ else editor.tf.insertFragment(formattedBlocks$1);
483
+ editor.tf.focus();
484
+ return;
485
+ }
486
+ editor.tf.insertFragment(sourceEditor.children);
487
+ editor.tf.focus();
488
+ return;
489
+ }
490
+ const selectedBlocks = editor.getApi(BlockSelectionPlugin).blockSelection.getNodes();
491
+ if (selectedBlocks.length === 0) return;
492
+ if (format === "none" || format === "single" && selectedBlocks.length > 1) {
493
+ editor.tf.withoutNormalizing(() => {
494
+ removeBlockSelectionNodes(editor);
495
+ editor.tf.withNewBatch(() => {
496
+ editor.getTransforms(BlockSelectionPlugin).blockSelection.insertBlocksAndSelect(cloneDeep(sourceEditor.children), { at: selectedBlocks[0][1] });
497
+ });
498
+ });
499
+ editor.getApi(BlockSelectionPlugin).blockSelection.focus();
500
+ return;
501
+ }
502
+ const [, firstBlockPath] = selectedBlocks[0];
503
+ const formattedBlocks = createFormattedBlocks({
504
+ blocks: cloneDeep(sourceEditor.children),
505
+ format,
506
+ sourceBlock: selectedBlocks[0]
507
+ });
508
+ if (!formattedBlocks) return;
509
+ editor.tf.withoutNormalizing(() => {
510
+ removeBlockSelectionNodes(editor);
511
+ editor.tf.withNewBatch(() => {
512
+ editor.getTransforms(BlockSelectionPlugin).blockSelection.insertBlocksAndSelect(formattedBlocks, { at: firstBlockPath });
513
+ });
514
+ });
515
+ editor.getApi(BlockSelectionPlugin).blockSelection.focus();
516
+ };
517
+
518
+ //#endregion
519
+ //#region src/react/ai-chat/transforms/insertBelowAIChat.ts
520
+ const insertBelowAIChat = (editor, sourceEditor, { format = "single" } = {}) => {
521
+ const { toolName } = editor.getOptions(AIChatPlugin);
522
+ if (toolName === "generate") return insertBelowGenerate(editor, sourceEditor, { format });
523
+ const selectedBlocks = editor.getApi(BlockSelectionPlugin).blockSelection.getNodes();
524
+ const selectedIds = editor.getOptions(BlockSelectionPlugin).selectedIds;
525
+ editor.getTransforms(AIPlugin).ai.undo();
526
+ const insertBlocksAndSelect = editor.getTransforms(BlockSelectionPlugin).blockSelection.insertBlocksAndSelect;
527
+ if (!selectedIds || selectedIds.size === 0) return;
528
+ const lastBlock = selectedBlocks.at(-1);
529
+ if (!lastBlock) return;
530
+ const nextPath = PathApi.next(lastBlock[1]);
531
+ insertBlocksAndSelect(selectedBlocks.map((block) => block[0]), {
532
+ at: nextPath,
533
+ insertedCallback: () => {
534
+ withAIBatch(editor, () => {
535
+ acceptAISuggestions(editor);
536
+ });
537
+ }
538
+ });
539
+ editor.getApi(AIChatPlugin).aiChat.hide({ focus: false });
540
+ };
541
+ const insertBelowGenerate = (editor, sourceEditor, { format = "single" } = {}) => {
542
+ if (!sourceEditor || sourceEditor.api.isEmpty()) return;
543
+ const isBlockSelecting = editor.getOption(BlockSelectionPlugin, "isSelectingSome");
544
+ editor.getApi({ key: KEYS.ai }).aiChat.hide();
545
+ const insertBlocksAndSelect = editor.getTransforms(BlockSelectionPlugin).blockSelection.insertBlocksAndSelect;
546
+ if (isBlockSelecting) {
547
+ const selectedBlocks = editor.getApi(BlockSelectionPlugin).blockSelection.getNodes();
548
+ const selectedIds = editor.getOptions(BlockSelectionPlugin).selectedIds;
549
+ if (!selectedIds || selectedIds.size === 0) return;
550
+ const lastBlock = selectedBlocks.at(-1);
551
+ if (!lastBlock) return;
552
+ const nextPath = PathApi.next(lastBlock[1]);
553
+ if (format === "none") {
554
+ insertBlocksAndSelect(cloneDeep(sourceEditor.children), { at: nextPath });
555
+ return;
556
+ }
557
+ const formattedBlocks = createFormattedBlocks({
558
+ blocks: cloneDeep(sourceEditor.children),
559
+ format,
560
+ sourceBlock: lastBlock
561
+ });
562
+ if (!formattedBlocks) return;
563
+ insertBlocksAndSelect(formattedBlocks, { at: nextPath });
564
+ } else {
565
+ const [, end] = RangeApi.edges(editor.selection);
566
+ const endPath = [end.path[0]];
567
+ const currentBlock = editor.api.node({
568
+ at: endPath,
569
+ block: true,
570
+ mode: "lowest"
571
+ });
572
+ if (!currentBlock) return;
573
+ if (format === "none") {
574
+ insertBlocksAndSelect(cloneDeep(sourceEditor.children), { at: PathApi.next(endPath) });
575
+ return;
576
+ }
577
+ const formattedBlocks = createFormattedBlocks({
578
+ blocks: cloneDeep(sourceEditor.children),
579
+ format,
580
+ sourceBlock: currentBlock
581
+ });
582
+ if (!formattedBlocks) return;
583
+ insertBlocksAndSelect(formattedBlocks, { at: PathApi.next(endPath) });
584
+ }
585
+ };
586
+
587
+ //#endregion
588
+ //#region src/react/ai-chat/transforms/removeAnchorAIChat.ts
589
+ const removeAnchorAIChat = (editor, options) => {
590
+ editor.tf.withoutSaving(() => {
591
+ editor.tf.removeNodes({
592
+ at: [],
593
+ match: (n) => ElementApi.isElement(n) && n.type === getPluginType(editor, KEYS.aiChat),
594
+ ...options
595
+ });
596
+ });
597
+ };
598
+
599
+ //#endregion
600
+ //#region src/react/ai-chat/withAIChat.ts
601
+ const withAIChat = ({ api, editor, getOptions, tf: { insertText, normalizeNode }, type }) => {
602
+ const tf = editor.getTransforms(AIPlugin);
603
+ const matchesTrigger = (text) => {
604
+ const { trigger } = getOptions();
605
+ if (trigger instanceof RegExp) return trigger.test(text);
606
+ if (Array.isArray(trigger)) return trigger.includes(text);
607
+ return text === trigger;
608
+ };
609
+ return { transforms: {
610
+ insertText(text, options) {
611
+ const { triggerPreviousCharPattern, triggerQuery } = getOptions();
612
+ const fn = () => {
613
+ if (!editor.selection || !matchesTrigger(text) || triggerQuery && !triggerQuery(editor)) return;
614
+ const previousChar = editor.api.string(editor.api.range("before", editor.selection));
615
+ if (!triggerPreviousCharPattern?.test(previousChar)) return;
616
+ const nodeEntry = editor.api.block({ highest: true });
617
+ if (!nodeEntry || !editor.api.isEmpty(nodeEntry[0])) return;
618
+ api.aiChat.show();
619
+ return true;
620
+ };
621
+ if (fn()) return;
622
+ return insertText(text, options);
623
+ },
624
+ normalizeNode(entry) {
625
+ const [node, path] = entry;
626
+ if (node[KEYS.ai] && !getOptions().open) {
627
+ tf.ai.removeMarks({ at: path });
628
+ return;
629
+ }
630
+ if (ElementApi.isElement(node) && node.type === type && !getOptions().open) {
631
+ editor.getTransforms(AIChatPlugin).aiChat.removeAnchor({ at: path });
632
+ return;
633
+ }
634
+ return normalizeNode(entry);
635
+ }
636
+ } };
637
+ };
638
+
639
+ //#endregion
640
+ //#region src/react/ai-chat/AIChatPlugin.ts
641
+ const AIChatPlugin = createTPlatePlugin({
642
+ key: KEYS.aiChat,
643
+ dependencies: ["ai"],
644
+ node: { isElement: true },
645
+ options: {
646
+ _blockChunks: "",
647
+ _blockPath: null,
648
+ _mdxName: null,
649
+ _replaceIds: [],
650
+ aiEditor: null,
651
+ chat: { messages: [] },
652
+ chatNodes: [],
653
+ chatSelection: null,
654
+ experimental_lastTextId: null,
655
+ mode: "insert",
656
+ open: false,
657
+ streaming: false,
658
+ toolName: null,
659
+ trigger: " ",
660
+ triggerPreviousCharPattern: /^\s?$/
661
+ }
662
+ }).overrideEditor(withAIChat).extendApi(({ editor, getOption, getOptions, setOption, type }) => ({
663
+ reset: bindFirst(resetAIChat, editor),
664
+ submit: bindFirst(submitAIChat, editor),
665
+ node: (options = {}) => {
666
+ const { anchor = false, streaming = false, ...rest } = options;
667
+ if (anchor) return editor.api.node({
668
+ at: [],
669
+ match: (n) => ElementApi.isElement(n) && n.type === type,
670
+ ...rest
671
+ });
672
+ if (streaming) {
673
+ if (!getOption("streaming")) return;
674
+ const path = getOption("_blockPath");
675
+ if (!path) return;
676
+ return editor.api.node({
677
+ at: path,
678
+ mode: "lowest",
679
+ reverse: true,
680
+ match: (t) => !!t[getPluginType(editor, KEYS.ai)],
681
+ ...rest
682
+ });
683
+ }
684
+ return editor.api.node({
685
+ match: (n) => n[getPluginType(editor, KEYS.ai)],
686
+ ...rest
687
+ });
688
+ },
689
+ reload: () => {
690
+ const { chat, chatNodes, chatSelection } = getOptions();
691
+ editor.getTransforms(AIPlugin).ai.undo();
692
+ if (chatSelection) editor.tf.setSelection(chatSelection);
693
+ else editor.getApi(BlockSelectionPlugin).blockSelection.set(chatNodes.map((node) => node.id));
694
+ const blocks = editor.getApi(BlockSelectionPlugin).blockSelection.getNodes();
695
+ const selection = blocks.length > 0 ? editor.api.nodesRange(blocks) : editor.selection;
696
+ const ctx = {
697
+ children: editor.children,
698
+ selection: selection ?? null,
699
+ toolName: getOption("toolName")
700
+ };
701
+ chat.regenerate?.({ body: { ctx } });
702
+ },
703
+ stop: () => {
704
+ setOption("streaming", false);
705
+ getOptions().chat.stop?.();
706
+ }
707
+ })).extendApi(({ api, editor, getOptions, setOption, tf }) => ({
708
+ hide: ({ focus = true, undo = true } = {}) => {
709
+ api.aiChat.reset({ undo });
710
+ setOption("open", false);
711
+ if (focus) if (editor.getOption(BlockSelectionPlugin, "isSelectingSome")) editor.getApi(BlockSelectionPlugin).blockSelection.focus();
712
+ else editor.tf.focus();
713
+ const lastBatch = editor.history.undos.at(-1);
714
+ if (lastBatch?.ai) lastBatch.ai = void 0;
715
+ tf.aiChat.removeAnchor();
716
+ },
717
+ show: () => {
718
+ api.aiChat.reset();
719
+ setOption("toolName", null);
720
+ getOptions().chat.setMessages?.([]);
721
+ setOption("open", true);
722
+ }
723
+ })).extendTransforms(({ editor }) => ({
724
+ accept: bindFirst(acceptAIChat, editor),
725
+ insertBelow: bindFirst(insertBelowAIChat, editor),
726
+ removeAnchor: bindFirst(removeAnchorAIChat, editor),
727
+ replaceSelection: bindFirst(replaceSelectionAIChat, editor)
728
+ }));
729
+
730
+ //#endregion
731
+ //#region src/react/ai-chat/hooks/useAIChatEditor.ts
732
+ /**
733
+ * Register an editor in the AI chat plugin, and deserializes the content into
734
+ * `editor.children` with block-level memoization.
735
+ *
736
+ * @returns Deserialized children to pass as `value` prop to PlateStatic
737
+ */
738
+ const useAIChatEditor = (editor, content, { parser } = {}) => {
739
+ const { setOption } = useEditorPlugin(AIChatPlugin);
740
+ const children = useMemo(() => {
741
+ return editor.getApi(MarkdownPlugin).markdown.deserialize(content, {
742
+ memoize: true,
743
+ parser
744
+ });
745
+ }, [content]);
746
+ editor.children = children;
747
+ useEffect(() => {
748
+ setOption("aiEditor", editor);
749
+ }, [editor, setOption]);
750
+ return children;
751
+ };
752
+
753
+ //#endregion
754
+ //#region src/react/ai-chat/hooks/useChatChunk.ts
755
+ const useChatChunk = ({ onChunk, onFinish }) => {
756
+ const { status } = usePluginOption({ key: KEYS.aiChat }, "chat");
757
+ const isLoading = status === "streaming" || status === "submitted";
758
+ const content = useLastAssistantMessage()?.parts.find((part) => part.type === "text")?.text;
759
+ const insertedTextRef = useRef("");
760
+ const prevIsLoadingRef = useRef(isLoading);
761
+ useEffect(() => {
762
+ if (!isLoading) insertedTextRef.current = "";
763
+ if (prevIsLoadingRef.current && !isLoading) onFinish?.({ content: content ?? "" });
764
+ prevIsLoadingRef.current = isLoading;
765
+ }, [isLoading]);
766
+ useEffect(() => {
767
+ if (!content) return;
768
+ const chunk = content.slice(insertedTextRef.current.length);
769
+ const nodes = [];
770
+ if (chunk) {
771
+ const isFirst = insertedTextRef.current === "";
772
+ nodes.push({ text: chunk });
773
+ onChunk({
774
+ chunk,
775
+ isFirst,
776
+ nodes,
777
+ text: content
778
+ });
779
+ }
780
+ insertedTextRef.current = content;
781
+ }, [content]);
782
+ };
783
+
784
+ //#endregion
785
+ //#region src/react/ai-chat/hooks/useEditorChat.ts
786
+ const useEditorChat = ({ onOpenBlockSelection, onOpenChange, onOpenCursor, onOpenSelection }) => {
787
+ const { editor } = useEditorPlugin(AIChatPlugin);
788
+ const open = usePluginOption(AIChatPlugin, "open");
789
+ useEffect(() => {
790
+ onOpenChange?.(open);
791
+ if (open) {
792
+ if (onOpenBlockSelection) {
793
+ const blockSelectionApi = editor.getApi(BlockSelectionPlugin).blockSelection;
794
+ if (editor.getOption(BlockSelectionPlugin, "isSelectingSome")) {
795
+ onOpenBlockSelection(blockSelectionApi.getNodes());
796
+ return;
797
+ }
798
+ }
799
+ if (onOpenCursor && editor.api.isCollapsed()) {
800
+ onOpenCursor();
801
+ return;
802
+ }
803
+ if (onOpenSelection && editor.api.isExpanded()) {
804
+ onOpenSelection();
805
+ return;
806
+ }
807
+ }
808
+ }, [open]);
809
+ };
810
+
811
+ //#endregion
812
+ //#region src/react/ai-chat/streaming/streamDeserializeInlineMd.ts
813
+ const streamDeserializeInlineMd = (editor, text, options) => editor.getApi(MarkdownPlugin).markdown.deserializeInline(text, options);
814
+
815
+ //#endregion
816
+ //#region src/react/ai-chat/streaming/utils/utils.ts
817
+ const getChunkTrimmed = (chunk, { direction = "right" } = {}) => {
818
+ const str = direction === "right" ? chunk.trimEnd() : chunk.trimStart();
819
+ if (direction === "right") return chunk.slice(str.length);
820
+ return chunk.slice(0, chunk.length - str.length);
821
+ };
822
+ function isCompleteCodeBlock(str) {
823
+ const trimmed = str.trim();
824
+ const startsWithCodeBlock = trimmed.startsWith("```");
825
+ const endsWithCodeBlock = trimmed.endsWith("```");
826
+ return startsWithCodeBlock && endsWithCodeBlock;
827
+ }
828
+ function isCompleteMath(str) {
829
+ const trimmed = str.trim();
830
+ const startsWithMath = trimmed.startsWith("$$");
831
+ const endsWithMath = trimmed.endsWith("$$");
832
+ return startsWithMath && endsWithMath;
833
+ }
834
+
835
+ //#endregion
836
+ //#region src/react/ai-chat/streaming/utils/escapeInput.ts
837
+ const escapeInput = (data) => {
838
+ let res = data;
839
+ if (data.startsWith("$$") && !data.startsWith("$$\n") && !isCompleteMath(data)) res = data.replace("$$", String.raw`\$\$`);
840
+ return res;
841
+ };
842
+
843
+ //#endregion
844
+ //#region src/react/ai-chat/streaming/utils/getListNode.ts
845
+ const getListNode = (editor, node) => {
846
+ if (node.listStyleType && node.listStart) {
847
+ const previousNode = editor.api.previous({ at: editor.selection?.focus })?.[0];
848
+ if (previousNode?.listStyleType && previousNode?.listStart) return node;
849
+ if (node.listStart === 1) return node;
850
+ return {
851
+ ...node,
852
+ listRestartPolite: node.listStart
853
+ };
854
+ }
855
+ return node;
856
+ };
857
+
858
+ //#endregion
859
+ //#region src/react/ai-chat/streaming/utils/isSameNode.ts
860
+ const LIST_STYLE_TYPE = "listStyleType";
861
+ const isSameNode = (editor, node1, node2) => {
862
+ if (node1.type !== editor.getType(KEYS.p) || node2.type !== editor.getType(KEYS.p)) return node1.type === node2.type;
863
+ if (isDefined(node1[LIST_STYLE_TYPE]) || isDefined(node2[LIST_STYLE_TYPE])) return node1[LIST_STYLE_TYPE] === node2[LIST_STYLE_TYPE];
864
+ return node1.type === node2.type;
865
+ };
866
+
867
+ //#endregion
868
+ //#region src/react/ai-chat/streaming/utils/nodesWithProps.ts
869
+ const nodesWithProps = (editor, nodes, options) => nodes.map((node) => {
870
+ if (ElementApi.isElement(node)) return {
871
+ ...getListNode(editor, node),
872
+ ...options.elementProps,
873
+ children: nodesWithProps(editor, node.children, options)
874
+ };
875
+ return {
876
+ ...options.textProps,
877
+ ...node,
878
+ text: node.text
879
+ };
880
+ });
881
+
882
+ //#endregion
883
+ //#region src/react/ai-chat/streaming/streamDeserializeMd.ts
884
+ const statMdxTagRegex = /<([A-Za-z][A-Za-z0-9._:-]*)(?:\s[^>]*?)?(?<!\/)>/;
885
+ const streamDeserializeMd = (editor, data, options) => {
886
+ const input = escapeInput(data);
887
+ const value = withoutDeserializeInMdx(editor, input);
888
+ if (Array.isArray(value)) return value;
889
+ let blocks = [];
890
+ blocks = editor.getApi(MarkdownPlugin).markdown.deserialize(input, {
891
+ ...options,
892
+ preserveEmptyParagraphs: false
893
+ });
894
+ const trimmedData = getChunkTrimmed(data);
895
+ const lastBlock = blocks.at(-1);
896
+ const addNewLine = trimmedData === "\n\n";
897
+ const unshiftNewLine = getChunkTrimmed(data, { direction: "left" }) === "\n\n";
898
+ const isCodeBlockOrTable = lastBlock?.type === "code_block" || lastBlock?.type === "table";
899
+ let result = blocks;
900
+ /**
901
+ * Deserialize the sting like `123\n\n` will be `123` base on markdown spec
902
+ * but we want to keep the `\n\n`
903
+ */
904
+ if (lastBlock && !isCodeBlockOrTable && trimmedData.length > 0 && !addNewLine) {
905
+ const textNode = [{ text: trimmedData }];
906
+ const lastChild = lastBlock.children.at(-1);
907
+ /** It’s like normalizing and merging the text nodes. */
908
+ if (lastChild && TextApi.isText(lastChild) && Object.keys(lastChild).length === 1) {
909
+ lastBlock.children.pop();
910
+ const textNode$1 = [{ text: lastChild.text + trimmedData }];
911
+ lastBlock.children.push(...textNode$1);
912
+ } else lastBlock.children.push(...textNode);
913
+ result = [...blocks.slice(0, -1), lastBlock];
914
+ }
915
+ if (addNewLine && !isCodeBlockOrTable) result.push({
916
+ children: [{ text: "" }],
917
+ type: KEYS.p
918
+ });
919
+ if (unshiftNewLine && !isCodeBlockOrTable) result.unshift({
920
+ children: [{ text: "" }],
921
+ type: KEYS.p
922
+ });
923
+ return result;
924
+ };
925
+ const withoutDeserializeInMdx = (editor, input) => {
926
+ const mdxName = editor.getOption(AIChatPlugin, "_mdxName");
927
+ if (mdxName) {
928
+ if (input.includes(`</${mdxName}>`)) {
929
+ editor.setOption(AIChatPlugin, "_mdxName", null);
930
+ return false;
931
+ }
932
+ return [{
933
+ children: [{ text: input }],
934
+ type: getPluginType(editor, KEYS.p)
935
+ }];
936
+ }
937
+ const newMdxName = statMdxTagRegex.exec(input)?.[1];
938
+ if (input.startsWith(`<${newMdxName}`)) editor.setOption(AIChatPlugin, "_mdxName", newMdxName ?? null);
939
+ };
940
+
941
+ //#endregion
942
+ //#region src/react/ai-chat/streaming/streamSerializeMd.ts
943
+ const trimEndHeading = (editor, value) => {
944
+ const headingKeys = new Set([
945
+ KEYS.h1,
946
+ KEYS.h2,
947
+ KEYS.h3,
948
+ KEYS.h4,
949
+ KEYS.h5,
950
+ KEYS.h6
951
+ ]);
952
+ const lastBlock = value.at(-1);
953
+ if (lastBlock && headingKeys.has(getPluginKey(editor, lastBlock.type) ?? lastBlock.type) && ElementApi.isElement(lastBlock)) {
954
+ const lastTextNode = lastBlock.children.at(-1);
955
+ if (TextApi.isText(lastTextNode)) {
956
+ const trimmedText = getChunkTrimmed(lastTextNode?.text);
957
+ const newChildren = [...lastBlock.children.slice(0, -1), { text: lastTextNode.text.trimEnd() }];
958
+ const newLastBlock = {
959
+ ...lastBlock,
960
+ children: newChildren
961
+ };
962
+ return {
963
+ trimmedText,
964
+ value: [...value.slice(0, -1), newLastBlock]
965
+ };
966
+ }
967
+ }
968
+ return {
969
+ trimmedText: "",
970
+ value
971
+ };
972
+ };
973
+ const streamSerializeMd = (editor, options, chunk) => {
974
+ const { value: optionsValue, ...restOptions } = options;
975
+ const { value } = trimEndHeading(editor, optionsValue ?? editor.children);
976
+ let result = "";
977
+ result = editor.getApi(MarkdownPlugin).markdown.serialize({
978
+ value,
979
+ ...restOptions
980
+ });
981
+ const trimmedChunk = getChunkTrimmed(chunk);
982
+ if (isCompleteCodeBlock(result) && !chunk.endsWith("```")) result = result.trimEnd().slice(0, -3) + trimmedChunk;
983
+ if (isCompleteMath(result) && !chunk.endsWith("$$")) result = result.trimEnd().slice(0, -3) + trimmedChunk;
984
+ result = result.replace(/&#x20;/g, " ");
985
+ result = result.replace(/&#x200B;/g, " ");
986
+ result = result.replace(/\u200B/g, "");
987
+ if (trimmedChunk !== "\n\n") result = result.trimEnd() + trimmedChunk;
988
+ if (chunk.endsWith("\n\n")) {
989
+ if (result === "\n") result = "";
990
+ else if (result.endsWith("\n\n")) result = result.slice(0, -1);
991
+ }
992
+ result = result.replace(/\\([\\`*_{}\\[\]()#+\-\\.!~<>|$])/g, "$1");
993
+ return result;
994
+ };
995
+
996
+ //#endregion
997
+ //#region src/react/ai-chat/streaming/streamInsertChunk.ts
998
+ const getNextPath = (path, length) => {
999
+ let result = path;
1000
+ for (let i = 0; i < length; i++) result = PathApi.next(result);
1001
+ return result;
1002
+ };
1003
+ /** @experimental */
1004
+ function streamInsertChunk(editor, chunk, options = {}) {
1005
+ const { _blockChunks, _blockPath } = editor.getOptions(AIChatPlugin);
1006
+ if (_blockPath === null) {
1007
+ const blocks = streamDeserializeMd(editor, chunk);
1008
+ const path = getCurrentBlockPath(editor);
1009
+ const startBlock = editor.api.node(path)[0];
1010
+ const startInEmptyParagraph = NodeApi.string(startBlock).length === 0 && startBlock.type === getPluginType(editor, KEYS.p);
1011
+ if (startInEmptyParagraph) editor.tf.removeNodes({ at: path });
1012
+ if (blocks.length > 0) {
1013
+ editor.tf.insertNodes(nodesWithProps(editor, [blocks[0]], options), {
1014
+ at: path,
1015
+ nextBlock: !startInEmptyParagraph,
1016
+ select: true
1017
+ });
1018
+ editor.setOption(AIChatPlugin, "_blockPath", getCurrentBlockPath(editor));
1019
+ editor.setOption(AIChatPlugin, "_blockChunks", chunk);
1020
+ if (blocks.length > 1) {
1021
+ const nextBlocks = blocks.slice(1);
1022
+ const nextPath = getCurrentBlockPath(editor);
1023
+ editor.tf.insertNodes(nodesWithProps(editor, nextBlocks, options), {
1024
+ at: nextPath,
1025
+ nextBlock: true,
1026
+ select: true
1027
+ });
1028
+ const lastBlock = editor.api.node(getNextPath(nextPath, nextBlocks.length));
1029
+ editor.setOption(AIChatPlugin, "_blockPath", lastBlock[1]);
1030
+ const lastBlockChunks = streamSerializeMd(editor, { value: [lastBlock[0]] }, chunk);
1031
+ editor.setOption(AIChatPlugin, "_blockChunks", lastBlockChunks);
1032
+ }
1033
+ }
1034
+ } else {
1035
+ const tempBlockChunks = _blockChunks + chunk;
1036
+ const tempBlocks = streamDeserializeMd(editor, tempBlockChunks);
1037
+ if (tempBlocks.length === 0) return console.warn(`unsupport md nodes: ${JSON.stringify(tempBlockChunks)}`);
1038
+ if (tempBlocks.length === 1) {
1039
+ const currentBlock = editor.api.node(_blockPath)[0];
1040
+ if (isSameNode(editor, currentBlock, tempBlocks[0])) {
1041
+ const chunkNodes = streamDeserializeInlineMd(editor, chunk);
1042
+ editor.tf.insertNodes(nodesWithProps(editor, chunkNodes, options), {
1043
+ at: editor.api.end(_blockPath),
1044
+ select: true
1045
+ });
1046
+ const serializedBlock = streamSerializeMd(editor, { value: [editor.api.node(_blockPath)[0]] }, tempBlockChunks);
1047
+ const blockText = NodeApi.string(tempBlocks[0]);
1048
+ if (serializedBlock === tempBlockChunks && blockText === serializedBlock) editor.setOption(AIChatPlugin, "_blockChunks", tempBlockChunks);
1049
+ else {
1050
+ editor.tf.replaceNodes(nodesWithProps(editor, [tempBlocks[0]], options), {
1051
+ at: _blockPath,
1052
+ select: true
1053
+ });
1054
+ const serializedBlock$1 = streamSerializeMd(editor, { value: [tempBlocks[0]] }, tempBlockChunks);
1055
+ editor.setOption(AIChatPlugin, "_blockChunks", tempBlocks[0].type === getPluginType(editor, KEYS.codeBlock) || tempBlocks[0].type === getPluginType(editor, KEYS.table) || tempBlocks[0].type === getPluginType(editor, KEYS.equation) ? tempBlockChunks : serializedBlock$1);
1056
+ }
1057
+ } else {
1058
+ const serializedBlock = streamSerializeMd(editor, { value: [tempBlocks[0]] }, tempBlockChunks);
1059
+ editor.tf.replaceNodes(nodesWithProps(editor, [tempBlocks[0]], options), {
1060
+ at: _blockPath,
1061
+ select: true
1062
+ });
1063
+ editor.setOption(AIChatPlugin, "_blockChunks", serializedBlock);
1064
+ }
1065
+ } else {
1066
+ editor.tf.replaceNodes(nodesWithProps(editor, [tempBlocks[0]], options), {
1067
+ at: _blockPath,
1068
+ select: true
1069
+ });
1070
+ if (tempBlocks.length > 1) {
1071
+ const newEndBlockPath = getNextPath(_blockPath, tempBlocks.length - 1);
1072
+ editor.tf.insertNodes(nodesWithProps(editor, tempBlocks.slice(1), options), {
1073
+ at: PathApi.next(_blockPath),
1074
+ select: true
1075
+ });
1076
+ editor.setOption(AIChatPlugin, "_blockPath", newEndBlockPath);
1077
+ const endBlock = editor.api.node(newEndBlockPath)[0];
1078
+ const serializedBlock = streamSerializeMd(editor, { value: [endBlock] }, tempBlockChunks);
1079
+ editor.setOption(AIChatPlugin, "_blockChunks", serializedBlock);
1080
+ }
1081
+ }
1082
+ }
1083
+ }
1084
+ const getCurrentBlockPath = (editor) => {
1085
+ const getAnchorPreviousPath = (editor$1) => {
1086
+ const anchorNode = editor$1.getApi(AIChatPlugin).aiChat.node({ anchor: true });
1087
+ if (anchorNode) return PathApi.previous(anchorNode[1]);
1088
+ };
1089
+ const getFocusPath = (editor$1) => editor$1.selection?.focus.path.slice(0, 1);
1090
+ const path = getAnchorPreviousPath(editor) ?? getFocusPath(editor) ?? [0];
1091
+ const entry = editor.api.node(path);
1092
+ if (entry && (entry[0].type === getPluginType(editor, KEYS.columnGroup) || entry[0].type === getPluginType(editor, KEYS.table))) return editor.api.above()?.[1] ?? path;
1093
+ return path;
1094
+ };
1095
+
1096
+ //#endregion
1097
+ //#region src/react/copilot/renderCopilotBelowNodes.tsx
1098
+ const renderCopilotBelowNodes = ({ editor }) => {
1099
+ const { renderGhostText: GhostText } = getEditorPlugin(editor, { key: KEYS.copilot }).getOptions();
1100
+ if (!GhostText) return;
1101
+ return ({ children }) => /* @__PURE__ */ React.createElement(React.Fragment, null, children, /* @__PURE__ */ React.createElement(GhostText, null));
1102
+ };
1103
+
1104
+ //#endregion
1105
+ //#region src/react/copilot/transforms/acceptCopilot.ts
1106
+ const acceptCopilot = (editor) => {
1107
+ const { suggestionText } = editor.getOptions({ key: KEYS.copilot });
1108
+ if (!suggestionText?.length) return false;
1109
+ editor.tf.insertFragment(deserializeInlineMd(editor, suggestionText));
1110
+ };
1111
+
1112
+ //#endregion
1113
+ //#region src/react/copilot/utils/callCompletionApi.ts
1114
+ const getOriginalFetch = () => fetch;
1115
+ async function callCompletionApi({ api = "/api/completion", body, credentials, fetch: fetch$1 = getOriginalFetch(), headers, prompt, setAbortController = () => {}, setCompletion = () => {}, setError = () => {}, setLoading = () => {}, onError, onFinish, onResponse }) {
1116
+ try {
1117
+ setLoading(true);
1118
+ setError(null);
1119
+ const abortController = new AbortController();
1120
+ setAbortController(abortController);
1121
+ setCompletion("");
1122
+ const res = await fetch$1(api, {
1123
+ body: JSON.stringify({
1124
+ prompt,
1125
+ ...body
1126
+ }),
1127
+ credentials,
1128
+ headers: {
1129
+ "Content-Type": "application/json",
1130
+ ...headers
1131
+ },
1132
+ method: "POST",
1133
+ signal: abortController.signal
1134
+ }).catch((error) => {
1135
+ throw error;
1136
+ });
1137
+ if (onResponse) await onResponse(res);
1138
+ if (!res.ok) throw new Error(await res.text() || "Failed to fetch the chat response.");
1139
+ if (!res.body) throw new Error("The response body is empty.");
1140
+ const { text } = await res.json();
1141
+ if (!text) throw new Error("The response does not contain a text field.");
1142
+ setCompletion(text);
1143
+ if (onFinish) onFinish(prompt, text);
1144
+ setAbortController(null);
1145
+ return text;
1146
+ } catch (error) {
1147
+ if (error.name === "AbortError") {
1148
+ setAbortController(null);
1149
+ return null;
1150
+ }
1151
+ if (error instanceof Error && onError) onError(error);
1152
+ setError(error);
1153
+ } finally {
1154
+ setLoading(false);
1155
+ }
1156
+ }
1157
+
1158
+ //#endregion
1159
+ //#region src/react/copilot/utils/getNextWord.ts
1160
+ const nonSpaceRegex = /^\s*(\S)/;
1161
+ const cjkCharRegex = /[\u1100-\u11FF\u3040-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF]/;
1162
+ const cjkMatchRegex = /^(\s*)([\u1100-\u11FF\u3040-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF])([\u3000-\u303F\uFF00-\uFFEF])?/;
1163
+ const nonCjkMatchRegex = /^(\s*\S+?)(?=[\s\u1100-\u11FF\u3040-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF]|$)/;
1164
+ const getNextWord = ({ text }) => {
1165
+ if (!text) return {
1166
+ firstWord: "",
1167
+ remainingText: ""
1168
+ };
1169
+ const nonSpaceMatch = nonSpaceRegex.exec(text);
1170
+ if (!nonSpaceMatch) return {
1171
+ firstWord: "",
1172
+ remainingText: ""
1173
+ };
1174
+ const firstNonSpaceChar = nonSpaceMatch[1];
1175
+ const isCJKChar = cjkCharRegex.test(firstNonSpaceChar);
1176
+ let firstWord;
1177
+ let remainingText;
1178
+ if (isCJKChar) {
1179
+ const match = cjkMatchRegex.exec(text);
1180
+ if (match) {
1181
+ const [, spaces = "", char = "", punctuation = ""] = match;
1182
+ firstWord = spaces + char + punctuation;
1183
+ remainingText = text.slice(firstWord.length);
1184
+ } else {
1185
+ firstWord = "";
1186
+ remainingText = text;
1187
+ }
1188
+ } else {
1189
+ const match = nonCjkMatchRegex.exec(text);
1190
+ if (match) {
1191
+ firstWord = match[0];
1192
+ remainingText = text.slice(firstWord.length);
1193
+ } else {
1194
+ firstWord = text;
1195
+ remainingText = "";
1196
+ }
1197
+ }
1198
+ return {
1199
+ firstWord,
1200
+ remainingText
1201
+ };
1202
+ };
1203
+
1204
+ //#endregion
1205
+ //#region src/react/copilot/utils/triggerCopilotSuggestion.ts
1206
+ const triggerCopilotSuggestion = async (editor) => {
1207
+ const { api, getOptions, setOption } = getEditorPlugin(editor, { key: KEYS.copilot });
1208
+ const { completeOptions, getPrompt, isLoading, triggerQuery } = getOptions();
1209
+ if (isLoading || editor.getOptions({ key: KEYS.aiChat }).chat?.isLoading) return false;
1210
+ if (!triggerQuery({ editor })) return false;
1211
+ const prompt = getPrompt({ editor });
1212
+ if (prompt.length === 0) return false;
1213
+ api.copilot.stop();
1214
+ await callCompletionApi({
1215
+ prompt,
1216
+ onFinish: (_, completion) => {
1217
+ api.copilot.setBlockSuggestion({ text: completion });
1218
+ },
1219
+ ...completeOptions,
1220
+ setAbortController: (controller) => setOption("abortController", controller),
1221
+ setCompletion: (completion) => setOption("completion", completion),
1222
+ setError: (error) => setOption("error", error),
1223
+ setLoading: (loading) => setOption("isLoading", loading),
1224
+ onError: (error) => {
1225
+ setOption("error", error);
1226
+ completeOptions?.onError?.(error);
1227
+ }
1228
+ });
1229
+ };
1230
+
1231
+ //#endregion
1232
+ //#region src/react/copilot/utils/withoutAbort.ts
1233
+ const withoutAbort = (editor, fn) => {
1234
+ editor.setOption(CopilotPlugin, "shouldAbort", false);
1235
+ fn();
1236
+ editor.setOption(CopilotPlugin, "shouldAbort", true);
1237
+ };
1238
+
1239
+ //#endregion
1240
+ //#region src/react/copilot/transforms/acceptCopilotNextWord.ts
1241
+ const acceptCopilotNextWord = (editor) => {
1242
+ const { api, getOptions } = getEditorPlugin(editor, { key: KEYS.copilot });
1243
+ const { getNextWord: getNextWord$1, suggestionText } = getOptions();
1244
+ if (!suggestionText?.length) return false;
1245
+ const { firstWord, remainingText } = getNextWord$1({ text: suggestionText });
1246
+ api.copilot.setBlockSuggestion({ text: remainingText });
1247
+ withoutAbort(editor, () => {
1248
+ editor.tf.insertFragment(deserializeInlineMd(editor, firstWord));
1249
+ });
1250
+ };
1251
+
1252
+ //#endregion
1253
+ //#region src/react/copilot/withCopilot.ts
1254
+ const getPatchString = (editor, operations) => {
1255
+ let string = "";
1256
+ for (const operation of operations) if (operation.type === "insert_node") {
1257
+ const node = operation.node;
1258
+ const text = serializeInlineMd(editor, { value: [node] });
1259
+ string += text;
1260
+ } else if (operation.type === "insert_text") string += operation.text;
1261
+ return string;
1262
+ };
1263
+ const withCopilot = ({ api, editor, getOptions, setOption, tf: { apply, insertText, redo, setSelection, undo, writeHistory } }) => {
1264
+ let prevSelection = null;
1265
+ return { transforms: {
1266
+ apply(operation) {
1267
+ const { shouldAbort } = getOptions();
1268
+ if (shouldAbort) api.copilot.reject();
1269
+ apply(operation);
1270
+ },
1271
+ insertText(text, options) {
1272
+ const suggestionText = getOptions().suggestionText;
1273
+ if (suggestionText?.startsWith(text)) {
1274
+ withoutAbort(editor, () => {
1275
+ editor.tf.withoutMerging(() => {
1276
+ const newText = suggestionText?.slice(text.length);
1277
+ setOption("suggestionText", newText);
1278
+ insertText(text);
1279
+ });
1280
+ });
1281
+ return;
1282
+ }
1283
+ insertText(text, options);
1284
+ },
1285
+ redo() {
1286
+ if (!getOptions().suggestionText) return redo();
1287
+ const topRedo = editor.history.redos.at(-1);
1288
+ const prevSuggestion = getOptions().suggestionText;
1289
+ if (topRedo && topRedo.shouldAbort === false && prevSuggestion) {
1290
+ withoutAbort(editor, () => {
1291
+ const shouldRemoveText = getPatchString(editor, topRedo.operations);
1292
+ setOption("suggestionText", prevSuggestion.slice(shouldRemoveText.length));
1293
+ redo();
1294
+ });
1295
+ return;
1296
+ }
1297
+ return redo();
1298
+ },
1299
+ setSelection(props) {
1300
+ setSelection(props);
1301
+ if (editor.selection && (!prevSelection || !RangeApi.equals(prevSelection, editor.selection)) && getOptions().autoTriggerQuery({ editor }) && editor.api.isFocused()) api.copilot.triggerSuggestion();
1302
+ prevSelection = editor.selection;
1303
+ },
1304
+ undo() {
1305
+ if (!getOptions().suggestionText) return undo();
1306
+ const lastUndos = editor.history.undos.at(-1);
1307
+ const oldText = getOptions().suggestionText;
1308
+ if (lastUndos && lastUndos.shouldAbort === false && oldText) {
1309
+ withoutAbort(editor, () => {
1310
+ setOption("suggestionText", getPatchString(editor, lastUndos.operations) + oldText);
1311
+ undo();
1312
+ });
1313
+ return;
1314
+ }
1315
+ return undo();
1316
+ },
1317
+ writeHistory(stacks, batch) {
1318
+ if (!getOptions().isLoading) batch.shouldAbort = getOptions().shouldAbort;
1319
+ return writeHistory(stacks, batch);
1320
+ }
1321
+ } };
1322
+ };
1323
+
1324
+ //#endregion
1325
+ //#region src/react/copilot/CopilotPlugin.tsx
1326
+ const CopilotPlugin = createTPlatePlugin({
1327
+ key: KEYS.copilot,
1328
+ handlers: {
1329
+ onBlur: ({ api }) => {
1330
+ api.copilot.reject();
1331
+ },
1332
+ onMouseDown: ({ api }) => {
1333
+ api.copilot.reject();
1334
+ }
1335
+ },
1336
+ options: {
1337
+ abortController: null,
1338
+ completeOptions: {},
1339
+ completion: "",
1340
+ debounceDelay: 0,
1341
+ error: null,
1342
+ getNextWord,
1343
+ isLoading: false,
1344
+ renderGhostText: null,
1345
+ shouldAbort: true,
1346
+ suggestionNodeId: null,
1347
+ suggestionText: null,
1348
+ autoTriggerQuery: ({ editor }) => {
1349
+ if (editor.getOptions({ key: KEYS.copilot }).suggestionText) return false;
1350
+ if (editor.api.isEmpty(editor.selection, { block: true })) return false;
1351
+ const blockAbove = editor.api.block();
1352
+ if (!blockAbove) return false;
1353
+ return NodeApi.string(blockAbove[0]).at(-1) === " ";
1354
+ },
1355
+ getPrompt: ({ editor }) => {
1356
+ const contextEntry = editor.api.block({ highest: true });
1357
+ if (!contextEntry) return "";
1358
+ return serializeMd(editor, { value: [contextEntry[0]] });
1359
+ },
1360
+ triggerQuery: ({ editor }) => {
1361
+ if (editor.api.isExpanded()) return false;
1362
+ if (!editor.api.isAt({ end: true })) return false;
1363
+ return true;
1364
+ }
1365
+ }
1366
+ }).overrideEditor(withCopilot).extendSelectors(({ getOptions }) => ({ isSuggested: (id) => getOptions().suggestionNodeId === id })).extendTransforms(({ editor }) => ({
1367
+ accept: bindFirst(acceptCopilot, editor),
1368
+ acceptNextWord: bindFirst(acceptCopilotNextWord, editor)
1369
+ })).extendApi(({ api, editor, getOptions, setOption, setOptions }) => {
1370
+ const debounceDelay = getOptions().debounceDelay;
1371
+ let triggerSuggestion = bindFirst(triggerCopilotSuggestion, editor);
1372
+ if (debounceDelay) triggerSuggestion = debounce(bindFirst(triggerCopilotSuggestion, editor), debounceDelay);
1373
+ return {
1374
+ triggerSuggestion,
1375
+ setBlockSuggestion: ({ id = getOptions().suggestionNodeId, text }) => {
1376
+ if (!id) id = editor.api.block()[0].id;
1377
+ setOptions({
1378
+ suggestionNodeId: id,
1379
+ suggestionText: text
1380
+ });
1381
+ },
1382
+ stop: () => {
1383
+ const { abortController } = getOptions();
1384
+ api.copilot.triggerSuggestion?.cancel();
1385
+ if (abortController) {
1386
+ abortController.abort();
1387
+ setOption("abortController", null);
1388
+ }
1389
+ }
1390
+ };
1391
+ }).extendApi(({ api, getOptions, setOptions }) => ({ reject: () => {
1392
+ if (!getOptions().suggestionText?.length) return false;
1393
+ api.copilot.stop();
1394
+ setOptions({
1395
+ completion: null,
1396
+ suggestionNodeId: null,
1397
+ suggestionText: null
1398
+ });
1399
+ } })).extend({
1400
+ render: { belowNodes: renderCopilotBelowNodes },
1401
+ shortcuts: {
1402
+ accept: { keys: "tab" },
1403
+ reject: { keys: "escape" }
1404
+ }
1405
+ });
1406
+
1407
+ //#endregion
1408
+ export { AIChatPlugin, AIPlugin, CopilotPlugin, acceptAIChat, acceptAISuggestions, acceptCopilot, acceptCopilotNextWord, aiCommentToRange, applyAISuggestions, applyTableCellSuggestion, callCompletionApi, createFormattedBlocks, escapeInput, findTextRangeInBlock, getChunkTrimmed, getCurrentBlockPath, getLastAssistantMessage, getListNode, getNextWord, getTableCellChildren, insertBelowAIChat, insertBelowGenerate, isCompleteCodeBlock, isCompleteMath, isSameNode, isSingleCellTable, nodesWithProps, rejectAISuggestions, removeAnchorAIChat, renderCopilotBelowNodes, replaceSelectionAIChat, resetAIChat, streamDeserializeInlineMd, streamDeserializeMd, streamInsertChunk, streamSerializeMd, submitAIChat, triggerCopilotSuggestion, useAIChatEditor, useChatChunk, useEditorChat, useLastAssistantMessage, withAIChat, withCopilot, withTransient, withoutAbort, withoutSuggestionAndComments };
1409
+ //# sourceMappingURL=index.js.map