From 107f14456b0e4b51fd2b934ee7cceced78b2e0cc Mon Sep 17 00:00:00 2001 From: Keegan George Date: Mon, 17 Mar 2025 07:38:25 -0700 Subject: [PATCH] DEV: Convert tool editor to form kit (#1135) * DEV: Make tool presets a dropdown * DEV: Select tool presets via DMenu instead * WIP * WIP: Add parameter types, uploader, script, etc. * WIP * updates * fix lint * FIX: spec * fixes --- ...min-plugins-show-discourse-ai-tools-new.js | 6 +- .../show/discourse-ai-tools/new.hbs | 1 + .../components/ai-tool-editor-form.gjs | 317 ++++++++++++++++++ .../discourse/components/ai-tool-editor.gjs | 282 +--------------- .../components/ai-tool-list-editor.gjs | 55 ++- .../components/ai-tool-parameter-editor.gjs | 155 --------- .../discourse/components/rag-uploader.gjs | 5 +- .../modules/ai-bot/common/ai-persona.scss | 9 +- .../modules/ai-bot/common/ai-tools.scss | 53 +-- config/locales/client.en.yml | 7 +- spec/system/ai_bot/tool_spec.rb | 19 +- 11 files changed, 418 insertions(+), 491 deletions(-) create mode 100644 assets/javascripts/discourse/components/ai-tool-editor-form.gjs delete mode 100644 assets/javascripts/discourse/components/ai-tool-parameter-editor.gjs diff --git a/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-tools-new.js b/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-tools-new.js index 1e645097..463ae21a 100644 --- a/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-tools-new.js +++ b/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-tools-new.js @@ -1,6 +1,10 @@ import DiscourseRoute from "discourse/routes/discourse"; export default class DiscourseAiToolsNewRoute extends DiscourseRoute { + beforeModel(transition) { + this.preset = transition.to.queryParams.presetId || "empty_tool"; + } + async model() { return this.store.createRecord("ai-tool"); } @@ -8,10 +12,10 @@ export default class DiscourseAiToolsNewRoute extends DiscourseRoute { setupController(controller) { super.setupController(...arguments); const toolsModel = this.modelFor("adminPlugins.show.discourse-ai-tools"); - controller.set("allTools", toolsModel); controller.set("presets", toolsModel.resultSetMeta.presets); controller.set("llms", toolsModel.resultSetMeta.llms); controller.set("settings", toolsModel.resultSetMeta.settings); + controller.set("selectedPreset", this.preset); } } diff --git a/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-tools/new.hbs b/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-tools/new.hbs index 444f6966..cc036e76 100644 --- a/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-tools/new.hbs +++ b/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-tools/new.hbs @@ -5,5 +5,6 @@ @presets={{this.presets}} @llms={{this.llms}} @settings={{this.settings}} + @selectedPreset={{this.selectedPreset}} /> \ No newline at end of file diff --git a/assets/javascripts/discourse/components/ai-tool-editor-form.gjs b/assets/javascripts/discourse/components/ai-tool-editor-form.gjs new file mode 100644 index 00000000..aed8e30d --- /dev/null +++ b/assets/javascripts/discourse/components/ai-tool-editor-form.gjs @@ -0,0 +1,317 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { fn, hash } from "@ember/helper"; +import { action } from "@ember/object"; +import { service } from "@ember/service"; +import Form from "discourse/components/form"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { i18n } from "discourse-i18n"; +import AiToolTestModal from "./modal/ai-tool-test-modal"; +import RagOptions from "./rag-options"; +import RagUploader from "./rag-uploader"; + +export default class AiToolEditorForm extends Component { + @service modal; + @service siteSettings; + @service dialog; + @service router; + @service toasts; + + @tracked uploadedFiles = []; + @tracked isSaving = false; + + PARAMETER_TYPES = [ + { name: "string", id: "string" }, + { name: "number", id: "number" }, + { name: "boolean", id: "boolean" }, + { name: "array", id: "array" }, + ]; + + get formData() { + return { + name: this.args.editingModel.name || "", + tool_name: this.args.editingModel.tool_name || "", + description: this.args.editingModel.description || "", + summary: this.args.editingModel.summary || "", + parameters: this.args.editingModel.parameters || [], + script: this.args.editingModel.script || "", + rag_uploads: this.args.editingModel.rag_uploads || [], + }; + } + + @action + async save(data) { + this.isSaving = true; + + try { + await this.args.model.save(data); + + this.toasts.success({ + data: { message: i18n("discourse_ai.tools.saved") }, + duration: 2000, + }); + + if (!this.args.tools.any((tool) => tool.id === this.args.model.id)) { + this.args.tools.pushObject(this.args.model); + } + + this.router.transitionTo( + "adminPlugins.show.discourse-ai-tools.edit", + this.args.model + ); + } catch (e) { + popupAjaxError(e); + } finally { + this.isSaving = false; + } + } + + @action + delete() { + return this.dialog.confirm({ + message: i18n("discourse_ai.tools.confirm_delete"), + + didConfirm: async () => { + await this.args.model.destroyRecord(); + this.args.tools.removeObject(this.args.model); + this.router.transitionTo("adminPlugins.show.discourse-ai-tools.index"); + }, + }); + } + + @action + updateUploads(addItemToCollection, uploads) { + const uniqueUploads = uploads.filter( + (upload) => !this.uploadedFiles.some((file) => file.id === upload.id) + ); + addItemToCollection("rag_uploads", uniqueUploads); + this.uploadedFiles = [...this.uploadedFiles, ...uniqueUploads]; + } + + @action + removeUpload(form, upload) { + this.uploadedFiles = this.uploadedFiles.filter( + (file) => file.id !== upload.id + ); + form.set("rag_uploads", this.uploadedFiles); + } + + @action + openTestModal() { + this.modal.show(AiToolTestModal, { + model: { + tool: this.args.editingModel, + }, + }); + } + + currentParameterSelection(data, index) { + return data.parameters[index].type; + } + + get ragUploadsDescription() { + return this.siteSettings.rag_images_enabled + ? i18n("discourse_ai.rag.uploads.description_with_images") + : i18n("discourse_ai.rag.uploads.description"); + } + + +} diff --git a/assets/javascripts/discourse/components/ai-tool-editor.gjs b/assets/javascripts/discourse/components/ai-tool-editor.gjs index 0a3b4922..98fffe5b 100644 --- a/assets/javascripts/discourse/components/ai-tool-editor.gjs +++ b/assets/javascripts/discourse/components/ai-tool-editor.gjs @@ -1,285 +1,39 @@ import Component from "@glimmer/component"; -import { tracked } from "@glimmer/tracking"; -import { fn } from "@ember/helper"; -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 { service } from "@ember/service"; -import AceEditor from "discourse/components/ace-editor"; import BackButton from "discourse/components/back-button"; -import DButton from "discourse/components/d-button"; -import DTooltip from "discourse/components/d-tooltip"; -import withEventValue from "discourse/helpers/with-event-value"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import { i18n } from "discourse-i18n"; -import ComboBox from "select-kit/components/combo-box"; -import AiToolParameterEditor from "./ai-tool-parameter-editor"; -import AiToolTestModal from "./modal/ai-tool-test-modal"; -import RagOptions from "./rag-options"; -import RagUploader from "./rag-uploader"; - -const ACE_EDITOR_MODE = "javascript"; -const ACE_EDITOR_THEME = "chrome"; +import AiToolEditorForm from "./ai-tool-editor-form"; export default class AiToolEditor extends Component { - @service router; - @service dialog; - @service modal; - @service toasts; @service store; - @service siteSettings; - @tracked isSaving = false; - @tracked editingModel = null; - @tracked showDelete = false; - @tracked selectedPreset = null; - - get presets() { - return this.args.presets.map((preset) => { - return { - name: preset.preset_name, - id: preset.preset_id, - }; - }); - } - - get showPresets() { - return !this.selectedPreset && this.args.model.isNew; - } - - @action - updateModel() { - this.editingModel = this.args.model.workingCopy(); - this.showDelete = !this.args.model.isNew; - } - - @action - configurePreset() { - this.selectedPreset = this.args.presets.findBy("preset_id", this.presetId); - this.editingModel = this.store - .createRecord("ai-tool", this.selectedPreset) - .workingCopy(); - this.showDelete = false; - } - - @action - updateUploads(uploads) { - this.editingModel.rag_uploads = uploads; - } - - @action - removeUpload(upload) { - this.editingModel.rag_uploads.removeObject(upload); - if (!this.args.model.isNew) { - this.save(); + get selectedPreset() { + if (!this.args.selectedPreset) { + return this.args.presets.findBy("preset_id", "empty_tool"); } + + return this.args.presets.findBy("preset_id", this.args.selectedPreset); } - @action - async save() { - this.isSaving = true; - - try { - const data = this.editingModel.getProperties( - "name", - "tool_name", - "description", - "parameters", - "script", - "summary", - "rag_uploads", - "rag_chunk_tokens", - "rag_chunk_overlap_tokens", - "rag_llm_model_id" - ); - - await this.args.model.save(data); - - this.toasts.success({ - data: { message: i18n("discourse_ai.tools.saved") }, - duration: 2000, - }); - if (!this.args.tools.any((tool) => tool.id === this.args.model.id)) { - this.args.tools.pushObject(this.args.model); - } - - this.router.transitionTo( - "adminPlugins.show.discourse-ai-tools.edit", - this.args.model - ); - } catch (e) { - popupAjaxError(e); - } finally { - this.isSaving = false; + get editingModel() { + if (this.args.model.isNew) { + return this.store.createRecord("ai-tool", this.selectedPreset); + } else { + return this.args.model; } } - @action - delete() { - return this.dialog.confirm({ - message: i18n("discourse_ai.tools.confirm_delete"), - didConfirm: async () => { - await this.args.model.destroyRecord(); - - this.args.tools.removeObject(this.args.model); - this.router.transitionTo("adminPlugins.show.discourse-ai-tools.index"); - }, - }); - } - - @action - openTestModal() { - this.modal.show(AiToolTestModal, { - model: { - tool: this.editingModel, - }, - }); - } - } diff --git a/assets/javascripts/discourse/components/ai-tool-list-editor.gjs b/assets/javascripts/discourse/components/ai-tool-list-editor.gjs index 77fbcc22..e5bd276c 100644 --- a/assets/javascripts/discourse/components/ai-tool-list-editor.gjs +++ b/assets/javascripts/discourse/components/ai-tool-list-editor.gjs @@ -1,13 +1,36 @@ import Component from "@glimmer/component"; +import { fn } from "@ember/helper"; +import { action } from "@ember/object"; import { LinkTo } from "@ember/routing"; import { service } from "@ember/service"; +import { eq } from "truth-helpers"; import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item"; +import DButton from "discourse/components/d-button"; import DPageSubheader from "discourse/components/d-page-subheader"; +import DropdownMenu from "discourse/components/dropdown-menu"; import { i18n } from "discourse-i18n"; import AdminConfigAreaEmptyList from "admin/components/admin-config-area-empty-list"; +import DMenu from "float-kit/components/d-menu"; export default class AiToolListEditor extends Component { @service adminPluginNavManager; + @service router; + + get lastIndexOfPresets() { + return this.args.tools.resultSetMeta.presets.length - 1; + } + + @action + routeToNewTool(preset) { + return this.router.transitionTo( + "adminPlugins.show.discourse-ai-tools.new", + { + queryParams: { + presetId: preset.preset_id, + }, + } + ); + }