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

231 lines
6.7 KiB
JavaScript
Raw Normal View History

2023-11-03 07:30:09 -04:00
import { hbs } from "ember-cli-htmlbars";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
2023-11-03 07:30:09 -04:00
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 streamText from "../discourse/lib/ai-streamer";
import copyConversation from "../discourse/lib/copy-conversation";
const AUTO_COPY_THRESHOLD = 4;
import AiBotHeaderIcon from "../discourse/components/ai-bot-header-icon";
import { showShareConversationModal } from "../discourse/lib/ai-bot-helper";
let enabledChatBotIds = [];
let allowDebug = false;
function isGPTBot(user) {
return user && enabledChatBotIds.includes(user.id);
}
function attachHeaderIcon(api) {
api.headerIcons.add("ai", AiBotHeaderIcon);
}
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);
});
api.modifyClass("controller:topic", {
pluginId: "discourse-ai",
onAIBotStreamedReply: function (data) {
streamText(this.model.postStream, data);
},
subscribe: function () {
this._super();
if (
this.model.isPrivateMessage &&
this.model.details.allowed_users &&
this.model.details.allowed_users.filter(isGPTBot).length >= 1
) {
// we attempt to recover the last message in the bus
// so we subscribe at -2
this.messageBus.subscribe(
`discourse-ai/ai-bot/topic/${this.model.id}`,
this.onAIBotStreamedReply.bind(this),
-2
);
}
},
unsubscribe: function () {
this.messageBus.unsubscribe("discourse-ai/ai-bot/topic/*");
this._super();
},
});
}
function initializePersonaDecorator(api) {
let topicController = null;
api.decorateWidget(`poster-name:after`, (dec) => {
if (!isGPTBot(dec.attrs.user)) {
return;
}
// this is hacky and will need to change
// trouble is we need to get the model for the topic
// and it is not available in the decorator
// long term this will not be a problem once we remove widgets and
// have a saner structure for our model
topicController =
topicController || api.container.lookup("controller:topic");
return dec.widget.attach("persona-flair", {
topicController,
});
});
registerWidgetShim(
"persona-flair",
"span.persona-flair",
hbs`{{@data.topicController.model.ai_persona_name}}`
);
}
const MAX_PERSONA_USER_ID = -1200;
function initializeDebugButton(api) {
const currentUser = api.getCurrentUser();
if (!currentUser || !currentUser.ai_enabled_chat_bots || !allowDebug) {
return;
}
let debugAiResponse = async function ({ post }) {
const modal = api.container.lookup("service:modal");
modal.show(DebugAiModal, { model: post });
};
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;
}
}
return {
action: debugAiResponse,
icon: "info",
className: "post-action-menu__debug-ai",
title: "discourse_ai.ai_bot.debug_ai",
position: "first",
};
});
}
function initializeShareButton(api) {
const currentUser = api.getCurrentUser();
if (!currentUser || !currentUser.ai_enabled_chat_bots) {
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 });
}
};
api.addPostMenuButton("share", (post) => {
// for backwards compat so we don't break if topic is undefined
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;
}
}
return {
action: shareAiResponse,
icon: "far-copy",
className: "post-action-menu__share",
title: "discourse_ai.ai_bot.share",
position: "first",
};
});
}
function initializeShareTopicButton(api) {
const modal = api.container.lookup("service:modal");
const currentUser = api.container.lookup("current-user:main");
api.registerTopicFooterButton({
id: "share-ai-conversation",
icon: "share-alt",
label: "discourse_ai.ai_bot.share_ai_conversation.name",
title: "discourse_ai.ai_bot.share_ai_conversation.title",
action() {
showShareConversationModal(modal, this.topic.id);
},
classNames: ["share-ai-conversation-button"],
dependentKeys: ["topic.ai_persona_name"],
displayed() {
return (
currentUser?.can_share_ai_bot_conversations &&
this.topic.ai_persona_name
);
},
});
}
export default {
name: "discourse-ai-bot-replies",
initialize(container) {
const user = container.lookup("service:current-user");
if (user?.ai_enabled_chat_bots) {
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.6.0", initializePersonaDecorator);
withPluginApi("1.22.0", (api) => initializeDebugButton(api, container));
withPluginApi("1.22.0", (api) => initializeShareButton(api, container));
withPluginApi("1.22.0", (api) =>
initializeShareTopicButton(api, container)
);
}
},
};