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