@ecsplico/editor 2.0.0 → 2.1.1

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.
Files changed (278) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +4 -3
  3. package/dist/adapters/anchors.d.ts +80 -0
  4. package/dist/adapters/anchors.js +270 -0
  5. package/dist/adapters/anchors.js.map +1 -0
  6. package/dist/adapters/comments/client.d.ts +64 -0
  7. package/dist/adapters/comments/client.js +536 -0
  8. package/dist/adapters/comments/client.js.map +1 -0
  9. package/dist/adapters/comments/drizzle.d.ts +430 -0
  10. package/dist/adapters/comments/drizzle.js +236 -0
  11. package/dist/adapters/comments/drizzle.js.map +1 -0
  12. package/dist/adapters/comments/errors.d.ts +9 -0
  13. package/dist/adapters/comments/errors.js +19 -0
  14. package/dist/adapters/comments/errors.js.map +1 -0
  15. package/dist/adapters/comments/json.d.ts +19 -0
  16. package/dist/adapters/comments/json.js +94 -0
  17. package/dist/adapters/comments/json.js.map +1 -0
  18. package/dist/adapters/comments/record.js +78 -0
  19. package/dist/adapters/comments/record.js.map +1 -0
  20. package/dist/adapters/comments/server.d.ts +23 -0
  21. package/dist/adapters/comments/server.js +102 -0
  22. package/dist/adapters/comments/server.js.map +1 -0
  23. package/dist/adapters/comments/types.d.ts +109 -0
  24. package/dist/adapters/documents.js +108 -0
  25. package/dist/adapters/documents.js.map +1 -0
  26. package/dist/adapters/drizzle.d.ts +293 -0
  27. package/dist/adapters/drizzle.js +157 -0
  28. package/dist/adapters/drizzle.js.map +1 -0
  29. package/dist/adapters/files.js +30 -0
  30. package/dist/adapters/files.js.map +1 -0
  31. package/dist/adapters/forge.d.ts +58 -0
  32. package/dist/adapters/forge.js +91 -0
  33. package/dist/adapters/forge.js.map +1 -0
  34. package/dist/adapters/index.d.ts +13 -0
  35. package/dist/adapters/index.js +0 -0
  36. package/dist/adapters/json.d.ts +24 -0
  37. package/dist/adapters/json.js +128 -0
  38. package/dist/adapters/json.js.map +1 -0
  39. package/dist/adapters/nitro.d.ts +61 -0
  40. package/dist/adapters/nitro.js +212 -0
  41. package/dist/adapters/nitro.js.map +1 -0
  42. package/dist/adapters/routes.js +16 -0
  43. package/dist/adapters/routes.js.map +1 -0
  44. package/dist/adapters/shared.d.ts +37 -0
  45. package/dist/adapters/shared.js +32 -0
  46. package/dist/adapters/shared.js.map +1 -0
  47. package/dist/adapters/vue.d.ts +67 -0
  48. package/dist/adapters/vue.js +187 -0
  49. package/dist/adapters/vue.js.map +1 -0
  50. package/dist/code/CodeSurface.vue.d.ts +3 -3
  51. package/dist/core/doc-anchors.d.ts +5 -0
  52. package/dist/core/doc-anchors.js +4 -3
  53. package/dist/core/doc-anchors.js.map +1 -1
  54. package/dist/core/editor-ui.js +57 -1
  55. package/dist/core/editor-ui.js.map +1 -1
  56. package/dist/core/surface-session.js +139 -0
  57. package/dist/core/surface-session.js.map +1 -0
  58. package/dist/document/DocumentSurface.d.ts +1 -1
  59. package/dist/document/DocumentSurface.vue.d.ts +3 -3
  60. package/dist/features/ai/agent-op-review.d.ts +2 -2
  61. package/dist/features/ai/agent-op-review.js.map +1 -1
  62. package/dist/features/ai/agent-ops.d.ts +1 -1
  63. package/dist/features/ai/agent-ops.js +9 -6
  64. package/dist/features/ai/agent-ops.js.map +1 -1
  65. package/dist/features/ai/ai-apply.d.ts +1 -1
  66. package/dist/features/ai/ai-review.js +10 -1
  67. package/dist/features/ai/ai-review.js.map +1 -1
  68. package/dist/features/ai/components/AiSelectionMenu.js +78 -252
  69. package/dist/features/ai/components/AiSelectionMenu.js.map +1 -1
  70. package/dist/features/ai/components/AiSelectionMenu.vue.d.ts +2 -1
  71. package/dist/features/ai/components/AiSidebar.js +2 -2
  72. package/dist/features/ai/components/DocumentChatAnswer.js +1 -1
  73. package/dist/features/ai/components/DocumentChatOps.js +1 -0
  74. package/dist/features/ai/components/DocumentChatOps.js.map +1 -1
  75. package/dist/features/ai/components/ModelQuickSwitch.js +1 -1
  76. package/dist/features/ai/components/SettingsAiTab.js +1 -1
  77. package/dist/features/ai/components/SettingsModelProfiles.js +1 -1
  78. package/dist/features/ai/document-chat.d.ts +3 -2
  79. package/dist/features/ai/document-chat.js +16 -19
  80. package/dist/features/ai/document-chat.js.map +1 -1
  81. package/dist/features/ai/index.d.ts +5 -3
  82. package/dist/features/ai/index.js +4 -2
  83. package/dist/features/ai/protocol/agent-ops.d.ts +22 -17
  84. package/dist/features/ai/protocol/agent-ops.js +65 -39
  85. package/dist/features/ai/protocol/agent-ops.js.map +1 -1
  86. package/dist/features/ai/protocol/block-lines.d.ts +89 -0
  87. package/dist/features/ai/protocol/block-lines.js +177 -0
  88. package/dist/features/ai/protocol/block-lines.js.map +1 -0
  89. package/dist/features/ai/protocol/chat.js +10 -10
  90. package/dist/features/ai/protocol/chat.js.map +1 -1
  91. package/dist/features/ai/protocol/index.d.ts +3 -2
  92. package/dist/features/ai/protocol/index.js +3 -2
  93. package/dist/features/ai/react/AiSelectionMenu.d.ts +3 -2
  94. package/dist/features/ai/react/AiSelectionMenu.js +64 -255
  95. package/dist/features/ai/react/AiSelectionMenu.js.map +1 -1
  96. package/dist/features/ai/react/AiSidebar.js +2 -2
  97. package/dist/features/ai/react/DocumentChatAnswer.js +1 -1
  98. package/dist/features/ai/react/DocumentChatOps.js +1 -0
  99. package/dist/features/ai/react/DocumentChatOps.js.map +1 -1
  100. package/dist/features/ai/react/ModelQuickSwitch.js +1 -1
  101. package/dist/features/ai/react/SettingsAiTab.js +1 -1
  102. package/dist/features/ai/react/SettingsModelProfiles.js +1 -1
  103. package/dist/features/ai/react/use-ai-selection-flow.js +34 -0
  104. package/dist/features/ai/react/use-ai-selection-flow.js.map +1 -0
  105. package/dist/features/ai/selection-flow.d.ts +133 -0
  106. package/dist/features/ai/selection-flow.js +283 -0
  107. package/dist/features/ai/selection-flow.js.map +1 -0
  108. package/dist/features/ai/use-ai-assistant.d.ts +1 -1
  109. package/dist/features/ai/use-document-chat.d.ts +1 -1
  110. package/dist/features/authors/components/AuthorColorsPanel.js +1 -1
  111. package/dist/features/authors/feature.js +1 -1
  112. package/dist/features/authors/index.js +1 -1
  113. package/dist/features/authors/react/AuthorColorsPanel.js +1 -1
  114. package/dist/features/comments/components/CommentBubble.js +1 -1
  115. package/dist/features/comments/components/CommentComposer.js +1 -1
  116. package/dist/features/comments/components/CommentThreadView.js +5 -2
  117. package/dist/features/comments/components/CommentThreadView.js.map +1 -1
  118. package/dist/features/comments/components/CommentsMargin.js +108 -230
  119. package/dist/features/comments/components/CommentsMargin.js.map +1 -1
  120. package/dist/features/comments/components/CommentsOverlay.js +1 -1
  121. package/dist/features/comments/feature.js +1 -1
  122. package/dist/features/comments/index.d.ts +4 -2
  123. package/dist/features/comments/index.js +4 -2
  124. package/dist/features/comments/margin-controller.d.ts +44 -0
  125. package/dist/features/comments/margin-controller.js +132 -0
  126. package/dist/features/comments/margin-controller.js.map +1 -0
  127. package/dist/features/comments/margin-layout.d.ts +55 -0
  128. package/dist/features/comments/margin-layout.js +79 -0
  129. package/dist/features/comments/margin-layout.js.map +1 -0
  130. package/dist/features/comments/model.d.ts +8 -0
  131. package/dist/features/comments/model.js +60 -34
  132. package/dist/features/comments/model.js.map +1 -1
  133. package/dist/features/comments/react/CommentBubble.js +1 -1
  134. package/dist/features/comments/react/CommentComposer.js +1 -1
  135. package/dist/features/comments/react/CommentThreadView.js +2 -2
  136. package/dist/features/comments/react/CommentThreadView.js.map +1 -1
  137. package/dist/features/comments/react/CommentsMargin.d.ts +0 -5
  138. package/dist/features/comments/react/CommentsMargin.js +85 -251
  139. package/dist/features/comments/react/CommentsMargin.js.map +1 -1
  140. package/dist/features/comments/react/CommentsOverlay.js +1 -1
  141. package/dist/features/comments/react/use-comments.js +1 -1
  142. package/dist/features/comments/react/use-margin-layout.js +45 -0
  143. package/dist/features/comments/react/use-margin-layout.js.map +1 -0
  144. package/dist/features/comments/react.d.ts +1 -1
  145. package/dist/features/comments/schema.d.ts +23 -0
  146. package/dist/features/comments/schema.js.map +1 -1
  147. package/dist/features/comments/snapshot-equality.js +40 -0
  148. package/dist/features/comments/snapshot-equality.js.map +1 -0
  149. package/dist/features/comments/thread-anchor.js +33 -0
  150. package/dist/features/comments/thread-anchor.js.map +1 -0
  151. package/dist/features/comments/use-comments.d.ts +3 -1
  152. package/dist/features/comments/use-comments.js +1 -0
  153. package/dist/features/comments/use-comments.js.map +1 -1
  154. package/dist/features/comments/use-margin-layout.js +29 -0
  155. package/dist/features/comments/use-margin-layout.js.map +1 -0
  156. package/dist/features/comments/vue.d.ts +1 -1
  157. package/dist/features/composites/accepted-children.d.ts +21 -0
  158. package/dist/features/composites/accepted-children.js +126 -0
  159. package/dist/features/composites/accepted-children.js.map +1 -0
  160. package/dist/features/composites/components/CompositeDetails.js +1 -1
  161. package/dist/features/composites/components/CompositeInspector.js +38 -164
  162. package/dist/features/composites/components/CompositeInspector.js.map +1 -1
  163. package/dist/features/composites/feature.d.ts +2 -1
  164. package/dist/features/composites/feature.js +8 -2
  165. package/dist/features/composites/feature.js.map +1 -1
  166. package/dist/features/composites/index.d.ts +5 -3
  167. package/dist/features/composites/index.js +8 -6
  168. package/dist/features/composites/inspector-model.d.ts +97 -0
  169. package/dist/features/composites/inspector-model.js +255 -0
  170. package/dist/features/composites/inspector-model.js.map +1 -0
  171. package/dist/features/composites/kinds.d.ts +11 -0
  172. package/dist/features/composites/kinds.js +17 -2
  173. package/dist/features/composites/kinds.js.map +1 -1
  174. package/dist/features/composites/outline.d.ts +18 -5
  175. package/dist/features/composites/outline.js +44 -12
  176. package/dist/features/composites/outline.js.map +1 -1
  177. package/dist/features/composites/react/CompositeDetails.js +1 -1
  178. package/dist/features/composites/react/CompositeInspector.d.ts +1 -1
  179. package/dist/features/composites/react/CompositeInspector.js +21 -180
  180. package/dist/features/composites/react/CompositeInspector.js.map +1 -1
  181. package/dist/features/composites/react/CompositeStatusMenu.d.ts +1 -1
  182. package/dist/features/composites/react/CompositeView.js +1 -1
  183. package/dist/features/composites/react/use-composite-inspector.js +29 -0
  184. package/dist/features/composites/react/use-composite-inspector.js.map +1 -0
  185. package/dist/features/composites/schema.d.ts +2 -2
  186. package/dist/features/composites/schema.js +2 -2
  187. package/dist/features/composites/schema.js.map +1 -1
  188. package/dist/features/composites/use-composite-anchors.d.ts +1 -1
  189. package/dist/features/composites/use-composite-inspector.js +41 -0
  190. package/dist/features/composites/use-composite-inspector.js.map +1 -0
  191. package/dist/features/composites/use-composite-rows.d.ts +1 -1
  192. package/dist/features/media/index.d.ts +1 -1
  193. package/dist/features/media/vue.d.ts +1 -1
  194. package/dist/features/mentions/index.d.ts +3 -3
  195. package/dist/features/mentions/index.js +3 -3
  196. package/dist/features/mentions/schema.d.ts +1 -1
  197. package/dist/features/mentions/schema.js +1 -1
  198. package/dist/features/themes/index.d.ts +1 -1
  199. package/dist/features/themes/index.js +1 -1
  200. package/dist/features/transcript/feature.js +1 -1
  201. package/dist/features/transcript/index.js +2 -2
  202. package/dist/i18n/de/ai.js +2 -0
  203. package/dist/i18n/de/ai.js.map +1 -1
  204. package/dist/i18n/de/composites.js +8 -1
  205. package/dist/i18n/de/composites.js.map +1 -1
  206. package/dist/i18n/de/tree.js +14 -1
  207. package/dist/i18n/de/tree.js.map +1 -1
  208. package/dist/i18n/de.d.ts +22 -0
  209. package/dist/i18n/en/ai.js +2 -0
  210. package/dist/i18n/en/ai.js.map +1 -1
  211. package/dist/i18n/en/composites.js +8 -1
  212. package/dist/i18n/en/composites.js.map +1 -1
  213. package/dist/i18n/en/tree.js +14 -1
  214. package/dist/i18n/en/tree.js.map +1 -1
  215. package/dist/integrations/composite-ai/composite-fill.js +2 -2
  216. package/dist/integrations/composite-ai/composite-summary.js +2 -2
  217. package/dist/integrations/composite-ai/index.js +2 -2
  218. package/dist/integrations/transcript-ai/transcript-cleanup.js +1 -1
  219. package/dist/markdown/MarkdownSurface.d.ts +1 -1
  220. package/dist/markdown/MarkdownSurface.js +2 -2
  221. package/dist/markdown/MarkdownSurface.vue.d.ts +9 -9
  222. package/dist/markdown/index.d.ts +2 -2
  223. package/dist/nuxt/runtime/server/routes/collab/[id].js +1 -1
  224. package/dist/react/EditorSurface.d.ts +1 -1
  225. package/dist/react/EditorSurface.js +43 -149
  226. package/dist/react/EditorSurface.js.map +1 -1
  227. package/dist/react/EditorToolbar.js +3 -40
  228. package/dist/react/EditorToolbar.js.map +1 -1
  229. package/dist/schema/advanced.js +2 -2
  230. package/dist/server/adapter.d.ts +2 -2
  231. package/dist/server/collab.js +1 -1
  232. package/dist/server/handlers/ai.d.ts +4 -2
  233. package/dist/server/handlers/ai.js +11 -9
  234. package/dist/server/handlers/ai.js.map +1 -1
  235. package/dist/server/handlers/context.d.ts +1 -1
  236. package/dist/server/handlers/editor.d.ts +1 -1
  237. package/dist/server/handlers/editor.js +2 -2
  238. package/dist/server/host.d.ts +1 -1
  239. package/dist/server/index.d.ts +2 -2
  240. package/dist/settings/index.d.ts +1 -1
  241. package/dist/styles/tokens.css +1 -1
  242. package/dist/tree/DocumentTree.js +159 -387
  243. package/dist/tree/DocumentTree.js.map +1 -1
  244. package/dist/tree/drop-rules.js +37 -0
  245. package/dist/tree/drop-rules.js.map +1 -0
  246. package/dist/tree/react/DocumentTree.d.ts +2 -2
  247. package/dist/tree/react/DocumentTree.js +135 -397
  248. package/dist/tree/react/DocumentTree.js.map +1 -1
  249. package/dist/tree/react/use-tree-model.js +129 -0
  250. package/dist/tree/react/use-tree-model.js.map +1 -0
  251. package/dist/tree/react.js +1 -1
  252. package/dist/tree/tree-host.js +100 -0
  253. package/dist/tree/tree-host.js.map +1 -0
  254. package/dist/tree/tree-items.js +22 -43
  255. package/dist/tree/tree-items.js.map +1 -1
  256. package/dist/tree/tree-model.js +580 -0
  257. package/dist/tree/tree-model.js.map +1 -0
  258. package/dist/tree/tree-rows.js +77 -0
  259. package/dist/tree/tree-rows.js.map +1 -0
  260. package/dist/tree/use-tree-model.js +94 -0
  261. package/dist/tree/use-tree-model.js.map +1 -0
  262. package/dist/tree/vue.js +1 -1
  263. package/dist/vue/AdvancedEditor.vue.d.ts +6 -6
  264. package/dist/vue/SimpleEditor.vue.d.ts +2 -2
  265. package/dist/vue/components/EditorSurface.js +27 -87
  266. package/dist/vue/components/EditorSurface.js.map +1 -1
  267. package/dist/vue/components/EditorSurface.vue.d.ts +6 -6
  268. package/dist/vue/components/EditorToolbar.js +7 -44
  269. package/dist/vue/components/EditorToolbar.js.map +1 -1
  270. package/dist/vue/context.d.ts +1 -1
  271. package/dist/vue/context.js +1 -1
  272. package/dist/vue/context.js.map +1 -1
  273. package/dist/vue/use-editor-ui.d.ts +1 -1
  274. package/dist/vue/use-notifier.d.ts +1 -1
  275. package/docs/integration.md +97 -4
  276. package/docs/llms.md +9 -1
  277. package/docs/usage.md +10 -5
  278. package/package.json +67 -5
