diff --git a/assets/javascripts/discourse/components/ai-persona-llm-selector.gjs b/assets/javascripts/discourse/components/ai-persona-llm-selector.gjs new file mode 100644 index 00000000..2fc16ed0 --- /dev/null +++ b/assets/javascripts/discourse/components/ai-persona-llm-selector.gjs @@ -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; + }); + } + } + } + + +} diff --git a/assets/javascripts/discourse/connectors/composer-fields/persona-llm-selector.gjs b/assets/javascripts/discourse/connectors/composer-fields/persona-llm-selector.gjs index ff58e024..3eaaaf71 100644 --- a/assets/javascripts/discourse/connectors/composer-fields/persona-llm-selector.gjs +++ b/assets/javascripts/discourse/connectors/composer-fields/persona-llm-selector.gjs @@ -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); } } diff --git a/assets/javascripts/discourse/controllers/discourse-ai-bot-conversations.js b/assets/javascripts/discourse/controllers/discourse-ai-bot-conversations.js index c23923a3..d5037fa6 100644 --- a/assets/javascripts/discourse/controllers/discourse-ai-bot-conversations.js +++ b/assets/javascripts/discourse/controllers/discourse-ai-bot-conversations.js @@ -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 diff --git a/assets/javascripts/discourse/services/ai-bot-conversations-hidden-submit.js b/assets/javascripts/discourse/services/ai-bot-conversations-hidden-submit.js index 7796007d..7ded2bfd 100644 --- a/assets/javascripts/discourse/services/ai-bot-conversations-hidden-submit.js +++ b/assets/javascripts/discourse/services/ai-bot-conversations-hidden-submit.js @@ -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; } } } diff --git a/assets/javascripts/discourse/templates/discourse-ai-bot-conversations.gjs b/assets/javascripts/discourse/templates/discourse-ai-bot-conversations.gjs index e9b4032d..b1fa6eb9 100644 --- a/assets/javascripts/discourse/templates/discourse-ai-bot-conversations.gjs +++ b/assets/javascripts/discourse/templates/discourse-ai-bot-conversations.gjs @@ -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(