@liveblocks/react-tiptap 2.16.1-ai2 → 2.16.1-ai3

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.
@@ -1,15 +1,15 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import { hide, offset, autoUpdate, useFloating } from '@floating-ui/react-dom';
2
+ import { hide, offset, shift, limitShift, autoUpdate, useFloating } from '@floating-ui/react-dom';
3
3
  import { useLayoutEffect } from '@liveblocks/react/_private';
4
- import { CheckIcon, UndoIcon, CrossIcon, SparklesIcon, ShortcutTooltip, Button, SendIcon, EditIcon, ShortenIcon, LengthenIcon, QuestionMarkIcon, useRefs, TooltipProvider } from '@liveblocks/react-ui/_private';
4
+ import { CheckIcon, UndoIcon, CrossIcon, ArrowCornerDownRightIcon, SparklesIcon, ShortcutTooltip, Button, SendIcon, WarningIcon, EditIcon, ShortenIcon, LengthenIcon, SparklesTextIcon, QuestionMarkIcon, useRefs, TooltipProvider } from '@liveblocks/react-ui/_private';
5
5
  import { useEditorState } from '@tiptap/react';
6
6
  import { Command, useCommandState } from 'cmdk';
7
- import { createContext, useContext, forwardRef, useCallback, useRef, useMemo, useEffect } from 'react';
7
+ import { createContext, useContext, forwardRef, useCallback, useRef, useMemo, useEffect, useState } from 'react';
8
8
  import { createPortal } from 'react-dom';
9
9
  import { classNames } from '../classnames.mjs';
10
10
  import { useCurrentEditor, EditorProvider } from '../context.mjs';
11
- import { getDomRangeFromSelection } from '../utils.mjs';
12
- import { DEFAULT_STATE } from './AiExtension.mjs';
11
+ import { getDomRange } from '../utils.mjs';
12
+ import { isAiToolbarDiffOutput, DEFAULT_STATE } from './AiExtension.mjs';
13
13
 
14
14
  const AI_TOOLBAR_COLLISION_PADDING = 10;
15
15
  const AiToolbarContext = createContext(null);
@@ -43,26 +43,36 @@ function tiptapFloating(editor) {
43
43
  }
44
44
  };
45
45
  }
