@ecsplico/editor 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +4 -3
- package/dist/adapters/anchors.d.ts +80 -0
- package/dist/adapters/anchors.js +270 -0
- package/dist/adapters/anchors.js.map +1 -0
- package/dist/adapters/comments/client.d.ts +64 -0
- package/dist/adapters/comments/client.js +536 -0
- package/dist/adapters/comments/client.js.map +1 -0
- package/dist/adapters/comments/drizzle.d.ts +430 -0
- package/dist/adapters/comments/drizzle.js +236 -0
- package/dist/adapters/comments/drizzle.js.map +1 -0
- package/dist/adapters/comments/errors.d.ts +9 -0
- package/dist/adapters/comments/errors.js +19 -0
- package/dist/adapters/comments/errors.js.map +1 -0
- package/dist/adapters/comments/json.d.ts +19 -0
- package/dist/adapters/comments/json.js +94 -0
- package/dist/adapters/comments/json.js.map +1 -0
- package/dist/adapters/comments/record.js +78 -0
- package/dist/adapters/comments/record.js.map +1 -0
- package/dist/adapters/comments/server.d.ts +23 -0
- package/dist/adapters/comments/server.js +102 -0
- package/dist/adapters/comments/server.js.map +1 -0
- package/dist/adapters/comments/types.d.ts +109 -0
- package/dist/adapters/documents.js +108 -0
- package/dist/adapters/documents.js.map +1 -0
- package/dist/adapters/drizzle.d.ts +293 -0
- package/dist/adapters/drizzle.js +157 -0
- package/dist/adapters/drizzle.js.map +1 -0
- package/dist/adapters/files.js +30 -0
- package/dist/adapters/files.js.map +1 -0
- package/dist/adapters/forge.d.ts +58 -0
- package/dist/adapters/forge.js +91 -0
- package/dist/adapters/forge.js.map +1 -0
- package/dist/adapters/index.d.ts +13 -0
- package/dist/adapters/index.js +0 -0
- package/dist/adapters/json.d.ts +24 -0
- package/dist/adapters/json.js +128 -0
- package/dist/adapters/json.js.map +1 -0
- package/dist/adapters/nitro.d.ts +61 -0
- package/dist/adapters/nitro.js +212 -0
- package/dist/adapters/nitro.js.map +1 -0
- package/dist/adapters/routes.js +16 -0
- package/dist/adapters/routes.js.map +1 -0
- package/dist/adapters/shared.d.ts +37 -0
- package/dist/adapters/shared.js +32 -0
- package/dist/adapters/shared.js.map +1 -0
- package/dist/adapters/vue.d.ts +67 -0
- package/dist/adapters/vue.js +187 -0
- package/dist/adapters/vue.js.map +1 -0
- package/dist/code/CodeSurface.vue.d.ts +3 -3
- package/dist/core/doc-anchors.d.ts +5 -0
- package/dist/core/doc-anchors.js +4 -3
- package/dist/core/doc-anchors.js.map +1 -1
- package/dist/core/editor-ui.js +57 -1
- package/dist/core/editor-ui.js.map +1 -1
- package/dist/core/surface-session.js +139 -0
- package/dist/core/surface-session.js.map +1 -0
- package/dist/document/DocumentSurface.d.ts +1 -1
- package/dist/document/DocumentSurface.vue.d.ts +3 -3
- package/dist/features/ai/agent-op-review.d.ts +2 -2
- package/dist/features/ai/agent-op-review.js.map +1 -1
- package/dist/features/ai/agent-ops.d.ts +1 -1
- package/dist/features/ai/agent-ops.js +9 -6
- package/dist/features/ai/agent-ops.js.map +1 -1
- package/dist/features/ai/ai-apply.d.ts +1 -1
- package/dist/features/ai/ai-review.js +10 -1
- package/dist/features/ai/ai-review.js.map +1 -1
- package/dist/features/ai/components/AiSelectionMenu.js +78 -252
- package/dist/features/ai/components/AiSelectionMenu.js.map +1 -1
- package/dist/features/ai/components/AiSelectionMenu.vue.d.ts +2 -1
- package/dist/features/ai/components/AiSidebar.js +2 -2
- package/dist/features/ai/components/DocumentChatAnswer.js +1 -1
- package/dist/features/ai/components/DocumentChatOps.js +1 -0
- package/dist/features/ai/components/DocumentChatOps.js.map +1 -1
- package/dist/features/ai/components/ModelQuickSwitch.js +1 -1
- package/dist/features/ai/components/SettingsAiTab.js +1 -1
- package/dist/features/ai/components/SettingsModelProfiles.js +1 -1
- package/dist/features/ai/document-chat.d.ts +3 -2
- package/dist/features/ai/document-chat.js +16 -19
- package/dist/features/ai/document-chat.js.map +1 -1
- package/dist/features/ai/index.d.ts +5 -3
- package/dist/features/ai/index.js +4 -2
- package/dist/features/ai/protocol/agent-ops.d.ts +22 -17
- package/dist/features/ai/protocol/agent-ops.js +65 -39
- package/dist/features/ai/protocol/agent-ops.js.map +1 -1
- package/dist/features/ai/protocol/block-lines.d.ts +89 -0
- package/dist/features/ai/protocol/block-lines.js +177 -0
- package/dist/features/ai/protocol/block-lines.js.map +1 -0
- package/dist/features/ai/protocol/chat.js +10 -10
- package/dist/features/ai/protocol/chat.js.map +1 -1
- package/dist/features/ai/protocol/index.d.ts +3 -2
- package/dist/features/ai/protocol/index.js +3 -2
- package/dist/features/ai/react/AiSelectionMenu.d.ts +3 -2
- package/dist/features/ai/react/AiSelectionMenu.js +64 -255
- package/dist/features/ai/react/AiSelectionMenu.js.map +1 -1
- package/dist/features/ai/react/AiSidebar.js +2 -2
- package/dist/features/ai/react/DocumentChatAnswer.js +1 -1
- package/dist/features/ai/react/DocumentChatOps.js +1 -0
- package/dist/features/ai/react/DocumentChatOps.js.map +1 -1
- package/dist/features/ai/react/ModelQuickSwitch.js +1 -1
- package/dist/features/ai/react/SettingsAiTab.js +1 -1
- package/dist/features/ai/react/SettingsModelProfiles.js +1 -1
- package/dist/features/ai/react/use-ai-selection-flow.js +34 -0
- package/dist/features/ai/react/use-ai-selection-flow.js.map +1 -0
- package/dist/features/ai/selection-flow.d.ts +133 -0
- package/dist/features/ai/selection-flow.js +283 -0
- package/dist/features/ai/selection-flow.js.map +1 -0
- package/dist/features/ai/use-ai-assistant.d.ts +1 -1
- package/dist/features/ai/use-document-chat.d.ts +1 -1
- package/dist/features/authors/components/AuthorColorsPanel.js +1 -1
- package/dist/features/authors/feature.js +1 -1
- package/dist/features/authors/index.js +1 -1
- package/dist/features/authors/react/AuthorColorsPanel.js +1 -1
- package/dist/features/comments/components/CommentBubble.js +1 -1
- package/dist/features/comments/components/CommentComposer.js +1 -1
- package/dist/features/comments/components/CommentThreadView.js +5 -2
- package/dist/features/comments/components/CommentThreadView.js.map +1 -1
- package/dist/features/comments/components/CommentsMargin.js +108 -230
- package/dist/features/comments/components/CommentsMargin.js.map +1 -1
- package/dist/features/comments/components/CommentsOverlay.js +1 -1
- package/dist/features/comments/feature.js +1 -1
- package/dist/features/comments/index.d.ts +4 -2
- package/dist/features/comments/index.js +4 -2
- package/dist/features/comments/margin-controller.d.ts +44 -0
- package/dist/features/comments/margin-controller.js +132 -0
- package/dist/features/comments/margin-controller.js.map +1 -0
- package/dist/features/comments/margin-layout.d.ts +55 -0
- package/dist/features/comments/margin-layout.js +79 -0
- package/dist/features/comments/margin-layout.js.map +1 -0
- package/dist/features/comments/model.d.ts +8 -0
- package/dist/features/comments/model.js +60 -34
- package/dist/features/comments/model.js.map +1 -1
- package/dist/features/comments/react/CommentBubble.js +1 -1
- package/dist/features/comments/react/CommentComposer.js +1 -1
- package/dist/features/comments/react/CommentThreadView.js +2 -2
- package/dist/features/comments/react/CommentThreadView.js.map +1 -1
- package/dist/features/comments/react/CommentsMargin.d.ts +0 -5
- package/dist/features/comments/react/CommentsMargin.js +85 -251
- package/dist/features/comments/react/CommentsMargin.js.map +1 -1
- package/dist/features/comments/react/CommentsOverlay.js +1 -1
- package/dist/features/comments/react/use-comments.js +1 -1
- package/dist/features/comments/react/use-margin-layout.js +45 -0
- package/dist/features/comments/react/use-margin-layout.js.map +1 -0
- package/dist/features/comments/react.d.ts +1 -1
- package/dist/features/comments/schema.d.ts +23 -0
- package/dist/features/comments/schema.js.map +1 -1
- package/dist/features/comments/snapshot-equality.js +40 -0
- package/dist/features/comments/snapshot-equality.js.map +1 -0
- package/dist/features/comments/thread-anchor.js +33 -0
- package/dist/features/comments/thread-anchor.js.map +1 -0
- package/dist/features/comments/use-comments.d.ts +3 -1
- package/dist/features/comments/use-comments.js +1 -0
- package/dist/features/comments/use-comments.js.map +1 -1
- package/dist/features/comments/use-margin-layout.js +29 -0
- package/dist/features/comments/use-margin-layout.js.map +1 -0
- package/dist/features/comments/vue.d.ts +1 -1
- package/dist/features/composites/accepted-children.d.ts +21 -0
- package/dist/features/composites/accepted-children.js +126 -0
- package/dist/features/composites/accepted-children.js.map +1 -0
- package/dist/features/composites/components/CompositeDetails.js +1 -1
- package/dist/features/composites/components/CompositeInspector.js +38 -164
- package/dist/features/composites/components/CompositeInspector.js.map +1 -1
- package/dist/features/composites/feature.d.ts +2 -1
- package/dist/features/composites/feature.js +8 -2
- package/dist/features/composites/feature.js.map +1 -1
- package/dist/features/composites/index.d.ts +5 -3
- package/dist/features/composites/index.js +8 -6
- package/dist/features/composites/inspector-model.d.ts +97 -0
- package/dist/features/composites/inspector-model.js +255 -0
- package/dist/features/composites/inspector-model.js.map +1 -0
- package/dist/features/composites/kinds.d.ts +11 -0
- package/dist/features/composites/kinds.js +17 -2
- package/dist/features/composites/kinds.js.map +1 -1
- package/dist/features/composites/outline.d.ts +18 -5
- package/dist/features/composites/outline.js +44 -12
- package/dist/features/composites/outline.js.map +1 -1
- package/dist/features/composites/react/CompositeDetails.js +1 -1
- package/dist/features/composites/react/CompositeInspector.d.ts +1 -1
- package/dist/features/composites/react/CompositeInspector.js +21 -180
- package/dist/features/composites/react/CompositeInspector.js.map +1 -1
- package/dist/features/composites/react/CompositeStatusMenu.d.ts +1 -1
- package/dist/features/composites/react/CompositeView.js +1 -1
- package/dist/features/composites/react/use-composite-inspector.js +29 -0
- package/dist/features/composites/react/use-composite-inspector.js.map +1 -0
- package/dist/features/composites/schema.d.ts +2 -2
- package/dist/features/composites/schema.js +2 -2
- package/dist/features/composites/schema.js.map +1 -1
- package/dist/features/composites/use-composite-anchors.d.ts +1 -1
- package/dist/features/composites/use-composite-inspector.js +41 -0
- package/dist/features/composites/use-composite-inspector.js.map +1 -0
- package/dist/features/composites/use-composite-rows.d.ts +1 -1
- package/dist/features/media/index.d.ts +1 -1
- package/dist/features/media/vue.d.ts +1 -1
- package/dist/features/mentions/index.d.ts +3 -3
- package/dist/features/mentions/index.js +3 -3
- package/dist/features/mentions/schema.d.ts +1 -1
- package/dist/features/mentions/schema.js +1 -1
- package/dist/features/themes/index.d.ts +1 -1
- package/dist/features/themes/index.js +1 -1
- package/dist/features/transcript/feature.js +1 -1
- package/dist/features/transcript/index.js +2 -2
- package/dist/i18n/de/ai.js +2 -0
- package/dist/i18n/de/ai.js.map +1 -1
- package/dist/i18n/de/composites.js +8 -1
- package/dist/i18n/de/composites.js.map +1 -1
- package/dist/i18n/de/tree.js +14 -1
- package/dist/i18n/de/tree.js.map +1 -1
- package/dist/i18n/de.d.ts +22 -0
- package/dist/i18n/en/ai.js +2 -0
- package/dist/i18n/en/ai.js.map +1 -1
- package/dist/i18n/en/composites.js +8 -1
- package/dist/i18n/en/composites.js.map +1 -1
- package/dist/i18n/en/tree.js +14 -1
- package/dist/i18n/en/tree.js.map +1 -1
- package/dist/integrations/composite-ai/composite-fill.js +2 -2
- package/dist/integrations/composite-ai/composite-summary.js +2 -2
- package/dist/integrations/composite-ai/index.js +2 -2
- package/dist/integrations/transcript-ai/transcript-cleanup.js +1 -1
- package/dist/markdown/MarkdownSurface.d.ts +1 -1
- package/dist/markdown/MarkdownSurface.js +2 -2
- package/dist/markdown/MarkdownSurface.vue.d.ts +7 -7
- package/dist/markdown/index.d.ts +2 -2
- package/dist/nuxt/runtime/server/routes/collab/[id].js +1 -1
- package/dist/react/EditorSurface.d.ts +1 -1
- package/dist/react/EditorSurface.js +43 -149
- package/dist/react/EditorSurface.js.map +1 -1
- package/dist/react/EditorToolbar.js +3 -40
- package/dist/react/EditorToolbar.js.map +1 -1
- package/dist/schema/advanced.js +2 -2
- package/dist/server/adapter.d.ts +2 -2
- package/dist/server/collab.js +1 -1
- package/dist/server/handlers/ai.d.ts +4 -2
- package/dist/server/handlers/ai.js +11 -9
- package/dist/server/handlers/ai.js.map +1 -1
- package/dist/server/handlers/context.d.ts +1 -1
- package/dist/server/handlers/editor.d.ts +1 -1
- package/dist/server/handlers/editor.js +2 -2
- package/dist/server/host.d.ts +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/settings/index.d.ts +1 -1
- package/dist/styles/tokens.css +1 -1
- package/dist/tree/DocumentTree.js +159 -387
- package/dist/tree/DocumentTree.js.map +1 -1
- package/dist/tree/drop-rules.js +37 -0
- package/dist/tree/drop-rules.js.map +1 -0
- package/dist/tree/react/DocumentTree.d.ts +2 -2
- package/dist/tree/react/DocumentTree.js +135 -397
- package/dist/tree/react/DocumentTree.js.map +1 -1
- package/dist/tree/react/use-tree-model.js +129 -0
- package/dist/tree/react/use-tree-model.js.map +1 -0
- package/dist/tree/react.js +1 -1
- package/dist/tree/tree-host.js +100 -0
- package/dist/tree/tree-host.js.map +1 -0
- package/dist/tree/tree-items.js +22 -43
- package/dist/tree/tree-items.js.map +1 -1
- package/dist/tree/tree-model.js +580 -0
- package/dist/tree/tree-model.js.map +1 -0
- package/dist/tree/tree-rows.js +77 -0
- package/dist/tree/tree-rows.js.map +1 -0
- package/dist/tree/use-tree-model.js +94 -0
- package/dist/tree/use-tree-model.js.map +1 -0
- package/dist/tree/vue.js +1 -1
- package/dist/vue/AdvancedEditor.vue.d.ts +4 -4
- package/dist/vue/SimpleEditor.vue.d.ts +2 -2
- package/dist/vue/components/EditorSurface.js +27 -87
- package/dist/vue/components/EditorSurface.js.map +1 -1
- package/dist/vue/components/EditorSurface.vue.d.ts +4 -4
- package/dist/vue/components/EditorToolbar.js +7 -44
- package/dist/vue/components/EditorToolbar.js.map +1 -1
- package/dist/vue/context.d.ts +1 -1
- package/dist/vue/context.js +1 -1
- package/dist/vue/context.js.map +1 -1
- package/dist/vue/use-editor-ui.d.ts +1 -1
- package/dist/vue/use-notifier.d.ts +1 -1
- package/docs/integration.md +97 -4
- package/docs/llms.md +9 -1
- package/docs/usage.md +10 -5
- package/package.json +67 -5
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
6
|
-
import {
|
|
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 {
|
|
12
|
-
import {
|
|
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 {
|
|
37
|
+
const { model: assistant } = useAiAssistant(editor);
|
|
44
38
|
const { settings } = useEditorSettings();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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
|
|
88
|
-
const
|
|
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 (
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
159
|
+
open: state.open,
|
|
351
160
|
onOpenChange: (value) => {
|
|
352
|
-
if (!value) close(
|
|
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
|
|
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
|
|
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
|
-
|
|
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: () =>
|
|
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";
|