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