46
- const AiToolbarDropdownGroup = forwardRef(({ children, label, ...props }, forwardedRef) => {
47
- return /* @__PURE__ */ jsx(Command.Group, {
48
- heading: /* @__PURE__ */ jsx("span", {
49
- className: "lb-dropdown-label",
50
- children: label
51
- }),
46
+ function flipToolbar() {
47
+ return {
48
+ name: "flipToolbar",
49
+ fn({ elements, middlewareData, rects }) {
50
+ const shiftOffsetY = middlewareData.shift?.y ?? 0;
51
+ if (Math.abs(shiftOffsetY) >= rects.floating.height) {
52
+ elements.floating.setAttribute("data-liveblocks-ai-toolbar-flip", "");
53
+ } else {
54
+ elements.floating.removeAttribute("data-liveblocks-ai-toolbar-flip");
55
+ }
56
+ return {};
57
+ }
58
+ };
59
+ }
60
+ const AiToolbarDropdownSeparator = forwardRef(({ className, ...props }, forwardedRef) => {
61
+ return /* @__PURE__ */ jsx(Command.Separator, {
62
+ className: classNames("lb-dropdown-separator", className),
52
63
  ...props,
53
- ref: forwardedRef,
54
- children
64
+ ref: forwardedRef
55
65
  });
56
66
  });
57
- const AiToolbarSuggestionsGroup = forwardRef((props, forwardedRef) => {
58
- return /* @__PURE__ */ jsx(AiToolbarDropdownGroup, {
67
+ const AiToolbarSuggestionsSeparator = forwardRef((props, forwardedRef) => {
68
+ return /* @__PURE__ */ jsx(AiToolbarDropdownSeparator, {
59
69
  ref: forwardedRef,
60
70
  ...props
61
71
  });
62
72
  });
63
- const AiToolbarDropdownItem = forwardRef(({ children, onSelect, icon, ...props }, forwardedRef) => {
73
+ const AiToolbarDropdownItem = forwardRef(({ children, onSelect, icon, className, ...props }, forwardedRef) => {
64
74
  return /* @__PURE__ */ jsxs(Command.Item, {
65
- className: "lb-dropdown-item",
75
+ className: classNames("lb-dropdown-item", className),
66
76
  onSelect,
67
77
  ...props,
68
78
  ref: forwardedRef,
@@ -78,6 +88,14 @@ const AiToolbarDropdownItem = forwardRef(({ children, onSelect, icon, ...props }
78
88
  ]
79
89
  });
80
90
  });
91
+ const AiToolbarSuggestionsLabel = forwardRef(({ children, className, ...props }, forwardedRef) => {
92
+ return /* @__PURE__ */ jsx("span", {
93
+ ref: forwardedRef,
94
+ className: classNames("lb-dropdown-label", className),
95
+ ...props,
96
+ children
97
+ });
98
+ });
81
99
  const AiToolbarSuggestion = forwardRef(({ prompt: manualPrompt, ...props }, forwardedRef) => {
82
100
  const editor = useCurrentEditor("Suggestion", "AiToolbar");
83
101
  const handleSelect = useCallback(
@@ -97,62 +115,56 @@ const AiToolbarSuggestion = forwardRef(({ prompt: manualPrompt, ...props }, forw
97
115
  function AiToolbarReviewingSuggestions() {
98
116
  const editor = useCurrentEditor("ReviewingSuggestions", "AiToolbar");
99
117
  const { state } = useAiToolbarContext();
100
- const { output } = state;
101
- const handleDiscard = useCallback(() => {
102
- editor.commands.$closeAiToolbar();
103
- }, [editor]);
104
- const handleAccept = useCallback(() => {
105
- editor.commands.$acceptAiToolbarOutput();
106
- }, [editor]);
107
- if (output.type === "insert" || output.type === "modification") {
118
+ const { prompt, output } = state;
119
+ const retry = useCallback(() => {
120
+ editor.commands.$startAiToolbarThinking(
121
+ prompt,
122
+ false
123
+ );
124
+ }, [editor, prompt]);
125
+ if (isAiToolbarDiffOutput(output)) {
108
126
  return /* @__PURE__ */ jsxs(Fragment, {
109
127
  children: [
110
128
  /* @__PURE__ */ jsx(AiToolbarDropdownItem, {
111
129
  icon: /* @__PURE__ */ jsx(CheckIcon, {}),
112
- onSelect: handleAccept,
130
+ onSelect: editor.commands.$acceptAiToolbarOutput,
113
131
  children: "Accept"
114
132
  }),
115
133
  /* @__PURE__ */ jsx(AiToolbarDropdownItem, {
116
134
  icon: /* @__PURE__ */ jsx(UndoIcon, {}),
117
- disabled: true,
135
+ onSelect: retry,
118
136
  children: "Try again"
119
137
  }),
120
138
  /* @__PURE__ */ jsx(AiToolbarDropdownItem, {
121
139
  icon: /* @__PURE__ */ jsx(CrossIcon, {}),
122
- onSelect: handleDiscard,
140
+ onSelect: editor.commands.$closeAiToolbar,
123
141
  children: "Discard"
124
142
  })
125
143
  ]
126
144
  });
127
- } else if (output.type === "other") {
145
+ } else {
128
146
  return /* @__PURE__ */ jsxs(Fragment, {
129
147
  children: [
130
148
  /* @__PURE__ */ jsx(AiToolbarDropdownItem, {
131
- icon: /* @__PURE__ */ jsx(CheckIcon, {}),
132
- disabled: true,
133
- children: "Replace selection"
134
- }),
135
- /* @__PURE__ */ jsx(AiToolbarDropdownItem, {
136
- icon: /* @__PURE__ */ jsx(CheckIcon, {}),
137
- disabled: true,
149
+ icon: /* @__PURE__ */ jsx(ArrowCornerDownRightIcon, {}),
150
+ onSelect: editor.commands.$acceptAiToolbarOutput,
138
151
  children: "Insert below"
139
152
  }),
140
153
  /* @__PURE__ */ jsx(AiToolbarDropdownItem, {
141
154
  icon: /* @__PURE__ */ jsx(UndoIcon, {}),
142
- disabled: true,
155
+ onSelect: retry,
143
156
  children: "Try again"
144
157
  }),
145
158
  /* @__PURE__ */ jsx(AiToolbarDropdownItem, {
146
159
  icon: /* @__PURE__ */ jsx(CrossIcon, {}),
147
- onSelect: handleDiscard,
160
+ onSelect: editor.commands.$closeAiToolbar,
148
161
  children: "Discard"
149
162
  })
150
163
  ]
151
164
  });
152
165
  }
153
- return null;
154
166
  }
155
- function AiToolbarCustomPromptContent({ disabled }) {
167
+ function AiToolbarCustomPromptContent() {
156
168
  const editor = useCurrentEditor("CustomPromptContent", "AiToolbar");
157
169
  const aiName = editor.storage.liveblocksAi.name;
158
170
  const textAreaRef = useRef(null);
@@ -194,7 +206,8 @@ function AiToolbarCustomPromptContent({ disabled }) {
194
206
  selectedDropdownItem.click();
195
207
  } else if (!isCustomPromptEmpty) {
196
208
  editor.commands.$startAiToolbarThinking(
197
- customPrompt
209
+ customPrompt,
210
+ state.phase === "reviewing"
198
211
  );
199
212
  }
200
213
  }
@@ -213,9 +226,10 @@ function AiToolbarCustomPromptContent({ disabled }) {
213
226
  return;
214
227
  }
215
228
  editor.commands.$startAiToolbarThinking(
216
- customPrompt
229
+ customPrompt,
230
+ state.phase === "reviewing"
217
231
  );
218
- }, [editor, customPrompt, isCustomPromptEmpty]);
232
+ }, [editor, customPrompt, isCustomPromptEmpty, state.phase]);
219
233
  return /* @__PURE__ */ jsxs("div", {
220
234
  className: "lb-tiptap-ai-toolbar-content",
221
235
  children: [
@@ -236,8 +250,7 @@ function AiToolbarCustomPromptContent({ disabled }) {
236
250
  placeholder: `Ask ${aiName} anything\u2026`,
237
251
  onKeyDown: handlePromptKeyDown,
238
252
  rows: 1,
239
- autoFocus: true,
240
- disabled
253
+ autoFocus: true
241
254
  })
242
255
  })
243
256
  }),
@@ -251,7 +264,7 @@ function AiToolbarCustomPromptContent({ disabled }) {
251
264
  variant: "primary",
252
265
  "aria-label": `Ask ${aiName}`,
253
266
  icon: /* @__PURE__ */ jsx(SendIcon, {}),
254
- disabled: isCustomPromptEmpty || disabled,
267
+ disabled: isCustomPromptEmpty,
255
268
  onClick: handleSendClick
256
269
  })
257
270
  })
@@ -260,7 +273,23 @@ function AiToolbarCustomPromptContent({ disabled }) {
260
273
  });
261
274
  }
262
275
  function AiToolbarAsking() {
263
- return /* @__PURE__ */ jsx(AiToolbarCustomPromptContent, {});
276
+ const { state } = useAiToolbarContext();
277
+ const { error } = state;
278
+ return /* @__PURE__ */ jsxs(Fragment, {
279
+ children: [
280
+ /* @__PURE__ */ jsx(AiToolbarCustomPromptContent, {}),
281
+ error ? /* @__PURE__ */ jsxs("div", {
282
+ className: "lb-tiptap-ai-toolbar-error",
283
+ children: [
284
+ /* @__PURE__ */ jsx("span", {
285
+ className: "lb-icon-container",
286
+ children: /* @__PURE__ */ jsx(WarningIcon, {})
287
+ }),
288
+ "There was a problem with your request."
289
+ ]
290
+ }) : null
291
+ ]
292
+ });
264
293
  }
265
294
  function AiToolbarThinking() {
266
295
  const editor = useCurrentEditor("AiToolbarThinking", "AiToolbar");
@@ -320,9 +349,7 @@ function AiToolbarReviewing() {
320
349
  children: output.text
321
350
  })
322
351
  }) : null,
323
- /* @__PURE__ */ jsx(AiToolbarCustomPromptContent, {
324
- disabled: true
325
- })
352
+ /* @__PURE__ */ jsx(AiToolbarCustomPromptContent, {})
326
353
  ]
327
354
  });
328
355
  }
@@ -363,7 +390,12 @@ function AiToolbarContainer({
363
390
  };
364
391
  }, [editor, state.phase]);
