From 01f833f86eec820c52509b23b1d207cddd22ea63 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 17 Aug 2023 06:29:58 +1000 Subject: [PATCH] FEATURE: optional warning attached to all AI bot conversations (#137) * FEATURE: optional warning attached to all AI bot conversations This commit introduces `ai_bot_enable_chat_warning` which can be used to warn people prior to starting a chat with the bot. In particular this is useful if moderators are regularly reading chat transcripts as it sets expectations early. By default this is disabled. Also: - Stops making ajax call prior to opening composer - Hides PM title when starting a bot PM Co-authored-by: Rafael dos Santos Silva --- .../components/ai-bot-header-panel.js | 4 +-- .../composer-open.hbs | 8 +++++ .../composer-open.js | 32 +++++++++++++++++++ .../discourse/lib/ai-bot-helper.js | 13 ++++---- .../modules/ai-bot/common/bot-replies.scss | 19 +++++++++++ config/locales/client.en.yml | 1 + config/locales/server.en.yml | 1 + config/settings.yml | 3 ++ lib/modules/ai_bot/entry_point.rb | 27 ++++++++++++++++ 9 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 assets/javascripts/discourse/connectors/composer-after-composer-editor/composer-open.hbs create mode 100644 assets/javascripts/discourse/connectors/composer-after-composer-editor/composer-open.js diff --git a/assets/javascripts/discourse/components/ai-bot-header-panel.js b/assets/javascripts/discourse/components/ai-bot-header-panel.js index 90e7b638..892bfdad 100644 --- a/assets/javascripts/discourse/components/ai-bot-header-panel.js +++ b/assets/javascripts/discourse/components/ai-bot-header-panel.js @@ -10,7 +10,7 @@ export default class AiBotHeaderPanel extends Component { @service composer; @action - async composeMessageWithTargetBot(target) { + composeMessageWithTargetBot(target) { this.#composeAiBotMessage(target); } @@ -27,7 +27,7 @@ export default class AiBotHeaderPanel extends Component { return this.siteSettings.ai_bot_enabled_chat_bots.split("|"); } - async #composeAiBotMessage(targetBot) { + #composeAiBotMessage(targetBot) { this.args.closePanel(); composeAiBotMessage(targetBot, this.composer); } diff --git a/assets/javascripts/discourse/connectors/composer-after-composer-editor/composer-open.hbs b/assets/javascripts/discourse/connectors/composer-after-composer-editor/composer-open.hbs new file mode 100644 index 00000000..02110038 --- /dev/null +++ b/assets/javascripts/discourse/connectors/composer-after-composer-editor/composer-open.hbs @@ -0,0 +1,8 @@ +{{#if this.isAiBotChat}} + + {{#if this.renderChatWarning}} +
{{i18n + "discourse_ai.ai_bot.pm_warning" + }}
+ {{/if}} +{{/if}} \ No newline at end of file diff --git a/assets/javascripts/discourse/connectors/composer-after-composer-editor/composer-open.js b/assets/javascripts/discourse/connectors/composer-after-composer-editor/composer-open.js new file mode 100644 index 00000000..a31b7188 --- /dev/null +++ b/assets/javascripts/discourse/connectors/composer-after-composer-editor/composer-open.js @@ -0,0 +1,32 @@ +import Component from "@glimmer/component"; +import { inject as service } from "@ember/service"; +import { computed } from "@ember/object"; + +export default class extends Component { + static shouldRender() { + return true; + } + + @service currentUser; + @service siteSettings; + + get composerModel() { + return this.args.outletArgs.model; + } + + get renderChatWarning() { + return this.siteSettings.ai_bot_enable_chat_warning; + } + + @computed("composerModel.targetRecipients") + get isAiBotChat() { + if (this.composerModel.targetRecipients) { + let reciepients = this.composerModel.targetRecipients.split(","); + + return this.currentUser.ai_enabled_chat_bots.any((bot) => + reciepients.any((username) => username === bot.username) + ); + } + return false; + } +} diff --git a/assets/javascripts/discourse/lib/ai-bot-helper.js b/assets/javascripts/discourse/lib/ai-bot-helper.js index b90d03db..5ed0e19d 100644 --- a/assets/javascripts/discourse/lib/ai-bot-helper.js +++ b/assets/javascripts/discourse/lib/ai-bot-helper.js @@ -1,13 +1,12 @@ -import { ajax } from "discourse/lib/ajax"; import Composer from "discourse/models/composer"; import I18n from "I18n"; -export async function composeAiBotMessage(targetBot, composer) { - let botUsername = await ajax("/discourse-ai/ai-bot/bot-username", { - data: { username: targetBot }, - }).then((data) => { - return data.bot_username; - }); +export function composeAiBotMessage(targetBot, composer) { + const currentUser = composer.currentUser; + + let botUsername = currentUser.ai_enabled_chat_bots.find( + (bot) => bot.model_name === targetBot + ).username; composer.focusComposer({ fallbackToNewTopic: true, diff --git a/assets/stylesheets/modules/ai-bot/common/bot-replies.scss b/assets/stylesheets/modules/ai-bot/common/bot-replies.scss index fa0ce2e5..79e2a79b 100644 --- a/assets/stylesheets/modules/ai-bot/common/bot-replies.scss +++ b/assets/stylesheets/modules/ai-bot/common/bot-replies.scss @@ -2,6 +2,25 @@ nav.post-controls .actions button.cancel-streaming { display: none; } +.ai-bot-chat #reply-control { + .title-input { + display: none; + } +} + +.ai-bot-chat-warning { + color: var(--tertiary); + background-color: var(--tertiary-low); + box-shadow: 0px 0px 0px 2px var(--tertiary-medium); + opacity: 0.75; + .d-icon { + color: var(--tertiary); + } + margin: 10px 2px 0; + padding: 4px 10px; + width: fit-content; +} + article.streaming nav.post-controls .actions button.cancel-streaming { display: inline-block; } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index a051eb62..6aaecdc1 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -19,6 +19,7 @@ en: semantic_search: "Topics (Semantic)" ai_bot: + pm_warning: "AI chatbot messages are monitored regularly by moderators." cancel_streaming: "Stop reply" default_pm_prefix: "[Untitled AI bot PM]" shortcut_title: "Start a PM with an AI bot" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 6b9839c4..b64f9d0e 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -58,6 +58,7 @@ en: ai_summarization_discourse_service_api_key: "API key for the Discourse summarization API." ai_bot_enabled: "Enable the AI Bot module." + ai_bot_enable_chat_warning: "Display a warning when PM chat is initiated. Can be overriden by editing the translation string: discourse_ai.ai_bot.pm_warning" ai_bot_allowed_groups: "When the GPT Bot has access to the PM, it will reply to members of these groups." ai_bot_enabled_chat_bots: "Available models to act as an AI Bot" ai_bot_enabled_chat_commands: "Available GPT integrations used to provide external functionality to the model. Only works with GPT-4 and GPT-3.5" diff --git a/config/settings.yml b/config/settings.yml index 85a08ac9..a2dce5cb 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -179,6 +179,9 @@ plugins: ai_bot_enabled: default: false client: true + ai_bot_enable_chat_warning: + default: false + client: true ai_bot_allowed_groups: client: true type: group_list diff --git a/lib/modules/ai_bot/entry_point.rb b/lib/modules/ai_bot/entry_point.rb index 116e520e..f2dae10a 100644 --- a/lib/modules/ai_bot/entry_point.rb +++ b/lib/modules/ai_bot/entry_point.rb @@ -43,6 +43,33 @@ module DiscourseAi Rails.root.join("plugins", "discourse-ai", "db", "fixtures", "ai_bot"), ) + plugin.add_to_serializer( + :current_user, + :ai_enabled_chat_bots, + include_condition: -> do + SiteSetting.ai_bot_enabled && scope.authenticated? && + scope.user.in_any_groups?(SiteSetting.ai_bot_allowed_groups_map) + end, + ) do + model_map = {} + SiteSetting + .ai_bot_enabled_chat_bots + .split("|") + .each do |bot_name| + model_map[ + ::DiscourseAi::AiBot::EntryPoint.map_bot_model_to_user_id(bot_name) + ] = bot_name + end + + # not 100% ideal, cause it is one extra query, but we need it + bots = DB.query_hash(<<~SQL, user_ids: model_map.keys) + SELECT username, id FROM users WHERE id IN (:user_ids) + SQL + + bots.each { |hash| hash["model_name"] = model_map[hash["id"]] } + bots + end + plugin.register_svg_icon("robot") plugin.on(:post_created) do |post|