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

Commit 23aa739

Browse files
committed
DEV: Added compatibility with the Glimmer Post Menu
1 parent e8f0633 commit 23aa739

File tree

5 files changed

+262
-46
lines changed

5 files changed

+262
-46
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import Component from "@glimmer/component";
2+
import { action } from "@ember/object";
3+
import DButton from "discourse/components/d-button";
4+
import { ajax } from "discourse/lib/ajax";
5+
import { popupAjaxError } from "discourse/lib/ajax-error";
6+
7+
export default class AiCancelStreamingButton extends Component {
8+
// TODO (glimmer-post-menu): Remove this static function and move the code into the button action after the widget code is removed
9+
static async cancelStreaming(post) {
10+
try {
11+
await ajax(`/discourse-ai/ai-bot/post/${post.id}/stop-streaming`, {
12+
type: "POST",
13+
});
14+
15+
document
16+
.querySelector(`#post_${post.post_number}`)
17+
.classList.remove("streaming");
18+
} catch (e) {
19+
popupAjaxError(e);
20+
}
21+
}
22+
23+
@action
24+
cancelStreaming() {
25+
this.constructor.cancelStreaming(this.args.post);
26+
}
27+
28+
<template>
29+
<DButton
30+
class="post-action-menu__ai-cancel-streaming cancel-streaming"
31+
...attributes
32+
@action={{this.cancelStreaming}}
33+
@icon="pause"
34+
@title="discourse_ai.ai_bot.cancel_streaming"
35+
/>
36+
</template>
37+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Component from "@glimmer/component";
2+
import { action } from "@ember/object";
3+
import { inject as service } from "@ember/service";
4+
import DButton from "discourse/components/d-button";
5+
import DebugAiModal from "../modal/debug-ai-modal";
6+
7+
const MAX_PERSONA_USER_ID = -1200;
8+
9+
export default class AiDebugButton extends Component {
10+
static shouldRender(args) {
11+
if (
12+
!args.state.currentUser.ai_enabled_chat_bots.any(
13+
(bot) => args.post.username === bot.username
14+
)
15+
) {
16+
// special handling for personas (persona bot users start at ID -1200 and go down)
17+
if (args.post.user_id > MAX_PERSONA_USER_ID) {
18+
return false;
19+
}
20+
}
21+
22+
return true;
23+
}
24+
25+
// TODO (glimmer-post-menu): Remove this static function and move the code into the button action after the widget code is removed
26+
static debugAiResponse(post, modal) {
27+
modal.show(DebugAiModal, { model: post });
28+
}
29+
30+
@service modal;
31+
32+
@action
33+
debugAiResponse() {
34+
this.constructor.debugAiResponse(this.args.post, this.modal);
35+
}
36+
37+
<template>
38+
<DButton
39+
class="post-action-menu__debug-ai"
40+
...attributes
41+
@action={{this.debugAiResponse}}
42+
@icon="info"
43+
@title="discourse_ai.ai_bot.debug_ai"
44+
/>
45+
</template>
46+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import Component from "@glimmer/component";
2+
import { action } from "@ember/object";
3+
import { inject as service } from "@ember/service";
4+
import DButton from "discourse/components/d-button";
5+
import copyConversation from "../../lib/copy-conversation";
6+
import ShareModal from "../modal/share-modal";
7+
8+
const AUTO_COPY_THRESHOLD = 4;
9+
const MAX_PERSONA_USER_ID = -1200;
10+
11+
export default class AiDebugButton extends Component {
12+
static shouldRender(args) {
13+
if (
14+
!args.state.currentUser.ai_enabled_chat_bots.any(
15+
(bot) => args.post.username === bot.username
16+
)
17+
) {
18+
// special handling for personas (persona bot users start at ID -1200 and go down)
19+
if (args.post.user_id > MAX_PERSONA_USER_ID) {
20+
return false;
21+
}
22+
}
23+
24+
return true;
25+
}
26+
27+
// TODO (glimmer-post-menu): Remove this static function and move the code into the button action after the widget code is removed
28+
static async shareAiResponse(post, modal, showFeedback) {
29+
if (post.post_number <= AUTO_COPY_THRESHOLD) {
30+
await copyConversation(post.topic, 1, post.post_number);
31+
showFeedback("discourse_ai.ai_bot.conversation_shared");
32+
} else {
33+
modal.show(ShareModal, { model: post });
34+
}
35+
}
36+
37+
@service modal;
38+
39+
@action
40+
shareAiResponse() {
41+
this.constructor.shareAiResponse(
42+
this.args.post,
43+
this.modal,
44+
this.args.showFeedback
45+
);
46+
}
47+
48+
<template>
49+
<DButton
50+
class="post-action-menu__share-ai"
51+
...attributes
52+
@action={{this.shareAiResponse}}
53+
@icon="far-copy"
54+
@title="discourse_ai.ai_bot.share"
55+
/>
56+
</template>
57+
}

assets/javascripts/initializers/ai-bot-replies.js

Lines changed: 119 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
import { hbs } from "ember-cli-htmlbars";
2-
import { ajax } from "discourse/lib/ajax";
3-
import { popupAjaxError } from "discourse/lib/ajax-error";
2+
import {
3+
POST_MENU_COPY_LINK_BUTTON_KEY,
4+
POST_MENU_LIKE_BUTTON_KEY,
5+
POST_MENU_SHARE_BUTTON_KEY,
6+
POST_MENU_SHOW_MORE_BUTTON_KEY,
7+
} from "discourse/components/post/menu";
48
import { withPluginApi } from "discourse/lib/plugin-api";
59
import { registerWidgetShim } from "discourse/widgets/render-glimmer";
6-
import DebugAiModal from "../discourse/components/modal/debug-ai-modal";
7-
import ShareModal from "../discourse/components/modal/share-modal";
8-
import { streamPostText } from "../discourse/lib/ai-streamer/progress-handlers";
9-
import copyConversation from "../discourse/lib/copy-conversation";
10-
const AUTO_COPY_THRESHOLD = 4;
10+
import { withSilencedDeprecations } from "discourse-common/lib/deprecated";
1111
import AiBotHeaderIcon from "../discourse/components/ai-bot-header-icon";
12+
import AiCancelStreamingButton from "../discourse/components/post-menu/ai-cancel-streaming-button";
13+
import AiDebugButton from "../discourse/components/post-menu/ai-debug-button";
14+
import AiShareButton from "../discourse/components/post-menu/ai-share-button";
1215
import { showShareConversationModal } from "../discourse/lib/ai-bot-helper";
16+
import { streamPostText } from "../discourse/lib/ai-streamer/progress-handlers";
1317

1418
let enabledChatBotIds = [];
1519
let allowDebug = false;
20+
1621
function isGPTBot(user) {
1722
return user && enabledChatBotIds.includes(user.id);
1823
}
@@ -22,29 +27,7 @@ function attachHeaderIcon(api) {
2227
}
2328

2429
function initializeAIBotReplies(api) {
25-
api.addPostMenuButton("cancel-gpt", (post) => {
26-
if (isGPTBot(post.user)) {
27-
return {
28-
icon: "pause",
29-
action: "cancelStreaming",
30-
title: "discourse_ai.ai_bot.cancel_streaming",
31-
className: "btn btn-default cancel-streaming",
32-
position: "first",
33-
};
34-
}
35-
});
36-
37-
api.attachWidgetAction("post", "cancelStreaming", function () {
38-
ajax(`/discourse-ai/ai-bot/post/${this.model.id}/stop-streaming`, {
39-
type: "POST",
40-
})
41-
.then(() => {
42-
document
43-
.querySelector(`#post_${this.model.post_number}`)
44-
.classList.remove("streaming");
45-
})
46-
.catch(popupAjaxError);
47-
});
30+
initializePauseButton(api);
4831

4932
api.modifyClass("controller:topic", {
5033
pluginId: "discourse-ai",
@@ -104,16 +87,87 @@ function initializePersonaDecorator(api) {
10487

10588
const MAX_PERSONA_USER_ID = -1200;
10689

90+
function initializePauseButton(api) {
91+
const transformerRegistered = api.registerValueTransformer(
92+
"post-menu-buttons",
93+
({ value: dag, context: { post } }) => {
94+
if (isGPTBot(post.user)) {
95+
dag.add("ai-cancel-gpt", AiCancelStreamingButton, {
96+
before: [
97+
POST_MENU_LIKE_BUTTON_KEY,
98+
POST_MENU_COPY_LINK_BUTTON_KEY,
99+
POST_MENU_SHARE_BUTTON_KEY,
100+
POST_MENU_SHOW_MORE_BUTTON_KEY,
101+
],
102+
after: ["ai-share", "ai-debug"],
103+
});
104+
}
105+
106+
return dag;
107+
}
108+
);
109+
110+
const silencedKey =
111+
transformerRegistered && "discourse.post-menu-widget-overrides";
112+
113+
withSilencedDeprecations(silencedKey, () => initializePauseWidgetButton(api));
114+
}
115+
116+
function initializePauseWidgetButton(api) {
117+
api.addPostMenuButton("cancel-gpt", (post) => {
118+
if (isGPTBot(post.user)) {
119+
return {
120+
icon: "pause",
121+
action: "cancelStreaming",
122+
title: "discourse_ai.ai_bot.cancel_streaming",
123+
className: "btn btn-default cancel-streaming",
124+
position: "first",
125+
};
126+
}
127+
});
128+
129+
api.attachWidgetAction("post", "cancelStreaming", function () {
130+
AiCancelStreamingButton.cancelStreaming(this.model);
131+
});
132+
}
133+
107134
function initializeDebugButton(api) {
108135
const currentUser = api.getCurrentUser();
109136
if (!currentUser || !currentUser.ai_enabled_chat_bots || !allowDebug) {
110137
return;
111138
}
112139

140+
const transformerRegistered = api.registerValueTransformer(
141+
"post-menu-buttons",
142+
({ value: dag, context: { post } }) => {
143+
if (post.topic?.archetype === "private_message") {
144+
dag.add("ai-debug", AiDebugButton, {
145+
before: [
146+
POST_MENU_LIKE_BUTTON_KEY,
147+
POST_MENU_COPY_LINK_BUTTON_KEY,
148+
POST_MENU_SHARE_BUTTON_KEY,
149+
POST_MENU_SHOW_MORE_BUTTON_KEY,
150+
],
151+
after: "ai-share",
152+
});
153+
}
154+
155+
return dag;
156+
}
157+
);
158+
159+
const silencedKey =
160+
transformerRegistered && "discourse.post-menu-widget-overrides";
161+
162+
withSilencedDeprecations(silencedKey, () => initializeDebugWidgetButton(api));
163+
}
164+
165+
function initializeDebugWidgetButton(api) {
166+
const currentUser = api.getCurrentUser();
167+
113168
let debugAiResponse = async function ({ post }) {
114169
const modal = api.container.lookup("service:modal");
115-
116-
modal.show(DebugAiModal, { model: post });
170+
AiDebugButton.debugAiResponse(post, modal);
117171
};
118172

119173
api.addPostMenuButton("debugAi", (post) => {
@@ -148,14 +202,36 @@ function initializeShareButton(api) {
148202
return;
149203
}
150204

151-
let shareAiResponse = async function ({ post, showFeedback }) {
152-
if (post.post_number <= AUTO_COPY_THRESHOLD) {
153-
await copyConversation(post.topic, 1, post.post_number);
154-
showFeedback("discourse_ai.ai_bot.conversation_shared");
155-
} else {
156-
const modal = api.container.lookup("service:modal");
157-
modal.show(ShareModal, { model: post });
205+
const transformerRegistered = api.registerValueTransformer(
206+
"post-menu-buttons",
207+
({ value: dag, context: { post } }) => {
208+
if (post.topic?.archetype === "private_message") {
209+
dag.add("ai-share", AiShareButton, {
210+
before: [
211+
POST_MENU_LIKE_BUTTON_KEY,
212+
POST_MENU_COPY_LINK_BUTTON_KEY,
213+
POST_MENU_SHARE_BUTTON_KEY,
214+
POST_MENU_SHOW_MORE_BUTTON_KEY,
215+
],
216+
});
217+
}
218+
219+
return dag;
158220
}
221+
);
222+
223+
const silencedKey =
224+
transformerRegistered && "discourse.post-menu-widget-overrides";
225+
226+
withSilencedDeprecations(silencedKey, () => initializeShareWidgetButton(api));
227+
}
228+
229+
function initializeShareWidgetButton(api) {
230+
const currentUser = api.getCurrentUser();
231+
232+
let shareAiResponse = async function ({ post, showFeedback }) {
233+
const modal = api.container.lookup("service:modal");
234+
AiShareButton.shareAiResponse(post, modal, showFeedback);
159235
};
160236

161237
api.addPostMenuButton("share", (post) => {
@@ -178,7 +254,7 @@ function initializeShareButton(api) {
178254
return {
179255
action: shareAiResponse,
180256
icon: "far-copy",
181-
className: "post-action-menu__share",
257+
className: "post-action-menu__share-ai",
182258
title: "discourse_ai.ai_bot.share",
183259
position: "first",
184260
};
@@ -218,10 +294,10 @@ export default {
218294
enabledChatBotIds = user.ai_enabled_chat_bots.map((bot) => bot.id);
219295
allowDebug = user.can_debug_ai_bot_conversations;
220296
withPluginApi("1.6.0", attachHeaderIcon);
221-
withPluginApi("1.6.0", initializeAIBotReplies);
297+
withPluginApi("1.34.0", initializeAIBotReplies);
222298
withPluginApi("1.6.0", initializePersonaDecorator);
223-
withPluginApi("1.22.0", (api) => initializeDebugButton(api, container));
224-
withPluginApi("1.22.0", (api) => initializeShareButton(api, container));
299+
withPluginApi("1.34.0", (api) => initializeDebugButton(api, container));
300+
withPluginApi("1.34.0", (api) => initializeShareButton(api, container));
225301
withPluginApi("1.22.0", (api) =>
226302
initializeShareTopicButton(api, container)
227303
);

spec/system/ai_bot/share_spec.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454

5555
visit(pm.url)
5656

57-
find("#post_2 .post-action-menu__share").click
57+
find("#post_2 .post-action-menu__share-ai").click
5858

5959
try_until_success do
6060
clip_text = cdp.read_clipboard
@@ -89,7 +89,7 @@
8989

9090
visit(pm.url)
9191

92-
find("#post_2 .post-action-menu__share").click
92+
find("#post_2 .post-action-menu__share-ai").click
9393

9494
try_until_success do
9595
clip_text = cdp.read_clipboard
@@ -117,7 +117,7 @@
117117

118118
page.execute_script("window.navigator.clipboard.writeText('')")
119119

120-
find("#post_6 .post-action-menu__share").click
120+
find("#post_6 .post-action-menu__share-ai").click
121121
find(".ai-share-modal__slider input").set("2")
122122
find(".ai-share-modal button.btn-primary").click
123123

0 commit comments

Comments
 (0)