Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit 2e4317a

Browse files
committed
DEV: makes ai menu helper a standalone menu
The current menu was rendering inside the post text toolbar (on desktop). This is not ideal as the post text toolbar rendering is conditioned on the presence of text selection, when you click a button on the toolbar, by design of the web browsers you will lose your text selection, making all of this super tricky. This commit makes desktop and mobile behave in the same way by rendering their own menu and capturing the quote state when we render the post text selection toolbar, this allows us to reason a much simpler way about the AI helper. This commit also removes what appears to be an unused file and corrects which was seemingly copy/paste mistakes.
1 parent b5a2ee3 commit 2e4317a

File tree

3 files changed

+51
-150
lines changed

3 files changed

+51
-150
lines changed

assets/javascripts/discourse/components/ai-post-helper-menu.gjs

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -78,23 +78,6 @@ export default class AiPostHelperMenu extends Component {
7878

7979
@tracked _activeAiRequest = null;
8080

81-
constructor() {
82-
super(...arguments);
83-
84-
withPluginApi((api) => {
85-
api.registerValueTransformer(
86-
"post-text-selection-prevent-close",
87-
({ value }) => {
88-
if (this.menuState === this.MENU_STATES.result) {
89-
return true;
90-
}
91-
92-
return value;
93-
}
94-
);
95-
});
96-
}
97-
9881
get footnoteDisabled() {
9982
return this.streaming || !this.supportsAddFootnote;
10083
}
@@ -338,7 +321,7 @@ export default class AiPostHelperMenu extends Component {
338321
(and this.site.mobileView (eq this.menuState this.MENU_STATES.options))
339322
}}
340323
<div class="ai-post-helper-menu__selected-text">
341-
{{@data.selectedText}}
324+
{{@data.quoteState.buffer}}
342325
</div>
343326
{{/if}}
344327

assets/javascripts/discourse/connectors/post-text-buttons/ai-post-helper-trigger.gjs

Lines changed: 50 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import { tracked } from "@glimmer/tracking";
33
import { action } from "@ember/object";
44
import { service } from "@ember/service";
55
import DButton from "discourse/components/d-button";
6-
import virtualElementFromTextRange from "discourse/lib/virtual-element-from-text-range";
7-
import eq from "truth-helpers/helpers/eq";
6+
import { selectedRange } from "discourse/lib/utilities";
87
import AiPostHelperMenu from "../../components/ai-post-helper-menu";
98
import { showPostAIHelper } from "../../lib/show-ai-helper";
109

@@ -13,12 +12,8 @@ export default class AiPostHelperTrigger extends Component {
1312
return showPostAIHelper(outletArgs, helper);
1413
}
1514

16-
@service site;
1715
@service menu;
1816

