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

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