DEV: Clean up after #677 (#694)

Follow up to b863ddc94b

Ruby:
* Validate `summary` (the column is `not null`)
* Fix `name` validation (the column has `max_length` 100)
* Fix table annotations
* Accept missing `parameter` attributes (`required, `enum`, `enum_values`)

JS:
* Use native classes
* Don't use ember's array extensions
* Add explicit service injections
* Correct class names
* Use `||=` operator
* Use `store` service to create records
* Remove unused service injections
* Extract consts
* Group actions together
* Use `async`/`await`
* Use `withEventValue`
* Sort html attributes
* Use DButtons `@label` arg
* Use `input` elements instead of Ember's `Input` component (same w/ textarea)
* Remove `btn-default` class (automatically applied by DButton)
* Don't mix `I18n.t` and `i18n` in the same template
* Don't track props that aren't used in a template
* Correct invalid `target.value` code
* Remove unused/invalid `this.parameter`/`onChange` code
* Whitespace
* Use the new service import `inject as service` -> `service`
* Use `Object.entries()`
* Add missing i18n strings
* Fix an error in `addEnumValue` (calling `pushObject` on `undefined`)
* Use `TrackedArray`/`TrackedObject`
* Transform tool `parameters` keys (`enumValues` -> `enum_values`)
This commit is contained in:
Jarek Radosz 2024-06-28 00:59:51 +02:00 committed by GitHub
parent a708d4dfa2
commit a5a39dd2ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 197 additions and 178 deletions

View File

@ -1,16 +1,15 @@
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({ export default class DiscourseAiToolsNewRoute extends DiscourseRoute {
async model() { async model() {
const record = this.store.createRecord("ai-tool"); return this.store.createRecord("ai-tool");
return record; }
},
setupController(controller, model) { setupController(controller) {
this._super(controller, model); super.setupController(...arguments);
const toolsModel = this.modelFor("adminPlugins.show.discourse-ai-tools"); const toolsModel = this.modelFor("adminPlugins.show.discourse-ai-tools");
controller.set("allTools", toolsModel); controller.set("allTools", toolsModel);
controller.set("presets", toolsModel.resultSetMeta.presets); controller.set("presets", toolsModel.resultSetMeta.presets);
}, }
}); }

View File

@ -1,17 +1,18 @@
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({ export default class DiscourseAiToolsShowRoute extends DiscourseRoute {
async model(params) { async model(params) {
const allTools = this.modelFor("adminPlugins.show.discourse-ai-tools"); const allTools = this.modelFor("adminPlugins.show.discourse-ai-tools");
const id = parseInt(params.id, 10); const id = parseInt(params.id, 10);
return allTools.findBy("id", id);
},
setupController(controller, model) { return allTools.find((tool) => tool.id === id);
this._super(controller, model); }
setupController(controller) {
super.setupController(...arguments);
const toolsModel = this.modelFor("adminPlugins.show.discourse-ai-tools"); const toolsModel = this.modelFor("adminPlugins.show.discourse-ai-tools");
controller.set("allTools", toolsModel); controller.set("allTools", toolsModel);
controller.set("presets", toolsModel.resultSetMeta.presets); controller.set("presets", toolsModel.resultSetMeta.presets);
}, }
}); }

View File

@ -1,6 +1,9 @@
import { service } from "@ember/service";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
export default class DiscourseAiToolsRoute extends DiscourseRoute { export default class DiscourseAiToolsRoute extends DiscourseRoute {
@service store;
model() { model() {
return this.store.findAll("ai-tool"); return this.store.findAll("ai-tool");
} }

View File

@ -81,7 +81,7 @@ module DiscourseAi
:description, :description,
:script, :script,
:summary, :summary,
parameters: %i[name type description], parameters: [:name, :type, :description, :required, :enum, enum_values: []],
) )
end end
end end

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class AiTool < ActiveRecord::Base class AiTool < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 255 } validates :name, presence: true, length: { maximum: 100 }
validates :description, presence: true, length: { maximum: 1000 } validates :description, presence: true, length: { maximum: 1000 }
validates :summary, presence: true, length: { maximum: 255 }
validates :script, presence: true, length: { maximum: 100_000 } validates :script, presence: true, length: { maximum: 100_000 }
validates :created_by_id, presence: true validates :created_by_id, presence: true
belongs_to :created_by, class_name: "User" belongs_to :created_by, class_name: "User"
@ -174,10 +175,12 @@ end
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# name :string not null # name :string not null
# description :text not null # description :string not null
# summary :string not null
# parameters :jsonb not null # parameters :jsonb not null
# script :text not null # script :text not null
# created_by_id :integer not null # created_by_id :integer not null
# enabled :boolean default(TRUE), not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# #

View File

@ -1,6 +1,6 @@
import RestAdapter from "discourse/adapters/rest"; import RestAdapter from "discourse/adapters/rest";
export default class Adapter extends RestAdapter { export default class AiToolAdapter extends RestAdapter {
jsonMode = true; jsonMode = true;
basePath() { basePath() {

View File

@ -1,3 +1,4 @@
import { TrackedArray, TrackedObject } from "@ember-compat/tracked-built-ins";
import RestModel from "discourse/models/rest"; import RestModel from "discourse/models/rest";
const CREATE_ATTRIBUTES = [ const CREATE_ATTRIBUTES = [
@ -20,8 +21,21 @@ export default class AiTool extends RestModel {
} }
workingCopy() { workingCopy() {
let attrs = this.getProperties(CREATE_ATTRIBUTES); const attrs = this.getProperties(CREATE_ATTRIBUTES);
attrs.parameters = attrs.parameters || [];
return AiTool.create(attrs); attrs.parameters = new TrackedArray(
attrs.parameters?.map((p) => {
const parameter = new TrackedObject(p);
if (parameter.enum_values) {
parameter.enumValues = new TrackedArray(parameter.enum_values);
delete parameter.enum_values;
}
return parameter;
})
);
return this.store.createRecord("ai-tool", attrs);
} }
} }

View File

@ -1,14 +1,15 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking"; import { tracked } from "@glimmer/tracking";
import { Input } from "@ember/component"; import { fn } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object"; import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert"; import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import didUpdate from "@ember/render-modifiers/modifiers/did-update"; import didUpdate from "@ember/render-modifiers/modifiers/did-update";
import { inject as service } from "@ember/service"; import { service } from "@ember/service";
import BackButton from "discourse/components/back-button"; import BackButton from "discourse/components/back-button";
import DButton from "discourse/components/d-button"; import DButton from "discourse/components/d-button";
import Textarea from "discourse/components/d-textarea";
import DTooltip from "discourse/components/d-tooltip"; import DTooltip from "discourse/components/d-tooltip";
import withEventValue from "discourse/helpers/with-event-value";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
import AceEditor from "admin/components/ace-editor"; import AceEditor from "admin/components/ace-editor";
@ -16,9 +17,11 @@ import ComboBox from "select-kit/components/combo-box";
import AiToolParameterEditor from "./ai-tool-parameter-editor"; import AiToolParameterEditor from "./ai-tool-parameter-editor";
import AiToolTestModal from "./modal/ai-tool-test-modal"; import AiToolTestModal from "./modal/ai-tool-test-modal";
const ACE_EDITOR_MODE = "javascript";
const ACE_EDITOR_THEME = "chrome";
export default class AiToolEditor extends Component { export default class AiToolEditor extends Component {
@service router; @service router;
@service store;
@service dialog; @service dialog;
@service modal; @service modal;
@service toasts; @service toasts;
@ -26,18 +29,8 @@ export default class AiToolEditor extends Component {
@tracked isSaving = false; @tracked isSaving = false;
@tracked editingModel = null; @tracked editingModel = null;
@tracked showDelete = false; @tracked showDelete = false;
@tracked selectedPreset = null; @tracked selectedPreset = null;
aceEditorMode = "javascript";
aceEditorTheme = "chrome";
@action
updateModel() {
this.editingModel = this.args.model.workingCopy();
this.showDelete = !this.args.model.isNew;
}
get presets() { get presets() {
return this.args.presets.map((preset) => { return this.args.presets.map((preset) => {
return { return {
@ -51,6 +44,12 @@ export default class AiToolEditor extends Component {
return !this.selectedPreset && this.args.model.isNew; return !this.selectedPreset && this.args.model.isNew;
} }
@action
updateModel() {
this.editingModel = this.args.model.workingCopy();
this.showDelete = !this.args.model.isNew;
}
@action @action
configurePreset() { configurePreset() {
this.selectedPreset = this.args.presets.findBy("preset_id", this.presetId); this.selectedPreset = this.args.presets.findBy("preset_id", this.presetId);
@ -64,16 +63,23 @@ export default class AiToolEditor extends Component {
this.isSaving = true; this.isSaving = true;
try { try {
await this.args.model.save( const data = this.editingModel.getProperties(
this.editingModel.getProperties( "name",
"name", "description",
"description", "parameters",
"parameters", "script",
"script", "summary"
"summary"
)
); );
for (const p of data.parameters) {
if (p.enumValues) {
p.enum_values = p.enumValues;
delete p.enumValues;
}
}
await this.args.model.save(data);
this.toasts.success({ this.toasts.success({
data: { message: I18n.t("discourse_ai.tools.saved") }, data: { message: I18n.t("discourse_ai.tools.saved") },
duration: 2000, duration: 2000,
@ -97,22 +103,15 @@ export default class AiToolEditor extends Component {
delete() { delete() {
return this.dialog.confirm({ return this.dialog.confirm({
message: I18n.t("discourse_ai.tools.confirm_delete"), message: I18n.t("discourse_ai.tools.confirm_delete"),
didConfirm: () => { didConfirm: async () => {
return this.args.model.destroyRecord().then(() => { await this.args.model.destroyRecord();
this.args.tools.removeObject(this.args.model);
this.router.transitionTo( this.args.tools.removeObject(this.args.model);
"adminPlugins.show.discourse-ai-tools.index" this.router.transitionTo("adminPlugins.show.discourse-ai-tools.index");
);
});
}, },
}); });
} }
@action
updateScript(script) {
this.editingModel.script = script;
}
@action @action
openTestModal() { openTestModal() {
this.modal.show(AiToolTestModal, { this.modal.show(AiToolTestModal, {
@ -129,9 +128,9 @@ export default class AiToolEditor extends Component {
/> />
<form <form
class="form-horizontal ai-tool-editor"
{{didUpdate this.updateModel @model.id}}
{{didInsert this.updateModel @model.id}} {{didInsert this.updateModel @model.id}}
{{didUpdate this.updateModel @model.id}}
class="form-horizontal ai-tool-editor"
> >
{{#if this.showPresets}} {{#if this.showPresets}}
<div class="control-group"> <div class="control-group">
@ -145,18 +144,18 @@ export default class AiToolEditor extends Component {
<div class="control-group ai-llm-editor__action_panel"> <div class="control-group ai-llm-editor__action_panel">
<DButton <DButton
class="ai-tool-editor__next"
@action={{this.configurePreset}} @action={{this.configurePreset}}
> @label="discourse_ai.tools.next.title"
{{I18n.t "discourse_ai.tools.next.title"}} class="ai-tool-editor__next"
</DButton> />
</div> </div>
{{else}} {{else}}
<div class="control-group"> <div class="control-group">
<label>{{I18n.t "discourse_ai.tools.name"}}</label> <label>{{I18n.t "discourse_ai.tools.name"}}</label>
<Input <input
@type="text" {{on "input" (withEventValue (fn (mut this.editingModel.name)))}}
@value={{this.editingModel.name}} value={{this.editingModel.name}}
type="text"
class="ai-tool-editor__name" class="ai-tool-editor__name"
/> />
<DTooltip <DTooltip
@ -164,19 +163,25 @@ export default class AiToolEditor extends Component {
@content={{I18n.t "discourse_ai.tools.name_help"}} @content={{I18n.t "discourse_ai.tools.name_help"}}
/> />
</div> </div>
<div class="control-group"> <div class="control-group">
<label>{{I18n.t "discourse_ai.tools.description"}}</label> <label>{{I18n.t "discourse_ai.tools.description"}}</label>
<Textarea <textarea
@value={{this.editingModel.description}} {{on
class="ai-tool-editor__description input-xxlarge" "input"
(withEventValue (fn (mut this.editingModel.description)))
}}
placeholder={{I18n.t "discourse_ai.tools.description_help"}} placeholder={{I18n.t "discourse_ai.tools.description_help"}}
/> class="ai-tool-editor__description input-xxlarge"
>{{this.editingModel.description}}</textarea>
</div> </div>
<div class="control-group"> <div class="control-group">
<label>{{I18n.t "discourse_ai.tools.summary"}}</label> <label>{{I18n.t "discourse_ai.tools.summary"}}</label>
<Input <input
@type="text" {{on "input" (withEventValue (fn (mut this.editingModel.summary)))}}
@value={{this.editingModel.summary}} value={{this.editingModel.summary}}
type="text"
class="ai-tool-editor__summary input-xxlarge" class="ai-tool-editor__summary input-xxlarge"
/> />
<DTooltip <DTooltip
@ -184,37 +189,43 @@ export default class AiToolEditor extends Component {
@content={{I18n.t "discourse_ai.tools.summary_help"}} @content={{I18n.t "discourse_ai.tools.summary_help"}}
/> />
</div> </div>
<div class="control-group"> <div class="control-group">
<label>{{I18n.t "discourse_ai.tools.parameters"}}</label> <label>{{I18n.t "discourse_ai.tools.parameters"}}</label>
<AiToolParameterEditor @parameters={{this.editingModel.parameters}} /> <AiToolParameterEditor @parameters={{this.editingModel.parameters}} />
</div> </div>
<div class="control-group"> <div class="control-group">
<label>{{I18n.t "discourse_ai.tools.script"}}</label> <label>{{I18n.t "discourse_ai.tools.script"}}</label>
<AceEditor <AceEditor
@content={{this.editingModel.script}} @content={{this.editingModel.script}}
@mode={{this.aceEditorMode}} @onChange={{withEventValue (fn (mut this.editingModel.script))}}
@theme={{this.aceEditorTheme}} @mode={{ACE_EDITOR_MODE}}
@onChange={{this.updateScript}} @theme={{ACE_EDITOR_THEME}}
@editorId="ai-tool-script-editor" @editorId="ai-tool-script-editor"
/> />
</div> </div>
<div class="control-group ai-tool-editor__action_panel"> <div class="control-group ai-tool-editor__action_panel">
<DButton <DButton
@action={{this.openTestModal}} @action={{this.openTestModal}}
class="btn-default ai-tool-editor__test-button" @label="discourse_ai.tools.test"
>{{I18n.t "discourse_ai.tools.test"}}</DButton> class="ai-tool-editor__test-button"
/>
<DButton <DButton
class="btn-primary ai-tool-editor__save"
@action={{this.save}} @action={{this.save}}
@label="discourse_ai.tools.save"
@disabled={{this.isSaving}} @disabled={{this.isSaving}}
>{{I18n.t "discourse_ai.tools.save"}}</DButton> class="btn-primary ai-tool-editor__save"
/>
{{#if this.showDelete}} {{#if this.showDelete}}
<DButton <DButton
@action={{this.delete}} @action={{this.delete}}
@label="discourse_ai.tools.delete"
class="btn-danger ai-tool-editor__delete" class="btn-danger ai-tool-editor__delete"
> />
{{I18n.t "discourse_ai.tools.delete"}}
</DButton>
{{/if}} {{/if}}
</div> </div>
{{/if}} {{/if}}

View File

@ -1,12 +1,11 @@
import { LinkTo } from "@ember/routing"; import { LinkTo } from "@ember/routing";
import icon from "discourse-common/helpers/d-icon"; import icon from "discourse-common/helpers/d-icon";
import i18n from "discourse-common/helpers/i18n";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
<template> <template>
<section class="ai-tool-list-editor__current admin-detail pull-left"> <section class="ai-tool-list-editor__current admin-detail pull-left">
<div class="ai-tool-list-editor__header"> <div class="ai-tool-list-editor__header">
<h3>{{i18n "discourse_ai.tools.short_title"}}</h3> <h3>{{I18n.t "discourse_ai.tools.short_title"}}</h3>
<LinkTo <LinkTo
@route="adminPlugins.show.discourse-ai-tools.new" @route="adminPlugins.show.discourse-ai-tools.new"
class="btn btn-small btn-primary ai-tool-list-editor__new-button" class="btn btn-small btn-primary ai-tool-list-editor__new-button"
@ -37,7 +36,7 @@ import I18n from "discourse-i18n";
@route="adminPlugins.show.discourse-ai-tools.show" @route="adminPlugins.show.discourse-ai-tools.show"
@model={{tool}} @model={{tool}}
class="btn btn-text btn-small" class="btn btn-text btn-small"
>{{i18n "discourse_ai.tools.edit"}} </LinkTo> >{{I18n.t "discourse_ai.tools.edit"}}</LinkTo>
</td> </td>
</tr> </tr>
{{/each}} {{/each}}

View File

@ -1,10 +1,10 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { Input } from "@ember/component";
import { fn } from "@ember/helper"; import { fn } from "@ember/helper";
import { on } from "@ember/modifier"; import { on } from "@ember/modifier";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { TrackedArray, TrackedObject } from "@ember-compat/tracked-built-ins";
import DButton from "discourse/components/d-button"; import DButton from "discourse/components/d-button";
import withEventValue from "discourse/helpers/with-event-value";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
import ComboBox from "select-kit/components/combo-box"; import ComboBox from "select-kit/components/combo-box";
@ -16,145 +16,135 @@ const PARAMETER_TYPES = [
]; ];
export default class AiToolParameterEditor extends Component { export default class AiToolParameterEditor extends Component {
@tracked parameters = [];
@action @action
addParameter() { addParameter() {
this.args.parameters.pushObject({ this.args.parameters.push(
name: "", new TrackedObject({
description: "", name: "",
type: "string", description: "",
required: false, type: "string",
enum: false, required: false,
enumValues: [], enum: false,
}); enumValues: null,
})
);
} }
@action @action
removeParameter(parameter) { removeParameter(parameter) {
this.args.parameters.removeObject(parameter); const index = this.args.parameters.indexOf(parameter);
this.args.parameters.splice(index, 1);
} }
@action @action
updateParameter(parameter, field, value) { toggleRequired(parameter, event) {
parameter[field] = value; parameter.required = event.target.checked;
} }
@action @action
toggleEnum(parameter) { toggleEnum(parameter) {
parameter.enum = !parameter.enum; parameter.enum = !parameter.enum;
if (!parameter.enum) { if (!parameter.enum) {
parameter.enumValues = []; parameter.enumValues = null;
} }
this.args.onChange(this.parameters);
} }
@action @action
addEnumValue(parameter) { addEnumValue(parameter) {
parameter.enumValues.pushObject(""); parameter.enumValues ||= new TrackedArray();
parameter.enumValues.push("");
} }
@action @action
removeEnumValue(parameter, index) { removeEnumValue(parameter, index) {
parameter.enumValues.removeAt(index); parameter.enumValues.splice(index, 1);
}
@action
updateEnumValue(parameter, index, event) {
parameter.enumValues[index] = event.target.value;
} }
<template> <template>
{{#each @parameters as |parameter|}} {{#each @parameters as |parameter|}}
<div class="ai-tool-parameter"> <div class="ai-tool-parameter">
<div class="parameter-row"> <div class="parameter-row">
<Input <input
@type="text" {{on "input" (withEventValue (fn (mut parameter.name)))}}
@value={{parameter.name}} value={{parameter.name}}
type="text"
placeholder={{I18n.t "discourse_ai.tools.parameter_name"}} placeholder={{I18n.t "discourse_ai.tools.parameter_name"}}
/> />
<ComboBox @value={{parameter.type}} @content={{PARAMETER_TYPES}} /> <ComboBox @value={{parameter.type}} @content={{PARAMETER_TYPES}} />
</div> </div>
<div class="parameter-row"> <div class="parameter-row">
<Input <input
@type="text" {{on "input" (withEventValue (fn (mut parameter.description)))}}
@value={{parameter.description}} value={{parameter.description}}
type="text"
placeholder={{I18n.t "discourse_ai.tools.parameter_description"}} placeholder={{I18n.t "discourse_ai.tools.parameter_description"}}
{{on
"input"
(fn
this.updateParameter
parameter
"description"
value="target.value"
)
}}
/> />
</div> </div>
<div class="parameter-row"> <div class="parameter-row">
<label> <label>
<Input <input
@type="checkbox" {{on "input" (fn this.toggleRequired parameter)}}
@checked={{parameter.required}} checked={{parameter.required}}
{{on type="checkbox"
"change"
(fn
this.updateParameter
parameter
"required"
value="target.checked"
)
}}
/> />
{{I18n.t "discourse_ai.tools.parameter_required"}} {{I18n.t "discourse_ai.tools.parameter_required"}}
</label> </label>
<label> <label>
<Input <input
@type="checkbox" {{on "input" (fn this.toggleEnum parameter)}}
@checked={{parameter.enum}} checked={{parameter.enum}}
{{on "change" (fn this.toggleEnum parameter)}} type="checkbox"
/> />
{{I18n.t "discourse_ai.tools.parameter_enum"}} {{I18n.t "discourse_ai.tools.parameter_enum"}}
</label> </label>
<DButton <DButton
@icon="trash-alt"
@action={{fn this.removeParameter parameter}} @action={{fn this.removeParameter parameter}}
@icon="trash-alt"
class="btn-danger" class="btn-danger"
/> />
</div> </div>
{{#if parameter.enum}} {{#if parameter.enum}}
<div class="parameter-enum-values"> <div class="parameter-enum-values">
{{#each parameter.enumValues as |enumValue enumIndex|}} {{#each parameter.enumValues as |enumValue enumIndex|}}
<div class="enum-value-row"> <div class="enum-value-row">
<Input <input
@type="text" {{on "change" (fn this.updateEnumValue parameter enumIndex)}}
@value={{enumValue}} value={{enumValue}}
type="text"
placeholder={{I18n.t "discourse_ai.tools.enum_value"}} placeholder={{I18n.t "discourse_ai.tools.enum_value"}}
{{on
"input"
(fn
this.updateParameter
parameter.enumValues
enumIndex
value="target.value"
)
}}
/> />
<DButton <DButton
@icon="trash-alt"
@action={{fn this.removeEnumValue parameter enumIndex}} @action={{fn this.removeEnumValue parameter enumIndex}}
@icon="trash-alt"
class="btn-danger" class="btn-danger"
/> />
</div> </div>
{{/each}} {{/each}}
<DButton <DButton
@icon="plus"
@action={{fn this.addEnumValue parameter}} @action={{fn this.addEnumValue parameter}}
@label="discourse_ai.tools.add_enum_value" @label="discourse_ai.tools.add_enum_value"
@icon="plus"
/> />
</div> </div>
{{/if}} {{/if}}
</div> </div>
{{/each}} {{/each}}
<DButton <DButton
@icon="plus"
@action={{this.addParameter}} @action={{this.addParameter}}
@label="discourse_ai.tools.add_parameter" @label="discourse_ai.tools.add_parameter"
@icon="plus"
/> />
</template> </template>
} }

View File

@ -11,9 +11,9 @@ import I18n from "discourse-i18n";
import { jsonToHtml } from "../../lib/utilities"; import { jsonToHtml } from "../../lib/utilities";
export default class AiToolTestModal extends Component { export default class AiToolTestModal extends Component {
@tracked testResult = null; @tracked testResult;
@tracked isLoading = false; @tracked isLoading = false;
@tracked parameterValues = {}; parameterValues = {};
@action @action
updateParameter(name, event) { updateParameter(name, event) {
@ -24,16 +24,14 @@ export default class AiToolTestModal extends Component {
async runTest() { async runTest() {
this.isLoading = true; this.isLoading = true;
try { try {
const data = JSON.stringify({
ai_tool: this.args.model.tool,
parameters: this.parameterValues,
}); // required given this is a POST
const response = await ajax( const response = await ajax(
"/admin/plugins/discourse-ai/ai-tools/test.json", "/admin/plugins/discourse-ai/ai-tools/test.json",
{ {
type: "POST", type: "POST",
data, data: JSON.stringify({
ai_tool: this.args.model.tool,
parameters: this.parameterValues,
}),
contentType: "application/json", contentType: "application/json",
} }
); );
@ -49,20 +47,21 @@ export default class AiToolTestModal extends Component {
<DModal <DModal
@title={{I18n.t "discourse_ai.tools.test_modal.title"}} @title={{I18n.t "discourse_ai.tools.test_modal.title"}}
@closeModal={{@closeModal}} @closeModal={{@closeModal}}
class="ai-tool-test-modal"
@bodyClass="ai-tool-test-modal__body" @bodyClass="ai-tool-test-modal__body"
class="ai-tool-test-modal"
> >
<:body> <:body>
{{#each @model.tool.parameters as |param|}} {{#each @model.tool.parameters as |param|}}
<div class="control-group"> <div class="control-group">
<label>{{param.name}}</label> <label>{{param.name}}</label>
<input <input
type="text"
name={{param.name}}
{{on "input" (fn this.updateParameter param.name)}} {{on "input" (fn this.updateParameter param.name)}}
name={{param.name}}
type="text"
/> />
</div> </div>
{{/each}} {{/each}}
{{#if this.testResult}} {{#if this.testResult}}
<div class="ai-tool-test-modal__test-result"> <div class="ai-tool-test-modal__test-result">
<h3>{{I18n.t "discourse_ai.tools.test_modal.result"}}</h3> <h3>{{I18n.t "discourse_ai.tools.test_modal.result"}}</h3>
@ -70,6 +69,7 @@ export default class AiToolTestModal extends Component {
</div> </div>
{{/if}} {{/if}}
</:body> </:body>
<:footer> <:footer>
<DButton <DButton
@action={{this.runTest}} @action={{this.runTest}}

View File

@ -8,30 +8,27 @@ export function jsonToHtml(json) {
if (typeof json !== "object") { if (typeof json !== "object") {
return escapeExpression(json); return escapeExpression(json);
} }
let html = "<ul>"; let html = "<ul>";
for (let key in json) {
if (!json.hasOwnProperty(key)) { for (let [key, value] of Object.entries(json)) {
continue;
}
html += "<li>"; html += "<li>";
if (typeof json[key] === "object" && Array.isArray(json[key])) { key = escapeExpression(key);
html += `<strong>${escapeExpression(key)}:</strong> ${jsonToHtml(
json[key] if (typeof value === "object" && Array.isArray(value)) {
)}`; html += `<strong>${key}:</strong> ${jsonToHtml(value)}`;
} else if (typeof json[key] === "object") { } else if (typeof value === "object") {
html += `<strong>${escapeExpression(key)}:</strong> <ul><li>${jsonToHtml( html += `<strong>${key}:</strong> <ul><li>${jsonToHtml(value)}</li></ul>`;
json[key]
)}</li></ul>`;
} else { } else {
let value = json[key];
if (typeof value === "string") { if (typeof value === "string") {
value = escapeExpression(value); value = escapeExpression(value).replace(/\n/g, "<br>");
value = value.replace(/\n/g, "<br>");
} }
html += `<strong>${escapeExpression(key)}:</strong> ${value}`; html += `<strong>${key}:</strong> ${value}`;
} }
html += "</li>"; html += "</li>";
} }
html += "</ul>"; html += "</ul>";
return htmlSafe(html); return htmlSafe(html);
} }

View File

@ -200,6 +200,8 @@ en:
parameter_enum: "Enum" parameter_enum: "Enum"
parameter_name: "Parameter Name" parameter_name: "Parameter Name"
parameter_description: "Parameter Description" parameter_description: "Parameter Description"
enum_value: "Enum value"
add_enum_value: "Add enum value"
edit: "Edit" edit: "Edit"
test: "Run Test" test: "Run Test"
delete: "Delete" delete: "Delete"