19-
@tracked menuState = this.MENU_STATES.triggers;
20-
@tracked showMainButtons = true;
21-
@tracked showAiButtons = true;
2217
@tracked postHighlighted = false;
2318
@tracked currentMenu = this.menu.getByIdentifier(
2419
"post-text-selection-toolbar"
@@ -27,14 +22,20 @@ export default class AiPostHelperTrigger extends Component {
2722
MENU_STATES = {
2823
triggers: "TRIGGERS",
2924
options: "OPTIONS",
25+
// so we ensure change of state (selection change for example)
26+
// is not impacting the menu data
27+
menuData = {
28+
...this.args.outletArgs.data,
29+
quoteState: {
30+
buffer: this.args.outletArgs.data.quoteState.buffer,
31+
opts: this.args.outletArgs.data.quoteState.opts,
32+
postId: this.args.outletArgs.data.quoteState.postId,
33+
},
34+
post: this.args.outletArgs.post,
35+
selectedRange: selectedRange(),
3036
};
3137

32-
willDestroy() {
33-
super.willDestroy(...arguments);
34-
this.removeHighlightedText();
35-
}
36-
37-
highlightSelectedText() {
38+
highlightSelectedText(selection) {
3839
const postId = this.args.outletArgs.data.quoteState.postId;
3940
const postElement = document.querySelector(
4041
`article[data-post-id='${postId}'] .cooked`
@@ -44,14 +45,7 @@ export default class AiPostHelperTrigger extends Component {
4445
return;
4546
}
4647

47-
this.selectedText = this.args.outletArgs.data.quoteState.buffer;
48-
49-
const selection = window.getSelection();
50-
if (!selection.rangeCount) {
51-
return;
52-
}
53-
54-
const range = selection.getRangeAt(0);
48+
const range = this.menuData.selectedRange;
5549

5650
// Split start/end text nodes at their range boundary
5751
if (
@@ -97,11 +91,10 @@ export default class AiPostHelperTrigger extends Component {
9791
// Replace textNode with highlighted clone
9892
const clone = textNode.cloneNode(true);
9993
highlight.appendChild(clone);
100-
10194
textNode.parentNode.replaceChild(highlight, textNode);
10295
}
10396

104-
selection.removeAllRanges();
97+
window.getSelection().removeAllRanges();
10598
this.postHighlighted = true;
10699
}
107100

@@ -110,16 +103,7 @@ export default class AiPostHelperTrigger extends Component {
110103
return;
111104
}
112105

113-
const postId = this.args.outletArgs.data.quoteState.postId;
114-
const postElement = document.querySelector(
115-
`article[data-post-id='${postId}'] .cooked`
116-
);
117-
118-
if (!postElement) {
119-
return;
120-
}
121-
122-
const highlightedSpans = postElement.querySelectorAll(
106+
const highlightedSpans = document.querySelectorAll(
123107
"span.ai-helper-highlighted-selection"
124108
);
125109

@@ -133,65 +117,44 @@ export default class AiPostHelperTrigger extends Component {
133117

134118
@action
135119
async showAiPostHelperMenu() {
136-
this.highlightSelectedText();
137-
if (this.site.mobileView) {
138-
this.currentMenu.close();
139-
140-
await this.menu.show(virtualElementFromTextRange(), {
141-
identifier: "ai-post-helper-menu",
142-
component: AiPostHelperMenu,
143-
inline: true,
144-
interactive: true,
145-
placement: this.shouldRenderUnder ? "bottom-start" : "top-start",
146-
fallbackPlacements: this.shouldRenderUnder
147-
? ["bottom-end", "top-start"]
148-
: ["bottom-start"],
149-
trapTab: false,
150-
closeOnScroll: false,
151-
modalForMobile: true,
152-
data: this.menuData,
153-
});
154-
}
155-
156-
this.showMainButtons = false;
157-
this.menuState = this.MENU_STATES.options;
158-
}
120+
const existingRect = this.currentMenu.trigger.rect;
121+
const virtualElement = {
122+
getBoundingClientRect: () => existingRect,
123+
getClientRects: () => [existingRect],
124+
};
159125

160-
get menuData() {
161-
// Streamline of data model to be passed to the component when
162-
// instantiated as a DMenu or a simple component in the template
163-
return {
164-
...this.args.outletArgs.data,
165-
quoteState: {
166-
buffer: this.args.outletArgs.data.quoteState.buffer,
167-
opts: this.args.outletArgs.data.quoteState.opts,
168-
postId: this.args.outletArgs.data.quoteState.postId,
126+
await this.currentMenu.close();
127+
128+
await this.menu.show(virtualElement, {
129+
identifier: "ai-post-helper-menu",
130+
component: AiPostHelperMenu,
131+
interactive: true,
132+
trapTab: false,
133+
closeOnScroll: false,
134+
modalForMobile: true,
135+
data: this.menuData,
136+
placement: "bottom",
137+
fallbackPlacements: ["top"],
138+
inline: true,
139+
onClose: () => {
140+
this.removeHighlightedText();
169141
},
170-
post: this.args.outletArgs.post,
171-
selectedText: this.selectedText,
172-
};
142+
});
143+
144+
this.highlightSelectedText();
173145
}
174146

175147
<template>
176-
{{#if this.showMainButtons}}
177-
{{yield}}
178-
{{/if}}
179-
180-
{{#if this.showAiButtons}}
181-
<div class="ai-post-helper">
182-
{{#if (eq this.menuState this.MENU_STATES.triggers)}}
183-
<DButton
184-
@icon="discourse-sparkles"
185-
@title="discourse_ai.ai_helper.post_options_menu.title"
186-
@label="discourse_ai.ai_helper.post_options_menu.trigger"
187-
@action={{this.showAiPostHelperMenu}}
188-
class="btn-flat ai-post-helper__trigger"
189-
/>
190-
191-
{{else if (eq this.menuState this.MENU_STATES.options)}}
192-
<AiPostHelperMenu @data={{this.menuData}} />
193-
{{/if}}
194-
</div>
195-
{{/if}}
148+
{{yield}}
149+
150+
<div class="ai-post-helper">
151+
<DButton
152+
@icon="discourse-sparkles"
153+
@title="discourse_ai.ai_helper.post_options_menu.title"
154+
@label="discourse_ai.ai_helper.post_options_menu.trigger"
155+
@action={{this.showAiPostHelperMenu}}
156+
class="btn-flat ai-post-helper__trigger"
157+
/>
158+
</div>
196159
</template>
197160
}

assets/javascripts/discourse/lib/virtual-element-from-caret-coords.js

Lines changed: 0 additions & 45 deletions
This file was deleted.

0 commit comments

Comments
 (0)