mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-02-07 20:18:16 +00:00
This is a rather huge refactor with 1 new feature (tool details can be suppressed) Previously we use the name "Command" to describe "Tools", this unifies all the internal language and simplifies the code. We also amended the persona UI to use less DToggles which aligns with our design guidelines. Co-authored-by: Martin Brennan <martin@discourse.org>
594 lines
18 KiB
Plaintext
594 lines
18 KiB
Plaintext
import Component from "@glimmer/component";
|
|
import { cached, tracked } from "@glimmer/tracking";
|
|
import { Input } from "@ember/component";
|
|
import { on } from "@ember/modifier";
|
|
import { action } from "@ember/object";
|
|
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
|
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
|
import { LinkTo } from "@ember/routing";
|
|
import { later } from "@ember/runloop";
|
|
import { inject as service } from "@ember/service";
|
|
import BackButton from "discourse/components/back-button";
|
|
import DButton from "discourse/components/d-button";
|
|
import Textarea from "discourse/components/d-textarea";
|
|
import DToggleSwitch from "discourse/components/d-toggle-switch";
|
|
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 ComboBox from "select-kit/components/combo-box";
|
|
import GroupChooser from "select-kit/components/group-chooser";
|
|
import DTooltip from "float-kit/components/d-tooltip";
|
|
import AiLlmSelector from "./ai-llm-selector";
|
|
import AiPersonaToolOptions from "./ai-persona-tool-options";
|
|
import AiToolSelector from "./ai-tool-selector";
|
|
import PersonaRagUploader from "./persona-rag-uploader";
|
|
|
|
export default class PersonaEditor extends Component {
|
|
@service router;
|
|
@service store;
|
|
@service dialog;
|
|
@service toasts;
|
|
@service siteSettings;
|
|
|
|
@tracked allGroups = [];
|
|
@tracked isSaving = false;
|
|
@tracked editingModel = null;
|
|
@tracked showDelete = false;
|
|
@tracked maxPixelsValue = null;
|
|
@tracked ragIndexingStatuses = null;
|
|
@tracked showIndexingOptions = false;
|
|
|
|
get chatPluginEnabled() {
|
|
return this.siteSettings.chat_enabled;
|
|
}
|
|
|
|
@action
|
|
updateModel() {
|
|
this.editingModel = this.args.model.workingCopy();
|
|
this.showDelete = !this.args.model.isNew && !this.args.model.system;
|
|
this.maxPixelsValue = this.findClosestPixelValue(
|
|
this.editingModel.vision_max_pixels
|
|
);
|
|
}
|
|
|
|
@action
|
|
toggleIndexingOptions(event) {
|
|
this.showIndexingOptions = !this.showIndexingOptions;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
|
|
findClosestPixelValue(pixels) {
|
|
let value = "high";
|
|
this.maxPixelValues.forEach((info) => {
|
|
if (pixels === info.pixels) {
|
|
value = info.id;
|
|
}
|
|
});
|
|
return value;
|
|
}
|
|
|
|
@cached
|
|
get maxPixelValues() {
|
|
const l = (key) =>
|
|
I18n.t(`discourse_ai.ai_persona.vision_max_pixel_sizes.${key}`);
|
|
return [
|
|
{ id: "low", name: l("low"), pixels: 65536 },
|
|
{ id: "medium", name: l("medium"), pixels: 262144 },
|
|
{ id: "high", name: l("high"), pixels: 1048576 },
|
|
];
|
|
}
|
|
|
|
get indexingOptionsText() {
|
|
return this.showIndexingOptions
|
|
? I18n.t("discourse_ai.ai_persona.hide_indexing_options")
|
|
: I18n.t("discourse_ai.ai_persona.show_indexing_options");
|
|
}
|
|
|
|
@action
|
|
async updateAllGroups() {
|
|
this.allGroups = await Group.findAll();
|
|
}
|
|
|
|
@action
|
|
async save() {
|
|
const isNew = this.args.model.isNew;
|
|
this.isSaving = true;
|
|
|
|
const backupModel = this.args.model.workingCopy();
|
|
|
|
this.args.model.setProperties(this.editingModel);
|
|
try {
|
|
await this.args.model.save();
|
|
this.#sortPersonas();
|
|
if (isNew && this.args.model.rag_uploads.length === 0) {
|
|
this.args.personas.addObject(this.args.model);
|
|
this.router.transitionTo(
|
|
"adminPlugins.show.discourse-ai-personas.show",
|
|
this.args.model
|
|
);
|
|
} else {
|
|
this.toasts.success({
|
|
data: { message: I18n.t("discourse_ai.ai_persona.saved") },
|
|
duration: 2000,
|
|
});
|
|
}
|
|
} catch (e) {
|
|
this.args.model.setProperties(backupModel);
|
|
popupAjaxError(e);
|
|
} finally {
|
|
later(() => {
|
|
this.isSaving = false;
|
|
}, 1000);
|
|
}
|
|
}
|
|
|
|
get showTemperature() {
|
|
return this.editingModel?.temperature || !this.editingModel?.system;
|
|
}
|
|
|
|
get showTopP() {
|
|
return this.editingModel?.top_p || !this.editingModel?.system;
|
|
}
|
|
|
|
get adminUser() {
|
|
return AdminUser.create(this.editingModel?.user);
|
|
}
|
|
|
|
get mappedQuestionConsolidatorLlm() {
|
|
return this.editingModel?.question_consolidator_llm || "blank";
|
|
}
|
|
|
|
set mappedQuestionConsolidatorLlm(value) {
|
|
if (value === "blank") {
|
|
this.editingModel.question_consolidator_llm = null;
|
|
} else {
|
|
this.editingModel.question_consolidator_llm = value;
|
|
}
|
|
}
|
|
|
|
get mappedDefaultLlm() {
|
|
return this.editingModel?.default_llm || "blank";
|
|
}
|
|
|
|
set mappedDefaultLlm(value) {
|
|
if (value === "blank") {
|
|
this.editingModel.default_llm = null;
|
|
} else {
|
|
this.editingModel.default_llm = value;
|
|
}
|
|
}
|
|
|
|
@action
|
|
onChangeMaxPixels(value) {
|
|
const entry = this.maxPixelValues.findBy("id", value);
|
|
if (!entry) {
|
|
return;
|
|
}
|
|
this.maxPixelsValue = value;
|
|
this.editingModel.vision_max_pixels = entry.pixels;
|
|
}
|
|
|
|
@action
|
|
delete() {
|
|
return this.dialog.confirm({
|
|
message: I18n.t("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
|
|
updateAllowedGroups(ids) {
|
|
this.editingModel.set("allowed_group_ids", ids);
|
|
}
|
|
|
|
@action
|
|
async toggleEnabled() {
|
|
await this.toggleField("enabled");
|
|
}
|
|
|
|
@action
|
|
async togglePriority() {
|
|
await this.toggleField("priority", true);
|
|
}
|
|
|
|
@action
|
|
async createUser() {
|
|
try {
|
|
let user = await this.args.model.createUser();
|
|
this.editingModel.set("user", user);
|
|
this.editingModel.set("user_id", user.id);
|
|
} catch (e) {
|
|
popupAjaxError(e);
|
|
}
|
|
}
|
|
|
|
@action
|
|
updateUploads(uploads) {
|
|
this.editingModel.rag_uploads = uploads;
|
|
}
|
|
|
|
@action
|
|
removeUpload(upload) {
|
|
this.editingModel.rag_uploads.removeObject(upload);
|
|
if (!this.args.model.isNew) {
|
|
this.save();
|
|
}
|
|
}
|
|
|
|
async toggleField(field, sortPersonas) {
|
|
this.args.model.set(field, !this.args.model[field]);
|
|
this.editingModel.set(field, this.args.model[field]);
|
|
if (!this.args.model.isNew) {
|
|
try {
|
|
const args = {};
|
|
args[field] = this.args.model[field];
|
|
|
|
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);
|
|
}
|
|
|
|
<template>
|
|
<BackButton
|
|
@route="adminPlugins.show.discourse-ai-personas"
|
|
@label="discourse_ai.ai_persona.back"
|
|
/>
|
|
<form
|
|
class="form-horizontal ai-persona-editor"
|
|
{{didUpdate this.updateModel @model.id}}
|
|
{{didInsert this.updateModel @model.id}}
|
|
{{didInsert this.updateAllGroups @model.id}}
|
|
>
|
|
<div class="control-group">
|
|
<DToggleSwitch
|
|
class="ai-persona-editor__enabled"
|
|
@state={{@model.enabled}}
|
|
@label="discourse_ai.ai_persona.enabled"
|
|
{{on "click" this.toggleEnabled}}
|
|
/>
|
|
</div>
|
|
<div class="control-group ai-persona-editor__priority">
|
|
<DToggleSwitch
|
|
class="ai-persona-editor__priority"
|
|
@state={{@model.priority}}
|
|
@label="discourse_ai.ai_persona.priority"
|
|
{{on "click" this.togglePriority}}
|
|
/>
|
|
<DTooltip
|
|
@icon="question-circle"
|
|
@content={{I18n.t "discourse_ai.ai_persona.priority_help"}}
|
|
/>
|
|
</div>
|
|
<div class="control-group">
|
|
<label>{{I18n.t "discourse_ai.ai_persona.name"}}</label>
|
|
<Input
|
|
class="ai-persona-editor__name"
|
|
@type="text"
|
|
@value={{this.editingModel.name}}
|
|
disabled={{this.editingModel.system}}
|
|
/>
|
|
</div>
|
|
<div class="control-group">
|
|
<label>{{I18n.t "discourse_ai.ai_persona.description"}}</label>
|
|
<Textarea
|
|
class="ai-persona-editor__description"
|
|
@value={{this.editingModel.description}}
|
|
disabled={{this.editingModel.system}}
|
|
/>
|
|
</div>
|
|
<div class="control-group">
|
|
<label>{{I18n.t "discourse_ai.ai_persona.default_llm"}}</label>
|
|
<AiLlmSelector
|
|
class="ai-persona-editor__llms"
|
|
@value={{this.mappedDefaultLlm}}
|
|
@llms={{@personas.resultSetMeta.llms}}
|
|
/>
|
|
<DTooltip
|
|
@icon="question-circle"
|
|
@content={{I18n.t "discourse_ai.ai_persona.default_llm_help"}}
|
|
/>
|
|
</div>
|
|
{{#unless @model.isNew}}
|
|
<div class="control-group">
|
|
<label>{{I18n.t "discourse_ai.ai_persona.user"}}</label>
|
|
{{#if this.editingModel.user}}
|
|
<a
|
|
class="avatar"
|
|
href={{this.editingModel.user.path}}
|
|
data-user-card={{this.editingModel.user.username}}
|
|
>
|
|
{{Avatar this.editingModel.user.avatar_template "small"}}
|
|
</a>
|
|
<LinkTo @route="adminUser" @model={{this.adminUser}}>
|
|
{{this.editingModel.user.username}}
|
|
</LinkTo>
|
|
{{else}}
|
|
<DButton
|
|
@action={{this.createUser}}
|
|
class="ai-persona-editor__create-user"
|
|
>
|
|
{{I18n.t "discourse_ai.ai_persona.create_user"}}
|
|
</DButton>
|
|
<DTooltip
|
|
@icon="question-circle"
|
|
@content={{I18n.t "discourse_ai.ai_persona.create_user_help"}}
|
|
/>
|
|
{{/if}}
|
|
</div>
|
|
{{/unless}}
|
|
<div class="control-group">
|
|
<label>{{I18n.t "discourse_ai.ai_persona.tools"}}</label>
|
|
<AiToolSelector
|
|
class="ai-persona-editor__tools"
|
|
@value={{this.editingModel.tools}}
|
|
@disabled={{this.editingModel.system}}
|
|
@tools={{@personas.resultSetMeta.tools}}
|
|
/>
|
|
</div>
|
|
{{#unless this.editingModel.system}}
|
|
<AiPersonaToolOptions
|
|
@persona={{this.editingModel}}
|
|
@tools={{this.editingModel.tools}}
|
|
@allTools={{@personas.resultSetMeta.tools}}
|
|
/>
|
|
{{/unless}}
|
|
<div class="control-group">
|
|
<label>{{I18n.t "discourse_ai.ai_persona.allowed_groups"}}</label>
|
|
<GroupChooser
|
|
@value={{this.editingModel.allowed_group_ids}}
|
|
@content={{this.allGroups}}
|
|
@onChange={{this.updateAllowedGroups}}
|
|
/>
|
|
</div>
|
|
<div class="control-group">
|
|
<label for="ai-persona-editor__system_prompt">{{I18n.t
|
|
"discourse_ai.ai_persona.system_prompt"
|
|
}}</label>
|
|
<Textarea
|
|
class="ai-persona-editor__system_prompt"
|
|
@value={{this.editingModel.system_prompt}}
|
|
disabled={{this.editingModel.system}}
|
|
/>
|
|
</div>
|
|
{{#if this.editingModel.user}}
|
|
{{#if this.chatPluginEnabled}}
|
|
<div class="control-group ai-persona-editor__allow_chat">
|
|
<label>
|
|
<Input
|
|
@type="checkbox"
|
|
@checked={{this.editingModel.allow_chat}}
|
|
/>
|
|
{{I18n.t "discourse_ai.ai_persona.allow_chat"}}</label>
|
|
<DTooltip
|
|
@icon="question-circle"
|
|
@content={{I18n.t "discourse_ai.ai_persona.allow_chat_help"}}
|
|
/>
|
|
</div>
|
|
{{/if}}
|
|
<div class="control-group ai-persona-editor__mentionable">
|
|
<label>
|
|
<Input
|
|
@type="checkbox"
|
|
@checked={{this.editingModel.mentionable}}
|
|
/>
|
|
{{I18n.t "discourse_ai.ai_persona.mentionable"}}</label>
|
|
<DTooltip
|
|
@icon="question-circle"
|
|
@content={{I18n.t "discourse_ai.ai_persona.mentionable_help"}}
|
|
/>
|
|
</div>
|
|
{{/if}}
|
|
<div class="control-group ai-persona-editor__tool-details">
|
|
<label>
|
|
<Input @type="checkbox" @checked={{this.editingModel.tool_details}} />
|
|
{{I18n.t "discourse_ai.ai_persona.tool_details"}}</label>
|
|
<DTooltip
|
|
@icon="question-circle"
|
|
@content={{I18n.t "discourse_ai.ai_persona.tool_details_help"}}
|
|
/>
|
|
</div>
|
|
<div class="control-group ai-persona-editor__vision_enabled">
|
|
<label>
|
|
<Input
|
|
@type="checkbox"
|
|
@checked={{this.editingModel.vision_enabled}}
|
|
/>
|
|
{{I18n.t "discourse_ai.ai_persona.vision_enabled"}}</label>
|
|
<DTooltip
|
|
@icon="question-circle"
|
|
@content={{I18n.t "discourse_ai.ai_persona.vision_enabled_help"}}
|
|
/>
|
|
</div>
|
|
{{#if this.editingModel.vision_enabled}}
|
|
<div class="control-group">
|
|
<label>{{I18n.t "discourse_ai.ai_persona.vision_max_pixels"}}</label>
|
|
<ComboBox
|
|
@value={{this.maxPixelsValue}}
|
|
@content={{this.maxPixelValues}}
|
|
@onChange={{this.onChangeMaxPixels}}
|
|
/>
|
|
</div>
|
|
{{/if}}
|
|
<div class="control-group">
|
|
<label>{{I18n.t "discourse_ai.ai_persona.max_context_posts"}}</label>
|
|
<Input
|
|
@type="number"
|
|
lang="en"
|
|
class="ai-persona-editor__max_context_posts"
|
|
@value={{this.editingModel.max_context_posts}}
|
|
/>
|
|
<DTooltip
|
|
@icon="question-circle"
|
|
@content={{I18n.t "discourse_ai.ai_persona.max_context_posts_help"}}
|
|
/>
|
|
</div>
|
|
{{#if this.showTemperature}}
|
|
<div class="control-group">
|
|
<label>{{I18n.t "discourse_ai.ai_persona.temperature"}}</label>
|
|
<Input
|
|
@type="number"
|
|
class="ai-persona-editor__temperature"
|
|
step="any"
|
|
lang="en"
|
|
@value={{this.editingModel.temperature}}
|
|
disabled={{this.editingModel.system}}
|
|
/>
|
|
<DTooltip
|
|
@icon="question-circle"
|
|
@content={{I18n.t "discourse_ai.ai_persona.temperature_help"}}
|
|
/>
|
|
</div>
|
|
{{/if}}
|
|
{{#if this.showTopP}}
|
|
<div class="control-group">
|
|
<label>{{I18n.t "discourse_ai.ai_persona.top_p"}}</label>
|
|
<Input
|
|
@type="number"
|
|
step="any"
|
|
lang="en"
|
|
class="ai-persona-editor__top_p"
|
|
@value={{this.editingModel.top_p}}
|
|
disabled={{this.editingModel.system}}
|
|
/>
|
|
<DTooltip
|
|
@icon="question-circle"
|
|
@content={{I18n.t "discourse_ai.ai_persona.top_p_help"}}
|
|
/>
|
|
</div>
|
|
{{/if}}
|
|
{{#if this.siteSettings.ai_embeddings_enabled}}
|
|
<div class="control-group">
|
|
<PersonaRagUploader
|
|
@persona={{this.editingModel}}
|
|
@updateUploads={{this.updateUploads}}
|
|
@onRemove={{this.removeUpload}}
|
|
/>
|
|
{{#if this.editingModel.rag_uploads}}
|
|
<a
|
|
href="#"
|
|
class="ai-persona-editor__indexing-options"
|
|
{{on "click" this.toggleIndexingOptions}}
|
|
>{{this.indexingOptionsText}}</a>
|
|
{{/if}}
|
|
</div>
|
|
{{#if this.showIndexingOptions}}
|
|
<div class="control-group">
|
|
<label>{{I18n.t "discourse_ai.ai_persona.rag_chunk_tokens"}}</label>
|
|
<Input
|
|
@type="number"
|
|
step="any"
|
|
lang="en"
|
|
class="ai-persona-editor__rag_chunk_tokens"
|
|
@value={{this.editingModel.rag_chunk_tokens}}
|
|
/>
|
|
<DTooltip
|
|
@icon="question-circle"
|
|
@content={{I18n.t
|
|
"discourse_ai.ai_persona.rag_chunk_tokens_help"
|
|
}}
|
|
/>
|
|
</div>
|
|
<div class="control-group">
|
|
<label>{{I18n.t
|
|
"discourse_ai.ai_persona.rag_chunk_overlap_tokens"
|
|
}}</label>
|
|
<Input
|
|
@type="number"
|
|
step="any"
|
|
lang="en"
|
|
class="ai-persona-editor__rag_chunk_overlap_tokens"
|
|
@value={{this.editingModel.rag_chunk_overlap_tokens}}
|
|
/>
|
|
<DTooltip
|
|
@icon="question-circle"
|
|
@content={{I18n.t
|
|
"discourse_ai.ai_persona.rag_chunk_overlap_tokens_help"
|
|
}}
|
|
/>
|
|
</div>
|
|
<div class="control-group">
|
|
<label>{{I18n.t
|
|
"discourse_ai.ai_persona.rag_conversation_chunks"
|
|
}}</label>
|
|
<Input
|
|
@type="number"
|
|
step="any"
|
|
lang="en"
|
|
class="ai-persona-editor__rag_conversation_chunks"
|
|
@value={{this.editingModel.rag_conversation_chunks}}
|
|
/>
|
|
<DTooltip
|
|
@icon="question-circle"
|
|
@content={{I18n.t
|
|
"discourse_ai.ai_persona.rag_conversation_chunks_help"
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>{{I18n.t
|
|
"discourse_ai.ai_persona.question_consolidator_llm"
|
|
}}</label>
|
|
<AiLlmSelector
|
|
class="ai-persona-editor__llms"
|
|
@value={{this.mappedQuestionConsolidatorLlm}}
|
|
@llms={{@personas.resultSetMeta.llms}}
|
|
/>
|
|
|
|
<DTooltip
|
|
@icon="question-circle"
|
|
@content={{I18n.t
|
|
"discourse_ai.ai_persona.question_consolidator_llm_help"
|
|
}}
|
|
/>
|
|
</div>
|
|
{{/if}}
|
|
{{/if}}
|
|
<div class="control-group ai-persona-editor__action_panel">
|
|
<DButton
|
|
class="btn-primary ai-persona-editor__save"
|
|
@action={{this.save}}
|
|
@disabled={{this.isSaving}}
|
|
>{{I18n.t "discourse_ai.ai_persona.save"}}</DButton>
|
|
{{#if this.showDelete}}
|
|
<DButton
|
|
@action={{this.delete}}
|
|
class="btn-danger ai-persona-editor__delete"
|
|
>
|
|
{{I18n.t "discourse_ai.ai_persona.delete"}}
|
|
</DButton>
|
|
{{/if}}
|
|
</div>
|
|
</form>
|
|
</template>
|
|
}
|