import Component from "@glimmer/component"; import { cached, tracked } from "@glimmer/tracking"; import { concat, fn, get } from "@ember/helper"; 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"; export default class AiEmbeddingEditor extends Component { @service toasts; @service router; @service dialog; @service store; @tracked isSaving = false; @tracked selectedPreset = null; @tracked testRunning = false; @tracked testResult = null; @tracked testError = 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) => { return i18n(`discourse_ai.embeddings.providers.${provName}`); }; return this.args.embeddings.resultSetMeta.providers.map((prov) => { return { id: prov, name: t(prov) }; }); } get distanceFunctions() { const t = (df) => { return i18n(`discourse_ai.embeddings.distance_functions.${df}`); }; return this.args.embeddings.resultSetMeta.distance_functions.map((df) => { return { id: df, name: t(df), }; }); } get presets() { const presets = this.args.embeddings.resultSetMeta.presets.map((preset) => { return { name: preset.display_name, id: preset.preset_id, provider: preset.provider, }; }); presets.unshiftObject({ name: i18n("discourse_ai.embeddings.configure_manually"), id: "manual", provider: "fake", }); return presets; } get showPresets() { return !this.selectedPreset && this.args.model.isNew; } get metaProviderParams() { 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() { return i18n("discourse_ai.llms.tests.failure", { error: this.testError }); } get displayTestResult() { return this.testRunning || this.testResult !== null; } get seeded() { return this.args.model.id < 0; } @action configurePreset(preset) { this.selectedPreset = this.args.embeddings.resultSetMeta.presets.findBy( "preset_id", preset.id ) || {}; if (this.selectedPreset.provider) { this.currentProvider = this.selectedPreset.provider; } } @action 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 resetForm() { this.selectedPreset = null; this.currentProvider = null; } @action async save(formData) { this.isSaving = true; const isNew = this.args.model.isNew; try { 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.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, }); } } catch (e) { popupAjaxError(e); } finally { later(() => { this.isSaving = false; }, 1000); } } @action async test(data) { this.testRunning = true; try { let testModel; // new embeddings if (this.args.model.isNew || this.selectedPreset) { testModel = this.store.createRecord("ai-embedding", { ...this.selectedPreset, ...data, }); } else { // existing embeddings testModel = this.args.model; } const configTestResult = await testModel.testConfig(data); this.testResult = configTestResult.success; if (this.testResult) { this.testError = null; } else { this.testError = configTestResult.error; } } catch (e) { popupAjaxError(e); } finally { later(() => { this.testRunning = false; }, 1000); } } @action delete() { return this.dialog.confirm({ message: i18n("discourse_ai.embeddings.confirm_delete"), didConfirm: () => { return this.args.model .destroyRecord() .then(() => { this.args.embeddings.removeObject(this.args.model); this.router.transitionTo( "adminPlugins.show.discourse-ai-embeddings.index" ); }) .catch(popupAjaxError); }, }); } @action providerKeys(providerParams) { return providerParams ? Object.keys(providerParams) : []; } }