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:
Sam 2023-05-18 16:10:08 +10:00 committed by GitHub
parent 739b314312
commit deb34bb52f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 184 additions and 178 deletions

View File

@ -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}}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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,
},
});
}

View File

@ -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 />`
),
];
},
});

View File

@ -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");
},
});

View File

@ -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 {};
}
);
}
}

View File

@ -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);
}

View File

@ -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