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.
This commit is contained in:
parent
739b314312
commit
deb34bb52f
|
@ -1,31 +0,0 @@
|
|||
{{#if this.singleBotEnabled}}
|
||||
<DButton
|
||||
@class="icon btn-flat"
|
||||
@action={{this.singleComposeAiBotMessage}}
|
||||
@icon="robot"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@class="icon btn-flat ai-bot-toggle-available-bots"
|
||||
@action={{this.toggleBotOptions}}
|
||||
@icon="robot"
|
||||
/>
|
||||
{{#if this.open}}
|
||||
<div class="ai-bot-available-bot-options">
|
||||
<div
|
||||
class="ai-bot-available-bot-options-wrapper"
|
||||
{{did-insert this.registerClickListener}}
|
||||
{{will-destroy this.unregisterClickListener}}
|
||||
>
|
||||
{{#each this.botNames as |bot|}}
|
||||
<DButton
|
||||
@class="btn-flat ai-bot-available-bot-content"
|
||||
@translatedTitle={{bot.humanized}}
|
||||
@translatedLabel={{bot.humanized}}
|
||||
@action={{action "composeMessageWithTargetBot" bot.modelName}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<div class="bot-panel ai-bot-available-bot-options">
|
||||
<div class="menu-panel drop-down">
|
||||
<div class="panel-body">
|
||||
<div class="panel-body-contents">
|
||||
<div class="sidebar-hamburger-dropdown">
|
||||
{{#each this.botNames as |bot|}}
|
||||
<DButton
|
||||
@class="btn-flat ai-bot-available-bot-content"
|
||||
@translatedTitle={{bot.humanized}}
|
||||
@translatedLabel={{bot.humanized}}
|
||||
@action={{action "composeMessageWithTargetBot" bot.modelName}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
|
@ -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`<AiBotHeaderIcon />`
|
||||
),
|
||||
];
|
||||
},
|
||||
});
|
|
@ -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`<AiBotHeaderPanel />`
|
||||
),
|
||||
];
|
||||
},
|
||||
|
||||
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");
|
||||
},
|
||||
});
|
|
@ -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 {};
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue