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,6 +1,10 @@
import DiscourseRoute from "discourse/routes/discourse";

export default DiscourseRoute.extend({
queryParams: {
llmTemplate: { refreshModel: true },
},

async model() {
const record = this.store.createRecord("ai-llm");
record.provider_params = {};
Expand All @@ -13,5 +17,9 @@ export default DiscourseRoute.extend({
"allLlms",
this.modelFor("adminPlugins.show.discourse-ai-llms")
);
controller.set(
"llmTemplate",
this.paramsFor(this.routeName).llmTemplate || null
);
},
});
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
<AiLlmsListEditor @llms={{this.allLlms}} @currentLlm={{this.model}} />
<AiLlmsListEditor
@llms={{this.allLlms}}
@currentLlm={{this.model}}
@llmTemplate={{this.llmTemplate}}
/>
2 changes: 1 addition & 1 deletion app/controllers/discourse_ai/admin/ai_llms_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def create
llm_model = LlmModel.new(ai_llm_params)
if llm_model.save
llm_model.toggle_companion_user
render json: { ai_persona: LlmModelSerializer.new(llm_model) }, status: :created
render json: LlmModelSerializer.new(llm_model), status: :created
else
render_json_error llm_model
end
Expand Down
7 changes: 5 additions & 2 deletions app/serializers/llm_model_serializer.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# frozen_string_literal: true

class LlmModelSerializer < ApplicationSerializer
root "llm"

# TODO: we probably should rename the table LlmModel to AiLlm
# it is consistent with AiPersona and AiTool
# LLM model is a bit confusing given that large langauge model model is a confusing
# name
root "ai_llm"
attributes :id,
:display_name,
:name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,7 @@ export default class AiLlmEditorForm extends Component {
const isNew = this.args.model.isNew;

try {
const result = await this.args.model.save();

this.args.model.setProperties(result.responseJson.ai_persona);
await this.args.model.save();

if (isNew) {
this.args.llms.addObject(this.args.model);
Expand Down
63 changes: 7 additions & 56 deletions assets/javascripts/discourse/components/ai-llm-editor.gjs
Original file line number Diff line number Diff line change
@@ -1,49 +1,19 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import BackButton from "discourse/components/back-button";
import DButton from "discourse/components/d-button";
import I18n from "discourse-i18n";
import ComboBox from "select-kit/components/combo-box";
import AiLlmEditorForm from "./ai-llm-editor-form";

export default class AiLlmEditor extends Component {
@tracked presetConfigured = false;
presetId = "none";

get showPresets() {
return (
this.args.model.isNew && !this.presetConfigured && !this.args.model.url
);
}

get preConfiguredLlms() {
let options = [
{
id: "none",
name: I18n.t(`discourse_ai.llms.preconfigured.none`),
},
];

this.args.llms.resultSetMeta.presets.forEach((llm) => {
if (llm.models) {
llm.models.forEach((model) => {
options.push({
id: `${llm.id}-${model.name}`,
name: model.display_name,
});
});
}
});

return options;
constructor() {
super(...arguments);
if (this.args.llmTemplate) {
this.configurePreset();
}
}

@action
configurePreset() {
this.presetConfigured = true;

let [id, model] = this.presetId.split(/-(.*)/);
let [id, model] = this.args.llmTemplate.split(/-(.*)/);
if (id === "none") {
return;
}
Expand All @@ -66,25 +36,6 @@ export default class AiLlmEditor extends Component {
@route="adminPlugins.show.discourse-ai-llms"
@label="discourse_ai.llms.back"
/>
{{#if this.showPresets}}
<form class="form-horizontal ai-llm-editor">
<div class="control-group">
<label>{{I18n.t "discourse_ai.llms.preconfigured_llms"}}</label>
<ComboBox
@value={{this.presetId}}
@content={{this.preConfiguredLlms}}
class="ai-llm-editor__presets"
/>
</div>

<div class="control-group ai-llm-editor__action_panel">
<DButton class="ai-llm-editor__next" @action={{this.configurePreset}}>
{{I18n.t "discourse_ai.llms.next.title"}}
</DButton>
</div>
</form>
{{else}}
<AiLlmEditorForm @model={{@model}} @llms={{@llms}} />
{{/if}}
<AiLlmEditorForm @model={{@model}} @llms={{@llms}} />
</template>
}
214 changes: 163 additions & 51 deletions assets/javascripts/discourse/components/ai-llms-list-editor.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,100 @@ import { action } from "@ember/object";
import { LinkTo } from "@ember/routing";
import { inject as service } from "@ember/service";
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
import DButton from "discourse/components/d-button";
import DToggleSwitch from "discourse/components/d-toggle-switch";
import { popupAjaxError } from "discourse/lib/ajax-error";
import icon from "discourse-common/helpers/d-icon";
import i18n from "discourse-common/helpers/i18n";
import I18n from "discourse-i18n";
import AdminPageSubheader from "admin/components/admin-page-subheader";
import AiLlmEditor from "./ai-llm-editor";

export default class AiLlmsListEditor extends Component {
@service adminPluginNavManager;
@service router;

get hasLLMElements() {
@action
modelDescription(llm) {
// this is a bit of an odd object, it can be an llm model or a preset model
// handle both flavors

// in the case of model
let key = "";
if (typeof llm.id === "number") {
key = `${llm.provider}-${llm.name}`;
} else {
// case of preset
key = llm.id.replace(/\./g, "-");
}

key = `discourse_ai.llms.model_description.${key}`;
if (I18n.lookup(key, { ignoreMissing: true })) {
return I18n.t(key);
}
return "";
}

sanitizedTranslationKey(id) {
return id.replace(/\./g, "-");
}

get hasLlmElements() {
return this.args.llms.length !== 0;
}

get preconfiguredTitle() {
if (this.hasLlmElements) {
return "discourse_ai.llms.preconfigured.title";
} else {
return "discourse_ai.llms.preconfigured.title_no_llms";
}
}

get preConfiguredLlms() {
const options = [
{
id: "none",
name: I18n.t("discourse_ai.llms.preconfigured.fake"),
provider: "fake",
},
];

const llmsContent = this.args.llms.content.map((llm) => ({
provider: llm.provider,
name: llm.name,
}));

this.args.llms.resultSetMeta.presets.forEach((llm) => {
if (llm.models) {
llm.models.forEach((model) => {
const id = `${llm.id}-${model.name}`;
const isConfigured = llmsContent.some(
(content) =>
content.provider === llm.provider && content.name === model.name
);

if (!isConfigured) {
options.push({
id,
name: model.display_name,
provider: llm.provider,
});
}
});
}
});

return options;
}

@action
transitionToLlmEditor(llmTemplate) {
this.router.transitionTo("adminPlugins.show.discourse-ai-llms.new", {
queryParams: { llmTemplate },
});
}

@action
async toggleEnabledChatBot(llm) {
const oldValue = llm.enabled_chat_bot;
Expand All @@ -39,60 +119,92 @@ export default class AiLlmsListEditor extends Component {
@path="/admin/plugins/{{this.adminPluginNavManager.currentPlugin.name}}/ai-llms"
@label={{i18n "discourse_ai.llms.short_title"}}
/>
<section class="ai-llms-list-editor admin-detail pull-left">

<section class="ai-llm-list-editor admin-detail">
{{#if @currentLlm}}
<AiLlmEditor @model={{@currentLlm}} @llms={{@llms}} />
<AiLlmEditor
@model={{@currentLlm}}
@llms={{@llms}}
@llmTemplate={{@llmTemplate}}
/>
{{else}}
<div class="ai-llms-list-editor__header">
<h3>{{i18n "discourse_ai.llms.short_title"}}</h3>
{{#unless @currentLlm.isNew}}
<LinkTo
@route="adminPlugins.show.discourse-ai-llms.new"
class="btn btn-small btn-primary ai-llms-list-editor__new"
>
{{icon "plus"}}
<span>{{I18n.t "discourse_ai.llms.new"}}</span>
</LinkTo>
{{/unless}}
</div>

{{#if this.hasLLMElements}}
<table class="content-list ai-persona-list-editor">
<thead>
<tr>
<th>{{i18n "discourse_ai.llms.display_name"}}</th>
<th>{{i18n "discourse_ai.llms.provider"}}</th>
<th>{{i18n "discourse_ai.llms.enabled_chat_bot"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{#each @llms as |llm|}}
<tr data-persona-id={{llm.id}} class="ai-llm-list__row">
<td><strong>{{llm.display_name}}</strong></td>
<td>{{i18n
(concat "discourse_ai.llms.providers." llm.provider)
}}</td>
<td>
<DToggleSwitch
@state={{llm.enabled_chat_bot}}
{{on "click" (fn this.toggleEnabledChatBot llm)}}
/>
</td>
<td>
<LinkTo
@route="adminPlugins.show.discourse-ai-llms.show"
current-when="true"
class="btn btn-text btn-small"
@model={{llm}}
>{{i18n "discourse_ai.llms.edit"}}</LinkTo>
</td>
{{#if this.hasLlmElements}}
<section class="ai-llms-list-editor__configured">
<AdminPageSubheader
@titleLabel="discourse_ai.llms.configured.title"
/>
<table>
<thead>
<tr>
<th>{{i18n "discourse_ai.llms.display_name"}}</th>
<th>{{i18n "discourse_ai.llms.provider"}}</th>
<th>{{i18n "discourse_ai.llms.enabled_chat_bot"}}</th>
<th></th>
</tr>
{{/each}}
</tbody>
</table>
</thead>
<tbody>
{{#each @llms as |llm|}}
<tr data-persona-id={{llm.id}} class="ai-llm-list__row">
<td class="column-name">
<h3>{{llm.display_name}}</h3>
<p>
{{this.modelDescription llm}}
</p>
</td>
<td>
{{i18n
(concat "discourse_ai.llms.providers." llm.provider)
}}
</td>
<td>
<DToggleSwitch
@state={{llm.enabled_chat_bot}}
{{on "click" (fn this.toggleEnabledChatBot llm)}}
/>
</td>
<td class="column-edit">
<LinkTo
@route="adminPlugins.show.discourse-ai-llms.show"
class="btn btn-default"
@model={{llm.id}}
>
{{icon "wrench"}}
<div class="d-button-label">
{{i18n "discourse_ai.llms.edit"}}
</div>
</LinkTo>
</td>
</tr>
{{/each}}
</tbody>
</table>
</section>
{{/if}}
<section class="ai-llms-list-editor__templates">
<AdminPageSubheader @titleLabel={{this.preconfiguredTitle}} />
<div class="ai-llms-list-editor__templates-list">
{{#each this.preConfiguredLlms as |llm|}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would really prefer these "section landing" tiles are reusable components, this is why I opened this draft PR https://github.com/discourse/discourse/pull/28477/files . At the least could you please extract this to a component in this PR then we can replace with a core version later?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets do this in a followup, really want to land this. I addressed the rest of the feedback.

<div
data-llm-id={{llm.id}}
class="ai-llms-list-editor__templates-list-item"
>
<h4>
{{i18n (concat "discourse_ai.llms.providers." llm.provider)}}
</h4>
<h3>
{{llm.name}}
</h3>
<p>
{{this.modelDescription llm}}
</p>
<DButton
@action={{fn this.transitionToLlmEditor llm.id}}
@icon="gear"
@label="discourse_ai.llms.preconfigured.button"
/>
</div>
{{/each}}
</div>
</section>
{{/if}}
</section>
</template>
Expand Down
Loading