Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import DButton from "discourse/components/d-button";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";

export default class AiCancelStreamingButton extends Component {
// TODO (glimmer-post-menu): Remove this static function and move the code into the button action after the widget code is removed
static async cancelStreaming(post) {
try {
await ajax(`/discourse-ai/ai-bot/post/${post.id}/stop-streaming`, {
type: "POST",
});

document
.querySelector(`#post_${post.post_number}`)
.classList.remove("streaming");
} catch (e) {
popupAjaxError(e);
}
}

@action
cancelStreaming() {
this.constructor.cancelStreaming(this.args.post);
}

<template>
<DButton
class="post-action-menu__ai-cancel-streaming cancel-streaming"
...attributes
@action={{this.cancelStreaming}}
@icon="pause"
@title="discourse_ai.ai_bot.cancel_streaming"
/>
</template>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import DButton from "discourse/components/d-button";
import { isPostFromAiBot } from "../../lib/ai-bot-helper";
import DebugAiModal from "../modal/debug-ai-modal";

export default class AiDebugButton extends Component {
static shouldRender(args) {
return isPostFromAiBot(args.post, args.state.currentUser);
}

// TODO (glimmer-post-menu): Remove this static function and move the code into the button action after the widget code is removed
static debugAiResponse(post, modal) {
modal.show(DebugAiModal, { model: post });
}

@service modal;

@action
debugAiResponse() {
this.constructor.debugAiResponse(this.args.post, this.modal);
}

<template>
<DButton
class="post-action-menu__debug-ai"
...attributes
@action={{this.debugAiResponse}}
@icon="info"
@title="discourse_ai.ai_bot.debug_ai"
/>
</template>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import DButton from "discourse/components/d-button";
import { isPostFromAiBot } from "../../lib/ai-bot-helper";
import copyConversation from "../../lib/copy-conversation";
import ShareModal from "../modal/share-modal";

const AUTO_COPY_THRESHOLD = 4;

export default class AiDebugButton extends Component {
static shouldRender(args) {
return isPostFromAiBot(args.post, args.state.currentUser);
}

// TODO (glimmer-post-menu): Remove this static function and move the code into the button action after the widget code is removed
static async shareAiResponse(post, modal, showFeedback) {
if (post.post_number <= AUTO_COPY_THRESHOLD) {
await copyConversation(post.topic, 1, post.post_number);
showFeedback("discourse_ai.ai_bot.conversation_shared");
} else {
modal.show(ShareModal, { model: post });
}
}

@service modal;

@action
shareAiResponse() {
this.constructor.shareAiResponse(
this.args.post,
this.modal,
this.args.showFeedback
);
}

<template>
<DButton
class="post-action-menu__share-ai"
...attributes
@action={{this.shareAiResponse}}
@icon="far-copy"
@title="discourse_ai.ai_bot.share"
/>
</template>
}
11 changes: 11 additions & 0 deletions assets/javascripts/discourse/lib/ai-bot-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ import Composer from "discourse/models/composer";
import I18n from "I18n";
import ShareFullTopicModal from "../components/modal/share-full-topic-modal";

const MAX_PERSONA_USER_ID = -1200;

export function isPostFromAiBot(post, currentUser) {
return (
post.user_id <= MAX_PERSONA_USER_ID ||
!!currentUser?.ai_enabled_chat_bots?.any(
(bot) => post.username === bot.username
)
);
}

export function showShareConversationModal(modal, topicId) {
ajax(`/discourse-ai/ai-bot/shared-ai-conversations/preview/${topicId}.json`)
.then((payload) => {
Expand Down
162 changes: 99 additions & 63 deletions assets/javascripts/initializers/ai-bot-replies.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { hbs } from "ember-cli-htmlbars";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { withPluginApi } from "discourse/lib/plugin-api";
import { registerWidgetShim } from "discourse/widgets/render-glimmer";
import DebugAiModal from "../discourse/components/modal/debug-ai-modal";
import ShareModal from "../discourse/components/modal/share-modal";
import { streamPostText } from "../discourse/lib/ai-streamer/progress-handlers";
import copyConversation from "../discourse/lib/copy-conversation";
const AUTO_COPY_THRESHOLD = 4;
import { withSilencedDeprecations } from "discourse-common/lib/deprecated";
import AiBotHeaderIcon from "../discourse/components/ai-bot-header-icon";
import { showShareConversationModal } from "../discourse/lib/ai-bot-helper";
import AiCancelStreamingButton from "../discourse/components/post-menu/ai-cancel-streaming-button";
import AiDebugButton from "../discourse/components/post-menu/ai-debug-button";
import AiShareButton from "../discourse/components/post-menu/ai-share-button";
import {
isPostFromAiBot,
showShareConversationModal,
} from "../discourse/lib/ai-bot-helper";
import { streamPostText } from "../discourse/lib/ai-streamer/progress-handlers";

let enabledChatBotIds = [];
let allowDebug = false;

function isGPTBot(user) {
return user && enabledChatBotIds.includes(user.id);
}
Expand All @@ -22,29 +24,7 @@ function attachHeaderIcon(api) {
}

function initializeAIBotReplies(api) {
api.addPostMenuButton("cancel-gpt", (post) => {
if (isGPTBot(post.user)) {
return {
icon: "pause",
action: "cancelStreaming",
title: "discourse_ai.ai_bot.cancel_streaming",
className: "btn btn-default cancel-streaming",
position: "first",
};
}
});

api.attachWidgetAction("post", "cancelStreaming", function () {
ajax(`/discourse-ai/ai-bot/post/${this.model.id}/stop-streaming`, {
type: "POST",
})
.then(() => {
document
.querySelector(`#post_${this.model.post_number}`)
.classList.remove("streaming");
})
.catch(popupAjaxError);
});
initializePauseButton(api);

api.modifyClass("controller:topic", {
pluginId: "discourse-ai",
Expand Down Expand Up @@ -102,34 +82,82 @@ function initializePersonaDecorator(api) {
);
}

const MAX_PERSONA_USER_ID = -1200;
function initializePauseButton(api) {
const transformerRegistered = api.registerValueTransformer(
"post-menu-buttons",
({ value: dag, context: { post, firstButtonKey } }) => {
if (isGPTBot(post.user)) {
dag.add("ai-cancel-gpt", AiCancelStreamingButton, {
before: firstButtonKey,
after: ["ai-share", "ai-debug"],
});
}
}
);

const silencedKey =
transformerRegistered && "discourse.post-menu-widget-overrides";

withSilencedDeprecations(silencedKey, () => initializePauseWidgetButton(api));
}

function initializePauseWidgetButton(api) {
api.addPostMenuButton("cancel-gpt", (post) => {
if (isGPTBot(post.user)) {
return {
icon: "pause",
action: "cancelStreaming",
title: "discourse_ai.ai_bot.cancel_streaming",
className: "btn btn-default cancel-streaming",
position: "first",
};
}
});

api.attachWidgetAction("post", "cancelStreaming", function () {
AiCancelStreamingButton.cancelStreaming(this.model);
});
}

function initializeDebugButton(api) {
const currentUser = api.getCurrentUser();
if (!currentUser || !currentUser.ai_enabled_chat_bots || !allowDebug) {
return;
}

const transformerRegistered = api.registerValueTransformer(
"post-menu-buttons",
({ value: dag, context: { post, firstButtonKey } }) => {
if (post.topic?.archetype === "private_message") {
dag.add("ai-debug", AiDebugButton, {
before: firstButtonKey,
after: "ai-share",
});
}
}
);

const silencedKey =
transformerRegistered && "discourse.post-menu-widget-overrides";

withSilencedDeprecations(silencedKey, () => initializeDebugWidgetButton(api));
}

function initializeDebugWidgetButton(api) {
const currentUser = api.getCurrentUser();

let debugAiResponse = async function ({ post }) {
const modal = api.container.lookup("service:modal");

modal.show(DebugAiModal, { model: post });
AiDebugButton.debugAiResponse(post, modal);
};

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

if (
!currentUser.ai_enabled_chat_bots.any(
(bot) => post.username === bot.username
)
) {
// special handling for personas (persona bot users start at ID -1200 and go down)
if (post.user_id > MAX_PERSONA_USER_ID) {
return;
}
if (!isPostFromAiBot(post, currentUser)) {
return;
}

return {
Expand All @@ -148,14 +176,29 @@ function initializeShareButton(api) {
return;
}

let shareAiResponse = async function ({ post, showFeedback }) {
if (post.post_number <= AUTO_COPY_THRESHOLD) {
await copyConversation(post.topic, 1, post.post_number);
showFeedback("discourse_ai.ai_bot.conversation_shared");
} else {
const modal = api.container.lookup("service:modal");
modal.show(ShareModal, { model: post });
const transformerRegistered = api.registerValueTransformer(
"post-menu-buttons",
({ value: dag, context: { post, firstButtonKey } }) => {
if (post.topic?.archetype === "private_message") {
dag.add("ai-share", AiShareButton, {
before: firstButtonKey,
});
}
}
);

const silencedKey =
transformerRegistered && "discourse.post-menu-widget-overrides";

withSilencedDeprecations(silencedKey, () => initializeShareWidgetButton(api));
}

function initializeShareWidgetButton(api) {
const currentUser = api.getCurrentUser();

let shareAiResponse = async function ({ post, showFeedback }) {
const modal = api.container.lookup("service:modal");
AiShareButton.shareAiResponse(post, modal, showFeedback);
};

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

if (
!currentUser.ai_enabled_chat_bots.any(
(bot) => post.username === bot.username
)
) {
// special handling for personas (persona bot users start at ID -1200 and go down)
if (post.user_id > MAX_PERSONA_USER_ID) {
return;
}
if (!isPostFromAiBot(post, currentUser)) {
return;
}

return {
action: shareAiResponse,
icon: "far-copy",
className: "post-action-menu__share",
className: "post-action-menu__share-ai",
title: "discourse_ai.ai_bot.share",
position: "first",
};
Expand Down Expand Up @@ -218,10 +254,10 @@ export default {
enabledChatBotIds = user.ai_enabled_chat_bots.map((bot) => bot.id);
allowDebug = user.can_debug_ai_bot_conversations;
withPluginApi("1.6.0", attachHeaderIcon);
withPluginApi("1.6.0", initializeAIBotReplies);
withPluginApi("1.34.0", initializeAIBotReplies);
withPluginApi("1.6.0", initializePersonaDecorator);
withPluginApi("1.22.0", (api) => initializeDebugButton(api, container));
withPluginApi("1.22.0", (api) => initializeShareButton(api, container));
withPluginApi("1.34.0", (api) => initializeDebugButton(api, container));
withPluginApi("1.34.0", (api) => initializeShareButton(api, container));
withPluginApi("1.22.0", (api) =>
initializeShareTopicButton(api, container)
);
Expand Down
Loading