mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-06-29 19:12:15 +00:00
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);
|
||||||
|
}
|
||||||
|
}
|
27
assets/javascripts/discourse/lib/ai-bot-helper.js
Normal file
27
assets/javascripts/discourse/lib/ai-bot-helper.js
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
@ -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 { ajax } from "discourse/lib/ajax";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import loadScript from "discourse/lib/load-script";
|
import loadScript from "discourse/lib/load-script";
|
||||||
|
import { composeAiBotMessage } from "discourse/plugins/discourse-ai/discourse/lib/ai-bot-helper";
|
||||||
|
|
||||||
function isGPTBot(user) {
|
function isGPTBot(user) {
|
||||||
return user && [-110, -111, -112].includes(user.id);
|
return user && [-110, -111, -112].includes(user.id);
|
||||||
@ -11,8 +12,52 @@ function isGPTBot(user) {
|
|||||||
function attachHeaderIcon(api) {
|
function attachHeaderIcon(api) {
|
||||||
const settings = api.container.lookup("service:site-settings");
|
const settings = api.container.lookup("service:site-settings");
|
||||||
|
|
||||||
if (settings.ai_helper_add_ai_pm_to_header) {
|
const enabledBots = settings.ai_helper_add_ai_pm_to_header
|
||||||
api.addToHeaderIcons("ai-bot-header-icon");
|
? 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 {
|
.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 {
|
.ai-bot-available-bot-content {
|
||||||
color: var(--primary-high);
|
color: var(--primary-high);
|
||||||
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
.d-button-label {
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--primary-low);
|
background: var(--primary-low);
|
||||||
}
|
}
|
||||||
|
22
spec/system/ai_bot/ai_bot_helper_spec.rb
Normal file
22
spec/system/ai_bot/ai_bot_helper_spec.rb
Normal 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
|
Loading…
x
Reference in New Issue
Block a user