DEV: migrates tools form to form-kit (#1204)

This PR is a retry of: #1135, where we migrate AiTools form to FormKit. The previous PR accidentally removed code related to setting enum values, and as a result was reverted. This update includes enums correctly along with the previous updates.
This commit is contained in:
Joffrey JAFFEUX
2025-04-22 09:23:25 -07:00
committed by GitHub
parent e8b0f86300
commit d2002f81a7
12 changed files with 505 additions and 586 deletions
@@ -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);
}
}
@@ -5,5 +5,6 @@
@presets={{this.presets}}
@llms={{this.llms}}
@settings={{this.settings}}
@selectedPreset={{this.selectedPreset}}
/>
</section>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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,
},
}
);
}
<template>
<DBreadcrumbsItem
@@ -20,13 +43,33 @@ export default class AiToolListEditor extends Component {
@learnMoreUrl="https://meta.discourse.org/t/ai-bot-custom-tools/314103"
@descriptionLabel={{i18n "discourse_ai.tools.subheader_description"}}
>
<:actions as |actions|>
<actions.Primary
@label="discourse_ai.tools.new"
@route="adminPlugins.show.discourse-ai-tools.new"
<:actions>
<DMenu
@triggerClass="btn-primary btn-small ai-tool-list-editor__new-button"
@label={{i18n "discourse_ai.tools.new"}}
@icon="plus"
class="ai-tool-list-editor__new-button"
/>
@placement="bottom-end"
>
<:content>
<DropdownMenu as |dropdown|>
{{#each @tools.resultSetMeta.presets as |preset index|}}
{{#if (eq index this.lastIndexOfPresets)}}
<dropdown.divider />
{{/if}}
<dropdown.item>
<DButton
@translatedLabel={{preset.preset_name}}
@action={{fn this.routeToNewTool preset}}
class="btn-transparent"
data-option={{preset.preset_id}}
/>
</dropdown.item>
{{/each}}
</DropdownMenu>
</:content>
</DMenu>
</:actions>
</DPageSubheader>
@@ -1,155 +0,0 @@
import Component from "@glimmer/component";
import { fn } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { TrackedArray, TrackedObject } from "@ember-compat/tracked-built-ins";
import DButton from "discourse/components/d-button";
import withEventValue from "discourse/helpers/with-event-value";
import { i18n } from "discourse-i18n";
import ComboBox from "select-kit/components/combo-box";
const PARAMETER_TYPES = [
{ name: "string", id: "string" },
{ name: "number", id: "number" },
{ name: "boolean", id: "boolean" },
{ name: "array", id: "array" },
];
export default class AiToolParameterEditor extends Component {
@action
addParameter() {
this.args.parameters.push(
new TrackedObject({
name: "",
description: "",
type: "string",
required: false,
enum: null,
})
);
}
@action
removeParameter(parameter) {
const index = this.args.parameters.indexOf(parameter);
this.args.parameters.splice(index, 1);
}
@action
toggleRequired(parameter, event) {
parameter.required = event.target.checked;
}
@action
toggleEnum(parameter) {
if (parameter.enum) {
parameter.enum = null;
} else {
this.addEnumValue(parameter);
}
}
@action
addEnumValue(parameter) {
parameter.enum ||= new TrackedArray();
parameter.enum.push("");
}
@action
removeEnumValue(parameter, index) {
parameter.enum.splice(index, 1);
if (parameter.enum.length === 0) {
parameter.enum = null;
}
}
@action
updateEnumValue(parameter, index, event) {
parameter.enum[index] = event.target.value;
}
<template>
{{#each @parameters as |parameter|}}
<div class="ai-tool-parameter">
<div class="parameter-row">
<input
{{on "input" (withEventValue (fn (mut parameter.name)))}}
value={{parameter.name}}
type="text"
placeholder={{i18n "discourse_ai.tools.parameter_name"}}
/>
<ComboBox @value={{parameter.type}} @content={{PARAMETER_TYPES}} />
</div>
<div class="parameter-row">
<input
{{on "input" (withEventValue (fn (mut parameter.description)))}}
value={{parameter.description}}
type="text"
placeholder={{i18n "discourse_ai.tools.parameter_description"}}
/>
</div>
<div class="parameter-row">
<label>
<input
{{on "input" (fn this.toggleRequired parameter)}}
checked={{parameter.required}}
type="checkbox"
class="parameter-row__required-toggle"
/>
{{i18n "discourse_ai.tools.parameter_required"}}
</label>
<label>
<input
{{on "input" (fn this.toggleEnum parameter)}}
checked={{parameter.enum}}
type="checkbox"
class="parameter-row__enum-toggle"
/>
{{i18n "discourse_ai.tools.parameter_enum"}}
</label>
<DButton
@action={{fn this.removeParameter parameter}}
@icon="trash-can"
class="btn-danger"
/>
</div>
{{#if parameter.enum}}
<div class="parameter-enum-values">
{{#each parameter.enum as |enumValue enumIndex|}}
<div class="enum-value-row">
<input
{{on "change" (fn this.updateEnumValue parameter enumIndex)}}
value={{enumValue}}
type="text"
placeholder={{i18n "discourse_ai.tools.enum_value"}}
/>
<DButton
@action={{fn this.removeEnumValue parameter enumIndex}}
@icon="trash-can"
class="btn-danger"
/>
</div>
{{/each}}
<DButton
@action={{fn this.addEnumValue parameter}}
@label="discourse_ai.tools.add_enum_value"
@icon="plus"
/>
</div>
{{/if}}
</div>
{{/each}}
<DButton
@action={{this.addParameter}}
@label="discourse_ai.tools.add_parameter"
@icon="plus"
/>
</template>
}
@@ -1,101 +0,0 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { Input } from "@ember/component";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import DTooltip from "discourse/components/d-tooltip";
import { i18n } from "discourse-i18n";
import AiLlmSelector from "./ai-llm-selector";
export default class RagOptions extends Component {
@tracked showIndexingOptions = false;
@action
toggleIndexingOptions(event) {
this.showIndexingOptions = !this.showIndexingOptions;
event.preventDefault();
event.stopPropagation();
}
get indexingOptionsText() {
return this.showIndexingOptions
? i18n("discourse_ai.rag.options.hide_indexing_options")
: i18n("discourse_ai.rag.options.show_indexing_options");
}
get visionLlms() {
return this.args.llms.filter((llm) => llm.vision_enabled);
}
get visionLlmId() {
return this.args.model.rag_llm_model_id ?? "blank";
}
set visionLlmId(value) {
if (value === "blank") {
this.args.model.rag_llm_model_id = null;
} else {
this.args.model.rag_llm_model_id = value;
}
}
<template>
{{#if @model.rag_uploads}}
<a
href="#"
class="rag-options__indexing-options"
{{on "click" this.toggleIndexingOptions}}
>{{this.indexingOptionsText}}</a>
{{/if}}
{{#if this.showIndexingOptions}}
<div class="control-group">
<label>{{i18n "discourse_ai.rag.options.rag_chunk_tokens"}}</label>
<Input
@type="number"
step="any"
lang="en"
class="rag-options__rag_chunk_tokens"
@value={{@model.rag_chunk_tokens}}
/>
<DTooltip
@icon="circle-question"
@content={{i18n "discourse_ai.rag.options.rag_chunk_tokens_help"}}
/>
</div>
<div class="control-group">
<label>{{i18n
"discourse_ai.rag.options.rag_chunk_overlap_tokens"
}}</label>
<Input
@type="number"
step="any"
lang="en"
class="rag-options__rag_chunk_overlap_tokens"
@value={{@model.rag_chunk_overlap_tokens}}
/>
<DTooltip
@icon="circle-question"
@content={{i18n
"discourse_ai.rag.options.rag_chunk_overlap_tokens_help"
}}
/>
</div>
{{#if @allowImages}}
<div class="control-group">
<label>{{i18n "discourse_ai.rag.options.rag_llm_model"}}</label>
<AiLlmSelector
class="ai-persona-editor__llms"
@value={{this.visionLlmId}}
@llms={{this.visionLlms}}
/>
<DTooltip
@icon="circle-question"
@content={{i18n "discourse_ai.rag.options.rag_llm_model_help"}}
/>
</div>
{{/if}}
{{yield}}
{{/if}}
</template>
}
@@ -135,7 +135,10 @@ export default class RagUploader extends Component {
{{#if this.ragUploads}}
<div class="rag-uploader__search-input-container">
<div class="rag-uploader__search-input">
{{icon "search" class="rag-uploader__search-input__search-icon"}}
{{icon
"magnifying-glass"
class="rag-uploader__search-input__search-icon"
}}
<Input
class="rag-uploader__search-input__input"
placeholder={{i18n "discourse_ai.rag.uploads.filter"}}
@@ -104,7 +104,9 @@
}
&__uploads-list {
margin-bottom: 20px;
&:has(tr) {
margin-bottom: 20px;
}
tbody {
border-top: none;
@@ -133,4 +135,9 @@
&__rag-file-icon {
margin-right: 5px;
}
.hidden-upload-field {
visibility: hidden;
position: absolute;
}
}
@@ -1,38 +1,27 @@
.ai-tool-parameter {
margin-bottom: 2em;
padding: 1.5em;
border: 1px solid var(--primary-low);
border-radius: 3px;
background-color: var(--secondary-very-low);
border: 1px solid var(--primary-low-mid);
border-radius: var(--d-input-border-radius);
width: 100%;
box-sizing: border-box;
.parameter-row {
display: flex;
align-items: center;
margin-bottom: 1em;
input[type="text"] {
flex-grow: 1;
margin-right: 1em;
}
label {
margin-right: 1em;
white-space: nowrap;
}
.form-kit__container-content {
flex-direction: column;
width: 100%;
}
}
.parameter-enum-values {
margin-top: 0.5em;
.ai-tool-parameter__enum-values {
margin-block: 1rem;
.enum-value-row {
display: flex;
align-items: center;
margin-bottom: 0.5em;
.form-kit__container-content {
display: grid;
grid-template-columns: 1fr auto;
position: relative;
input[type="text"] {
flex-grow: 1;
margin-right: 0.5em;
}
.form-kit__button.btn-icon-text {
justify-self: start;
grid-column: 1 / -1;
}
}
}
@@ -41,20 +30,10 @@
max-width: 80%;
position: relative;
.ace-wrapper {
border: 1px solid var(--primary-low);
max-width: 100%;
position: relative;
width: 100%;
height: 100%;
min-height: 500px;
.ace_editor {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
#control-rag_uploads .rag-uploader {
h3,
p {
display: none;
}
}
}
+3 -4
View File
@@ -371,7 +371,9 @@ en:
script: "Script"
parameters: "Parameters"
save: "Save"
add_parameter: "Add Parameter"
parameter_type: "Parameter type"
add_parameter: "Add parameter"
remove_parameter: "Remove"
parameter_required: "Required"
parameter_enum: "Enum"
parameter_name: "Parameter name"
@@ -382,10 +384,7 @@ en:
test: "Run test"
delete: "Delete"
saved: "Tool saved"
presets: "Select a preset..."
confirm_delete: "Are you sure you want to delete this tool?"
next:
title: "Next"
test_modal:
title: "Test AI tool"
run: "Run test"
+8 -11
View File
@@ -36,21 +36,18 @@ describe "AI Tool Management", type: :system do
it "allows admin to create a new AI tool from preset" do
visit "/admin/plugins/discourse-ai/ai-tools"
expect(page_header).to be_visible
expect(page).to have_content("Tools")
find(".ai-tool-list-editor__new-button").click
expect(page_header).to be_hidden
select_kit = PageObjects::Components::SelectKit.new(".ai-tool-editor__presets")
select_kit.expand
select_kit.select_row_by_value("exchange_rate")
tool_presets = PageObjects::Components::DMenu.new(find(".ai-tool-list-editor__new-button"))
tool_presets.option(".btn[data-option='exchange_rate'").click
find(".ai-tool-editor__next").click
required_toggle_css = "#control-parameters-0-required .form-kit__control-checkbox"
enum_toggle_css = "#control-parameters-0-isEnum .form-kit__control-checkbox"
expect(page.first(".parameter-row__required-toggle").checked?).to eq(true)
expect(page.first(".parameter-row__enum-toggle").checked?).to eq(false)
expect(page.find(required_toggle_css).checked?).to eq(true)
expect(page.find(enum_toggle_css).checked?).to eq(false)
# not allowed to test yet
expect(page).not_to have_button(".ai-tool-editor__test-button")
@@ -65,8 +62,8 @@ describe "AI Tool Management", type: :system do
ensure_can_run_test
expect(page.first(".parameter-row__required-toggle").checked?).to eq(true)
expect(page.first(".parameter-row__enum-toggle").checked?).to eq(false)
expect(page.first(required_toggle_css).checked?).to eq(true)
expect(page.first(enum_toggle_css).checked?).to eq(false)
visit "/admin/plugins/discourse-ai/ai-personas/new"