mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-06-27 01:52:18 +00:00
FEATURE: Allow for persona & llm selection in bot conversations page (#1276)
This commit is contained in:
parent
18dda31412
commit
b7b9179bc8
@ -0,0 +1,202 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { hash } from "@ember/helper";
|
||||
import { next } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import DropdownSelectBox from "select-kit/components/dropdown-select-box";
|
||||
|
||||
const PERSONA_SELECTOR_KEY = "ai_persona_selector_id";
|
||||
const LLM_SELECTOR_KEY = "ai_llm_selector_id";
|
||||
|
||||
export default class AiPersonaLlmSelector extends Component {
|
||||
@service currentUser;
|
||||
@service keyValueStore;
|
||||
|
||||
@tracked llm;
|
||||
@tracked allowLLMSelector = true;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
if (this.botOptions?.length) {
|
||||
this.#loadStoredPersona();
|
||||
this.#loadStoredLlm();
|
||||
|
||||
next(() => {
|
||||
this.resetTargetRecipients();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get composer() {
|
||||
return this.args?.outletArgs?.model;
|
||||
}
|
||||
|
||||
get hasLlmSelector() {
|
||||
return this.currentUser.ai_enabled_chat_bots.any((bot) => !bot.is_persona);
|
||||
}
|
||||
|
||||
get botOptions() {
|
||||
if (!this.currentUser.ai_enabled_personas) {
|
||||
return;
|
||||
}
|
||||
|
||||
let enabledPersonas = this.currentUser.ai_enabled_personas;
|
||||
|
||||
if (!this.hasLlmSelector) {
|
||||
enabledPersonas = enabledPersonas.filter((persona) => persona.username);
|
||||
}
|
||||
|
||||
return enabledPersonas.map((persona) => {
|
||||
return {
|
||||
id: persona.id,
|
||||
name: persona.name,
|
||||
description: persona.description,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
get filterable() {
|
||||
return this.botOptions.length > 8;
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
set value(newValue) {
|
||||
this._value = newValue;
|
||||
this.keyValueStore.setItem(PERSONA_SELECTOR_KEY, newValue);
|
||||
this.args.setPersonaId(newValue);
|
||||
this.setAllowLLMSelector();
|
||||
this.resetTargetRecipients();
|
||||
}
|
||||
|
||||
setAllowLLMSelector() {
|
||||
if (!this.hasLlmSelector) {
|
||||
this.allowLLMSelector = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const persona = this.currentUser.ai_enabled_personas.find(
|
||||
(innerPersona) => innerPersona.id === this._value
|
||||
);
|
||||
|
||||
this.allowLLMSelector = !persona?.force_default_llm;
|
||||
}
|
||||
|
||||
get currentLlm() {
|
||||
return this.llm;
|
||||
}
|
||||
|
||||
set currentLlm(newValue) {
|
||||
this.llm = newValue;
|
||||
this.keyValueStore.setItem(LLM_SELECTOR_KEY, newValue);
|
||||
|
||||
this.resetTargetRecipients();
|
||||
}
|
||||
|
||||
resetTargetRecipients() {
|
||||
if (this.allowLLMSelector) {
|
||||
const botUsername = this.currentUser.ai_enabled_chat_bots.find(
|
||||
(bot) => bot.id === this.llm
|
||||
).username;
|
||||
this.args.setTargetRecipient(botUsername);
|
||||
} else {
|
||||
const persona = this.currentUser.ai_enabled_personas.find(
|
||||
(innerPersona) => innerPersona.id === this._value
|
||||
);
|
||||
this.args.setTargetRecipient(persona.username || "");
|
||||
}
|
||||
}
|
||||
|
||||
get llmOptions() {
|
||||
const availableBots = this.currentUser.ai_enabled_chat_bots
|
||||
.filter((bot) => !bot.is_persona)
|
||||
.filter(Boolean);
|
||||
|
||||
return availableBots
|
||||
.map((bot) => {
|
||||
return {
|
||||
id: bot.id,
|
||||
name: bot.display_name,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
get showLLMSelector() {
|
||||
return this.allowLLMSelector && this.llmOptions.length > 1;
|
||||
}
|
||||
|
||||
#loadStoredPersona() {
|
||||
let personaId = this.keyValueStore.getItem(PERSONA_SELECTOR_KEY);
|
||||
|
||||
this._value = this.botOptions[0].id;
|
||||
if (personaId) {
|
||||
personaId = parseInt(personaId, 10);
|
||||
if (this.botOptions.any((bot) => bot.id === personaId)) {
|
||||
this._value = personaId;
|
||||
}
|
||||
}
|
||||
|
||||
this.args.setPersonaId(this._value);
|
||||
}
|
||||
|
||||
#loadStoredLlm() {
|
||||
this.setAllowLLMSelector();
|
||||
|
||||
if (this.hasLlmSelector) {
|
||||
let llm = this.keyValueStore.getItem(LLM_SELECTOR_KEY);
|
||||
|
||||
const llmOption =
|
||||
this.llmOptions.find((innerLlmOption) => innerLlmOption.id === llm) ||
|
||||
this.llmOptions[0];
|
||||
|
||||
if (llmOption) {
|
||||
llm = llmOption.id;
|
||||
} else {
|
||||
llm = "";
|
||||
}
|
||||
|
||||
if (llm) {
|
||||
next(() => {
|
||||
this.currentLlm = llm;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="persona-llm-selector">
|
||||
<div class="persona-llm-selector__selection-wrapper gpt-persona">
|
||||
{{#if @showLabels}}
|
||||
<label>{{i18n "discourse_ai.ai_bot.persona"}}</label>
|
||||
{{/if}}
|
||||
<DropdownSelectBox
|
||||
class="persona-llm-selector__persona-dropdown"
|
||||
@value={{this.value}}
|
||||
@content={{this.botOptions}}
|
||||
@options={{hash
|
||||
icon=(if @showLabels "angle-down" "robot")
|
||||
filterable=this.filterable
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{{#if this.showLLMSelector}}
|
||||
<div class="persona-llm-selector__selection-wrapper llm-selector">
|
||||
{{#if @showLabels}}
|
||||
<label>{{i18n "discourse_ai.ai_bot.llm"}}</label>
|
||||
{{/if}}
|
||||
<DropdownSelectBox
|
||||
class="persona-llm-selector__llm-dropdown"
|
||||
@value={{this.currentLlm}}
|
||||
@content={{this.llmOptions}}
|
||||
@options={{hash icon=(if @showLabels "angle-down" "globe")}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
@ -1,10 +1,7 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { hash } from "@ember/helper";
|
||||
import { next } from "@ember/runloop";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import KeyValueStore from "discourse/lib/key-value-store";
|
||||
import DropdownSelectBox from "select-kit/components/dropdown-select-box";
|
||||
import AiPersonaLlmSelector from "discourse/plugins/discourse-ai/discourse/components/ai-persona-llm-selector";
|
||||
|
||||
function isBotMessage(composer, currentUser) {
|
||||
if (
|
||||
@ -30,175 +27,21 @@ export default class BotSelector extends Component {
|
||||
}
|
||||
|
||||
@service currentUser;
|
||||
@service siteSettings;
|
||||
|
||||
@tracked llm;
|
||||
@tracked allowLLMSelector = true;
|
||||
|
||||
STORE_NAMESPACE = "discourse_ai_persona_selector_";
|
||||
LLM_STORE_NAMESPACE = "discourse_ai_llm_selector_";
|
||||
|
||||
preferredPersonaStore = new KeyValueStore(this.STORE_NAMESPACE);
|
||||
preferredLlmStore = new KeyValueStore(this.LLM_STORE_NAMESPACE);
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
if (this.botOptions && this.botOptions.length && this.composer) {
|
||||
let personaId = this.preferredPersonaStore.getObject("id");
|
||||
|
||||
this._value = this.botOptions[0].id;
|
||||
if (personaId) {
|
||||
personaId = parseInt(personaId, 10);
|
||||
if (this.botOptions.any((bot) => bot.id === personaId)) {
|
||||
this._value = personaId;
|
||||
}
|
||||
}
|
||||
|
||||
this.composer.metaData = { ai_persona_id: this._value };
|
||||
this.setAllowLLMSelector();
|
||||
|
||||
if (this.hasLlmSelector) {
|
||||
let llm = this.preferredLlmStore.getObject("id");
|
||||
|
||||
const llmOption =
|
||||
this.llmOptions.find((innerLlmOption) => innerLlmOption.id === llm) ||
|
||||
this.llmOptions[0];
|
||||
|
||||
if (llmOption) {
|
||||
llm = llmOption.id;
|
||||
} else {
|
||||
llm = "";
|
||||
}
|
||||
|
||||
if (llm) {
|
||||
next(() => {
|
||||
this.currentLlm = llm;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
next(() => {
|
||||
this.resetTargetRecipients();
|
||||
});
|
||||
}
|
||||
@action
|
||||
setPersonaIdOnComposer(id) {
|
||||
this.args.outletArgs.model.metaData = { ai_persona_id: id };
|
||||
}
|
||||
|
||||
get composer() {
|
||||
return this.args?.outletArgs?.model;
|
||||
}
|
||||
|
||||
get hasLlmSelector() {
|
||||
return this.currentUser.ai_enabled_chat_bots.any((bot) => !bot.is_persona);
|
||||
}
|
||||
|
||||
get botOptions() {
|
||||
if (this.currentUser.ai_enabled_personas) {
|
||||
let enabledPersonas = this.currentUser.ai_enabled_personas;
|
||||
|
||||
if (!this.hasLlmSelector) {
|
||||
enabledPersonas = enabledPersonas.filter((persona) => persona.username);
|
||||
}
|
||||
|
||||
return enabledPersonas.map((persona) => {
|
||||
return {
|
||||
id: persona.id,
|
||||
name: persona.name,
|
||||
description: persona.description,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get filterable() {
|
||||
return this.botOptions.length > 4;
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
set value(newValue) {
|
||||
this._value = newValue;
|
||||
this.preferredPersonaStore.setObject({ key: "id", value: newValue });
|
||||
this.composer.metaData = { ai_persona_id: newValue };
|
||||
this.setAllowLLMSelector();
|
||||
this.resetTargetRecipients();
|
||||
}
|
||||
|
||||
setAllowLLMSelector() {
|
||||
if (!this.hasLlmSelector) {
|
||||
this.allowLLMSelector = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const persona = this.currentUser.ai_enabled_personas.find(
|
||||
(innerPersona) => innerPersona.id === this._value
|
||||
);
|
||||
|
||||
this.allowLLMSelector = !persona?.force_default_llm;
|
||||
}
|
||||
|
||||
get currentLlm() {
|
||||
return this.llm;
|
||||
}
|
||||
|
||||
set currentLlm(newValue) {
|
||||
this.llm = newValue;
|
||||
this.preferredLlmStore.setObject({ key: "id", value: newValue });
|
||||
|
||||
this.resetTargetRecipients();
|
||||
}
|
||||
|
||||
resetTargetRecipients() {
|
||||
if (this.allowLLMSelector) {
|
||||
const botUsername = this.currentUser.ai_enabled_chat_bots.find(
|
||||
(bot) => bot.id === this.llm
|
||||
).username;
|
||||
this.composer.set("targetRecipients", botUsername);
|
||||
} else {
|
||||
const persona = this.currentUser.ai_enabled_personas.find(
|
||||
(innerPersona) => innerPersona.id === this._value
|
||||
);
|
||||
this.composer.set("targetRecipients", persona.username || "");
|
||||
}
|
||||
}
|
||||
|
||||
get llmOptions() {
|
||||
const availableBots = this.currentUser.ai_enabled_chat_bots
|
||||
.filter((bot) => !bot.is_persona)
|
||||
.filter(Boolean);
|
||||
|
||||
return availableBots
|
||||
.map((bot) => {
|
||||
return {
|
||||
id: bot.id,
|
||||
name: bot.display_name,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
@action
|
||||
setTargetRecipientsOnComposer(username) {
|
||||
this.args.outletArgs.model.set("targetRecipients", username);
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="persona-llm-selector">
|
||||
<div class="gpt-persona">
|
||||
<DropdownSelectBox
|
||||
class="persona-llm-selector__persona-dropdown"
|
||||
@value={{this.value}}
|
||||
@content={{this.botOptions}}
|
||||
@options={{hash icon="robot" filterable=this.filterable}}
|
||||
/>
|
||||
</div>
|
||||
{{#if this.allowLLMSelector}}
|
||||
<div class="llm-selector">
|
||||
<DropdownSelectBox
|
||||
class="persona-llm-selector__llm-dropdown"
|
||||
@value={{this.currentLlm}}
|
||||
@content={{this.llmOptions}}
|
||||
@options={{hash icon="globe"}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<AiPersonaLlmSelector
|
||||
@setPersonaId={{this.setPersonaIdOnComposer}}
|
||||
@setTargetRecipient={{this.setTargetRecipientsOnComposer}}
|
||||
/>
|
||||
</template>
|
||||
}
|
||||
|
@ -1,48 +1,29 @@
|
||||
import Controller from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { tracked } from "@ember-compat/tracked-built-ins";
|
||||
|
||||
export default class DiscourseAiBotConversations extends Controller {
|
||||
@service aiBotConversationsHiddenSubmit;
|
||||
@service currentUser;
|
||||
|
||||
@tracked selectedPersona = this.personaOptions[0].username;
|
||||
|
||||
textarea = null;
|
||||
|
||||
init() {
|
||||
super.init(...arguments);
|
||||
this.selectedPersonaChanged(this.selectedPersona);
|
||||
}
|
||||
|
||||
get personaOptions() {
|
||||
if (this.currentUser.ai_enabled_personas) {
|
||||
return this.currentUser.ai_enabled_personas
|
||||
.filter((persona) => persona.username)
|
||||
.map((persona) => {
|
||||
return {
|
||||
id: persona.id,
|
||||
username: persona.username,
|
||||
name: persona.name,
|
||||
description: persona.description,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get displayPersonaSelector() {
|
||||
return this.personaOptions.length > 1;
|
||||
}
|
||||
|
||||
get filterable() {
|
||||
return this.personaOptions.length > 4;
|
||||
get loading() {
|
||||
return this.aiBotConversationsHiddenSubmit?.loading;
|
||||
}
|
||||
|
||||
@action
|
||||
selectedPersonaChanged(username) {
|
||||
this.selectedPersona = username;
|
||||
this.aiBotConversationsHiddenSubmit.personaUsername = username;
|
||||
setPersonaId(id) {
|
||||
this.aiBotConversationsHiddenSubmit.personaId = id;
|
||||
}
|
||||
|
||||
@action
|
||||
setTargetRecipient(username) {
|
||||
this.aiBotConversationsHiddenSubmit.targetUsername = username;
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -1,16 +1,22 @@
|
||||
import { action } from "@ember/object";
|
||||
import { next } from "@ember/runloop";
|
||||
import Service, { service } from "@ember/service";
|
||||
import { tracked } from "@ember-compat/tracked-built-ins";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import { composeAiBotMessage } from "../lib/ai-bot-helper";
|
||||
|
||||
export default class AiBotConversationsHiddenSubmit extends Service {
|
||||
@service composer;
|
||||
@service aiConversationsSidebarManager;
|
||||
@service appEvents;
|
||||
@service composer;
|
||||
@service dialog;
|
||||
@service router;
|
||||
|
||||
personaUsername;
|
||||
@tracked loading = false;
|
||||
|
||||
personaId;
|
||||
targetUsername;
|
||||
|
||||
inputValue = "";
|
||||
|
||||
@ -25,9 +31,6 @@ export default class AiBotConversationsHiddenSubmit extends Service {
|
||||
|
||||
@action
|
||||
async submitToBot() {
|
||||
this.composer.destroyDraft();
|
||||
this.composer.close();
|
||||
|
||||
if (this.inputValue.length < 10) {
|
||||
return this.dialog.alert({
|
||||
message: i18n(
|
||||
@ -38,25 +41,31 @@ export default class AiBotConversationsHiddenSubmit extends Service {
|
||||
});
|
||||
}
|
||||
|
||||
// we are intentionally passing null as the targetBot to allow for the
|
||||
// function to select the first available bot. This will be refactored in the
|
||||
// future to allow for selecting a specific bot.
|
||||
await composeAiBotMessage(null, this.composer, {
|
||||
skipFocus: true,
|
||||
topicBody: this.inputValue,
|
||||
personaUsername: this.personaUsername,
|
||||
});
|
||||
this.loading = true;
|
||||
const title = i18n("discourse_ai.ai_bot.default_pm_prefix");
|
||||
|
||||
try {
|
||||
await this.composer.save();
|
||||
this.aiConversationsSidebarManager.newTopicForceSidebar = true;
|
||||
if (this.inputValue.length > 10) {
|
||||
// prevents submitting same message again when returning home
|
||||
// but avoids deleting too-short message on submit
|
||||
this.inputValue = "";
|
||||
}
|
||||
const response = await ajax("/posts.json", {
|
||||
method: "POST",
|
||||
data: {
|
||||
raw: this.inputValue,
|
||||
title,
|
||||
archetype: "private_message",
|
||||
target_recipients: this.targetUsername,
|
||||
meta_data: { ai_persona_id: this.personaId },
|
||||
},
|
||||
});
|
||||
|
||||
this.appEvents.trigger("discourse-ai:bot-pm-created", {
|
||||
id: response.topic_id,
|
||||
slug: response.topic_slug,
|
||||
title,
|
||||
});
|
||||
this.router.transitionTo(response.post_url);
|
||||
} catch (e) {
|
||||
popupAjaxError(e);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,50 +1,46 @@
|
||||
import { hash } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import DropdownSelectBox from "select-kit/components/dropdown-select-box";
|
||||
import AiPersonaLlmSelector from "discourse/plugins/discourse-ai/discourse/components/ai-persona-llm-selector";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
<div class="ai-bot-conversations">
|
||||
{{#if @controller.displayPersonaSelector}}
|
||||
<div class="ai-bot-conversations__persona-selector">
|
||||
<DropdownSelectBox
|
||||
class="persona-llm-selector__persona-dropdown"
|
||||
@value={{@controller.selectedPersona}}
|
||||
@valueProperty="username"
|
||||
@content={{@controller.personaOptions}}
|
||||
@options={{hash icon="angle-down"}}
|
||||
@onChange={{@controller.selectedPersonaChanged}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
<AiPersonaLlmSelector
|
||||
@showLabels={{true}}
|
||||
@setPersonaId={{@controller.setPersonaId}}
|
||||
@setTargetRecipient={{@controller.setTargetRecipient}}
|
||||
/>
|
||||
|
||||
<div class="ai-bot-conversations__content-wrapper">
|
||||
|
||||
<h1>{{i18n "discourse_ai.ai_bot.conversations.header"}}</h1>
|
||||
<div class="ai-bot-conversations__input-wrapper">
|
||||
<textarea
|
||||
{{didInsert @controller.setTextArea}}
|
||||
{{on "input" @controller.updateInputValue}}
|
||||
{{on "keydown" @controller.handleKeyDown}}
|
||||
id="ai-bot-conversations-input"
|
||||
placeholder={{i18n "discourse_ai.ai_bot.conversations.placeholder"}}
|
||||
minlength="10"
|
||||
rows="1"
|
||||
/>
|
||||
<DButton
|
||||
@action={{@controller.aiBotConversationsHiddenSubmit.submitToBot}}
|
||||
@icon="paper-plane"
|
||||
@title="discourse_ai.ai_bot.conversations.header"
|
||||
class="ai-bot-button btn-primary ai-conversation-submit"
|
||||
/>
|
||||
</div>
|
||||
<p class="ai-disclaimer">
|
||||
{{i18n "discourse_ai.ai_bot.conversations.disclaimer"}}
|
||||
</p>
|
||||
<ConditionalLoadingSpinner @condition={{@controller.loading}}>
|
||||
<h1>{{i18n "discourse_ai.ai_bot.conversations.header"}}</h1>
|
||||
<div class="ai-bot-conversations__input-wrapper">
|
||||
<textarea
|
||||
{{didInsert @controller.setTextArea}}
|
||||
{{on "input" @controller.updateInputValue}}
|
||||
{{on "keydown" @controller.handleKeyDown}}
|
||||
id="ai-bot-conversations-input"
|
||||
placeholder={{i18n
|
||||
"discourse_ai.ai_bot.conversations.placeholder"
|
||||
}}
|
||||
minlength="10"
|
||||
rows="1"
|
||||
/>
|
||||
<DButton
|
||||
@action={{@controller.aiBotConversationsHiddenSubmit.submitToBot}}
|
||||
@icon="paper-plane"
|
||||
@title="discourse_ai.ai_bot.conversations.header"
|
||||
class="ai-bot-button btn-primary ai-conversation-submit"
|
||||
/>
|
||||
</div>
|
||||
<p class="ai-disclaimer">
|
||||
{{i18n "discourse_ai.ai_bot.conversations.disclaimer"}}
|
||||
</p>
|
||||
</ConditionalLoadingSpinner>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -53,6 +53,10 @@ export default {
|
||||
this.topic = topic;
|
||||
}
|
||||
|
||||
get key() {
|
||||
return this.topic.id;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.topic.title;
|
||||
}
|
||||
@ -90,13 +94,21 @@ export default {
|
||||
super(...arguments);
|
||||
this.fetchMessages();
|
||||
|
||||
appEvents.on("topic:created", this, "addNewMessageToSidebar");
|
||||
appEvents.on(
|
||||
"discourse-ai:bot-pm-created",
|
||||
this,
|
||||
"addNewPMToSidebar"
|
||||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
willDestroy() {
|
||||
this.removeScrollListener();
|
||||
appEvents.on("topic:created", this, "addNewMessageToSidebar");
|
||||
appEvents.off(
|
||||
"discourse-ai:bot-pm-created",
|
||||
this,
|
||||
"addNewPMToSidebar"
|
||||
);
|
||||
}
|
||||
|
||||
get name() {
|
||||
@ -115,8 +127,8 @@ export default {
|
||||
);
|
||||
}
|
||||
|
||||
addNewMessageToSidebar(topic) {
|
||||
this.addNewMessage(topic);
|
||||
addNewPMToSidebar(topic) {
|
||||
this.links = [new AiConversationLink(topic), ...this.links];
|
||||
this.watchForTitleUpdate(topic);
|
||||
}
|
||||
|
||||
@ -201,28 +213,25 @@ export default {
|
||||
);
|
||||
}
|
||||
|
||||
addNewMessage(newTopic) {
|
||||
this.links = [new AiConversationLink(newTopic), ...this.links];
|
||||
}
|
||||
|
||||
watchForTitleUpdate(topic) {
|
||||
const channel = `/discourse-ai/ai-bot/topic/${topic.topic_id}`;
|
||||
const topicId = topic.topic_id;
|
||||
const channel = `/discourse-ai/ai-bot/topic/${topic.id}`;
|
||||
const callback = this.updateTopicTitle.bind(this);
|
||||
messageBus.subscribe(channel, ({ title }) => {
|
||||
callback(topicId, title);
|
||||
callback(topic, title);
|
||||
messageBus.unsubscribe(channel);
|
||||
});
|
||||
}
|
||||
|
||||
updateTopicTitle(topicId, title) {
|
||||
// update the topic title in the sidebar, instead of the default title
|
||||
const text = document.querySelector(
|
||||
`.sidebar-section-link-wrapper .ai-conversation-${topicId} .sidebar-section-link-content-text`
|
||||
updateTopicTitle(topic, title) {
|
||||
// update the data
|
||||
topic.title = title;
|
||||
|
||||
// force Glimmer to re-render that one link
|
||||
this.links = this.links.map((link) =>
|
||||
link.topic.id === topic.id
|
||||
? new AiConversationLink(topic)
|
||||
: link
|
||||
);
|
||||
if (text) {
|
||||
text.innerText = title;
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
@ -240,9 +249,10 @@ export default {
|
||||
if (
|
||||
topic?.archetype === "private_message" &&
|
||||
topic.user_id === currentUser.id &&
|
||||
topic.postStream.posts.some((post) =>
|
||||
isPostFromAiBot(post, currentUser)
|
||||
)
|
||||
(topic.is_bot_pm ||
|
||||
topic.postStream.posts.some((post) =>
|
||||
isPostFromAiBot(post, currentUser)
|
||||
))
|
||||
) {
|
||||
return aiConversationsSidebarManager.forceCustomSidebar();
|
||||
}
|
||||
|
@ -153,25 +153,40 @@ body.has-ai-conversations-sidebar {
|
||||
.ai-bot-conversations {
|
||||
height: calc(100dvh - var(--header-offset) - 1.25em);
|
||||
|
||||
&__persona-selector {
|
||||
.persona-llm-selector {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&__persona-selector .btn {
|
||||
background-color: transparent;
|
||||
font-weight: bold;
|
||||
}
|
||||
&__selection-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__persona-selector .btn:hover,
|
||||
&__persona-selector .btn:focus {
|
||||
background-color: transparent;
|
||||
color: var(--primary);
|
||||
}
|
||||
label {
|
||||
font-size: var(--font-down-1);
|
||||
font-weight: 300;
|
||||
margin-left: 1em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__persona-selector .btn:hover .d-icon,
|
||||
&__persona-selector .btn:focus .d-icon {
|
||||
color: var(--primary);
|
||||
.btn {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
background-color: transparent;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn:hover,
|
||||
.btn:focus {
|
||||
background-color: transparent;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.btn:hover .d-icon,
|
||||
.btn:focus .d-icon {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&__content-wrapper {
|
||||
@ -185,6 +200,10 @@ body.has-ai-conversations-sidebar {
|
||||
@include viewport.until(sm) {
|
||||
height: calc(75% - 1.25em - var(--header-offset) - 2em);
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
&__input-wrapper {
|
||||
|
@ -664,6 +664,8 @@ en:
|
||||
click_to_run_label: "Run Artifact"
|
||||
|
||||
ai_bot:
|
||||
persona: "Persona"
|
||||
llm: "Model"
|
||||
pm_warning: "AI chatbot messages are monitored regularly by moderators."
|
||||
cancel_streaming: "Stop reply"
|
||||
default_pm_prefix: "[Untitled AI bot PM]"
|
||||
|
@ -3,6 +3,7 @@
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
USER_AGENT = "Discourse AI Bot 1.0 (https://www.discourse.org)"
|
||||
TOPIC_AI_BOT_PM_FIELD = "is_ai_bot_pm"
|
||||
|
||||
class EntryPoint
|
||||
Bot = Struct.new(:id, :name, :llm)
|
||||
@ -64,6 +65,34 @@ module DiscourseAi
|
||||
end
|
||||
|
||||
def inject_into(plugin)
|
||||
plugin.register_topic_custom_field_type(TOPIC_AI_BOT_PM_FIELD, :string)
|
||||
|
||||
plugin.on(:topic_created) do |topic|
|
||||
next if !topic.private_message?
|
||||
creator = topic.user
|
||||
|
||||
# Only process if creator is not a bot or system user
|
||||
next if DiscourseAi::AiBot::Playground.is_bot_user_id?(creator.id)
|
||||
|
||||
# Get all bot user IDs defined by the discourse-ai plugin
|
||||
bot_ids = DiscourseAi::AiBot::EntryPoint.all_bot_ids
|
||||
|
||||
# Check if the only recipients are bots
|
||||
recipients = topic.topic_allowed_users.pluck(:user_id)
|
||||
|
||||
# Remove creator from recipients for checking
|
||||
recipients -= [creator.id]
|
||||
|
||||
# If all remaining recipients are AI bots and there's exactly one recipient
|
||||
if recipients.length == 1 && (recipients - bot_ids).empty?
|
||||
# The only recipient is an AI bot - add the custom field to the topic
|
||||
topic.custom_fields[TOPIC_AI_BOT_PM_FIELD] = true
|
||||
|
||||
# Save the custom fields
|
||||
topic.save_custom_fields
|
||||
end
|
||||
end
|
||||
|
||||
plugin.register_modifier(:chat_allowed_bot_user_ids) do |user_ids, guardian|
|
||||
if guardian.user
|
||||
allowed_chat =
|
||||
@ -102,6 +131,14 @@ module DiscourseAi
|
||||
Rails.root.join("plugins", "discourse-ai", "db", "fixtures", "ai_bot"),
|
||||
)
|
||||
|
||||
plugin.add_to_serializer(
|
||||
:topic_view,
|
||||
:is_bot_pm,
|
||||
include_condition: -> do
|
||||
object.personal_message && object.topic.custom_fields[TOPIC_AI_BOT_PM_FIELD]
|
||||
end,
|
||||
) { true }
|
||||
|
||||
plugin.add_to_serializer(
|
||||
:current_user,
|
||||
:ai_enabled_personas,
|
||||
|
@ -116,11 +116,7 @@ module DiscourseAi
|
||||
base_query
|
||||
.joins(:user)
|
||||
.joins("LEFT JOIN llm_models ON llm_models.name = language_model")
|
||||
.group(
|
||||
:user_id,
|
||||
"users.username",
|
||||
"users.uploaded_avatar_id",
|
||||
)
|
||||
.group(:user_id, "users.username", "users.uploaded_avatar_id")
|
||||
.order("usage_count DESC")
|
||||
.limit(USER_LIMIT)
|
||||
.select(
|
||||
@ -140,9 +136,7 @@ module DiscourseAi
|
||||
def feature_breakdown
|
||||
base_query
|
||||
.joins("LEFT JOIN llm_models ON llm_models.name = language_model")
|
||||
.group(
|
||||
:feature_name
|
||||
)
|
||||
.group(:feature_name)
|
||||
.order("usage_count DESC")
|
||||
.select(
|
||||
"case when coalesce(feature_name, '') = '' then '#{UNKNOWN_FEATURE}' else feature_name end as feature_name",
|
||||
|
@ -35,6 +35,20 @@ RSpec.describe DiscourseAi::AiBot::EntryPoint do
|
||||
expect(serializer[:current_user][:can_debug_ai_bot_conversations]).to eq(true)
|
||||
end
|
||||
|
||||
describe "adding TOPIC_AI_BOT_PM_FIELD to topic custom fields" do
|
||||
it "is added when user PMs a single bot" do
|
||||
topic = PostCreator.create!(admin, post_args).topic
|
||||
expect(topic.reload.custom_fields[DiscourseAi::AiBot::TOPIC_AI_BOT_PM_FIELD]).to eq("t")
|
||||
end
|
||||
|
||||
it "is not added when user PMs a bot and another user" do
|
||||
user = Fabricate(:user)
|
||||
post_args[:target_usernames] = [gpt_bot.username, user.username].join(",")
|
||||
topic = PostCreator.create!(admin, post_args).topic
|
||||
expect(topic.reload.custom_fields[DiscourseAi::AiBot::TOPIC_AI_BOT_PM_FIELD]).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
it "adds information about forcing default llm to current_user_serializer" do
|
||||
Group.refresh_automatic_groups!
|
||||
|
||||
|
@ -18,10 +18,20 @@ RSpec.describe "AI Bot - Homepage", type: :system do
|
||||
provider: "anthropic",
|
||||
url: "https://api.anthropic.com/v1/messages",
|
||||
name: "claude-2",
|
||||
display_name: "Claude 2",
|
||||
)
|
||||
end
|
||||
fab!(:claude_2_dup) do
|
||||
Fabricate(
|
||||
:llm_model,
|
||||
provider: "anthropic",
|
||||
url: "https://api.anthropic.com/v1/messages",
|
||||
name: "claude-2",
|
||||
display_name: "Duplicate",
|
||||
)
|
||||
end
|
||||
fab!(:bot_user) do
|
||||
toggle_enabled_bots(bots: [claude_2])
|
||||
toggle_enabled_bots(bots: [claude_2, claude_2_dup])
|
||||
SiteSetting.ai_bot_enabled = true
|
||||
claude_2.reload.user
|
||||
end
|
||||
@ -216,6 +226,18 @@ RSpec.describe "AI Bot - Homepage", type: :system do
|
||||
expect(ai_pm_homepage).to have_homepage
|
||||
expect(sidebar).to have_no_section_link(pm.title)
|
||||
end
|
||||
|
||||
it "Allows choosing persona and LLM" do
|
||||
ai_pm_homepage.visit
|
||||
|
||||
ai_pm_homepage.persona_selector.expand
|
||||
ai_pm_homepage.persona_selector.select_row_by_name(persona.name)
|
||||
ai_pm_homepage.persona_selector.collapse
|
||||
|
||||
ai_pm_homepage.llm_selector.expand
|
||||
ai_pm_homepage.llm_selector.select_row_by_name(claude_2_dup.display_name)
|
||||
ai_pm_homepage.llm_selector.collapse
|
||||
end
|
||||
end
|
||||
|
||||
context "when `ai_enable_experimental_bot_ux` is disabled" do
|
||||
|
@ -5,6 +5,10 @@ module PageObjects
|
||||
class AiPmHomepage < PageObjects::Components::Base
|
||||
HOMEPAGE_WRAPPER_CLASS = ".ai-bot-conversations__content-wrapper"
|
||||
|
||||
def visit
|
||||
page.visit("/discourse-ai/ai-bot/conversations")
|
||||
end
|
||||
|
||||
def input
|
||||
page.find("#ai-bot-conversations-input")
|
||||
end
|
||||
@ -27,6 +31,14 @@ module PageObjects
|
||||
def has_no_homepage?
|
||||
page.has_no_css?(HOMEPAGE_WRAPPER_CLASS)
|
||||
end
|
||||
|
||||
def persona_selector
|
||||
PageObjects::Components::SelectKit.new(".persona-llm-selector__persona-dropdown")
|
||||
end
|
||||
|
||||
def llm_selector
|
||||
PageObjects::Components::SelectKit.new(".persona-llm-selector__llm-dropdown")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
x
Reference in New Issue
Block a user