import Component from "@glimmer/component"; import { cached, tracked } from "@glimmer/tracking"; import { fn } from "@ember/helper"; import { action } from "@ember/object"; import didInsert from "@ember/render-modifiers/modifiers/did-insert"; import { LinkTo } from "@ember/routing"; import { later } from "@ember/runloop"; import { service } from "@ember/service"; import { gt, or } from "truth-helpers"; import BackButton from "discourse/components/back-button"; import Form from "discourse/components/form"; import Avatar from "discourse/helpers/bound-avatar-template"; import { popupAjaxError } from "discourse/lib/ajax-error"; import Group from "discourse/models/group"; import { i18n } from "discourse-i18n"; import AdminUser from "admin/models/admin-user"; import GroupChooser from "select-kit/components/group-chooser"; import AiPersonaResponseFormatEditor from "../components/modal/ai-persona-response-format-editor"; import AiLlmSelector from "./ai-llm-selector"; import AiPersonaCollapsableExample from "./ai-persona-example"; import AiPersonaToolOptions from "./ai-persona-tool-options"; import AiToolSelector from "./ai-tool-selector"; import RagOptionsFk from "./rag-options-fk"; import RagUploader from "./rag-uploader"; export default class PersonaEditor extends Component { @service router; @service store; @service dialog; @service toasts; @service siteSettings; @tracked allGroups = []; @tracked isSaving = false; dirtyFormData = null; @cached get formData() { // This is to recover a dirty state after persisting a single form field. // It's meant to be consumed only once. if (this.dirtyFormData) { const data = this.dirtyFormData; this.dirtyFormData = null; return data; } else { const data = this.args.model.toPOJO(); if (data.tools) { data.toolOptions = this.mapToolOptions(data.toolOptions, data.tools); } return data; } } get chatPluginEnabled() { return this.siteSettings.chat_enabled; } get allTools() { return this.args.personas.resultSetMeta.tools; } get maxPixelValues() { const l = (key) => i18n(`discourse_ai.ai_persona.vision_max_pixel_sizes.${key}`); return [ { name: l("low"), id: 65536 }, { name: l("medium"), id: 262144 }, { name: l("high"), id: 1048576 }, ]; } get forcedToolStrategies() { const content = [ { id: -1, name: i18n("discourse_ai.ai_persona.tool_strategies.all"), }, ]; [1, 2, 5].forEach((i) => { content.push({ id: i, name: i18n("discourse_ai.ai_persona.tool_strategies.replies", { count: i, }), }); }); return content; } @action async updateAllGroups() { const groups = await Group.findAll({ include_everyone: true }); // Backwards-compatibility code. TODO(roman): Remove 01-09-2025 const hasEveryoneGroup = groups.find((g) => g.id === 0); if (!hasEveryoneGroup) { const everyoneGroupName = "everyone"; groups.push({ id: 0, name: everyoneGroupName }); } this.allGroups = groups; } @action async save(data) { const isNew = this.args.model.isNew; this.isSaving = true; try { const personaToSave = Object.assign( this.args.model, this.args.model.fromPOJO(data) ); await personaToSave.save(); this.#sortPersonas(); if (isNew && this.args.model.rag_uploads.length === 0) { this.args.personas.addObject(personaToSave); this.router.transitionTo( "adminPlugins.show.discourse-ai-personas.edit", personaToSave ); } else { this.toasts.success({ data: { message: i18n("discourse_ai.ai_persona.saved") }, duration: 2000, }); } } catch (e) { popupAjaxError(e); } finally { later(() => { this.isSaving = false; }, 1000); } } get adminUser() { // Work around user not being extensible. const userClone = Object.assign({}, this.args.model?.user); return AdminUser.create(userClone); } @action delete() { return this.dialog.confirm({ message: i18n("discourse_ai.ai_persona.confirm_delete"), didConfirm: () => { return this.args.model.destroyRecord().then(() => { this.args.personas.removeObject(this.args.model); this.router.transitionTo( "adminPlugins.show.discourse-ai-personas.index" ); }); }, }); } @action async toggleEnabled(dirtyData, value, { set }) { set("enabled", value); await this.persistField(dirtyData, "enabled", value); } @action async togglePriority(dirtyData, value, { set }) { set("priority", value); await this.persistField(dirtyData, "priority", value, true); } @action async createUser(form) { try { let user = await this.args.model.createUser(); form.set("user", user); form.set("user_id", user.id); } catch (e) { popupAjaxError(e); } } @action updateUploads(form, newUploads) { form.set("rag_uploads", newUploads); } @action async removeUpload(form, dirtyData, currentUploads, upload) { const updatedUploads = currentUploads.filter( (file) => file.id !== upload.id ); form.set("rag_uploads", updatedUploads); if (!this.args.model.isNew) { await this.persistField(dirtyData, "rag_uploads", updatedUploads); } } @action updateToolNames(form, currentData, updatedTools) { const removedTools = currentData?.tools?.filter((ct) => !updatedTools.includes(ct)) || []; const updatedOptions = this.mapToolOptions( currentData.toolOptions, updatedTools ); form.setProperties({ tools: updatedTools, toolOptions: updatedOptions, }); if (currentData.forcedTools?.length > 0) { const updatedForcedTools = currentData.forcedTools.filter( (fct) => !removedTools.includes(fct) ); form.set("forcedTools", updatedForcedTools); } } @action availableForcedTools(tools) { return this.allTools.filter((tool) => tools.includes(tool.id)); } @action addExamplesPair(form, data) { const newExamples = [...data.examples, ["", ""]]; form.set("examples", newExamples); } mapToolOptions(currentOptions, toolNames) { const updatedOptions = Object.assign({}, currentOptions); toolNames.forEach((toolId) => { const tool = this.allTools.findBy("id", toolId); const toolOptions = tool?.options; if (!toolOptions || updatedOptions[toolId]) { return; } const mappedOptions = {}; Object.keys(toolOptions).forEach((key) => { mappedOptions[key] = null; }); updatedOptions[toolId] = mappedOptions; }); return updatedOptions; } async persistField(dirtyData, field, newValue, sortPersonas) { if (!this.args.model.isNew) { const updatedDirtyData = Object.assign({}, dirtyData); updatedDirtyData[field] = newValue; try { const args = {}; args[field] = newValue; this.dirtyFormData = updatedDirtyData; await this.args.model.update(args); if (sortPersonas) { this.#sortPersonas(); } } catch (e) { popupAjaxError(e); } } } #sortPersonas() { const sorted = this.args.personas.toArray().sort((a, b) => { if (a.priority && !b.priority) { return -1; } else if (!a.priority && b.priority) { return 1; } else { return a.name.localeCompare(b.name); } }); this.args.personas.clear(); this.args.personas.setObjects(sorted); } }