UX: Remove multi llm selector from header and move to composer (#619)

LLM selector control had no memory and was awkward to click.

Instead we now:

- Clearly display which llm you are talking to
- Allow you to change llm direct from composer
This commit is contained in:
Sam 2024-05-14 17:54:54 +10:00 committed by GitHub
parent 8eee6893d6
commit cb23ae614f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 74 additions and 78 deletions

View File

@ -4,9 +4,7 @@ import { service } from "@ember/service";
import { gt } from "truth-helpers"; import { gt } from "truth-helpers";
import DButton from "discourse/components/d-button"; import DButton from "discourse/components/d-button";
import i18n from "discourse-common/helpers/i18n"; import i18n from "discourse-common/helpers/i18n";
import DMenu from "float-kit/components/d-menu";
import { composeAiBotMessage } from "../lib/ai-bot-helper"; import { composeAiBotMessage } from "../lib/ai-bot-helper";
import AiBotHeaderPanel from "./ai-bot-header-panel";
export default class AiBotHeaderIcon extends Component { export default class AiBotHeaderIcon extends Component {
@service siteSettings; @service siteSettings;
@ -26,24 +24,12 @@ export default class AiBotHeaderIcon extends Component {
<template> <template>
{{#if (gt this.bots.length 0)}} {{#if (gt this.bots.length 0)}}
<li> <li>
{{#if (gt this.bots.length 1)}} <DButton
<DMenu @icon="robot"
@icon="robot" @title={{i18n "discourse_ai.ai_bot.shortcut_title"}}
@title={{i18n "discourse_ai.ai_bot.shortcut_title"}} class="ai-bot-button icon btn-flat"
class="ai-bot-button icon btn-flat" @action={{this.compose}}
> />
<:content as |args|>
<AiBotHeaderPanel @closePanel={{args.close}} />
</:content>
</DMenu>
{{else}}
<DButton
@icon="robot"
@title={{i18n "discourse_ai.ai_bot.shortcut_title"}}
class="ai-bot-button icon btn-flat"
@action={{this.compose}}
/>
{{/if}}
</li> </li>
{{/if}} {{/if}}
</template> </template>

View File

@ -1,10 +0,0 @@
<div class="bot-panel ai-bot-available-bot-options">
{{#each this.botNames as |bot|}}
<DButton
@translatedTitle={{bot.humanized}}
@translatedLabel={{bot.humanized}}
@action={{action "composeMessageWithTargetBot" bot.modelName}}
class="btn-flat ai-bot-available-bot-content"
/>
{{/each}}
</div>

View File

@ -1,33 +0,0 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import I18n from "I18n";
import { composeAiBotMessage } from "discourse/plugins/discourse-ai/discourse/lib/ai-bot-helper";
export default class AiBotHeaderPanel extends Component {
@service siteSettings;
@service composer;
@action
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("|");
}
#composeAiBotMessage(targetBot) {
this.args.closePanel();
composeAiBotMessage(targetBot, this.composer);
}
}

View File

@ -1,7 +1,10 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { hash } from "@ember/helper"; import { hash } from "@ember/helper";
import { next } from "@ember/runloop";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import KeyValueStore from "discourse/lib/key-value-store"; import KeyValueStore from "discourse/lib/key-value-store";
import I18n from "I18n";
import DropdownSelectBox from "select-kit/components/dropdown-select-box"; import DropdownSelectBox from "select-kit/components/dropdown-select-box";
function isBotMessage(composer, currentUser) { function isBotMessage(composer, currentUser) {
@ -28,9 +31,14 @@ export default class BotSelector extends Component {
} }
@service currentUser; @service currentUser;
@service siteSettings;
@tracked llm;
STORE_NAMESPACE = "discourse_ai_persona_selector_"; STORE_NAMESPACE = "discourse_ai_persona_selector_";
LLM_STORE_NAMESPACE = "discourse_ai_llm_selector_";
preferredPersonaStore = new KeyValueStore(this.STORE_NAMESPACE); preferredPersonaStore = new KeyValueStore(this.STORE_NAMESPACE);
preferredLlmStore = new KeyValueStore(this.LLM_STORE_NAMESPACE);
constructor() { constructor() {
super(...arguments); super(...arguments);
@ -47,6 +55,14 @@ export default class BotSelector extends Component {
} }
this.composer.metaData = { ai_persona_id: this._value }; this.composer.metaData = { ai_persona_id: this._value };
let llm = this.preferredLlmStore.getObject("id");
llm = llm || this.llmOptions[0].id;
if (llm) {
next(() => {
this.currentLlm = llm;
});
}
} }
} }
@ -80,14 +96,49 @@ export default class BotSelector extends Component {
this.composer.metaData = { ai_persona_id: newValue }; this.composer.metaData = { ai_persona_id: newValue };
} }
get currentLlm() {
return this.llm;
}
set currentLlm(newValue) {
this.llm = newValue;
const botUsername = this.currentUser.ai_enabled_chat_bots.find(
(bot) => bot.model_name === this.llm
).username;
this.preferredLlmStore.setObject({ key: "id", value: newValue });
this.composer.set("targetRecipients", botUsername);
}
get llmOptions() {
return this.siteSettings.ai_bot_enabled_chat_bots
.split("|")
.filter(Boolean)
.map((bot) => {
return {
id: bot,
name: I18n.t(`discourse_ai.ai_bot.bot_names.${bot}`),
};
});
}
<template> <template>
<div class="gpt-persona"> <div class="persona-llm-selector">
<DropdownSelectBox <div class="gpt-persona">
class="persona-selector__dropdown" <DropdownSelectBox
@value={{this.value}} class="persona-llm-selector__persona-dropdown"
@content={{this.botOptions}} @value={{this.value}}
@options={{hash icon="robot" filterable=this.filterable}} @content={{this.botOptions}}
/> @options={{hash icon="robot" filterable=this.filterable}}
/>
</div>
<div class="llm-selector">
<DropdownSelectBox
class="persona-llm-selector__llm-dropdown"
@value={{this.currentLlm}}
@content={{this.llmOptions}}
@options={{hash icon="globe"}}
/>
</div>
</div> </div>
</template> </template>
} }

View File

@ -9,12 +9,15 @@ nav.post-controls .actions button.cancel-streaming {
display: none; display: none;
} }
} }
.gpt-persona { .persona-llm-selector {
margin-bottom: 5px; display: flex;
margin-top: -10px; justify-content: space-between;
align-items: center;
.select-kit.single-select.dropdown-select-box ul.select-kit-collection { .select-kit.single-select.dropdown-select-box ul.select-kit-collection {
max-height: 200px; max-height: 200px;
} }
margin-bottom: 1em;
} }
} }