@@ -1,18 +1,14 @@
1
- import { ABILITY } from "../../../core/schema/abilities.js";
2
- import { abilitiesAtSelection, hasAbility } from "../../../core/abilities.js";
3
- import { aiContextOf, parseBlocks } from "../../../core/pipeline.js";
1
+ import { parseBlocks } from "../../../core/pipeline.js";
4
2
  import { AI_LANGUAGES } from "../../../settings/values.js";
5
- import { captureRange, releaseRanges, resolveRange } from "../../../core/doc-anchors.js";
6
- import { selectionRange } from "../../../core/selection-range.js";
3
+ import { resolveRange } from "../../../core/doc-anchors.js";
4
+ import { AI_LANGUAGE_KEYS } from "../protocol/models.js";
7
5
  import { useEditorI18n } from "../../../react/use-editor-i18n.js";
8
6
  import { Icon } from "../../../react/Icon.js";
7
+ import { useStore } from "../../../react/use-store.js";
9
8
  import { useEditorSettings } from "../../../react/settings.js";
10
9
  import { Button } from "../../../react/ui/button.js";
11
- import { AI_LANGUAGE_KEYS } from "../protocol/models.js";
12
- import { AI_LIMITS, AI_TASKS } from "../protocol/tasks.js";
13
- import { aiApplyFailure, applyAiResult, documentMarkdown, insertAtCaretLine, rangeMarkdown } from "../ai-apply.js";
14
- import { aiMenuActionsOf } from "../ai-menu.js";
15
- import { aiSelectionHighlightKey, hideAiSelectionHighlight, showAiSelectionHighlight } from "../ai-selection-highlight.js";
10
+ import { aiSelectionHighlightKey } from "../ai-selection-highlight.js";
11
+ import { AI_SELECTION_MENU_KEY } from "../selection-flow.js";
16
12
  import { Popover, PopoverAnchor, PopoverContent } from "../../../react/ui/popover.js";
