Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
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");
}

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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
@presets={{this.presets}}
@llms={{this.llms}}
@settings={{this.settings}}
@selectedPreset={{this.selectedPreset}}
/>
</section>
317 changes: 317 additions & 0 deletions assets/javascripts/discourse/components/ai-tool-editor-form.gjs
Original file line number Diff line number Diff line change
@@ -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");
}

<template>
<Form
@onSubmit={{this.save}}
@data={{this.formData}}
class="ai-tool-editor"
as |form data|
>

{{! NAME }}
<form.Field
@name="name"
@title={{i18n "discourse_ai.tools.name"}}
@validation="required|length:1,100"
@format="large"
@tooltip={{i18n "discourse_ai.tools.name_help"}}
as |field|
>
<field.Input class="ai-tool-editor__name" />
</form.Field>

{{! TOOL NAME }}
<form.Field
@name="tool_name"
@title={{i18n "discourse_ai.tools.tool_name"}}
@validation="required|length:1,100"
@format="large"
@tooltip={{i18n "discourse_ai.tools.tool_name_help"}}
as |field|
>
<field.Input class="ai-tool-editor__tool_name" />
</form.Field>

{{! DESCRIPTION }}
<form.Field
@name="description"
@title={{i18n "discourse_ai.tools.description"}}
@validation="required|length:1,1000"
@format="full"
@tooltip={{i18n "discourse_ai.tools.description_help"}}
as |field|
>
<field.Textarea
@height={{60}}
class="ai-tool-editor__description"
placeholder={{i18n "discourse_ai.tools.description_help"}}
/>
</form.Field>

{{! SUMMARY }}
<form.Field
@name="summary"
@title={{i18n "discourse_ai.tools.summary"}}
@validation="required|length:1,255"
@format="large"
@tooltip={{i18n "discourse_ai.tools.summary_help"}}
as |field|
>
<field.Input class="ai-tool-editor__summary" />
</form.Field>

{{! PARAMETERS }}
<form.Collection @name="parameters" as |collection index|>
<div class="ai-tool-parameter">
<form.Row as |row|>
<row.Col @size={{6}}>
<collection.Field
@name="name"
@title={{i18n "discourse_ai.tools.parameter_name"}}
@validation="required|length:1,100"
as |field|
>
<field.Input />
</collection.Field>
</row.Col>

<row.Col @size={{6}}>
<collection.Field
@name="type"
@title={{i18n "discourse_ai.tools.parameter_type"}}
@validation="required"
as |field|
>
<field.Menu
@selection={{this.currentParameterSelection data index}}
as |menu|
>
{{#each this.PARAMETER_TYPES as |type|}}
<menu.Item
@value={{type.id}}
data-type={{type.id}}
>{{type.name}}</menu.Item>
{{/each}}
</field.Menu>
</collection.Field>
</row.Col>
</form.Row>

<collection.Field
@name="description"
@title={{i18n "discourse_ai.tools.parameter_description"}}
@validation="required|length:1,1000"
as |field|
>
<field.Input class="ai-tool-editor__parameter-description" />
</collection.Field>

<form.Row as |row|>
<row.Col @size={{4}}>
<collection.Field @name="required" @title="Required" as |field|>
<field.Checkbox />
</collection.Field>
</row.Col>

<row.Col @size={{4}}>
<collection.Field @name="enum" @title="Enum" as |field|>
<field.Checkbox />
</collection.Field>
</row.Col>

<row.Col @size={{4}} class="ai-tool-parameter-actions">
<form.Button
@label="discourse_ai.tools.remove_parameter"
@icon="trash-can"
@action={{fn collection.remove index}}
class="btn-danger"
/>
</row.Col>
</form.Row>
</div>
</form.Collection>

<form.Button
@icon="plus"
@label="discourse_ai.tools.add_parameter"
@action={{fn
form.addItemToCollection
"parameters"
(hash name="" type="string" description="" required=false enum=false)
}}
/>

{{! SCRIPT }}
<form.Field
@name="script"
@title={{i18n "discourse_ai.tools.script"}}
@validation="required|length:1,100000"
@format="full"
as |field|
>
<field.Code @lang="javascript" @height={{600}} />
</form.Field>

{{! Uploads }}
{{#if this.siteSettings.ai_embeddings_enabled}}
<form.Field
@name="rag_uploads"
@title={{i18n "discourse_ai.rag.uploads.title"}}
@tooltip={{this.ragUploadsDescription}}
as |field|
>
<field.Custom>
<RagUploader
@target={{@editingModel}}
@updateUploads={{fn this.updateUploads form.addItemToCollection}}
@onRemove={{fn this.removeUpload form}}
@allowImages={{@settings.rag_images_enabled}}
/>
<RagOptions
@model={{@editingModel}}
@llms={{@llms}}
@allowImages={{@settings.rag_images_enabled}}
/>
</field.Custom>
</form.Field>
{{/if}}

<form.Actions>
{{#unless @isNew}}
<form.Button
@label="discourse_ai.tools.test"
@action={{this.openTestModal}}
class="ai-tool-editor__test-button"
/>

<form.Button
@label="discourse_ai.tools.delete"
@icon="trash-can"
@action={{this.delete}}
class="btn-danger ai-tool-editor__delete"
/>
{{/unless}}

<form.Submit
@label="discourse_ai.tools.save"
class="ai-tool-editor__save"
/>
</form.Actions>
</Form>
</template>
}
Loading