From deb34bb52f1e32e50aadefca813b1d3b26f283e2 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 18 May 2023 16:10:08 +1000 Subject: [PATCH] UX: improve drop down menu for enabled bots (#68) Previously we were not using using HeaderPanel for drop down, which caused it not to properly act like a header panel. - Not styled right - Not hidden when other buttons clicked Etc... Header is sadly full of legacy so this is somewhat hacky weaving widgets. --- .../components/ai-bot-header-icon.hbs | 31 ----- .../components/ai-bot-header-icon.js | 109 ------------------ .../components/ai-bot-header-panel.hbs | 18 +++ .../components/ai-bot-header-panel.js | 34 ++++++ .../discourse/lib/ai-bot-helper.js | 27 +++++ .../discourse/widgets/ai-bot-header-icon.js | 28 ----- .../widgets/ai-bot-header-panel-wrapper.js | 31 +++++ .../initializers/ai-bot-replies.js | 49 +++++++- .../modules/ai-bot/common/bot-replies.scss | 13 +-- spec/system/ai_bot/ai_bot_helper_spec.rb | 22 ++++ 10 files changed, 184 insertions(+), 178 deletions(-) delete mode 100644 assets/javascripts/discourse/components/ai-bot-header-icon.hbs delete mode 100644 assets/javascripts/discourse/components/ai-bot-header-icon.js create mode 100644 assets/javascripts/discourse/components/ai-bot-header-panel.hbs create mode 100644 assets/javascripts/discourse/components/ai-bot-header-panel.js create mode 100644 assets/javascripts/discourse/lib/ai-bot-helper.js delete mode 100644 assets/javascripts/discourse/widgets/ai-bot-header-icon.js create mode 100644 assets/javascripts/discourse/widgets/ai-bot-header-panel-wrapper.js create mode 100644 spec/system/ai_bot/ai_bot_helper_spec.rb diff --git a/assets/javascripts/discourse/components/ai-bot-header-icon.hbs b/assets/javascripts/discourse/components/ai-bot-header-icon.hbs deleted file mode 100644 index 893d2b7b..00000000 --- a/assets/javascripts/discourse/components/ai-bot-header-icon.hbs +++ /dev/null @@ -1,31 +0,0 @@ -{{#if this.singleBotEnabled}} - -{{else}} - - {{#if this.open}} -
-
- {{#each this.botNames as |bot|}} - - {{/each}} -
-
- {{/if}} -{{/if}} \ No newline at end of file diff --git a/assets/javascripts/discourse/components/ai-bot-header-icon.js b/assets/javascripts/discourse/components/ai-bot-header-icon.js deleted file mode 100644 index 1c413a59..00000000 --- a/assets/javascripts/discourse/components/ai-bot-header-icon.js +++ /dev/null @@ -1,109 +0,0 @@ -import { action } from "@ember/object"; -import { inject as service } from "@ember/service"; -import { ajax } from "discourse/lib/ajax"; -import Component from "@ember/component"; -import Composer from "discourse/models/composer"; -import { tracked } from "@glimmer/tracking"; -import { bind } from "discourse-common/utils/decorators"; -import I18n from "I18n"; - -export default class AiBotHeaderIcon extends Component { - @service siteSettings; - @service composer; - - @tracked open = false; - - @action - async toggleBotOptions() { - this.open = !this.open; - } - - @action - async composeMessageWithTargetBot(target) { - this._composeAiBotMessage(target); - } - - @action - async singleComposeAiBotMessage() { - this._composeAiBotMessage( - this.siteSettings.ai_bot_enabled_chat_bots.split("|")[0] - ); - } - - @action - registerClickListener() { - this.#addClickEventListener(); - } - - @action - unregisterClickListener() { - this.#removeClickEventListener(); - } - - @bind - closeDetails(event) { - if (this.open) { - const isLinkClick = Array.from(event.target.classList).includes( - "ai-bot-toggle-available-bots" - ); - - if (isLinkClick || this.#isOutsideDetailsClick(event)) { - this.open = false; - } - } - } - - #isOutsideDetailsClick(event) { - return !event.composedPath().some((element) => { - return element.className === "ai-bot-available-bot-content"; - }); - } - - #removeClickEventListener() { - document.removeEventListener("click", this.closeDetails); - } - - #addClickEventListener() { - document.addEventListener("click", this.closeDetails); - } - - get botNames() { - return this.enabledBotOptions.map((bot) => { - return { - humanized: I18n.t(`discourse_ai.ai_bot.bot_names.${bot}`), - modelName: bot, - }; - }); - } - - get enabledBotOptions() { - return this.siteSettings.ai_bot_enabled_chat_bots.split("|"); - } - - get singleBotEnabled() { - return this.enabledBotOptions.length === 1; - } - - async _composeAiBotMessage(targetBot) { - let botUsername = await ajax("/discourse-ai/ai-bot/bot-username", { - data: { username: targetBot }, - }).then((data) => { - return data.bot_username; - }); - - this.composer.focusComposer({ - fallbackToNewTopic: true, - openOpts: { - action: Composer.PRIVATE_MESSAGE, - recipients: botUsername, - topicTitle: I18n.t("discourse_ai.ai_bot.default_pm_prefix"), - archetypeId: "private_message", - draftKey: Composer.NEW_PRIVATE_MESSAGE_KEY, - hasGroups: false, - warningsDisabled: true, - }, - }); - - this.open = false; - } -} diff --git a/assets/javascripts/discourse/components/ai-bot-header-panel.hbs b/assets/javascripts/discourse/components/ai-bot-header-panel.hbs new file mode 100644 index 00000000..b01196e3 --- /dev/null +++ b/assets/javascripts/discourse/components/ai-bot-header-panel.hbs @@ -0,0 +1,18 @@ +
+ +
\ No newline at end of file diff --git a/assets/javascripts/discourse/components/ai-bot-header-panel.js b/assets/javascripts/discourse/components/ai-bot-header-panel.js new file mode 100644 index 00000000..b12341c6 --- /dev/null +++ b/assets/javascripts/discourse/components/ai-bot-header-panel.js @@ -0,0 +1,34 @@ +import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; +import Component from "@glimmer/component"; +import { composeAiBotMessage } from "discourse/plugins/discourse-ai/discourse/lib/ai-bot-helper"; + +import I18n from "I18n"; + +export default class AiBotHeaderPanel extends Component { + @service siteSettings; + @service composer; + @service appEvents; + + @action + async composeMessageWithTargetBot(target) { + this.#composeAiBotMessage(target); + } + + get botNames() { + return this.enabledBotOptions.map((bot) => { + return { + humanized: I18n.t(`discourse_ai.ai_bot.bot_names.${bot}`), + modelName: bot, + }; + }); + } + + get enabledBotOptions() { + return this.siteSettings.ai_bot_enabled_chat_bots.split("|"); + } + + async #composeAiBotMessage(targetBot) { + composeAiBotMessage(targetBot, this.composer, this.appEvents); + } +} diff --git a/assets/javascripts/discourse/lib/ai-bot-helper.js b/assets/javascripts/discourse/lib/ai-bot-helper.js new file mode 100644 index 00000000..5ea0dc2d --- /dev/null +++ b/assets/javascripts/discourse/lib/ai-bot-helper.js @@ -0,0 +1,27 @@ +import { ajax } from "discourse/lib/ajax"; +import Composer from "discourse/models/composer"; +import I18n from "I18n"; + +export async function composeAiBotMessage(targetBot, composer, appEvents) { + if (appEvents) { + appEvents.trigger("ai-bot-menu:close"); + } + let botUsername = await ajax("/discourse-ai/ai-bot/bot-username", { + data: { username: targetBot }, + }).then((data) => { + return data.bot_username; + }); + + composer.focusComposer({ + fallbackToNewTopic: true, + openOpts: { + action: Composer.PRIVATE_MESSAGE, + recipients: botUsername, + topicTitle: I18n.t("discourse_ai.ai_bot.default_pm_prefix"), + archetypeId: "private_message", + draftKey: Composer.NEW_PRIVATE_MESSAGE_KEY, + hasGroups: false, + warningsDisabled: true, + }, + }); +} diff --git a/assets/javascripts/discourse/widgets/ai-bot-header-icon.js b/assets/javascripts/discourse/widgets/ai-bot-header-icon.js deleted file mode 100644 index 3a7e19c3..00000000 --- a/assets/javascripts/discourse/widgets/ai-bot-header-icon.js +++ /dev/null @@ -1,28 +0,0 @@ -import { createWidget } from "discourse/widgets/widget"; -import RenderGlimmer from "discourse/widgets/render-glimmer"; -import { hbs } from "ember-cli-htmlbars"; - -export default createWidget("ai-bot-header-icon", { - tagName: "li.header-dropdown-toggle.ai-bot-header-icon", - title: "discourse_ai.ai_bot.shortcut_title", - - services: ["siteSettings"], - - html() { - const enabledBots = this.siteSettings.ai_bot_enabled_chat_bots - .split("|") - .filter(Boolean); - - if (!enabledBots || enabledBots.length === 0) { - return; - } - - return [ - new RenderGlimmer( - this, - "div.widget-component-connector", - hbs`` - ), - ]; - }, -}); diff --git a/assets/javascripts/discourse/widgets/ai-bot-header-panel-wrapper.js b/assets/javascripts/discourse/widgets/ai-bot-header-panel-wrapper.js new file mode 100644 index 00000000..4f4f2f0d --- /dev/null +++ b/assets/javascripts/discourse/widgets/ai-bot-header-panel-wrapper.js @@ -0,0 +1,31 @@ +import { createWidget } from "discourse/widgets/widget"; +import RenderGlimmer from "discourse/widgets/render-glimmer"; +import { hbs } from "ember-cli-htmlbars"; + +export default createWidget("ai-bot-header-panel-wrapper", { + buildAttributes() { + return { "data-click-outside": true }; + }, + + html() { + return [ + new RenderGlimmer( + this, + "div.widget-component-connector", + hbs`` + ), + ]; + }, + + init() { + this.appEvents.on("ai-bot-menu:close", this, this.clickOutside); + }, + + destroy() { + this.appEvents.off("ai-bot-menu:close", this, this.clickOutside); + }, + + clickOutside() { + this.sendWidgetAction("hideAiBotPanel"); + }, +}); diff --git a/assets/javascripts/initializers/ai-bot-replies.js b/assets/javascripts/initializers/ai-bot-replies.js index 531eeaaf..a8e8e051 100644 --- a/assets/javascripts/initializers/ai-bot-replies.js +++ b/assets/javascripts/initializers/ai-bot-replies.js @@ -3,6 +3,7 @@ import { cookAsync } from "discourse/lib/text"; import { ajax } from "discourse/lib/ajax"; import { popupAjaxError } from "discourse/lib/ajax-error"; import loadScript from "discourse/lib/load-script"; +import { composeAiBotMessage } from "discourse/plugins/discourse-ai/discourse/lib/ai-bot-helper"; function isGPTBot(user) { return user && [-110, -111, -112].includes(user.id); @@ -11,8 +12,52 @@ function isGPTBot(user) { function attachHeaderIcon(api) { const settings = api.container.lookup("service:site-settings"); - if (settings.ai_helper_add_ai_pm_to_header) { - api.addToHeaderIcons("ai-bot-header-icon"); + const enabledBots = settings.ai_helper_add_ai_pm_to_header + ? settings.ai_bot_enabled_chat_bots.split("|").filter(Boolean) + : []; + if (enabledBots.length > 0) { + api.attachWidgetAction("header", "showAiBotPanel", function () { + this.state.botSelectorVisible = true; + }); + + api.attachWidgetAction("header", "hideAiBotPanel", function () { + this.state.botSelectorVisible = false; + }); + + api.attachWidgetAction("header", "toggleAiBotPanel", function () { + this.state.botSelectorVisible = !this.state.botSelectorVisible; + }); + + api.decorateWidget("header-icons:before", (helper) => { + return helper.attach("header-dropdown", { + title: "blog.start_gpt_chat", + icon: "robot", + action: "clickStartAiBotChat", + active: false, + classNames: ["ai-bot-button"], + }); + }); + + if (enabledBots.length === 1) { + api.attachWidgetAction("header", "clickStartAiBotChat", function () { + composeAiBotMessage( + enabledBots[0], + api.container.lookup("service:composer") + ); + }); + } else { + api.attachWidgetAction("header", "clickStartAiBotChat", function () { + this.sendWidgetAction("showAiBotPanel"); + }); + } + + api.addHeaderPanel( + "ai-bot-header-panel-wrapper", + "botSelectorVisible", + function () { + return {}; + } + ); } } diff --git a/assets/stylesheets/modules/ai-bot/common/bot-replies.scss b/assets/stylesheets/modules/ai-bot/common/bot-replies.scss index 8585a2cb..fa0ce2e5 100644 --- a/assets/stylesheets/modules/ai-bot/common/bot-replies.scss +++ b/assets/stylesheets/modules/ai-bot/common/bot-replies.scss @@ -7,17 +7,14 @@ article.streaming nav.post-controls .actions button.cancel-streaming { } .ai-bot-available-bot-options { - position: absolute; - top: 100%; - z-index: z("modal", "content") + 1; - transition: background-color 0.25s; - background-color: var(--secondary); - min-width: 150px; - .ai-bot-available-bot-content { color: var(--primary-high); + display: flex; width: 100%; - + .d-button-label { + flex: 1; + text-align: left; + } &:hover { background: var(--primary-low); } diff --git a/spec/system/ai_bot/ai_bot_helper_spec.rb b/spec/system/ai_bot/ai_bot_helper_spec.rb new file mode 100644 index 00000000..80c9948b --- /dev/null +++ b/spec/system/ai_bot/ai_bot_helper_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +RSpec.describe "AI chat channel summarization", type: :system, js: true do + fab!(:user) { Fabricate(:admin) } + + before do + sign_in(user) + SiteSetting.ai_bot_enabled = true + SiteSetting.ai_bot_enabled_chat_bots = "gpt-4|gpt-3.5-turbo" + end + + it "shows the AI bot button, which is clickable" do + visit "/latest" + expect(page).to have_selector(".ai-bot-button") + find(".ai-bot-button").click + + expect(page).to have_selector(".ai-bot-available-bot-content") + find("button.ai-bot-available-bot-content:first-child").click + + # composer is open + expect(page).to have_selector(".d-editor-container") + end +end