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