View File

@ -23,9 +23,6 @@ RSpec.describe "AI chat channel summarization", type: :system, js: true do
expect(page).to have_selector(".ai-bot-button") expect(page).to have_selector(".ai-bot-button")
find(".ai-bot-button").click 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 # composer is open
expect(page).to have_selector(".d-editor-container") expect(page).to have_selector(".d-editor-container")

View File

@ -13,7 +13,8 @@ RSpec.describe "AI personas", type: :system, js: true do
it "remembers the last selected persona" do it "remembers the last selected persona" do
visit "/" visit "/"
find(".d-header .ai-bot-button").click() find(".d-header .ai-bot-button").click()
persona_selector = PageObjects::Components::SelectKit.new(".persona-selector__dropdown") persona_selector =
PageObjects::Components::SelectKit.new(".persona-llm-selector__persona-dropdown")
id = DiscourseAi::AiBot::Personas::Persona.all(user: admin).first.id id = DiscourseAi::AiBot::Personas::Persona.all(user: admin).first.id
@ -24,7 +25,8 @@ RSpec.describe "AI personas", type: :system, js: true do
visit "/" visit "/"
find(".d-header .ai-bot-button").click() find(".d-header .ai-bot-button").click()
persona_selector = PageObjects::Components::SelectKit.new(".persona-selector__dropdown") persona_selector =
PageObjects::Components::SelectKit.new(".persona-llm-selector__persona-dropdown")
persona_selector.expand persona_selector.expand
expect(persona_selector).to have_selected_value(-2) expect(persona_selector).to have_selected_value(-2)
end end