365
392
  return /* @__PURE__ */ jsxs(AiToolbarContext.Provider, {
366
- value: { state, toolbarRef, dropdownRef, isDropdownHidden },
393
+ value: {
394
+ state,
395
+ toolbarRef,
396
+ dropdownRef,
397
+ isDropdownHidden
398
+ },
367
399
  children: [
368
400
  /* @__PURE__ */ jsxs("div", {
369
401
  className: "lb-tiptap-ai-toolbar-container",
@@ -398,40 +430,42 @@ function AiToolbarContainer({
398
430
  }
399
431
  const defaultSuggestions = /* @__PURE__ */ jsxs(Fragment, {
400
432
  children: [
401
- /* @__PURE__ */ jsxs(AiToolbarSuggestionsGroup, {
402
- label: "Modify",
403
- children: [
404
- /* @__PURE__ */ jsx(AiToolbarSuggestion, {
405
- icon: /* @__PURE__ */ jsx(EditIcon, {}),
406
- children: "Improve writing"
407
- }),
408
- /* @__PURE__ */ jsx(AiToolbarSuggestion, {
409
- icon: /* @__PURE__ */ jsx(CheckIcon, {}),
410
- children: "Fix mistakes"
411
- }),
412
- /* @__PURE__ */ jsx(AiToolbarSuggestion, {
413
- icon: /* @__PURE__ */ jsx(ShortenIcon, {}),
414
- children: "Simplify"
415
- }),
416
- /* @__PURE__ */ jsx(AiToolbarSuggestion, {
417
- icon: /* @__PURE__ */ jsx(LengthenIcon, {}),
418
- children: "Add more detail"
419
- })
420
- ]
433
+ /* @__PURE__ */ jsx(AiToolbarSuggestion, {
434
+ icon: /* @__PURE__ */ jsx(EditIcon, {}),
435
+ prompt: "Improve the quality of the text",
436
+ children: "Improve writing"
421
437
  }),
422
- /* @__PURE__ */ jsx(AiToolbarSuggestionsGroup, {
423
- label: "Generate",
424
- children: /* @__PURE__ */ jsx(AiToolbarSuggestion, {
425
- icon: /* @__PURE__ */ jsx(QuestionMarkIcon, {}),
426
- children: "Explain"
427
- })
438
+ /* @__PURE__ */ jsx(AiToolbarSuggestion, {
439
+ icon: /* @__PURE__ */ jsx(CheckIcon, {}),
440
+ prompt: "Fix spelling & grammar errors in the text",
441
+ children: "Fix mistakes"
442
+ }),
443
+ /* @__PURE__ */ jsx(AiToolbarSuggestion, {
444
+ icon: /* @__PURE__ */ jsx(ShortenIcon, {}),
445
+ prompt: "Shorten the text, simplifying it",
446
+ children: "Simplify"
447
+ }),
448
+ /* @__PURE__ */ jsx(AiToolbarSuggestion, {
449
+ icon: /* @__PURE__ */ jsx(LengthenIcon, {}),
450
+ prompt: "Lengthen the text, going into more detail",
451
+ children: "Add more detail"
452
+ }),
453
+ /* @__PURE__ */ jsx(AiToolbarSuggestionsSeparator, {}),
454
+ /* @__PURE__ */ jsx(AiToolbarSuggestion, {
455
+ icon: /* @__PURE__ */ jsx(SparklesTextIcon, {}),
456
+ prompt: "Continue writing from the text's end",
457
+ children: "Continue writing"
458
+ }),
459
+ /* @__PURE__ */ jsx(AiToolbarSuggestion, {
460
+ icon: /* @__PURE__ */ jsx(QuestionMarkIcon, {}),
461
+ prompt: "Explain what the text is about",
462
+ children: "Explain"
428
463
  })
429
464
  ]
430
465
  });
431
466
  const AiToolbar = Object.assign(
432
467
  forwardRef(
433
468
  ({
434
- position = "bottom",
435
469
  offset: sideOffset = 6,
436
470
  editor,
437
471
  className,
@@ -444,18 +478,25 @@ const AiToolbar = Object.assign(
444
478
  return ctx.editor?.storage.liveblocksAi?.state;
445
479
  }
446
480
  }) ?? DEFAULT_STATE;
447
- const selection = state.selection ?? editor?.state.selection;
481
+ const selection = editor?.state.selection;
448
482
  const floatingOptions = useMemo(() => {
449
483
  const detectOverflowOptions = {
450
484
  padding: AI_TOOLBAR_COLLISION_PADDING
451
485
  };
452
486
  return {
453
487
  strategy: "fixed",
454
- placement: position,
488
+ placement: "bottom",
455
489
  middleware: [
456
490
  tiptapFloating(editor),
457
491
  hide(detectOverflowOptions),
458
- offset(sideOffset)
492
+ offset(sideOffset),
493
+ shift({
494
+ ...detectOverflowOptions,
495
+ mainAxis: false,
496
+ crossAxis: true,
497
+ limiter: limitShift()
498
+ }),
499
+ flipToolbar()
459
500
  ],
460
501
  whileElementsMounted: (...args) => {
461
502
  return autoUpdate(...args, {
@@ -463,7 +504,7 @@ const AiToolbar = Object.assign(
463
504
  });
464
505
  }
465
506
  };
466
- }, [editor, position, sideOffset]);
507
+ }, [editor, sideOffset]);
467
508
  const isOpen = selection !== void 0 && state.phase !== "closed";
468
509
  const {
469
510
  refs: { setReference, setFloating },
@@ -478,6 +519,28 @@ const AiToolbar = Object.assign(
478
519
  const toolbarRef = useRef(null);
479
520
  const mergedRefs = useRefs(forwardedRef, toolbarRef, setFloating);
480
521
  const dropdownRef = useRef(null);
522
+ const [selectedDropdownValue, setSelectedDropdownValue] = useState("");
523
+ useEffect(() => {
524
+ if (state.phase === "closed") {
525
+ setSelectedDropdownValue("");
526
+ }
527
+ }, [state.phase]);
528
+ useEffect(() => {
529
+ if (state.phase === "closed") {
530
+ setSelectedDropdownValue("");
531
+ return;
532
+ }
533
+ const selectedDropdownItem = dropdownRef.current?.querySelector(
534
+ "[role='option'][data-selected='true']"
535
+ );
536
+ if (selectedDropdownItem) {
537
+ return;
538
+ }
539
+ const firstDropdownItem = dropdownRef.current?.querySelector("[role='option']");
540
+ setSelectedDropdownValue(
541
+ firstDropdownItem?.dataset.value ?? ""
542
+ );
543
+ }, [state.phase, dropdownRef, setSelectedDropdownValue]);
481
544
  useEffect(() => {
482
545
  if (!editor) {
483
546
  return;
@@ -492,14 +555,40 @@ const AiToolbar = Object.assign(
492
555
  }
493
556
  setReference(null);
494
557
  setTimeout(() => {
495
- if (!selection) {
496
- setReference(null);
497
- } else {
498
- const domRange = getDomRangeFromSelection(selection, editor);
558
+ if (state.phase === "reviewing" && isAiToolbarDiffOutput(state.output)) {
559
+ const changes = editor.view.dom.querySelectorAll(
560
+ "ychange[data-liveblocks]"
561
+ );
562
+ setReference({
563
+ getBoundingClientRect: () => {
564
+ const rects = [];
565
+ changes.forEach((change) => {
566
+ rects.push(change.getBoundingClientRect());
567
+ });
568
+ const minX = Math.min(...rects.map((rect) => rect.left));
569
+ const minY = Math.min(...rects.map((rect) => rect.top));
570
+ const maxX = Math.max(...rects.map((rect) => rect.right));
571
+ const maxY = Math.max(...rects.map((rect) => rect.bottom));
572
+ return {
573
+ x: minX,
574
+ y: minY,
575
+ width: maxX - minX,
576
+ height: maxY - minY,
577
+ top: minY,
578
+ left: minX,
579
+ bottom: maxY,
580
+ right: maxX
581
+ };
582
+ }
583
+ });
584
+ } else if (selection) {
585
+ const domRange = getDomRange(editor, selection);
499
586
  setReference(domRange);
587
+ } else {
588
+ setReference(null);
500
589
  }
501
590
  }, 0);
502
- }, [selection, editor, isOpen, setReference]);
591
+ }, [selection, editor, isOpen, setReference, state.phase, state.output]);
503
592
  useEffect(() => {
504
593
  if (!editor || !isOpen) {
505
594
  return;
@@ -541,6 +630,8 @@ const AiToolbar = Object.assign(
541
630
  left: 0,
542
631
  transform: isPositioned ? `translate3d(${Math.round(x)}px, ${Math.round(y)}px, 0)` : "translate3d(0, -200%, 0)"
543
632
  },
633
+ value: selectedDropdownValue,
634
+ onValueChange: setSelectedDropdownValue,
544
635
  ...props,
545
636
  children: /* @__PURE__ */ jsx(AiToolbarContainer, {
546
637
  state,
@@ -558,8 +649,9 @@ const AiToolbar = Object.assign(
558
649
  }
559
650
  ),
560
651
  {
561
- SuggestionsGroup: AiToolbarSuggestionsGroup,
562
- Suggestion: AiToolbarSuggestion
652
+ Suggestion: AiToolbarSuggestion,
653
+ SuggestionsLabel: AiToolbarSuggestionsLabel,
654
+ SuggestionsSeparator: AiToolbarSuggestionsSeparator
563
655
  }
564
656
  );
565
657