17
13
  import { useNotifier } from "../../../react/use-notifier.js";
18
14
  import { Alert, AlertTitle } from "../../../react/ui/alert.js";
@@ -21,255 +17,70 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
21
17
  import { previewExtensions } from "../preview-extensions.js";
22
18
  import { ModelQuickSwitch } from "./ModelQuickSwitch.js";
23
19
  import { useAiAssistant } from "./use-ai-assistant.js";
20
+ import { useAiSelectionFlow } from "./use-ai-selection-flow.js";
24
21
  import { useEffect, useRef, useState } from "react";
25
22
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
26
23
  import { EditorContent, useEditor } from "@tiptap/react";
27
24
  import { BubbleMenu } from "@tiptap/react/menus";
28
25
  //#region src/features/ai/react/AiSelectionMenu.tsx
29
- const MENU_KEY = "aiSelectionMenu";
30
- const quickActions = Object.values(AI_TASKS).filter((task) => task.selectionMenu && !task.needsPrompt);
31
- const custom = AI_TASKS.custom;
32
- const caretActions = Object.values(AI_TASKS).filter((task) => task.caretMenu);
33
26
  /**
34
27
  * AI quick actions on a text selection: a bubble menu entry opens a popover with the selection tasks of the registry
35
28
  * (`selectionMenu`), the result streams into a preview and is applied over the range captured when the menu opened.
36
29
  * Caret mode (`/ai`, see `openAiMenuAtCaret`): the same popover opens at the caret with the `caretMenu` tasks and
37
30
  * `generate` as free prompt; the result is inserted at the caret line (`insertAtCaretLine`).
38
- * Needs the AiSelectionHighlight extension on the editor.
31
+ * Needs the AiSelectionHighlight extension on the editor. The flow (`createAiSelectionFlow`) owns every step; this
32
+ * component renders its state, places the popover and shows the preview.
39
33
  */
