@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/dist/index.mjs ADDED
@@ -0,0 +1,953 @@
1
+ // src/lib/BaseAIPlugin.ts
2
+ import {
3
+ bindFirst,
4
+ createTSlatePlugin,
5
+ KEYS
6
+ } from "platejs";
7
+
8
+ // src/lib/transforms/insertAINodes.ts
9
+ var insertAINodes = (editor, nodes, {
10
+ target
11
+ } = {}) => {
12
+ if (!target && !editor.selection?.focus.path) return;
13
+ const aiNodes = nodes.map((node) => ({
14
+ ...node,
15
+ ai: true
16
+ }));
17
+ editor.tf.withoutNormalizing(() => {
18
+ editor.tf.insertNodes(aiNodes, {
19
+ at: editor.api.end(target || editor.selection.focus.path),
20
+ select: true
21
+ });
22
+ editor.tf.collapse({ edge: "end" });
23
+ });
24
+ };
25
+
26
+ // src/lib/transforms/removeAIMarks.ts
27
+ var removeAIMarks = (editor, { at = [] } = {}) => {
28
+ editor.tf.unsetNodes("ai", {
29
+ at,
30
+ match: (n) => n.ai
31
+ });
32
+ };
33
+
34
+ // src/lib/transforms/removeAINodes.ts
35
+ import { TextApi } from "platejs";
36
+ var removeAINodes = (editor, { at = [] } = {}) => {
37
+ editor.tf.removeNodes({
38
+ at,
39
+ match: (n) => TextApi.isText(n) && !!n.ai
40
+ });
41
+ };
42
+
43
+ // src/lib/transforms/undoAI.ts
44
+ var undoAI = (editor) => {
45
+ if (editor.history.undos.at(-1)?.ai && editor.api.some({
46
+ at: [],
47
+ match: (n) => !!n.ai
48
+ })) {
49
+ editor.undo();
50
+ editor.history.redos.pop();
51
+ }
52
+ };
53
+
54
+ // src/lib/transforms/withAIBatch.ts
55
+ var withAIBatch = (editor, fn, {
56
+ split
57
+ } = {}) => {
58
+ if (split) {
59
+ editor.tf.withNewBatch(fn);
60
+ } else {
61
+ editor.tf.withMerging(fn);
62
+ }
63
+ const lastBatch = editor.history.undos?.at(-1);
64
+ if (lastBatch) {
65
+ lastBatch.ai = true;
66
+ }
67
+ };
68
+
69
+ // src/lib/BaseAIPlugin.ts
70
+ var BaseAIPlugin = createTSlatePlugin({
71
+ key: KEYS.ai,
72
+ node: { isDecoration: false, isLeaf: true }
73
+ }).extendTransforms(({ editor }) => ({
74
+ insertNodes: bindFirst(insertAINodes, editor),
75
+ removeMarks: bindFirst(removeAIMarks, editor),
76
+ removeNodes: bindFirst(removeAINodes, editor),
77
+ undo: bindFirst(undoAI, editor)
78
+ }));
79
+
80
+ // src/lib/streaming/streamDeserializeInlineMd.ts
81
+ import { MarkdownPlugin } from "@platejs/markdown";
82
+ var streamDeserializeInlineMd = (editor, text, options) => {
83
+ return editor.getApi(MarkdownPlugin).markdown.deserializeInline(text, options);
84
+ };
85
+
86
+ // src/lib/streaming/streamDeserializeMd.ts
87
+ import { MarkdownPlugin as MarkdownPlugin2 } from "@platejs/markdown";
88
+ import { KEYS as KEYS3, TextApi as TextApi2 } from "platejs";
89
+
90
+ // src/lib/streaming/utils/escapeInput.ts
91
+ var escapeInput = (data) => {
92
+ let res = data;
93
+ if (data.startsWith("$$") && !data.startsWith("$$\n") && !isCompleteMath(data)) {
94
+ res = data.replace("$$", String.raw`\$\$`);
95
+ }
96
+ return res;
97
+ };
98
+
99
+ // src/lib/streaming/utils/getListNode.ts
100
+ var getListNode = (editor, node) => {
101
+ if (node.listStyleType && node.listStart) {
102
+ const previousNode = editor.api.previous({
103
+ at: editor.selection?.focus
104
+ })?.[0];
105
+ if (previousNode?.listStyleType && previousNode?.listStart) {
106
+ return node;
107
+ } else {
108
+ if (node.listStart === 1) return node;
109
+ return {
110
+ ...node,
111
+ listRestartPolite: node.listStart
112
+ };
113
+ }
114
+ }
115
+ return node;
116
+ };
117
+
118
+ // src/lib/streaming/utils/isSameNode.ts
119
+ import { isDefined, KEYS as KEYS2 } from "platejs";
120
+ var LIST_STYLE_TYPE = "listStyleType";
121
+ var isSameNode = (editor, node1, node2) => {
122
+ if (node1.type !== editor.getType(KEYS2.p) || node2.type !== editor.getType(KEYS2.p))
123
+ return node1.type === node2.type;
124
+ if (isDefined(node1[LIST_STYLE_TYPE]) || isDefined(node2[LIST_STYLE_TYPE])) {
125
+ return node1[LIST_STYLE_TYPE] === node2[LIST_STYLE_TYPE];
126
+ }
127
+ return node1.type === node2.type;
128
+ };
129
+
130
+ // src/lib/streaming/utils/nodesWithProps.ts
131
+ import { ElementApi } from "platejs";
132
+ var nodesWithProps = (editor, nodes, options) => {
133
+ return nodes.map((node) => {
134
+ if (ElementApi.isElement(node)) {
135
+ return {
136
+ ...getListNode(editor, node),
137
+ ...options.elementProps,
138
+ children: nodesWithProps(editor, node.children, options)
139
+ };
140
+ } else {
141
+ return {
142
+ ...options.textProps,
143
+ ...node,
144
+ text: node.text
145
+ };
146
+ }
147
+ });
148
+ };
149
+
150
+ // src/lib/streaming/utils/utils.ts
151
+ var getChunkTrimmed = (chunk, {
152
+ direction = "right"
153
+ } = {}) => {
154
+ const str = direction === "right" ? chunk.trimEnd() : chunk.trimStart();
155
+ if (direction === "right") {
156
+ return chunk.slice(str.length);
157
+ } else {
158
+ return chunk.slice(0, chunk.length - str.length);
159
+ }
160
+ };
161
+ function isCompleteCodeBlock(str) {
162
+ const trimmed = str.trim();
163
+ const startsWithCodeBlock = trimmed.startsWith("```");
164
+ const endsWithCodeBlock = trimmed.endsWith("```");
165
+ return startsWithCodeBlock && endsWithCodeBlock;
166
+ }
167
+ function isCompleteMath(str) {
168
+ const trimmed = str.trim();
169
+ const startsWithMath = trimmed.startsWith("$$");
170
+ const endsWithMath = trimmed.endsWith("$$");
171
+ return startsWithMath && endsWithMath;
172
+ }
173
+
174
+ // src/lib/streaming/streamDeserializeMd.ts
175
+ var streamDeserializeMd = (editor, data, options) => {
176
+ const input = escapeInput(data);
177
+ let blocks = [];
178
+ blocks = editor.getApi(MarkdownPlugin2).markdown.deserialize(input, options);
179
+ const trimmedData = getChunkTrimmed(data);
180
+ const lastBlock = blocks.at(-1);
181
+ const addNewLine = trimmedData === "\n\n";
182
+ const unshiftNewLine = getChunkTrimmed(data, { direction: "left" }) === "\n\n";
183
+ const isCodeBlockOrTable = lastBlock?.type === "code_block" || lastBlock?.type === "table";
184
+ let result = blocks;
185
+ if (lastBlock && !isCodeBlockOrTable && trimmedData.length > 0 && !addNewLine) {
186
+ const textNode = [
187
+ {
188
+ text: trimmedData
189
+ }
190
+ ];
191
+ const lastChild = lastBlock.children.at(-1);
192
+ if (lastChild && TextApi2.isText(lastChild) && Object.keys(lastChild).length === 1) {
193
+ lastBlock.children.pop();
194
+ const textNode2 = [
195
+ {
196
+ text: lastChild.text + trimmedData
197
+ }
198
+ ];
199
+ lastBlock.children.push(...textNode2);
200
+ } else {
201
+ lastBlock.children.push(...textNode);
202
+ }
203
+ result = [...blocks.slice(0, -1), lastBlock];
204
+ }
205
+ if (addNewLine && !isCodeBlockOrTable) {
206
+ result.push({
207
+ children: [{ text: "" }],
208
+ type: KEYS3.p
209
+ });
210
+ }
211
+ if (unshiftNewLine && !isCodeBlockOrTable) {
212
+ result.unshift({
213
+ children: [{ text: "" }],
214
+ type: KEYS3.p
215
+ });
216
+ }
217
+ return result;
218
+ };
219
+
220
+ // src/lib/streaming/streamInsertChunk.ts
221
+ import { NodeApi as NodeApi2, PathApi as PathApi2 } from "platejs";
222
+
223
+ // src/react/ai/AIPlugin.ts
224
+ import { toPlatePlugin } from "platejs/react";
225
+ var AIPlugin = toPlatePlugin(BaseAIPlugin);
226
+
227
+ // src/react/ai-chat/AIChatPlugin.ts
228
+ import { BlockSelectionPlugin as BlockSelectionPlugin5 } from "@platejs/selection/react";
229
+ import {
230
+ bindFirst as bindFirst2,
231
+ ElementApi as ElementApi4,
232
+ KEYS as KEYS12
233
+ } from "platejs";
234
+ import { createTPlatePlugin } from "platejs/react";
235
+
236
+ // src/react/ai-chat/transforms/acceptAIChat.ts
237
+ import { KEYS as KEYS4 } from "platejs";
238
+ import { getEditorPlugin } from "platejs/react";
239
+ var acceptAIChat = (editor) => {
240
+ const { tf } = getEditorPlugin(editor, AIPlugin);
241
+ const api = editor.getApi({ key: KEYS4.ai });
242
+ const lastAINodePath = api.aiChat.node({ at: [], reverse: true })[1];
243
+ withAIBatch(editor, () => {
244
+ tf.ai.removeMarks();
245
+ editor.getTransforms(AIChatPlugin).aiChat.removeAnchor();
246
+ });
247
+ api.aiChat.hide();
248
+ editor.tf.focus();
249
+ const focusPoint = editor.api.end(lastAINodePath);
250
+ editor.tf.setSelection({
251
+ anchor: focusPoint,
252
+ focus: focusPoint
253
+ });
254
+ };
255
+
256
+ // src/react/ai-chat/transforms/insertBelowAIChat.ts
257
+ import { BlockSelectionPlugin as BlockSelectionPlugin2 } from "@platejs/selection/react";
258
+ import cloneDeep2 from "lodash/cloneDeep.js";
259
+ import { KEYS as KEYS6, PathApi, RangeApi } from "platejs";
260
+
261
+ // src/react/ai-chat/transforms/replaceSelectionAIChat.ts
262
+ import {
263
+ BlockSelectionPlugin,
264
+ removeBlockSelectionNodes
265
+ } from "@platejs/selection/react";
266
+ import cloneDeep from "lodash/cloneDeep.js";
267
+ import {
268
+ KEYS as KEYS5,
269
+ NodeApi,
270
+ TextApi as TextApi3
271
+ } from "platejs";
272
+ var createFormattedBlocks = ({
273
+ blocks,
274
+ format,
275
+ sourceBlock
276
+ }) => {
277
+ if (format === "none") return cloneDeep(blocks);
278
+ const [sourceNode] = sourceBlock;
279
+ const firstTextEntry = NodeApi.firstText(sourceNode);
280
+ if (!firstTextEntry) return null;
281
+ const blockProps = NodeApi.extractProps(sourceNode);
282
+ const textProps = NodeApi.extractProps(firstTextEntry[0]);
283
+ const applyTextFormatting = (node) => {
284
+ if (TextApi3.isText(node)) {
285
+ return { ...textProps, ...node };
286
+ }
287
+ if (node.children) {
288
+ return {
289
+ ...node,
290
+ children: node.children.map(applyTextFormatting)
291
+ };
292
+ }
293
+ return node;
294
+ };
295
+ return blocks.map((block, index) => {
296
+ if (format === "single" && index > 0) {
297
+ return block;
298
+ }
299
+ return applyTextFormatting({
300
+ ...block,
301
+ ...blockProps
302
+ });
303
+ });
304
+ };
305
+ var replaceSelectionAIChat = (editor, sourceEditor, { format = "single" } = {}) => {
306
+ if (!sourceEditor || sourceEditor.api.isEmpty()) return;
307
+ const isBlockSelecting = editor.getOption(
308
+ BlockSelectionPlugin,
309
+ "isSelectingSome"
310
+ );
311
+ editor.getApi({ key: KEYS5.ai }).aiChat.hide();
312
+ if (!isBlockSelecting) {
313
+ const firstBlock = editor.api.node({
314
+ block: true,
315
+ mode: "lowest"
316
+ });
317
+ if (firstBlock && editor.api.isSelected(firstBlock[1], { contains: true }) && format !== "none") {
318
+ const formattedBlocks2 = createFormattedBlocks({
319
+ blocks: cloneDeep(sourceEditor.children),
320
+ format,
321
+ sourceBlock: firstBlock
322
+ });
323
+ if (!formattedBlocks2) return;
324
+ editor.tf.insertFragment(formattedBlocks2);
325
+ editor.tf.focus();
326
+ return;
327
+ }
328
+ editor.tf.insertFragment(sourceEditor.children);
329
+ editor.tf.focus();
330
+ return;
331
+ }
332
+ const blockSelectionApi = editor.getApi(BlockSelectionPlugin).blockSelection;
333
+ const selectedBlocks = blockSelectionApi.getNodes();
334
+ if (selectedBlocks.length === 0) return;
335
+ if (format === "none" || format === "single" && selectedBlocks.length > 1) {
336
+ editor.tf.withoutNormalizing(() => {
337
+ removeBlockSelectionNodes(editor);
338
+ editor.tf.withNewBatch(() => {
339
+ editor.getTransforms(BlockSelectionPlugin).blockSelection.insertBlocksAndSelect(
340
+ cloneDeep(sourceEditor.children),
341
+ {
342
+ at: selectedBlocks[0][1]
343
+ }
344
+ );
345
+ });
346
+ });
347
+ editor.getApi(BlockSelectionPlugin).blockSelection.focus();
348
+ return;
349
+ }
350
+ const [, firstBlockPath] = selectedBlocks[0];
351
+ const formattedBlocks = createFormattedBlocks({
352
+ blocks: cloneDeep(sourceEditor.children),
353
+ format,
354
+ sourceBlock: selectedBlocks[0]
355
+ });
356
+ if (!formattedBlocks) return;
357
+ editor.tf.withoutNormalizing(() => {
358
+ removeBlockSelectionNodes(editor);
359
+ editor.tf.withNewBatch(() => {
360
+ editor.getTransforms(BlockSelectionPlugin).blockSelection.insertBlocksAndSelect(formattedBlocks, {
361
+ at: firstBlockPath
362
+ });
363
+ });
364
+ });
365
+ editor.getApi(BlockSelectionPlugin).blockSelection.focus();
366
+ };
367
+
368
+ // src/react/ai-chat/transforms/insertBelowAIChat.ts
369
+ var insertBelowAIChat = (editor, sourceEditor, { format = "single" } = {}) => {
370
+ if (!sourceEditor || sourceEditor.api.isEmpty()) return;
371
+ const isBlockSelecting = editor.getOption(
372
+ BlockSelectionPlugin2,
373
+ "isSelectingSome"
374
+ );
375
+ editor.getApi({ key: KEYS6.ai }).aiChat.hide();
376
+ const insertBlocksAndSelect = editor.getTransforms(BlockSelectionPlugin2).blockSelection.insertBlocksAndSelect;
377
+ if (isBlockSelecting) {
378
+ const selectedBlocks = editor.getApi(BlockSelectionPlugin2).blockSelection.getNodes();
379
+ const selectedIds = editor.getOptions(BlockSelectionPlugin2).selectedIds;
380
+ if (!selectedIds || selectedIds.size === 0) return;
381
+ const lastBlock = selectedBlocks.at(-1);
382
+ if (!lastBlock) return;
383
+ const nextPath = PathApi.next(lastBlock[1]);
384
+ if (format === "none") {
385
+ insertBlocksAndSelect(cloneDeep2(sourceEditor.children), {
386
+ at: nextPath
387
+ });
388
+ return;
389
+ }
390
+ const formattedBlocks = createFormattedBlocks({
391
+ blocks: cloneDeep2(sourceEditor.children),
392
+ format,
393
+ sourceBlock: lastBlock
394
+ });
395
+ if (!formattedBlocks) return;
396
+ insertBlocksAndSelect(formattedBlocks, {
397
+ at: nextPath
398
+ });
399
+ } else {
400
+ const [, end] = RangeApi.edges(editor.selection);
401
+ const endPath = [end.path[0]];
402
+ const currentBlock = editor.api.node({
403
+ at: endPath,
404
+ block: true,
405
+ mode: "lowest"
406
+ });
407
+ if (!currentBlock) return;
408
+ if (format === "none") {
409
+ insertBlocksAndSelect(cloneDeep2(sourceEditor.children), {
410
+ at: PathApi.next(endPath)
411
+ });
412
+ return;
413
+ }
414
+ const formattedBlocks = createFormattedBlocks({
415
+ blocks: cloneDeep2(sourceEditor.children),
416
+ format,
417
+ sourceBlock: currentBlock
418
+ });
419
+ if (!formattedBlocks) return;
420
+ insertBlocksAndSelect(formattedBlocks, {
421
+ at: PathApi.next(endPath)
422
+ });
423
+ }
424
+ };
425
+
426
+ // src/react/ai-chat/transforms/removeAnchorAIChat.ts
427
+ import { ElementApi as ElementApi2, KEYS as KEYS7 } from "platejs";
428
+ var removeAnchorAIChat = (editor, options) => {
429
+ editor.tf.withoutSaving(() => {
430
+ editor.tf.removeNodes({
431
+ at: [],
432
+ match: (n) => ElementApi2.isElement(n) && n.type === KEYS7.aiChat,
433
+ ...options
434
+ });
435
+ });
436
+ };
437
+
438
+ // src/react/ai-chat/utils/getEditorPrompt.ts
439
+ import { isSelecting } from "@platejs/selection";
440
+ import { BlockSelectionPlugin as BlockSelectionPlugin4 } from "@platejs/selection/react";
441
+
442
+ // src/react/ai-chat/utils/getMarkdown.ts
443
+ import { serializeMd } from "@platejs/markdown";
444
+ import { BlockSelectionPlugin as BlockSelectionPlugin3 } from "@platejs/selection/react";
445
+ import { KEYS as KEYS8 } from "platejs";
446
+ var getMarkdown = (editor, type) => {
447
+ if (type === "editor") {
448
+ return serializeMd(editor);
449
+ }
450
+ if (type === "block") {
451
+ const blocks = editor.getOption(BlockSelectionPlugin3, "isSelectingSome") ? editor.getApi(BlockSelectionPlugin3).blockSelection.getNodes() : editor.api.nodes({
452
+ mode: "highest",
453
+ match: (n) => editor.api.isBlock(n)
454
+ });
455
+ const nodes = Array.from(blocks, (entry) => entry[0]);
456
+ return serializeMd(editor, { value: nodes });
457
+ }
458
+ if (type === "selection") {
459
+ const fragment = editor.api.fragment();
460
+ if (fragment.length === 1) {
461
+ const modifiedFragment = [
462
+ {
463
+ children: fragment[0].children,
464
+ type: KEYS8.p
465
+ }
466
+ ];
467
+ return serializeMd(editor, { value: modifiedFragment });
468
+ }
469
+ return serializeMd(editor, { value: fragment });
470
+ }
471
+ return "";
472
+ };
473
+
474
+ // src/react/ai-chat/utils/getEditorPrompt.ts
475
+ var replacePlaceholders = (editor, text, {
476
+ prompt
477
+ }) => {
478
+ let result = text.replace("{prompt}", prompt || "");
479
+ const placeholders = {
480
+ "{block}": "block",
481
+ "{editor}": "editor",
482
+ "{selection}": "selection"
483
+ };
484
+ Object.entries(placeholders).forEach(([placeholder, type]) => {
485
+ if (result.includes(placeholder)) {
486
+ result = result.replace(placeholder, getMarkdown(editor, type));
487
+ }
488
+ });
489
+ return result;
490
+ };
491
+ var createPromptFromConfig = (config, params) => {
492
+ const { isBlockSelecting, isSelecting: isSelecting3 } = params;
493
+ if (isBlockSelecting && config.blockSelecting) {
494
+ return config.blockSelecting ?? config.default;
495
+ } else if (isSelecting3 && config.selecting) {
496
+ return config.selecting ?? config.default;
497
+ } else {
498
+ return config.default;
499
+ }
500
+ };
501
+ var getEditorPrompt = (editor, {
502
+ prompt = "",
503
+ promptTemplate = () => "{prompt}"
504
+ } = {}) => {
505
+ const params = {
506
+ editor,
507
+ isBlockSelecting: editor.getOption(BlockSelectionPlugin4, "isSelectingSome"),
508
+ isSelecting: isSelecting(editor)
509
+ };
510
+ const template = promptTemplate(params);
511
+ if (!template) return;
512
+ let promptText = "";
513
+ if (typeof prompt === "function") {
514
+ promptText = prompt(params);
515
+ } else if (typeof prompt === "object") {
516
+ promptText = createPromptFromConfig(prompt, params);
517
+ } else {
518
+ promptText = prompt;
519
+ }
520
+ return replacePlaceholders(editor, template, {
521
+ prompt: promptText
522
+ });
523
+ };
524
+
525
+ // src/react/ai-chat/utils/resetAIChat.ts
526
+ import { KEYS as KEYS9 } from "platejs";
527
+ import { getEditorPlugin as getEditorPlugin2 } from "platejs/react";
528
+ var resetAIChat = (editor) => {
529
+ const { api, getOptions } = getEditorPlugin2(editor, {
530
+ key: KEYS9.aiChat
531
+ });
532
+ api.aiChat.stop();
533
+ const chat = getOptions().chat;
534
+ if (chat.messages && chat.messages.length > 0) {
535
+ chat.setMessages?.([]);
536
+ }
537
+ editor.getTransforms(AIPlugin).ai.undo();
538
+ };
539
+
540
+ // src/react/ai-chat/utils/submitAIChat.ts
541
+ import { isSelecting as isSelecting2 } from "@platejs/selection";
542
+ import { KEYS as KEYS10 } from "platejs";
543
+ import { getEditorPlugin as getEditorPlugin3 } from "platejs/react";
544
+ var submitAIChat = (editor, {
545
+ mode,
546
+ options,
547
+ prompt,
548
+ system
549
+ } = {}) => {
550
+ const { getOptions, setOption } = getEditorPlugin3(
551
+ editor,
552
+ {
553
+ key: KEYS10.aiChat
554
+ }
555
+ );
556
+ const { chat, promptTemplate, systemTemplate } = getOptions();
557
+ if (!prompt && chat.input?.length === 0) {
558
+ return;
559
+ }
560
+ if (!prompt) {
561
+ prompt = chat.input;
562
+ }
563
+ if (!mode) {
564
+ mode = isSelecting2(editor) ? "chat" : "insert";
565
+ }
566
+ if (mode === "insert") {
567
+ editor.getTransforms(AIPlugin).ai.undo();
568
+ }
569
+ setOption("mode", mode);
570
+ chat.setInput?.("");
571
+ void chat.append?.(
572
+ {
573
+ content: getEditorPrompt(editor, {
574
+ prompt,
575
+ promptTemplate
576
+ }) ?? "",
577
+ role: "user"
578
+ },
579
+ {
580
+ body: {
581
+ system: getEditorPrompt(editor, {
582
+ prompt: system,
583
+ promptTemplate: systemTemplate
584
+ })
585
+ },
586
+ ...options
587
+ }
588
+ );
589
+ };
590
+
591
+ // src/react/ai-chat/withAIChat.ts
592
+ import { ElementApi as ElementApi3, KEYS as KEYS11 } from "platejs";
593
+ var withAIChat = ({
594
+ api,
595
+ editor,
596
+ getOptions,
597
+ tf: { insertText, normalizeNode, setSelection },
598
+ type
599
+ }) => {
600
+ const tf = editor.getTransforms(AIPlugin);
601
+ const matchesTrigger = (text) => {
602
+ const { trigger } = getOptions();
603
+ if (trigger instanceof RegExp) {
604
+ return trigger.test(text);
605
+ }
606
+ if (Array.isArray(trigger)) {
607
+ return trigger.includes(text);
608
+ }
609
+ return text === trigger;
610
+ };
611
+ return {
612
+ transforms: {
613
+ insertText(text, options) {
614
+ const { triggerPreviousCharPattern, triggerQuery } = getOptions();
615
+ const fn = () => {
616
+ if (!editor.selection || !matchesTrigger(text) || triggerQuery && !triggerQuery(editor)) {
617
+ return;
618
+ }
619
+ const previousChar = editor.api.string(
620
+ editor.api.range("before", editor.selection)
621
+ );
622
+ const matchesPreviousCharPattern = triggerPreviousCharPattern?.test(previousChar);
623
+ if (!matchesPreviousCharPattern) return;
624
+ const nodeEntry = editor.api.block({ highest: true });
625
+ if (!nodeEntry || !editor.api.isEmpty(nodeEntry[0])) return;
626
+ api.aiChat.show();
627
+ return true;
628
+ };
629
+ if (fn()) return;
630
+ return insertText(text, options);
631
+ },
632
+ normalizeNode(entry) {
633
+ const [node, path] = entry;
634
+ if (node[KEYS11.ai] && !getOptions().open) {
635
+ tf.ai.removeMarks({ at: path });
636
+ return;
637
+ }
638
+ if (ElementApi3.isElement(node) && node.type === type && !getOptions().open) {
639
+ editor.getTransforms(AIChatPlugin).aiChat.removeAnchor({ at: path });
640
+ return;
641
+ }
642
+ return normalizeNode(entry);
643
+ }
644
+ }
645
+ };
646
+ };
647
+
648
+ // src/react/ai-chat/AIChatPlugin.ts
649
+ var AIChatPlugin = createTPlatePlugin({
650
+ key: KEYS12.aiChat,
651
+ dependencies: ["ai"],
652
+ node: {
653
+ isElement: true
654
+ },
655
+ options: {
656
+ _blockChunks: "",
657
+ _blockPath: null,
658
+ aiEditor: null,
659
+ chat: { messages: [] },
660
+ experimental_lastTextId: null,
661
+ mode: "chat",
662
+ open: false,
663
+ streaming: false,
664
+ trigger: " ",
665
+ triggerPreviousCharPattern: /^\s?$/,
666
+ promptTemplate: () => "{prompt}",
667
+ systemTemplate: () => {
668
+ }
669
+ }
670
+ }).overrideEditor(withAIChat).extendApi(({ editor, getOption, getOptions, setOption, type }) => {
671
+ return {
672
+ reset: bindFirst2(resetAIChat, editor),
673
+ submit: bindFirst2(submitAIChat, editor),
674
+ node: (options = {}) => {
675
+ const { anchor = false, streaming = false, ...rest } = options;
676
+ if (anchor) {
677
+ return editor.api.node({
678
+ at: [],
679
+ match: (n) => ElementApi4.isElement(n) && n.type === type,
680
+ ...rest
681
+ });
682
+ }
683
+ if (streaming) {
684
+ if (!getOption("streaming")) return;
685
+ const path = getOption("_blockPath");
686
+ if (!path) return;
687
+ return editor.api.node({
688
+ at: path,
689
+ mode: "lowest",
690
+ reverse: true,
691
+ match: (t) => !!t.ai,
692
+ ...rest
693
+ });
694
+ }
695
+ return editor.api.node({
696
+ match: (n) => n.ai,
697
+ ...rest
698
+ });
699
+ },
700
+ reload: () => {
701
+ const { chat, mode } = getOptions();
702
+ if (mode === "insert") {
703
+ editor.getTransforms(AIPlugin).ai.undo();
704
+ }
705
+ void chat.reload?.({
706
+ body: {
707
+ system: getEditorPrompt(editor, {
708
+ promptTemplate: getOptions().systemTemplate
709
+ })
710
+ }
711
+ });
712
+ },
713
+ stop: () => {
714
+ setOption("streaming", false);
715
+ getOptions().chat.stop?.();
716
+ }
717
+ };
718
+ }).extendApi(({ api, editor, getOptions, setOption, tf, type }) => ({
719
+ hide: () => {
720
+ api.aiChat.reset();
721
+ setOption("open", false);
722
+ if (editor.getOption(BlockSelectionPlugin5, "isSelectingSome")) {
723
+ } else {
724
+ editor.tf.focus();
725
+ }
726
+ const lastBatch = editor.history.undos.at(-1);
727
+ if (lastBatch?.ai) {
728
+ delete lastBatch.ai;
729
+ }
730
+ tf.aiChat.removeAnchor();
731
+ },
732
+ show: () => {
733
+ api.aiChat.reset();
734
+ getOptions().chat.setMessages?.([]);
735
+ setOption("open", true);
736
+ }
737
+ })).extendTransforms(({ editor }) => ({
738
+ accept: bindFirst2(acceptAIChat, editor),
739
+ insertBelow: bindFirst2(insertBelowAIChat, editor),
740
+ removeAnchor: bindFirst2(removeAnchorAIChat, editor),
741
+ replaceSelection: bindFirst2(replaceSelectionAIChat, editor)
742
+ }));
743
+
744
+ // src/lib/streaming/streamSerializeMd.ts
745
+ import { MarkdownPlugin as MarkdownPlugin3 } from "@platejs/markdown";
746
+ import { ElementApi as ElementApi5, TextApi as TextApi4 } from "platejs";
747
+ var trimEndHeading = (value) => {
748
+ const headingKeys = /* @__PURE__ */ new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
749
+ const lastBlock = value.at(-1);
750
+ if (lastBlock && headingKeys.has(lastBlock.type) && ElementApi5.isElement(lastBlock)) {
751
+ const lastTextNode = lastBlock.children.at(-1);
752
+ if (TextApi4.isText(lastTextNode)) {
753
+ const trimmedText = getChunkTrimmed(lastTextNode?.text);
754
+ const newChildren = [
755
+ ...lastBlock.children.slice(0, -1),
756
+ { text: lastTextNode.text.trimEnd() }
757
+ ];
758
+ const newLastBlock = {
759
+ ...lastBlock,
760
+ children: newChildren
761
+ };
762
+ return {
763
+ trimmedText,
764
+ value: [...value.slice(0, -1), newLastBlock]
765
+ };
766
+ }
767
+ }
768
+ return { trimmedText: "", value };
769
+ };
770
+ var streamSerializeMd = (editor, options, chunk) => {
771
+ const { value: optionsValue, ...restOptions } = options;
772
+ const { value } = trimEndHeading(optionsValue ?? editor.children);
773
+ let result = "";
774
+ result = editor.getApi(MarkdownPlugin3).markdown.serialize({
775
+ value,
776
+ ...restOptions
777
+ });
778
+ const trimmedChunk = getChunkTrimmed(chunk);
779
+ if (isCompleteCodeBlock(result) && !chunk.endsWith("```")) {
780
+ result = result.trimEnd().slice(0, -3) + trimmedChunk;
781
+ }
782
+ if (isCompleteMath(result) && !chunk.endsWith("$$")) {
783
+ result = result.trimEnd().slice(0, -3) + trimmedChunk;
784
+ }
785
+ result = result.replace(/ /g, " ");
786
+ result = result.replace(/​/g, " ");
787
+ if (trimmedChunk !== "\n\n") {
788
+ result = result.trimEnd() + trimmedChunk;
789
+ }
790
+ result = result.replace(/\\([\\`*_{}\\[\]()#+\-\\.!~<>|$])/g, "$1");
791
+ return result;
792
+ };
793
+
794
+ // src/lib/streaming/streamInsertChunk.ts
795
+ function streamInsertChunk(editor, chunk, options = {}) {
796
+ const { _blockChunks, _blockPath } = editor.getOptions(AIChatPlugin);
797
+ if (_blockPath === null) {
798
+ const blocks = streamDeserializeMd(editor, chunk);
799
+ const path = getCurrentBlockPath(editor);
800
+ const startInEmptyParagraph = NodeApi2.string(editor.api.node(path)[0]).length === 0;
801
+ if (startInEmptyParagraph) {
802
+ editor.tf.removeNodes({ at: path });
803
+ }
804
+ if (blocks.length > 0) {
805
+ editor.tf.insertNodes(nodesWithProps(editor, [blocks[0]], options), {
806
+ at: path,
807
+ nextBlock: !startInEmptyParagraph,
808
+ select: true
809
+ });
810
+ editor.setOption(AIChatPlugin, "_blockPath", getCurrentBlockPath(editor));
811
+ editor.setOption(AIChatPlugin, "_blockChunks", chunk);
812
+ if (blocks.length > 1) {
813
+ const nextBlocks = blocks.slice(1);
814
+ const nextPath = getCurrentBlockPath(editor);
815
+ editor.tf.insertNodes(nodesWithProps(editor, nextBlocks, options), {
816
+ at: nextPath,
817
+ nextBlock: true,
818
+ select: true
819
+ });
820
+ const lastBlock = editor.api.node(nextBlocks.at(-1)) ?? editor.api.node([nextPath[0] + nextBlocks.length]);
821
+ editor.setOption(AIChatPlugin, "_blockPath", lastBlock[1]);
822
+ const lastBlockChunks = streamSerializeMd(
823
+ editor,
824
+ {
825
+ value: [lastBlock[0]]
826
+ },
827
+ chunk
828
+ );
829
+ editor.setOption(AIChatPlugin, "_blockChunks", lastBlockChunks);
830
+ }
831
+ }
832
+ } else {
833
+ const tempBlockChunks = _blockChunks + chunk;
834
+ const tempBlocks = streamDeserializeMd(editor, tempBlockChunks);
835
+ if (tempBlocks.length === 0) {
836
+ return console.warn(
837
+ `unsupport md nodes: ${JSON.stringify(tempBlockChunks)}`
838
+ );
839
+ }
840
+ if (tempBlocks.length === 1) {
841
+ const currentBlock = editor.api.node(_blockPath)[0];
842
+ if (isSameNode(editor, currentBlock, tempBlocks[0])) {
843
+ const chunkNodes = streamDeserializeInlineMd(editor, chunk);
844
+ editor.tf.insertNodes(nodesWithProps(editor, chunkNodes, options), {
845
+ at: editor.api.end(_blockPath),
846
+ select: true
847
+ });
848
+ const updatedBlock = editor.api.node(_blockPath);
849
+ const serializedBlock = streamSerializeMd(
850
+ editor,
851
+ {
852
+ value: [updatedBlock[0]]
853
+ },
854
+ tempBlockChunks
855
+ );
856
+ const blockText = NodeApi2.string(tempBlocks[0]);
857
+ if (serializedBlock === tempBlockChunks && blockText === serializedBlock) {
858
+ editor.setOption(AIChatPlugin, "_blockChunks", tempBlockChunks);
859
+ } else {
860
+ editor.tf.replaceNodes(
861
+ nodesWithProps(editor, [tempBlocks[0]], options),
862
+ {
863
+ at: _blockPath,
864
+ select: true
865
+ }
866
+ );
867
+ const serializedBlock2 = streamSerializeMd(
868
+ editor,
869
+ {
870
+ value: [tempBlocks[0]]
871
+ },
872
+ tempBlockChunks
873
+ );
874
+ editor.setOption(
875
+ AIChatPlugin,
876
+ "_blockChunks",
877
+ // one block includes multiple children
878
+ tempBlocks[0].type === "code_block" || tempBlocks[0].type === "table" || tempBlocks[0].type === "equation" ? tempBlockChunks : serializedBlock2
879
+ );
880
+ }
881
+ } else {
882
+ const serializedBlock = streamSerializeMd(
883
+ editor,
884
+ {
885
+ value: [tempBlocks[0]]
886
+ },
887
+ tempBlockChunks
888
+ );
889
+ editor.tf.replaceNodes(
890
+ nodesWithProps(editor, [tempBlocks[0]], options),
891
+ {
892
+ at: _blockPath,
893
+ select: true
894
+ }
895
+ );
896
+ editor.setOption(AIChatPlugin, "_blockChunks", serializedBlock);
897
+ }
898
+ } else {
899
+ editor.tf.replaceNodes(nodesWithProps(editor, [tempBlocks[0]], options), {
900
+ at: _blockPath,
901
+ select: true
902
+ });
903
+ if (tempBlocks.length > 1) {
904
+ const newEndBlockPath = [_blockPath[0] + tempBlocks.length - 1];
905
+ editor.tf.insertNodes(
906
+ nodesWithProps(editor, tempBlocks.slice(1), options),
907
+ {
908
+ at: PathApi2.next(_blockPath),
909
+ select: true
910
+ }
911
+ );
912
+ editor.setOption(AIChatPlugin, "_blockPath", newEndBlockPath);
913
+ const endBlock = editor.api.node(newEndBlockPath)[0];
914
+ const serializedBlock = streamSerializeMd(
915
+ editor,
916
+ {
917
+ value: [endBlock]
918
+ },
919
+ tempBlockChunks
920
+ );
921
+ editor.setOption(AIChatPlugin, "_blockChunks", serializedBlock);
922
+ }
923
+ }
924
+ }
925
+ }
926
+ var getCurrentBlockPath = (editor) => {
927
+ const anchorNode = editor.getApi(AIChatPlugin).aiChat.node({ anchor: true });
928
+ if (anchorNode) {
929
+ return PathApi2.previous(anchorNode[1]);
930
+ }
931
+ return editor.selection?.focus.path.slice(0, 1) ?? [0];
932
+ };
933
+ export {
934
+ BaseAIPlugin,
935
+ escapeInput,
936
+ getChunkTrimmed,
937
+ getCurrentBlockPath,
938
+ getListNode,
939
+ insertAINodes,
940
+ isCompleteCodeBlock,
941
+ isCompleteMath,
942
+ isSameNode,
943
+ nodesWithProps,
944
+ removeAIMarks,
945
+ removeAINodes,
946
+ streamDeserializeInlineMd,
947
+ streamDeserializeMd,
948
+ streamInsertChunk,
949
+ streamSerializeMd,
950
+ undoAI,
951
+ withAIBatch
952
+ };
953
+ //# sourceMappingURL=index.mjs.map