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

Commit 9583964

Browse files
authored
DEV: Added compatibility with the Glimmer Post Menu (#887)
1 parent 2fc0568 commit 9583964

File tree

6 files changed

+309
-137
lines changed

6 files changed

+309
-137
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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 { isPostFromAiBot } from "../../lib/ai-bot-helper";
6+
import DebugAiModal from "../modal/debug-ai-modal";
7+
8+
export default class AiDebugButton extends Component {
9+
static shouldRender(args) {
10+
return isPostFromAiBot(args.post, args.state.currentUser);
11+
}
12+
13+
// TODO (glimmer-post-menu): Remove this static function and move the code into the button action after the widget code is removed
14+
static debugAiResponse(post, modal) {
15+
modal.show(DebugAiModal, { model: post });
16+
}
17+
18+
@service modal;
19+
20+
@action
21+
debugAiResponse() {
22+
this.constructor.debugAiResponse(this.args.post, this.modal);
23+
}
24+
25+
<template>
26+
<DButton
27+
class="post-action-menu__debug-ai"
28+
...attributes
29+
@action={{this.debugAiResponse}}
30+
@icon="info"
31+
@title="discourse_ai.ai_bot.debug_ai"
32+
/>
33+
</template>
34+
}
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 { isPostFromAiBot } from "../../lib/ai-bot-helper";
6+
import copyConversation from "../../lib/copy-conversation";
7+
import ShareModal from "../modal/share-modal";
8+
9+
const AUTO_COPY_THRESHOLD = 4;
10+
11+
export default class AiDebugButton extends Component {
12+
static shouldRender(args) {
13+
return isPostFromAiBot(args.post, args.state.currentUser);
14+
}
15+
16+
// TODO (glimmer-post-menu): Remove this static function and move the code into the button action after the widget code is removed
17+
static async shareAiResponse(post, modal, showFeedback) {
18+
if (post.post_number <= AUTO_COPY_THRESHOLD) {
19+
await copyConversation(post.topic, 1, post.post_number);
20+
showFeedback("discourse_ai.ai_bot.conversation_shared");
21+
} else {
22+
modal.show(ShareModal, { model: post });
23+
}
24+
}
25+
26+
@service modal;
27+
28+
@action
29+
shareAiResponse() {
30+
this.constructor.shareAiResponse(
31+
this.args.post,
32+
this.modal,
33+
this.args.showFeedback
34+
);
35+
}
36+
37+
<template>
38+
<DButton
39+
class="post-action-menu__share-ai"
40+
...attributes
41+
@action={{this.shareAiResponse}}
42+
@icon="far-copy"
43+
@title="discourse_ai.ai_bot.share"
44+
/>
45+
</template>
46+
}

assets/javascripts/discourse/lib/ai-bot-helper.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ import Composer from "discourse/models/composer";
44
import I18n from "I18n";
55
import ShareFullTopicModal from "../components/modal/share-full-topic-modal";
66

7+
const MAX_PERSONA_USER_ID = -1200;
8+
9+
export function isPostFromAiBot(post, currentUser) {
10+
return (
11+
post.user_id <= MAX_PERSONA_USER_ID ||
12+
!!currentUser?.ai_enabled_chat_bots?.any(
13+
(bot) => post.username === bot.username
14+
)
15+
);
16+
}
17+
718
export function showShareConversationModal(modal, topicId) {
819
ajax(`/discourse-ai/ai-bot/shared-ai-conversations/preview/${topicId}.json`)
920
.then((payload) => {

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

Lines changed: 99 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import { hbs } from "ember-cli-htmlbars";
2-
import { ajax } from "discourse/lib/ajax";
3-
import { popupAjaxError } from "discourse/lib/ajax-error";
42
import { withPluginApi } from "discourse/lib/plugin-api";
53
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;
4+
import { withSilencedDeprecations } from "discourse-common/lib/deprecated";
115
import AiBotHeaderIcon from "../discourse/components/ai-bot-header-icon";
12-
import { showShareConversationModal } from "../discourse/lib/ai-bot-helper";
6+
import AiCancelStreamingButton from "../discourse/components/post-menu/ai-cancel-streaming-button";
7+
import AiDebugButton from "../discourse/components/post-menu/ai-debug-button";
8+
import AiShareButton from "../discourse/components/post-menu/ai-share-button";
9+
import {
10+
isPostFromAiBot,
11+
showShareConversationModal,
12+
} from "../discourse/lib/ai-bot-helper";
13+
import { streamPostText } from "../discourse/lib/ai-streamer/progress-handlers";
1314

1415
let enabledChatBotIds = [];
1516
let allowDebug = false;
17+
1618
function isGPTBot(user) {
1719
return user && enabledChatBotIds.includes(user.id);
1820
}
@@ -22,29 +24,7 @@ function attachHeaderIcon(api) {
2224
}
2325

2426
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-
});
27+
initializePauseButton(api);
4828

4929
api.modifyClass("controller:topic", {
5030
pluginId: "discourse-ai",
@@ -102,34 +82,82 @@ function initializePersonaDecorator(api) {
10282
);
10383
}
10484

105-
const MAX_PERSONA_USER_ID = -1200;
85+
function initializePauseButton(api) {
86+
const transformerRegistered = api.registerValueTransformer(
87+
"post-menu-buttons",
88+
({ value: dag, context: { post, firstButtonKey } }) => {
89+
if (isGPTBot(post.user)) {
90+
dag.add("ai-cancel-gpt", AiCancelStreamingButton, {
91+
before: firstButtonKey,
92+
after: ["ai-share", "ai-debug"],
93+
});
94+
}
95+
}
96+
);
97+
98+
const silencedKey =
99+
transformerRegistered && "discourse.post-menu-widget-overrides";
100+
101+
withSilencedDeprecations(silencedKey, () => initializePauseWidgetButton(api));
102+
}
103+
104+
function initializePauseWidgetButton(api) {
105+
api.addPostMenuButton("cancel-gpt", (post) => {
106+
if (isGPTBot(post.user)) {
107+
return {
108+
icon: "pause",
109+
action: "cancelStreaming",
110+
title: "discourse_ai.ai_bot.cancel_streaming",
111+
className: "btn btn-default cancel-streaming",
112+
position: "first",
113+
};
114+
}
115+
});
116+
117+
api.attachWidgetAction("post", "cancelStreaming", function () {
118+
AiCancelStreamingButton.cancelStreaming(this.model);
119+
});
120+
}
106121

107122
function initializeDebugButton(api) {
108123
const currentUser = api.getCurrentUser();
109124
if (!currentUser || !currentUser.ai_enabled_chat_bots || !allowDebug) {
110125
return;
111126
}
112127

128+
const transformerRegistered = api.registerValueTransformer(
129+
"post-menu-buttons",
130+
({ value: dag, context: { post, firstButtonKey } }) => {
131+
if (post.topic?.archetype === "private_message") {
132+
dag.add("ai-debug", AiDebugButton, {
133+
before: firstButtonKey,
134+
after: "ai-share",
135+
});
136+
}
137+
}
138+
);
139+
140+
const silencedKey =
141+
transformerRegistered && "discourse.post-menu-widget-overrides";
142+
143+
withSilencedDeprecations(silencedKey, () => initializeDebugWidgetButton(api));
144+
}
145+
146+
function initializeDebugWidgetButton(api) {
147+
const currentUser = api.getCurrentUser();
148+
113149
let debugAiResponse = async function ({ post }) {
114150
const modal = api.container.lookup("service:modal");
115-
116-
modal.show(DebugAiModal, { model: post });
151+
AiDebugButton.debugAiResponse(post, modal);
117152
};
118153

119154
api.addPostMenuButton("debugAi", (post) => {
120155
if (post.topic?.archetype !== "private_message") {
121156
return;
122157
}
123158

124-
if (
125-
!currentUser.ai_enabled_chat_bots.any(
126-
(bot) => post.username === bot.username
127-
)
128-
) {
129-
// special handling for personas (persona bot users start at ID -1200 and go down)
130-
if (post.user_id > MAX_PERSONA_USER_ID) {
131-
return;
132-
}
159+
if (!isPostFromAiBot(post, currentUser)) {
160+
return;
133161
}
134162

135163
return {
@@ -148,14 +176,29 @@ function initializeShareButton(api) {
148176
return;
149177
}
150178

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 });
179+
const transformerRegistered = api.registerValueTransformer(
180+
"post-menu-buttons",
181+
({ value: dag, context: { post, firstButtonKey } }) => {
182+
if (post.topic?.archetype === "private_message") {
183+
dag.add("ai-share", AiShareButton, {
184+
before: firstButtonKey,
185+
});
186+
}
158187
}
188+
);
189+
190+
const silencedKey =
191+
transformerRegistered && "discourse.post-menu-widget-overrides";
192+
193+
withSilencedDeprecations(silencedKey, () => initializeShareWidgetButton(api));
194+
}
195+
196+
function initializeShareWidgetButton(api) {
197+
const currentUser = api.getCurrentUser();
198+
199+
let shareAiResponse = async function ({ post, showFeedback }) {
200+
const modal = api.container.lookup("service:modal");
201+
AiShareButton.shareAiResponse(post, modal, showFeedback);
159202
};
160203

161204
api.addPostMenuButton("share", (post) => {
@@ -164,21 +207,14 @@ function initializeShareButton(api) {
164207
return;
165208
}
166209

167-
if (
168-
!currentUser.ai_enabled_chat_bots.any(
169-
(bot) => post.username === bot.username
170-
)
171-
) {
172-
// special handling for personas (persona bot users start at ID -1200 and go down)
173-
if (post.user_id > MAX_PERSONA_USER_ID) {
174-
return;
175-
}
210+
if (!isPostFromAiBot(post, currentUser)) {
211+
return;
176212
}
177213

178214
return {
179215
action: shareAiResponse,
180216
icon: "far-copy",
181-
className: "post-action-menu__share",
217+
className: "post-action-menu__share-ai",
182218
title: "discourse_ai.ai_bot.share",
183219
position: "first",
184220
};
@@ -218,10 +254,10 @@ export default {
218254
enabledChatBotIds = user.ai_enabled_chat_bots.map((bot) => bot.id);
219255
allowDebug = user.can_debug_ai_bot_conversations;
220256
withPluginApi("1.6.0", attachHeaderIcon);
221-
withPluginApi("1.6.0", initializeAIBotReplies);
257+
withPluginApi("1.34.0", initializeAIBotReplies);
222258
withPluginApi("1.6.0", initializePersonaDecorator);
223-
withPluginApi("1.22.0", (api) => initializeDebugButton(api, container));
224-
withPluginApi("1.22.0", (api) => initializeShareButton(api, container));
259+
withPluginApi("1.34.0", (api) => initializeDebugButton(api, container));
260+
withPluginApi("1.34.0", (api) => initializeShareButton(api, container));
225261
withPluginApi("1.22.0", (api) =>
226262
initializeShareTopicButton(api, container)
227263
);

0 commit comments

Comments
 (0)