40
34
  function AiSelectionMenu({ editor }) {
41
35
  const notify = useNotifier(editor);
42
36
  const { t } = useEditorI18n();
43
- const { status, output, error, run, abort, reset } = useAiAssistant(editor);
37
+ const { model: assistant } = useAiAssistant(editor);
44
38
  const { settings } = useEditorSettings();
45
- /** Target language of „Übersetzen“: the settings' default each time the menu opens, changeable per call. */
46
- const [language, setLanguage] = useState(settings.ai.translateLanguage);
47
- const [prompt, setPrompt] = useState("");
48
- /** 'selection': rewrites the captured selection; 'caret': writes new text at the captured (collapsed) caret. */
49
- const [mode, setModeState] = useState("selection");
50
- /** Caret mode: whether the document has text before the caret line, for tasks that continue it (`needsSelection`). */
51
- const [hasTextBefore, setHasTextBefore] = useState(false);
52
- /** The selection the menu works on, captured when it opened; null while closed. */
53
- const [range, setRangeState] = useState(null);
54
- /** The current request; „Erneut versuchen“ repeats it. */
55
- const [request, setRequest] = useState(null);
56
- const [runningAction, setRunningAction] = useState(null);
39
+ const flow = useAiSelectionFlow({
40
+ editor,
41
+ assistant,
42
+ notify,
43
+ t,
44
+ language: () => settings.ai.translateLanguage
45
+ });
46
+ return flow && /* @__PURE__ */ jsx(AiSelectionMenuView, {
47
+ editor,
48
+ flow
49
+ });
50
+ }
51
+ function AiSelectionMenuView({ editor, flow }) {
52
+ const state = useStore(flow.state);
53
+ const { mode, picking, canApply, status, output, request } = state;
57
54
  const promptInput = useRef(null);
58
- const rangeRef = useRef(null);
59
- const modeRef = useRef("selection");
60
- const setRange = (value) => {
61
- rangeRef.current = value;
62
- setRangeState(value);
63
- };
64
- const setMode = (value) => {
65
- modeRef.current = value;
66
- setModeState(value);
67
- };
68
55
  const latest = useRef({
69
56
  editor,
70
- output,
71
- settings
57
+ output
72
58
  });
73
59
  latest.current = {
74
60
  editor,
75
- output,
76
- settings
61
+ output
77
62
  };
78
- const actionController = useRef(null);
79
- const actions = mode === "caret" ? caretActions : quickActions;
80
- const freePrompt = mode === "caret" ? AI_TASKS.generate : custom;
81
- const picking = status === "idle" && !output;
82
- const canApply = status === "done" && !!output.trim();
63
+ const { t } = useEditorI18n();
83
64
  const requestLabel = request ? request.language ? t("ai.menu.requestWithLanguage", {
84
65
  task: t(request.task.labelKey),
85
66
  language: t(AI_LANGUAGE_KEYS[request.language])
86
67
  }) : t(request.task.labelKey) : void 0;
87
- const resolved = range && !editor.isDestroyed ? resolveRange(editor.state, range) : null;
88
- const menuActions = resolved ? aiMenuActionsOf(editor).filter((action) => action.available?.(editor, resolved) ?? true) : [];
89
- async function runMenuAction(action) {
90
- const captured = rangeRef.current;
91
- const at = captured && resolveRange(editor.state, captured);
92
- if (!at || actionController.current) return;
93
- const own = new AbortController();
94
- actionController.current = own;
95
- setRunningAction(action.id);
96
- try {
97
- await action.run(editor, at, own.signal);
98
- } finally {
99
- if (actionController.current === own) actionController.current = null;
100
- setRunningAction(null);
101
- }
102
- if (rangeRef.current === captured) close(false);
103
- }
104
- useEffect(() => {
105
- if (!range) {
106
- actionController.current?.abort();
107
- actionController.current = null;
108
- } else setLanguage(latest.current.settings.ai.translateLanguage);
109
- }, [range]);
110
- const shouldShow = ({ editor: current, element, view, state, from, to }) => {
111
- if (rangeRef.current || !current.isEditable || state.selection.empty) return false;
112
- if (!view.hasFocus() && !element.contains(document.activeElement)) return false;
113
- if (state.doc.textBetween(from, to, " ").trim().length === 0) return false;
114
- return abilitiesAtSelection(current).has(ABILITY.aiRewrite);
115
- };
116
- function openMenu() {
117
- const selected = selectionRange(editor.state);
118
- if (selected.from >= selected.to) return;
119
- const captured = captureRange(editor, selected);
120
- setRange(captured);
121
- showAiSelectionHighlight(editor, captured);
122
- editor.view.dispatch(editor.state.tr.setMeta(MENU_KEY, "hide"));
123
- }
124
- /** Opens the menu at the caret (requested by `openAiMenuAtCaret`, e.g. the `/ai` slash command). */
125
- function openAtCaret() {
126
- if (rangeRef.current || !editor.isEditable) return;
127
- const { $head, head } = editor.state.selection;
128
- setMode("caret");
129
- setHasTextBefore(editor.state.doc.textBetween(0, $head.depth ? $head.before() : head, " ", " ").trim().length > 0);
130
- setRange(captureRange(editor, {
131
- from: head,
132
- to: head
133
- }));
134
- }
135
- const onTransaction = useRef(() => {});
136
- onTransaction.current = ({ transaction }) => {
137
- if (transaction.getMeta("aiMenuAtCaret") && hasAbility(editor, ABILITY.aiGenerate)) openAtCaret();
138
- if (transaction.getMeta("aiMenuForSelection") && !rangeRef.current && editor.isEditable) openMenu();
139
- };
140
- useEffect(() => {
141
- const handler = (event) => onTransaction.current(event);
142
- editor.on("transaction", handler);
143
- return () => {
144
- editor.off("transaction", handler);
145
- };
146
- }, [editor]);
68
+ const menuActions = state.open ? flow.menuActions() : [];
69
+ const shouldShow = ({ element, view }) => (view.hasFocus() || element.contains(document.activeElement)) && flow.offersSelection();
147
70
  /** Caret mode: the prompt input gets the focus, so the instruction can be typed right away. */
148
71
  function onOpenAutoFocus(event) {
149
- if (modeRef.current !== "caret") return;
72
+ if (flow.state.get().mode !== "caret") return;
150
73
  event.preventDefault();
151
74
  promptInput.current?.focus();
152
75
  }
153
- function start(task, options = {}) {
154
- const next = {
155
- task,
156
- prompt: options.prompt ?? "",
157
- language: options.language
158
- };
159
- setRequest(next);
160
- generate(next);
161
- }
162
- /** Runs the request (default: the current one) on the current text of the captured range, with the current document as context. */
163
- function generate(current = request) {
164
- const captured = rangeRef.current;
165
- if (!captured || !current) return;
166
- if (modeRef.current === "caret") {
167
- generateAtCaret(captured, current);
168
- return;
169
- }
170
- const at = resolveRange(editor.state, captured);
171
- if (!at || at.from >= at.to) {
172
- notify({
173
- title: t("ai.apply.missing.title"),
174
- description: t("ai.menu.missing.selection"),
175
- kind: "error"
176
- });
177
- return;
178
- }
179
- const { blocks } = aiContextOf(editor);
180
- run({
181
- task: current.task.id,
182
- prompt: current.prompt,
183
- ...current.language && { language: current.language },
184
- selection: rangeMarkdown(editor, at),
185
- context: documentMarkdown(editor),
186
- blocks
187
- });
188
- }
189
- /**
190
- * Caret mode: tasks with `needsSelection` (continue) get the document up to the caret line as `selection` and the rest
191
- * as `context`; the others the whole document as context.
192
- */
193
- function generateAtCaret(captured, current) {
194
- const at = resolveRange(editor.state, captured);
195
- if (!at) {
196
- notify({
197
- title: t("ai.apply.missing.title"),
198
- description: t("ai.menu.missing.line"),
199
- kind: "error"
200
- });
201
- return;
202
- }
203
- const { doc } = editor.state;
204
- const $at = doc.resolve(at.from);
205
- const splitBefore = $at.depth ? $at.before() : at.from;
206
- const splitAfter = $at.depth ? $at.after() : at.from;
207
- const continues = current.task.needsSelection;
208
- const { blocks } = aiContextOf(editor);
209
- run({
210
- task: current.task.id,
211
- prompt: current.prompt,
212
- selection: continues ? rangeMarkdown(editor, {
213
- from: 0,
214
- to: splitBefore
215
- }).slice(-AI_LIMITS.selection) : "",
216
- context: continues ? rangeMarkdown(editor, {
217
- from: splitAfter,
218
- to: doc.content.size
219
- }).slice(0, AI_LIMITS.context) : documentMarkdown(editor),
220
- blocks
221
- });
222
- }
223
- /** 'replace' opens the in-document review of the result (AiReviewBar); nothing is written until accepted there. */
224
- function apply(action) {
225
- const captured = rangeRef.current;
226
- if (!captured || !canApply) return;
227
- const outcome = applyAiResult(editor, action, captured, output);
228
- if (outcome !== "ok") {
229
- notify(aiApplyFailure(outcome, t));
230
- if (outcome !== "unchanged") return;
231
- }
232
- close(false);
233
- }
234
- function insertAtCaret() {
235
- const captured = rangeRef.current;
236
- if (!captured || !canApply) return;
237
- if (!insertAtCaretLine(editor, captured, output)) {
238
- notify({
239
- title: t("ai.apply.missing.title"),
240
- description: t("ai.menu.missing.insert"),
241
- kind: "error"
242
- });
243
- return;
244
- }
245
- close(false);
246
- }
247
- /** Closes the menu and removes the highlight; `restoreSelection` selects the captured range (caret mode: caret) again. */
248
- function close(restoreSelection) {
249
- const captured = rangeRef.current;
250
- const caret = modeRef.current === "caret";
251
- reset();
252
- setRequest(null);
253
- setPrompt("");
254
- setRange(null);
255
- setMode("selection");
256
- if (editor.isDestroyed) return;
257
- hideAiSelectionHighlight(editor);
258
- const at = captured && restoreSelection ? resolveRange(editor.state, captured) : null;
259
- if (captured) releaseRanges(editor, [captured]);
260
- if (at && (caret || at.from < at.to)) editor.chain().focus().setTextSelection(at).run();
261
- }
262
76
  const [anchor] = useState(() => {
263
77
  let anchorRect = new DOMRect();
264
78
  let caretAt = null;
265
79
  const caretRect = (instance) => {
266
- if (caretAt?.state !== instance.state) {
267
- const captured = rangeRef.current;
268
- caretAt = {
269
- state: instance.state,
270
- pos: captured ? resolveRange(instance.state, captured)?.from ?? null : null
271
- };
272
- }
80
+ if (caretAt?.state !== instance.state) caretAt = {
81
+ state: instance.state,
82
+ pos: flow.range()?.from ?? null
83
+ };
273
84
  if (caretAt.pos === null) return anchorRect;
274
85
  const coords = instance.view.coordsAtPos(caretAt.pos);
275
86
  anchorRect = new DOMRect(coords.left, coords.top, 1, coords.bottom - coords.top);
@@ -279,7 +90,7 @@ function AiSelectionMenu({ editor }) {
279
90
  getBoundingClientRect() {
280
91
  const instance = latest.current.editor;
281
92
  if (instance.isDestroyed) return anchorRect;
282
- if (modeRef.current === "caret") return caretRect(instance);
93
+ if (flow.state.get().mode === "caret") return caretRect(instance);
283
94
  const highlight = aiSelectionHighlightKey.getState(instance.state);
284
95
  const highlighted = highlight && resolveRange(instance.state, highlight.anchor);
285
96
  if (!highlighted || highlighted.from >= highlighted.to) return anchorRect;
@@ -322,15 +133,13 @@ function AiSelectionMenu({ editor }) {
322
133
  useEffect(() => () => {
323
134
  clearTimeout(previewTimer.current);
324
135
  previewTimer.current = void 0;
325
- actionController.current?.abort();
326
- if (!editor.isDestroyed) hideAiSelectionHighlight(editor);
327
- }, [editor]);
136
+ }, []);
328
137
  const guard = (event) => {
329
138
  if (!picking) event.preventDefault();
330
139
  };
331
140
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(BubbleMenu, {
332
141
  editor,
333
- pluginKey: MENU_KEY,
142
+ pluginKey: AI_SELECTION_MENU_KEY,
334
143
  shouldShow,
335
144
  options: {
336
145
  placement: "top",
@@ -342,14 +151,14 @@ function AiSelectionMenu({ editor }) {
342
151
  children: /* @__PURE__ */ jsxs(Button, {
343
152
  size: "sm",
344
153
  variant: "ghost",
345
- onClick: openMenu,
154
+ onClick: () => flow.openForSelection(),
346
155
  children: [/* @__PURE__ */ jsx(Icon, { name: "i-lucide-sparkles" }), t("ai.menu.trigger")]
347
156
  })
348
157
  })
349
158
  }), /* @__PURE__ */ jsxs(Popover, {
350
- open: range !== null,
159
+ open: state.open,
351
160
  onOpenChange: (value) => {
352
- if (!value) close(modeRef.current === "caret");
161
+ if (!value) flow.close(flow.state.get().mode === "caret");
353
162
  },
354
163
  children: [/* @__PURE__ */ jsx(PopoverAnchor, { virtualRef: anchor }), /* @__PURE__ */ jsx(PopoverContent, {
355
164
  side: "bottom",
@@ -377,17 +186,17 @@ function AiSelectionMenu({ editor }) {
377
186
  className: "w-56"
378
187
  })]
379
188
  }),
380
- actions.map((task) => /* @__PURE__ */ jsxs("div", {
189
+ state.actions.map((task) => /* @__PURE__ */ jsxs("div", {
381
190
  className: "flex items-center gap-1",
382
191
  children: [/* @__PURE__ */ jsxs(Button, {
383
192
  variant: "ghost",
384
193
  className: "flex-1 justify-start",
385
- disabled: mode === "caret" && task.needsSelection && !hasTextBefore,
386
- onClick: () => start(task, task.needsLanguage ? { language } : {}),
194
+ disabled: mode === "caret" && task.needsSelection && !state.hasTextBefore,
195
+ onClick: () => void flow.start(task),
387
196
  children: [/* @__PURE__ */ jsx(Icon, { name: task.icon }), t(task.labelKey)]
388
197
  }), task.needsLanguage && /* @__PURE__ */ jsxs(Select, {
389
- value: language,
390
- onValueChange: (value) => setLanguage(AI_LANGUAGES.find((candidate) => candidate === value) ?? language),
198
+ value: state.language,
199
+ onValueChange: (value) => flow.setLanguage(AI_LANGUAGES.find((candidate) => candidate === value) ?? state.language),
391
200
  children: [/* @__PURE__ */ jsx(SelectTrigger, {
392
201
  size: "sm",
393
202
  "aria-label": t("ai.menu.targetLanguage"),
@@ -406,31 +215,31 @@ function AiSelectionMenu({ editor }) {
406
215
  menuActions.map((action) => /* @__PURE__ */ jsxs(Button, {
407
216
  variant: "ghost",
408
217
  className: "justify-start",
409
- disabled: runningAction !== null && runningAction !== action.id,
410
- onClick: () => void runMenuAction(action),
218
+ disabled: state.runningAction !== null && state.runningAction !== action.id,
219
+ onClick: () => void flow.runAction(action),
411
220
  children: [/* @__PURE__ */ jsx(Icon, {
412
- name: runningAction === action.id ? "i-lucide-loader-circle" : action.icon,
413
- className: runningAction === action.id ? "animate-spin" : void 0
221
+ name: state.runningAction === action.id ? "i-lucide-loader-circle" : action.icon,
222
+ className: state.runningAction === action.id ? "animate-spin" : void 0
414
223
  }), action.label]
415
224
  }, action.id)),
416
225
  /* @__PURE__ */ jsxs("form", {
417
226
  className: "mt-1 flex gap-1 border-t border-border pt-2",
418
227
  onSubmit: (event) => {
419
228
  event.preventDefault();
420
- if (prompt.trim()) start(freePrompt, { prompt });
229
+ flow.submitPrompt();
421
230
  },
422
231
  children: [/* @__PURE__ */ jsxs(InputGroup, {
423
232
  className: "flex-1",
424
- children: [/* @__PURE__ */ jsx(InputGroupAddon, { children: /* @__PURE__ */ jsx(Icon, { name: freePrompt.icon }) }), /* @__PURE__ */ jsx(InputGroupInput, {
233
+ children: [/* @__PURE__ */ jsx(InputGroupAddon, { children: /* @__PURE__ */ jsx(Icon, { name: state.freePrompt.icon }) }), /* @__PURE__ */ jsx(InputGroupInput, {
425
234
  ref: promptInput,
426
- value: prompt,
427
- placeholder: freePrompt.promptLabelKey && t(freePrompt.promptLabelKey),
428
- onChange: (event) => setPrompt(event.target.value)
235
+ value: state.prompt,
236
+ placeholder: state.freePrompt.promptLabelKey && t(state.freePrompt.promptLabelKey),
237
+ onChange: (event) => flow.setPrompt(event.target.value)
429
238
  })]
430
239
  }), /* @__PURE__ */ jsx(Button, {
431
240
  type: "submit",
432
241
  size: "icon",
433
- disabled: !prompt.trim(),
242
+ disabled: !state.prompt.trim(),
434
243
  "aria-label": t("ai.menu.sendInstruction"),
435
244
  children: /* @__PURE__ */ jsx(Icon, { name: "i-lucide-arrow-up" })
436
245
  })]
@@ -453,9 +262,9 @@ function AiSelectionMenu({ editor }) {
453
262
  })
454
263
  ]
455
264
  }),
456
- error && /* @__PURE__ */ jsx(Alert, {
265
+ state.error && /* @__PURE__ */ jsx(Alert, {
457
266
  variant: "destructive",
458
- children: /* @__PURE__ */ jsx(AlertTitle, { children: error })
267
+ children: /* @__PURE__ */ jsx(AlertTitle, { children: state.error })
459
268
  }),
460
269
  /* @__PURE__ */ jsx("div", {
461
270
  className: "max-h-72 min-h-16 overflow-auto rounded-md border border-border p-3 text-sm",
@@ -467,37 +276,37 @@ function AiSelectionMenu({ editor }) {
467
276
  status === "streaming" && /* @__PURE__ */ jsx(Button, {
468
277
  size: "sm",
469
278
  variant: "ghost",
470
- onClick: abort,
279
+ onClick: flow.abort,
471
280
  children: t("ai.menu.cancel")
472
281
  }),
473
282
  /* @__PURE__ */ jsx(Button, {
474
283
  size: "sm",
475
284
  variant: "ghost",
476
- onClick: () => close(true),
285
+ onClick: () => flow.close(true),
477
286
  children: t("ai.menu.discard")
478
287
  }),
479
288
  /* @__PURE__ */ jsxs(Button, {
480
289
  size: "sm",
481
290
  variant: "outline",
482
291
  disabled: status === "streaming",
483
- onClick: () => generate(),
292
+ onClick: () => void flow.retry(),
484
293
  children: [/* @__PURE__ */ jsx(Icon, { name: "i-lucide-rotate-ccw" }), t("ai.menu.retry")]
485
294
  }),
486
295
  mode === "caret" ? /* @__PURE__ */ jsxs(Button, {
487
296
  size: "sm",
488
297
  disabled: !canApply,
489
- onClick: insertAtCaret,
298
+ onClick: flow.insertAtCaret,
490
299
  children: [/* @__PURE__ */ jsx(Icon, { name: "i-lucide-text-cursor-input" }), t("ai.menu.insert")]
491
300
  }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(Button, {
492
301
  size: "sm",
493
302
  variant: "outline",
494
303
  disabled: !canApply,
495
- onClick: () => apply("insert-below"),
304
+ onClick: () => flow.apply("insert-below"),
496
305
  children: [/* @__PURE__ */ jsx(Icon, { name: "i-lucide-list-plus" }), t("ai.menu.insertBelow")]
497
306
  }), /* @__PURE__ */ jsxs(Button, {
498
307
  size: "sm",
499
308
  disabled: !canApply,
500
- onClick: () => apply("replace"),
309
+ onClick: () => flow.apply("replace"),
501
310
  children: [/* @__PURE__ */ jsx(Icon, { name: "i-lucide-replace" }), t("ai.menu.replace")]
502
311
  })] })
503
312
  ]
@@ -1 +1 @@
1
- {"version":3,"file":"AiSelectionMenu.js","names":[],"sources":["../../../../src/features/ai/react/AiSelectionMenu.tsx"],"sourcesContent":["import type { BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu'\nimport type { Editor, EditorEvents } from '@tiptap/core'\nimport type { EditorState } from '@tiptap/pm/state'\nimport { EditorContent, useEditor } from '@tiptap/react'\nimport { BubbleMenu } from '@tiptap/react/menus'\nimport { type FormEvent, useEffect, useRef, useState } from 'react'\nimport { abilitiesAtSelection, aiContextOf, captureRange, type DocAnchor, hasAbility, parseBlocks, releaseRanges, resolveRange, selectionRange } from '../../../core'\nimport { ABILITY } from '../../../core/schema'\nimport { Icon } from '../../../react/Icon'\nimport { useEditorSettings } from '../../../react/settings'\nimport { useEditorI18n } from '../../../react/use-editor-i18n'\nimport { Alert, AlertTitle } from '../../../react/ui/alert'\nimport { Button } from '../../../react/ui/button'\nimport { InputGroup, InputGroupAddon, InputGroupInput } from '../../../react/ui/input-group'\nimport { Popover, PopoverAnchor, PopoverContent } from '../../../react/ui/popover'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../react/ui/select'\nimport { AI_LANGUAGES, type AiLanguage } from '../../../settings'\nimport { aiApplyFailure, type AiResultAction, applyAiResult, documentMarkdown, insertAtCaretLine, rangeMarkdown } from '../ai-apply'\nimport { AI_MENU_AT_CARET, AI_MENU_FOR_SELECTION, type AiMenuAction, aiMenuActionsOf } from '../ai-menu'\nimport { aiSelectionHighlightKey, hideAiSelectionHighlight, showAiSelectionHighlight } from '../ai-selection-highlight'\nimport { previewExtensions } from '../preview-extensions'\nimport { AI_LANGUAGE_KEYS, AI_LIMITS, AI_TASKS, type AiTask } from '../protocol'\nimport { ModelQuickSwitch } from './ModelQuickSwitch'\nimport { useAiAssistant } from './use-ai-assistant'\nimport { useNotifier } from '../../../react/use-notifier'\n\nconst MENU_KEY = 'aiSelectionMenu'\n\n// Registry order; the free prompt (`custom`) gets its own input below the list.\nconst quickActions = Object.values(AI_TASKS).filter(task => task.selectionMenu && !task.needsPrompt)\nconst custom = AI_TASKS.custom\nconst caretActions = Object.values(AI_TASKS).filter(task => task.caretMenu)\n\ninterface Request { task: AiTask, prompt: string, language?: AiLanguage | undefined }\n\n/**\n * AI quick actions on a text selection: a bubble menu entry opens a popover with the selection tasks of the registry\n * (`selectionMenu`), the result streams into a preview and is applied over the range captured when the menu opened.\n * Caret mode (`/ai`, see `openAiMenuAtCaret`): the same popover opens at the caret with the `caretMenu` tasks and\n * `generate` as free prompt; the result is inserted at the caret line (`insertAtCaretLine`).\n * Needs the AiSelectionHighlight extension on the editor.\n */\nexport function AiSelectionMenu({ editor }: { editor: Editor }) {\n const notify = useNotifier(editor)\n const { t } = useEditorI18n()\n const { status, output, error, run, abort, reset } = useAiAssistant(editor)\n const { settings } = useEditorSettings()\n /** Target language of „Übersetzen“: the settings' default each time the menu opens, changeable per call. */\n const [language, setLanguage] = useState<AiLanguage>(settings.ai.translateLanguage)\n const [prompt, setPrompt] = useState('')\n /** 'selection': rewrites the captured selection; 'caret': writes new text at the captured (collapsed) caret. */\n const [mode, setModeState] = useState<'selection' | 'caret'>('selection')\n /** Caret mode: whether the document has text before the caret line, for tasks that continue it (`needsSelection`). */\n const [hasTextBefore, setHasTextBefore] = useState(false)\n /** The selection the menu works on, captured when it opened; null while closed. */\n const [range, setRangeState] = useState<DocAnchor | null>(null)\n /** The current request; „Erneut versuchen“ repeats it. */\n const [request, setRequest] = useState<Request | null>(null)\n const [runningAction, setRunningAction] = useState<string | null>(null)\n const promptInput = useRef<HTMLInputElement>(null)\n\n // The editor events and async flows below read the current values from refs, not from a render's closure.\n const rangeRef = useRef<DocAnchor | null>(null)\n const modeRef = useRef<'selection' | 'caret'>('selection')\n const setRange = (value: DocAnchor | null) => {\n rangeRef.current = value\n setRangeState(value)\n }\n const setMode = (value: 'selection' | 'caret') => {\n modeRef.current = value\n setModeState(value)\n }\n const latest = useRef({ editor, output, settings })\n latest.current = { editor, output, settings }\n const actionController = useRef<AbortController | null>(null)\n\n const actions = mode === 'caret' ? caretActions : quickActions\n const freePrompt = mode === 'caret' ? AI_TASKS.generate : custom\n const picking = status === 'idle' && !output\n const canApply = status === 'done' && !!output.trim()\n const requestLabel = request\n ? (request.language ? t('ai.menu.requestWithLanguage', { task: t(request.task.labelKey), language: t(AI_LANGUAGE_KEYS[request.language]) }) : t(request.task.labelKey))\n : undefined\n\n // Extra rows other features contribute (`aiMenuActions` of their extensions), offered while the selection qualifies.\n const resolved = range && !editor.isDestroyed ? resolveRange(editor.state, range) : null\n const menuActions = resolved ? aiMenuActionsOf(editor).filter(action => action.available?.(editor, resolved) ?? true) : []\n\n async function runMenuAction(action: AiMenuAction) {\n const captured = rangeRef.current\n const at = captured && resolveRange(editor.state, captured)\n if (!at || actionController.current) return\n const own = new AbortController()\n actionController.current = own\n setRunningAction(action.id)\n try {\n await action.run(editor, at, own.signal)\n }\n finally {\n if (actionController.current === own) actionController.current = null\n setRunningAction(null)\n }\n if (rangeRef.current === captured) close(false)\n }\n\n // Closing the menu cancels a running action; opening it starts from the default translation language.\n useEffect(() => {\n if (!range) {\n actionController.current?.abort()\n actionController.current = null\n }\n else setLanguage(latest.current.settings.ai.translateLanguage)\n }, [range])\n\n const shouldShow: NonNullable<BubbleMenuPluginProps['shouldShow']> = ({ editor: current, element, view, state, from, to }) => {\n if (rangeRef.current || !current.isEditable || state.selection.empty) return false\n if (!view.hasFocus() && !element.contains(document.activeElement)) return false\n if (state.doc.textBetween(from, to, ' ').trim().length === 0) return false\n return abilitiesAtSelection(current).has(ABILITY.aiRewrite)\n }\n\n function openMenu() {\n const selected = selectionRange(editor.state)\n if (selected.from >= selected.to) return\n const captured = captureRange(editor, selected)\n setRange(captured)\n showAiSelectionHighlight(editor, captured)\n editor.view.dispatch(editor.state.tr.setMeta(MENU_KEY, 'hide'))\n }\n\n /** Opens the menu at the caret (requested by `openAiMenuAtCaret`, e.g. the `/ai` slash command). */\n function openAtCaret() {\n if (rangeRef.current || !editor.isEditable) return\n const { $head, head } = editor.state.selection\n setMode('caret')\n setHasTextBefore(editor.state.doc.textBetween(0, $head.depth ? $head.before() : head, ' ', ' ').trim().length > 0)\n setRange(captureRange(editor, { from: head, to: head }))\n }\n\n const onTransaction = useRef<(event: EditorEvents['transaction']) => void>(() => {})\n onTransaction.current = ({ transaction }) => {\n if (transaction.getMeta(AI_MENU_AT_CARET) && hasAbility(editor, ABILITY.aiGenerate)) openAtCaret()\n // Requested by `openAiMenuForSelection` (the „Mit KI bearbeiten“ block action); a menu already open stays as it is.\n if (transaction.getMeta(AI_MENU_FOR_SELECTION) && !rangeRef.current && editor.isEditable) openMenu()\n }\n useEffect(() => {\n const handler = (event: EditorEvents['transaction']) => onTransaction.current(event)\n editor.on('transaction', handler)\n return () => { editor.off('transaction', handler) }\n }, [editor])\n\n /** Caret mode: the prompt input gets the focus, so the instruction can be typed right away. */\n function onOpenAutoFocus(event: Event) {\n if (modeRef.current !== 'caret') return\n event.preventDefault()\n promptInput.current?.focus()\n }\n\n function start(task: AiTask, options: { prompt?: string, language?: AiLanguage } = {}) {\n const next = { task, prompt: options.prompt ?? '', language: options.language }\n setRequest(next)\n generate(next)\n }\n\n /** Runs the request (default: the current one) on the current text of the captured range, with the current document as context. */\n function generate(current: Request | null = request) {\n const captured = rangeRef.current\n if (!captured || !current) return\n if (modeRef.current === 'caret') {\n generateAtCaret(captured, current)\n return\n }\n const at = resolveRange(editor.state, captured)\n if (!at || at.from >= at.to) {\n notify({ title: t('ai.apply.missing.title'), description: t('ai.menu.missing.selection'), kind: 'error' })\n return\n }\n const { blocks } = aiContextOf(editor)\n void run({\n task: current.task.id,\n prompt: current.prompt,\n ...(current.language && { language: current.language }),\n selection: rangeMarkdown(editor, at),\n context: documentMarkdown(editor),\n blocks,\n })\n }\n\n /**\n * Caret mode: tasks with `needsSelection` (continue) get the document up to the caret line as `selection` and the rest\n * as `context`; the others the whole document as context.\n */\n function generateAtCaret(captured: DocAnchor, current: Request) {\n const at = resolveRange(editor.state, captured)\n if (!at) {\n notify({ title: t('ai.apply.missing.title'), description: t('ai.menu.missing.line'), kind: 'error' })\n return\n }\n const { doc } = editor.state\n const $at = doc.resolve(at.from)\n const splitBefore = $at.depth ? $at.before() : at.from\n const splitAfter = $at.depth ? $at.after() : at.from\n const continues = current.task.needsSelection\n const { blocks } = aiContextOf(editor)\n void run({\n task: current.task.id,\n prompt: current.prompt,\n selection: continues ? rangeMarkdown(editor, { from: 0, to: splitBefore }).slice(-AI_LIMITS.selection) : '',\n context: continues ? rangeMarkdown(editor, { from: splitAfter, to: doc.content.size }).slice(0, AI_LIMITS.context) : documentMarkdown(editor),\n blocks,\n })\n }\n\n /** 'replace' opens the in-document review of the result (AiReviewBar); nothing is written until accepted there. */\n function apply(action: AiResultAction) {\n const captured = rangeRef.current\n if (!captured || !canApply) return\n const outcome = applyAiResult(editor, action, captured, output)\n if (outcome !== 'ok') {\n notify(aiApplyFailure(outcome, t))\n if (outcome !== 'unchanged') return\n }\n close(false)\n }\n\n function insertAtCaret() {\n const captured = rangeRef.current\n if (!captured || !canApply) return\n if (!insertAtCaretLine(editor, captured, output)) {\n notify({ title: t('ai.apply.missing.title'), description: t('ai.menu.missing.insert'), kind: 'error' })\n return\n }\n close(false)\n }\n\n /** Closes the menu and removes the highlight; `restoreSelection` selects the captured range (caret mode: caret) again. */\n function close(restoreSelection: boolean) {\n const captured = rangeRef.current\n const caret = modeRef.current === 'caret'\n reset()\n setRequest(null)\n setPrompt('')\n setRange(null)\n setMode('selection')\n if (editor.isDestroyed) return\n hideAiSelectionHighlight(editor)\n const at = captured && restoreSelection ? resolveRange(editor.state, captured) : null\n if (captured) releaseRanges(editor, [captured])\n if (at && (caret || at.from < at.to)) editor.chain().focus().setTextSelection(at).run()\n }\n\n // Popover anchor: the highlighted range (its plugin state follows local and remote edits); in caret mode the caret.\n const [anchor] = useState(() => {\n let anchorRect = new DOMRect()\n // The caret's position is resolved from its anchor once per editor state (the anchor is measured every frame).\n let caretAt: { state: EditorState, pos: number | null } | null = null\n const caretRect = (instance: Editor): DOMRect => {\n if (caretAt?.state !== instance.state) {\n const captured = rangeRef.current\n caretAt = { state: instance.state, pos: captured ? resolveRange(instance.state, captured)?.from ?? null : null }\n }\n if (caretAt.pos === null) return anchorRect\n const coords = instance.view.coordsAtPos(caretAt.pos)\n anchorRect = new DOMRect(coords.left, coords.top, 1, coords.bottom - coords.top)\n return anchorRect\n }\n return {\n current: {\n getBoundingClientRect(): DOMRect {\n const instance = latest.current.editor\n if (instance.isDestroyed) return anchorRect\n if (modeRef.current === 'caret') return caretRect(instance)\n const highlight = aiSelectionHighlightKey.getState(instance.state)\n const highlighted = highlight && resolveRange(instance.state, highlight.anchor)\n if (!highlighted || highlighted.from >= highlighted.to) return anchorRect\n const { view } = instance\n const start = view.coordsAtPos(highlighted.from, 1)\n const end = view.coordsAtPos(highlighted.to, -1)\n // Across lines the range spans the editor's width.\n const bounds = view.dom.getBoundingClientRect()\n const oneLine = end.top < start.bottom\n const left = oneLine ? start.left : bounds.left\n const right = oneLine ? end.right : bounds.right\n anchorRect = new DOMRect(left, start.top, Math.max(right - left, 1), end.bottom - start.top)\n return anchorRect\n },\n get contextElement() {\n return latest.current.editor.isDestroyed ? undefined : latest.current.editor.view.dom\n },\n },\n }\n })\n\n const [extensions] = useState(() => previewExtensions(editor))\n const preview = useEditor({\n editable: false,\n extensions,\n immediatelyRender: true,\n shouldRerenderOnTransaction: false,\n editorProps: { attributes: { class: 'tiptap ai-selection-preview' } },\n })\n\n // Coalesce streamed deltas: re-parse the Markdown at most every 80 ms; speaker lines show as segments, as applied.\n const previewTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n useEffect(() => {\n if (previewTimer.current) return\n previewTimer.current = setTimeout(() => {\n previewTimer.current = undefined\n if (!preview || preview.isDestroyed) return\n const { editor: source, output: text } = latest.current\n preview.commands.setContent({ type: 'doc', content: parseBlocks(source, text).blocks }, { emitUpdate: false })\n }, 80)\n }, [output, preview])\n\n useEffect(() => () => {\n clearTimeout(previewTimer.current)\n previewTimer.current = undefined\n actionController.current?.abort()\n if (!editor.isDestroyed) hideAiSelectionHighlight(editor)\n }, [editor])\n\n // Not dismissible while a result is shown or streams: only „Verwerfen“ ends it.\n const guard = (event: Event) => { if (!picking) event.preventDefault() }\n\n return (\n <>\n <BubbleMenu editor={editor} pluginKey={MENU_KEY} shouldShow={shouldShow} options={{ placement: 'top', offset: 8 }} className=\"z-10\">\n <div className=\"flex rounded-md border border-border bg-background p-0.5 shadow-md\">\n <Button size=\"sm\" variant=\"ghost\" onClick={openMenu}>\n <Icon name=\"i-lucide-sparkles\" />\n {t('ai.menu.trigger')}\n </Button>\n </div>\n </BubbleMenu>\n\n <Popover open={range !== null} onOpenChange={(value) => { if (!value) close(modeRef.current === 'caret') }}>\n <PopoverAnchor virtualRef={anchor} />\n <PopoverContent\n side=\"bottom\"\n align=\"start\"\n sideOffset={4}\n updatePositionStrategy=\"always\"\n className=\"w-[30rem] max-w-[calc(100vw-2rem)] gap-0 p-0\"\n onOpenAutoFocus={onOpenAutoFocus}\n onCloseAutoFocus={event => event.preventDefault()}\n onEscapeKeyDown={guard}\n // The model and language menus (portalled) count as inside.\n onInteractOutside={(event) => {\n if (!picking || (event.target instanceof Element && event.target.closest('[role=\"listbox\"]'))) event.preventDefault()\n }}\n >\n {picking\n ? (\n <div className=\"flex flex-col gap-1 p-2\">\n {/* Caret mode generates blocks, selection mode rewrites text in the block: each has its own model slot. */}\n <div className=\"flex items-center justify-end gap-2 px-1 pb-1\">\n <span className=\"text-xs text-muted-foreground\">{t('ai.menu.model')}</span>\n <ModelQuickSwitch slot={mode === 'caret' ? 'generate' : 'rewrite'} size=\"xs\" className=\"w-56\" />\n </div>\n {actions.map(task => (\n <div key={task.id} className=\"flex items-center gap-1\">\n <Button\n variant=\"ghost\"\n className=\"flex-1 justify-start\"\n disabled={mode === 'caret' && task.needsSelection && !hasTextBefore}\n onClick={() => start(task, task.needsLanguage ? { language } : {})}\n >\n <Icon name={task.icon} />\n {t(task.labelKey)}\n </Button>\n {task.needsLanguage && (\n <Select value={language} onValueChange={value => setLanguage(AI_LANGUAGES.find(candidate => candidate === value) ?? language)}>\n <SelectTrigger size=\"sm\" aria-label={t('ai.menu.targetLanguage')} className=\"w-36\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent position=\"popper\" align=\"end\">\n {AI_LANGUAGES.map(item => <SelectItem key={item} value={item}>{t(AI_LANGUAGE_KEYS[item])}</SelectItem>)}\n </SelectContent>\n </Select>\n )}\n </div>\n ))}\n {menuActions.map(action => (\n <Button\n key={action.id}\n variant=\"ghost\"\n className=\"justify-start\"\n disabled={runningAction !== null && runningAction !== action.id}\n onClick={() => void runMenuAction(action)}\n >\n <Icon name={runningAction === action.id ? 'i-lucide-loader-circle' : action.icon} className={runningAction === action.id ? 'animate-spin' : undefined} />\n {action.label}\n </Button>\n ))}\n <form\n className=\"mt-1 flex gap-1 border-t border-border pt-2\"\n onSubmit={(event: FormEvent) => {\n event.preventDefault()\n if (prompt.trim()) start(freePrompt, { prompt })\n }}\n >\n <InputGroup className=\"flex-1\">\n <InputGroupAddon>\n <Icon name={freePrompt.icon} />\n </InputGroupAddon>\n <InputGroupInput ref={promptInput} value={prompt} placeholder={freePrompt.promptLabelKey && t(freePrompt.promptLabelKey)} onChange={event => setPrompt(event.target.value)} />\n </InputGroup>\n <Button type=\"submit\" size=\"icon\" disabled={!prompt.trim()} aria-label={t('ai.menu.sendInstruction')}>\n <Icon name=\"i-lucide-arrow-up\" />\n </Button>\n </form>\n </div>\n )\n : (\n <div className=\"flex flex-col gap-3 p-3\">\n <div className=\"flex items-center gap-2 text-sm font-medium\">\n {request && <Icon name={request.task.icon} />}\n <span className=\"flex-1 truncate\">{requestLabel}</span>\n {status === 'streaming' && <Icon name=\"i-lucide-loader-circle\" className=\"animate-spin text-muted-foreground\" />}\n </div>\n {error && (\n <Alert variant=\"destructive\">\n <AlertTitle>{error}</AlertTitle>\n </Alert>\n )}\n <div className=\"max-h-72 min-h-16 overflow-auto rounded-md border border-border p-3 text-sm\">\n {preview && <EditorContent editor={preview} />}\n </div>\n <div className=\"flex flex-wrap justify-end gap-2\">\n {status === 'streaming' && <Button size=\"sm\" variant=\"ghost\" onClick={abort}>{t('ai.menu.cancel')}</Button>}\n <Button size=\"sm\" variant=\"ghost\" onClick={() => close(true)}>{t('ai.menu.discard')}</Button>\n <Button size=\"sm\" variant=\"outline\" disabled={status === 'streaming'} onClick={() => generate()}>\n <Icon name=\"i-lucide-rotate-ccw\" />\n {t('ai.menu.retry')}\n </Button>\n {mode === 'caret'\n ? (\n <Button size=\"sm\" disabled={!canApply} onClick={insertAtCaret}>\n <Icon name=\"i-lucide-text-cursor-input\" />\n {t('ai.menu.insert')}\n </Button>\n )\n : (\n <>\n <Button size=\"sm\" variant=\"outline\" disabled={!canApply} onClick={() => apply('insert-below')}>\n <Icon name=\"i-lucide-list-plus\" />\n {t('ai.menu.insertBelow')}\n </Button>\n <Button size=\"sm\" disabled={!canApply} onClick={() => apply('replace')}>\n <Icon name=\"i-lucide-replace\" />\n {t('ai.menu.replace')}\n </Button>\n </>\n )}\n </div>\n </div>\n )}\n </PopoverContent>\n </Popover>\n </>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AiSelectionMenu.js","names":[],"sources":["../../../../src/features/ai/react/AiSelectionMenu.tsx"],"sourcesContent":["import type { BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu'\nimport type { Editor } from '@tiptap/core'\nimport type { EditorState } from '@tiptap/pm/state'\nimport { EditorContent, useEditor } from '@tiptap/react'\nimport { BubbleMenu } from '@tiptap/react/menus'\nimport { type FormEvent, useEffect, useRef, useState } from 'react'\nimport { parseBlocks, resolveRange } from '../../../core'\nimport { Icon } from '../../../react/Icon'\nimport { useEditorSettings } from '../../../react/settings'\nimport { useEditorI18n } from '../../../react/use-editor-i18n'\nimport { Alert, AlertTitle } from '../../../react/ui/alert'\nimport { Button } from '../../../react/ui/button'\nimport { InputGroup, InputGroupAddon, InputGroupInput } from '../../../react/ui/input-group'\nimport { Popover, PopoverAnchor, PopoverContent } from '../../../react/ui/popover'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../react/ui/select'\nimport { useStore } from '../../../react/use-store'\nimport { AI_LANGUAGES } from '../../../settings'\nimport { aiSelectionHighlightKey } from '../ai-selection-highlight'\nimport { previewExtensions } from '../preview-extensions'\nimport { AI_LANGUAGE_KEYS } from '../protocol'\nimport { AI_SELECTION_MENU_KEY, type AiSelectionFlow } from '../selection-flow'\nimport { ModelQuickSwitch } from './ModelQuickSwitch'\nimport { useAiAssistant } from './use-ai-assistant'\nimport { useAiSelectionFlow } from './use-ai-selection-flow'\nimport { useNotifier } from '../../../react/use-notifier'\n\n\n/**\n * AI quick actions on a text selection: a bubble menu entry opens a popover with the selection tasks of the registry\n * (`selectionMenu`), the result streams into a preview and is applied over the range captured when the menu opened.\n * Caret mode (`/ai`, see `openAiMenuAtCaret`): the same popover opens at the caret with the `caretMenu` tasks and\n * `generate` as free prompt; the result is inserted at the caret line (`insertAtCaretLine`).\n * Needs the AiSelectionHighlight extension on the editor. The flow (`createAiSelectionFlow`) owns every step; this\n * component renders its state, places the popover and shows the preview.\n */\nexport function AiSelectionMenu({ editor }: { editor: Editor }) {\n const notify = useNotifier(editor)\n const { t } = useEditorI18n()\n const { model: assistant } = useAiAssistant(editor)\n const { settings } = useEditorSettings()\n const flow = useAiSelectionFlow({ editor, assistant, notify, t, language: () => settings.ai.translateLanguage })\n return flow && <AiSelectionMenuView editor={editor} flow={flow} />\n}\n\nfunction AiSelectionMenuView({ editor, flow }: { editor: Editor, flow: AiSelectionFlow }) {\n const state = useStore(flow.state)\n const { mode, picking, canApply, status, output, request } = state\n const promptInput = useRef<HTMLInputElement>(null)\n const latest = useRef({ editor, output })\n latest.current = { editor, output }\n\n const { t } = useEditorI18n()\n const requestLabel = request\n ? (request.language ? t('ai.menu.requestWithLanguage', { task: t(request.task.labelKey), language: t(AI_LANGUAGE_KEYS[request.language]) }) : t(request.task.labelKey))\n : undefined\n // Extra rows other features contribute (`aiMenuActions` of their extensions), offered while the selection qualifies.\n const menuActions = state.open ? flow.menuActions() : []\n\n const shouldShow: NonNullable<BubbleMenuPluginProps['shouldShow']> = ({ element, view }) =>\n (view.hasFocus() || element.contains(document.activeElement)) && flow.offersSelection()\n\n /** Caret mode: the prompt input gets the focus, so the instruction can be typed right away. */\n function onOpenAutoFocus(event: Event) {\n if (flow.state.get().mode !== 'caret') return\n event.preventDefault()\n promptInput.current?.focus()\n }\n\n\n // Popover anchor: the highlighted range (its plugin state follows local and remote edits); in caret mode the caret.\n const [anchor] = useState(() => {\n let anchorRect = new DOMRect()\n // The caret's position is resolved from its anchor once per editor state (the anchor is measured every frame).\n let caretAt: { state: EditorState, pos: number | null } | null = null\n const caretRect = (instance: Editor): DOMRect => {\n if (caretAt?.state !== instance.state) caretAt = { state: instance.state, pos: flow.range()?.from ?? null }\n if (caretAt.pos === null) return anchorRect\n const coords = instance.view.coordsAtPos(caretAt.pos)\n anchorRect = new DOMRect(coords.left, coords.top, 1, coords.bottom - coords.top)\n return anchorRect\n }\n return {\n current: {\n getBoundingClientRect(): DOMRect {\n const instance = latest.current.editor\n if (instance.isDestroyed) return anchorRect\n if (flow.state.get().mode === 'caret') return caretRect(instance)\n const highlight = aiSelectionHighlightKey.getState(instance.state)\n const highlighted = highlight && resolveRange(instance.state, highlight.anchor)\n if (!highlighted || highlighted.from >= highlighted.to) return anchorRect\n const { view } = instance\n const start = view.coordsAtPos(highlighted.from, 1)\n const end = view.coordsAtPos(highlighted.to, -1)\n // Across lines the range spans the editor's width.\n const bounds = view.dom.getBoundingClientRect()\n const oneLine = end.top < start.bottom\n const left = oneLine ? start.left : bounds.left\n const right = oneLine ? end.right : bounds.right\n anchorRect = new DOMRect(left, start.top, Math.max(right - left, 1), end.bottom - start.top)\n return anchorRect\n },\n get contextElement() {\n return latest.current.editor.isDestroyed ? undefined : latest.current.editor.view.dom\n },\n },\n }\n })\n\n const [extensions] = useState(() => previewExtensions(editor))\n const preview = useEditor({\n editable: false,\n extensions,\n immediatelyRender: true,\n shouldRerenderOnTransaction: false,\n editorProps: { attributes: { class: 'tiptap ai-selection-preview' } },\n })\n\n // Coalesce streamed deltas: re-parse the Markdown at most every 80 ms; speaker lines show as segments, as applied.\n const previewTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n useEffect(() => {\n if (previewTimer.current) return\n previewTimer.current = setTimeout(() => {\n previewTimer.current = undefined\n if (!preview || preview.isDestroyed) return\n const { editor: source, output: text } = latest.current\n preview.commands.setContent({ type: 'doc', content: parseBlocks(source, text).blocks }, { emitUpdate: false })\n }, 80)\n }, [output, preview])\n\n useEffect(() => () => {\n clearTimeout(previewTimer.current)\n previewTimer.current = undefined\n }, [])\n\n // Not dismissible while a result is shown or streams: only „Verwerfen“ ends it.\n const guard = (event: Event) => { if (!picking) event.preventDefault() }\n\n return (\n <>\n <BubbleMenu editor={editor} pluginKey={AI_SELECTION_MENU_KEY} shouldShow={shouldShow} options={{ placement: 'top', offset: 8 }} className=\"z-10\">\n <div className=\"flex rounded-md border border-border bg-background p-0.5 shadow-md\">\n <Button size=\"sm\" variant=\"ghost\" onClick={() => flow.openForSelection()}>\n <Icon name=\"i-lucide-sparkles\" />\n {t('ai.menu.trigger')}\n </Button>\n </div>\n </BubbleMenu>\n\n <Popover open={state.open} onOpenChange={(value) => { if (!value) flow.close(flow.state.get().mode === 'caret') }}>\n <PopoverAnchor virtualRef={anchor} />\n <PopoverContent\n side=\"bottom\"\n align=\"start\"\n sideOffset={4}\n updatePositionStrategy=\"always\"\n className=\"w-[30rem] max-w-[calc(100vw-2rem)] gap-0 p-0\"\n onOpenAutoFocus={onOpenAutoFocus}\n onCloseAutoFocus={event => event.preventDefault()}\n onEscapeKeyDown={guard}\n // The model and language menus (portalled) count as inside.\n onInteractOutside={(event) => {\n if (!picking || (event.target instanceof Element && event.target.closest('[role=\"listbox\"]'))) event.preventDefault()\n }}\n >\n {picking\n ? (\n <div className=\"flex flex-col gap-1 p-2\">\n {/* Caret mode generates blocks, selection mode rewrites text in the block: each has its own model slot. */}\n <div className=\"flex items-center justify-end gap-2 px-1 pb-1\">\n <span className=\"text-xs text-muted-foreground\">{t('ai.menu.model')}</span>\n <ModelQuickSwitch slot={mode === 'caret' ? 'generate' : 'rewrite'} size=\"xs\" className=\"w-56\" />\n </div>\n {state.actions.map(task => (\n <div key={task.id} className=\"flex items-center gap-1\">\n <Button\n variant=\"ghost\"\n className=\"flex-1 justify-start\"\n disabled={mode === 'caret' && task.needsSelection && !state.hasTextBefore}\n onClick={() => void flow.start(task)}\n >\n <Icon name={task.icon} />\n {t(task.labelKey)}\n </Button>\n {task.needsLanguage && (\n <Select value={state.language} onValueChange={value => flow.setLanguage(AI_LANGUAGES.find(candidate => candidate === value) ?? state.language)}>\n <SelectTrigger size=\"sm\" aria-label={t('ai.menu.targetLanguage')} className=\"w-36\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent position=\"popper\" align=\"end\">\n {AI_LANGUAGES.map(item => <SelectItem key={item} value={item}>{t(AI_LANGUAGE_KEYS[item])}</SelectItem>)}\n </SelectContent>\n </Select>\n )}\n </div>\n ))}\n {menuActions.map(action => (\n <Button\n key={action.id}\n variant=\"ghost\"\n className=\"justify-start\"\n disabled={state.runningAction !== null && state.runningAction !== action.id}\n onClick={() => void flow.runAction(action)}\n >\n <Icon name={state.runningAction === action.id ? 'i-lucide-loader-circle' : action.icon} className={state.runningAction === action.id ? 'animate-spin' : undefined} />\n {action.label}\n </Button>\n ))}\n <form\n className=\"mt-1 flex gap-1 border-t border-border pt-2\"\n onSubmit={(event: FormEvent) => {\n event.preventDefault()\n void flow.submitPrompt()\n }}\n >\n <InputGroup className=\"flex-1\">\n <InputGroupAddon>\n <Icon name={state.freePrompt.icon} />\n </InputGroupAddon>\n <InputGroupInput ref={promptInput} value={state.prompt} placeholder={state.freePrompt.promptLabelKey && t(state.freePrompt.promptLabelKey)} onChange={event => flow.setPrompt(event.target.value)} />\n </InputGroup>\n <Button type=\"submit\" size=\"icon\" disabled={!state.prompt.trim()} aria-label={t('ai.menu.sendInstruction')}>\n <Icon name=\"i-lucide-arrow-up\" />\n </Button>\n </form>\n </div>\n )\n : (\n <div className=\"flex flex-col gap-3 p-3\">\n <div className=\"flex items-center gap-2 text-sm font-medium\">\n {request && <Icon name={request.task.icon} />}\n <span className=\"flex-1 truncate\">{requestLabel}</span>\n {status === 'streaming' && <Icon name=\"i-lucide-loader-circle\" className=\"animate-spin text-muted-foreground\" />}\n </div>\n {state.error && (\n <Alert variant=\"destructive\">\n <AlertTitle>{state.error}</AlertTitle>\n </Alert>\n )}\n <div className=\"max-h-72 min-h-16 overflow-auto rounded-md border border-border p-3 text-sm\">\n {preview && <EditorContent editor={preview} />}\n </div>\n <div className=\"flex flex-wrap justify-end gap-2\">\n {status === 'streaming' && <Button size=\"sm\" variant=\"ghost\" onClick={flow.abort}>{t('ai.menu.cancel')}</Button>}\n <Button size=\"sm\" variant=\"ghost\" onClick={() => flow.close(true)}>{t('ai.menu.discard')}</Button>\n <Button size=\"sm\" variant=\"outline\" disabled={status === 'streaming'} onClick={() => void flow.retry()}>\n <Icon name=\"i-lucide-rotate-ccw\" />\n {t('ai.menu.retry')}\n </Button>\n {mode === 'caret'\n ? (\n <Button size=\"sm\" disabled={!canApply} onClick={flow.insertAtCaret}>\n <Icon name=\"i-lucide-text-cursor-input\" />\n {t('ai.menu.insert')}\n </Button>\n )\n : (\n <>\n <Button size=\"sm\" variant=\"outline\" disabled={!canApply} onClick={() => flow.apply('insert-below')}>\n <Icon name=\"i-lucide-list-plus\" />\n {t('ai.menu.insertBelow')}\n </Button>\n <Button size=\"sm\" disabled={!canApply} onClick={() => flow.apply('replace')}>\n <Icon name=\"i-lucide-replace\" />\n {t('ai.menu.replace')}\n </Button>\n </>\n )}\n </div>\n </div>\n )}\n </PopoverContent>\n </Popover>\n </>\n )\n}\n"],"mappings":"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"}
@@ -1,11 +1,11 @@
1
1
  import { conversionsOf } from "../../../core/pipeline.js";
2
+ import { CHAT_LIMITS } from "../protocol/chat.js";
3
+ import { AI_LIMITS, blockTasks, withTaskHeading } from "../protocol/tasks.js";
2
4
  import { useEditorI18n } from "../../../react/use-editor-i18n.js";
3
5
  import { cn } from "../../../react/ui/utils.js";
4
6
  import { Icon } from "../../../react/Icon.js";
5
7
  import { useStore } from "../../../react/use-store.js";
6
8
  import { Button } from "../../../react/ui/button.js";
7
- import { CHAT_LIMITS } from "../protocol/chat.js";
8
- import { AI_LIMITS, blockTasks, withTaskHeading } from "../protocol/tasks.js";
9
9
  import { selectionScope } from "../agent-ops.js";
10
10
  import { createAiSidebarFlow } from "../sidebar-flow.js";
11
11
  import { useNotifier } from "../../../react/use-notifier.js";
@@ -1,5 +1,5 @@
1
- import { useEditorI18n } from "../../../react/use-editor-i18n.js";
2
1
  import { citationBlockId } from "../protocol/chat.js";
2
+ import { useEditorI18n } from "../../../react/use-editor-i18n.js";
3
3
  import { documentChatAnswerContent } from "../document-chat.js";
4
4
  import { useNotifier } from "../../../react/use-notifier.js";
5
5
  import { previewExtensions } from "../preview-extensions.js";
@@ -22,6 +22,7 @@ const BADGE_TONES = {
22
22
  invalid: "warning",
23
23
  unchanged: "neutral",
24
24
  missing: "warning",
25
+ stale: "warning",
25
26
  overlap: "warning",
26
27
  empty: "warning",
27
28
  blocked: "warning"