diff --git a/assets/javascripts/discourse/components/ai-embedding-editor.gjs b/assets/javascripts/discourse/components/ai-embedding-editor.gjs index 98c32983..4b4777c8 100644 --- a/assets/javascripts/discourse/components/ai-embedding-editor.gjs +++ b/assets/javascripts/discourse/components/ai-embedding-editor.gjs @@ -1,23 +1,19 @@ import Component from "@glimmer/component"; -import { tracked } from "@glimmer/tracking"; -import { Input, Textarea } from "@ember/component"; +import { cached, tracked } from "@glimmer/tracking"; import { concat, fn, get } from "@ember/helper"; -import { on } from "@ember/modifier"; -import { action, computed } from "@ember/object"; -import didInsert from "@ember/render-modifiers/modifiers/did-insert"; -import didUpdate from "@ember/render-modifiers/modifiers/did-update"; +import { action } from "@ember/object"; import { later } from "@ember/runloop"; import { service } from "@ember/service"; +import { eq, not } from "truth-helpers"; import BackButton from "discourse/components/back-button"; +import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner"; import DButton from "discourse/components/d-button"; +import Form from "discourse/components/form"; import icon from "discourse/helpers/d-icon"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { i18n } from "discourse-i18n"; import AdminSectionLandingItem from "admin/components/admin-section-landing-item"; import AdminSectionLandingWrapper from "admin/components/admin-section-landing-wrapper"; -import ComboBox from "select-kit/components/combo-box"; -import DTooltip from "float-kit/components/d-tooltip"; -import not from "truth-helpers/helpers/not"; export default class AiEmbeddingEditor extends Component { @service toasts; @@ -27,12 +23,35 @@ export default class AiEmbeddingEditor extends Component { @tracked isSaving = false; @tracked selectedPreset = null; - @tracked testRunning = false; @tracked testResult = null; @tracked testError = null; - @tracked apiKeySecret = true; - @tracked editingModel = null; + @tracked currentProvider = null; + + constructor() { + super(...arguments); + if (this.args.model) { + this.currentProvider = this.args.model.provider; + } + } + + @cached + get formData() { + let data; + + if (this.selectedPreset) { + data = this.store + .createRecord("ai-embedding", this.selectedPreset) + .workingCopy(); + } else { + data = this.args.model.workingCopy(); + } + + const originalData = JSON.parse(JSON.stringify(data)); + this._originalFormData = originalData; + + return originalData; + } get selectedProviders() { const t = (provName) => { @@ -50,18 +69,9 @@ export default class AiEmbeddingEditor extends Component { }; return this.args.embeddings.resultSetMeta.distance_functions.map((df) => { - let iconName; - - if (df === "<=>") { - iconName = "discourse-spaceship-operator"; - } else if (df === "<#>") { - iconName = "discourse-negative-inner-product"; - } - return { id: df, name: t(df), - icon: iconName, }; }); } @@ -88,13 +98,17 @@ export default class AiEmbeddingEditor extends Component { return !this.selectedPreset && this.args.model.isNew; } - @computed("editingModel.provider") get metaProviderParams() { - return ( - this.args.embeddings.resultSetMeta.provider_params[ - this.editingModel?.provider - ] || {} - ); + const provider = this.currentProvider; + if (!provider) { + return {}; + } + + const embeddings = this.args.embeddings || {}; + const meta = embeddings.resultSetMeta || {}; + const providerParams = meta.provider_params || {}; + + return providerParams[provider] || {}; } get testErrorMessage() { @@ -105,6 +119,10 @@ export default class AiEmbeddingEditor extends Component { return this.testRunning || this.testResult !== null; } + get seeded() { + return this.args.model.id < 0; + } + @action configurePreset(preset) { this.selectedPreset = @@ -113,40 +131,96 @@ export default class AiEmbeddingEditor extends Component { preset.id ) || {}; - this.editingModel = this.store - .createRecord("ai-embedding", this.selectedPreset) - .workingCopy(); + if (this.selectedPreset.provider) { + this.currentProvider = this.selectedPreset.provider; + } } @action - updateModel() { - this.editingModel = this.args.model.workingCopy(); + setProvider(provider, { set }) { + set("provider", provider); + + this.currentProvider = provider; + + const providerParams = + this.args.embeddings?.resultSetMeta?.provider_params || {}; + const params = providerParams[provider] || {}; + + const initialParams = {}; + + if (params) { + const keys = Object.keys(params); + keys.forEach((key) => { + initialParams[key] = null; + }); + } + + set("provider_params", initialParams); + } + + get providerParams() { + const normalizeParam = (value) => { + if (!value) { + return { type: "text" }; + } + + if (typeof value === "string") { + return { type: value }; + } + + return { + type: value.type || "text", + values: (value.values || []).map((v) => ({ id: v, name: v })), + default: value.default, + }; + }; + + return Object.entries(this.metaProviderParams).reduce( + (acc, [field, value]) => { + acc[field] = normalizeParam(value); + return acc; + }, + {} + ); } @action - makeApiKeySecret() { - this.apiKeySecret = true; + resetForm() { + this.selectedPreset = null; + this.currentProvider = null; } @action - toggleApiKeySecret() { - this.apiKeySecret = !this.apiKeySecret; - } - - @action - async save() { + async save(formData) { this.isSaving = true; const isNew = this.args.model.isNew; try { - await this.editingModel.save(); + const dataToSave = { ...formData }; + + if (this.selectedPreset) { + // new embeddings + const newModel = this.store.createRecord("ai-embedding", { + ...this.selectedPreset, + ...dataToSave, + }); + await newModel.save(); + this.args.embeddings.addObject(newModel); + } else { + // existing embeddings + await this.args.model.save(dataToSave); + } if (isNew) { - this.args.embeddings.addObject(this.editingModel); this.router.transitionTo( "adminPlugins.show.discourse-ai-embeddings.index" ); } else { + const savedProvider = this.currentProvider; + + this._originalFormData = JSON.parse(JSON.stringify(dataToSave)); + this.currentProvider = savedProvider; + this.toasts.success({ data: { message: i18n("discourse_ai.embeddings.saved") }, duration: 2000, @@ -162,11 +236,11 @@ export default class AiEmbeddingEditor extends Component { } @action - async test() { + async test(data) { this.testRunning = true; try { - const configTestResult = await this.editingModel.testConfig(); + const configTestResult = await this.args.model.testConfig(data); this.testResult = configTestResult.success; if (this.testResult) { @@ -191,7 +265,7 @@ export default class AiEmbeddingEditor extends Component { return this.args.model .destroyRecord() .then(() => { - this.args.llms.removeObject(this.args.model); + this.args.embeddings.removeObject(this.args.model); this.router.transitionTo( "adminPlugins.show.discourse-ai-embeddings.index" ); @@ -201,52 +275,44 @@ export default class AiEmbeddingEditor extends Component { }); } - @action - resetForm() { - this.selectedPreset = null; - this.editingModel = null; - } -