Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit 18ecc84

Browse files
UX: move templates to main LLM config tab, restyle (#813)
Restructures LLM config page so it is far clearer. Also corrects bugs around adding LLMs and having LLMs not editable post addition --------- Co-authored-by: Sam Saffron <[email protected]>
1 parent 1002dc8 commit 18ecc84

File tree

11 files changed

+328
-140
lines changed

11 files changed

+328
-140
lines changed

admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-llms-new.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import DiscourseRoute from "discourse/routes/discourse";
22

33
export default DiscourseRoute.extend({
4+
queryParams: {
5+
llmTemplate: { refreshModel: true },
6+
},
7+
48
async model() {
59
const record = this.store.createRecord("ai-llm");
610
record.provider_params = {};
@@ -13,5 +17,9 @@ export default DiscourseRoute.extend({
1317
"allLlms",
1418
this.modelFor("adminPlugins.show.discourse-ai-llms")
1519
);
20+
controller.set(
21+
"llmTemplate",
22+
this.paramsFor(this.routeName).llmTemplate || null
23+
);
1624
},
1725
});
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
<AiLlmsListEditor @llms={{this.allLlms}} @currentLlm={{this.model}} />
1+
<AiLlmsListEditor
2+
@llms={{this.allLlms}}
3+
@currentLlm={{this.model}}
4+
@llmTemplate={{this.llmTemplate}}
5+
/>

app/controllers/discourse_ai/admin/ai_llms_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def create
3636
llm_model = LlmModel.new(ai_llm_params)
3737
if llm_model.save
3838
llm_model.toggle_companion_user
39-
render json: { ai_persona: LlmModelSerializer.new(llm_model) }, status: :created
39+
render json: LlmModelSerializer.new(llm_model), status: :created
4040
else
4141
render_json_error llm_model
4242
end

app/serializers/llm_model_serializer.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
# frozen_string_literal: true
22

33
class LlmModelSerializer < ApplicationSerializer
4-
root "llm"
5-
4+
# TODO: we probably should rename the table LlmModel to AiLlm
5+
# it is consistent with AiPersona and AiTool
6+
# LLM model is a bit confusing given that large langauge model model is a confusing
7+
# name
8+
root "ai_llm"
69
attributes :id,
710
:display_name,
811
:name,

assets/javascripts/discourse/components/ai-llm-editor-form.gjs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,7 @@ export default class AiLlmEditorForm extends Component {
8787
const isNew = this.args.model.isNew;
8888

8989
try {
90-
const result = await this.args.model.save();
91-
92-
this.args.model.setProperties(result.responseJson.ai_persona);
90+
await this.args.model.save();
9391

9492
if (isNew) {
9593
this.args.llms.addObject(this.args.model);
Lines changed: 7 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,19 @@
11
import Component from "@glimmer/component";
2-
import { tracked } from "@glimmer/tracking";
32
import { action } from "@ember/object";
43
import BackButton from "discourse/components/back-button";
5-
import DButton from "discourse/components/d-button";
6-
import I18n from "discourse-i18n";
7-
import ComboBox from "select-kit/components/combo-box";
84
import AiLlmEditorForm from "./ai-llm-editor-form";
95

106
export default class AiLlmEditor extends Component {
11-
@tracked presetConfigured = false;
12-
presetId = "none";
13-
14-
get showPresets() {
15-
return (
16-
this.args.model.isNew && !this.presetConfigured && !this.args.model.url
17-
);
18-
}
19-
20-
get preConfiguredLlms() {
21-
let options = [
22-
{
23-
id: "none",
24-
name: I18n.t(`discourse_ai.llms.preconfigured.none`),
25-
},
26-
];
27-
28-
this.args.llms.resultSetMeta.presets.forEach((llm) => {
29-
if (llm.models) {
30-
llm.models.forEach((model) => {
31-
options.push({
32-
id: `${llm.id}-${model.name}`,
33-
name: model.display_name,
34-
});
35-
});
36-
}
37-
});
38-
39-
return options;
7+
constructor() {
8+
super(...arguments);
9+
if (this.args.llmTemplate) {
10+
this.configurePreset();
11+
}
4012
}
4113

4214
@action
4315
configurePreset() {
44-
this.presetConfigured = true;
45-
46-
let [id, model] = this.presetId.split(/-(.*)/);
16+
let [id, model] = this.args.llmTemplate.split(/-(.*)/);
4717
if (id === "none") {
4818
return;
4919
}
@@ -66,25 +36,6 @@ export default class AiLlmEditor extends Component {
6636
@route="adminPlugins.show.discourse-ai-llms"
6737
@label="discourse_ai.llms.back"
6838
/>
69-
{{#if this.showPresets}}
70-
<form class="form-horizontal ai-llm-editor">
71-
<div class="control-group">
72-
<label>{{I18n.t "discourse_ai.llms.preconfigured_llms"}}</label>
73-
<ComboBox
74-
@value={{this.presetId}}
75-
@content={{this.preConfiguredLlms}}
76-
class="ai-llm-editor__presets"
77-
/>
78-
</div>
79-
80-
<div class="control-group ai-llm-editor__action_panel">
81-
<DButton class="ai-llm-editor__next" @action={{this.configurePreset}}>
82-
{{I18n.t "discourse_ai.llms.next.title"}}
83-
</DButton>
84-
</div>
85-
</form>
86-
{{else}}
87-
<AiLlmEditorForm @model={{@model}} @llms={{@llms}} />
88-
{{/if}}
39+
<AiLlmEditorForm @model={{@model}} @llms={{@llms}} />
8940
</template>
9041
}

assets/javascripts/discourse/components/ai-llms-list-editor.gjs

Lines changed: 163 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,100 @@ import { action } from "@ember/object";
55
import { LinkTo } from "@ember/routing";
66
import { inject as service } from "@ember/service";
77
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
8+
import DButton from "discourse/components/d-button";
89
import DToggleSwitch from "discourse/components/d-toggle-switch";
910
import { popupAjaxError } from "discourse/lib/ajax-error";
1011
import icon from "discourse-common/helpers/d-icon";
1112
import i18n from "discourse-common/helpers/i18n";
1213
import I18n from "discourse-i18n";
14+
import AdminPageSubheader from "admin/components/admin-page-subheader";
1315
import AiLlmEditor from "./ai-llm-editor";
1416

1517
export default class AiLlmsListEditor extends Component {
1618
@service adminPluginNavManager;
19+
@service router;
1720

18-
get hasLLMElements() {
21+
@action
22+
modelDescription(llm) {
23+
// this is a bit of an odd object, it can be an llm model or a preset model
24+
// handle both flavors
25+
26+
// in the case of model
27+
let key = "";
28+
if (typeof llm.id === "number") {
29+
key = `${llm.provider}-${llm.name}`;
30+
} else {
31+
// case of preset
32+
key = llm.id.replace(/\./g, "-");
33+
}
34+
35+
key = `discourse_ai.llms.model_description.${key}`;
36+
if (I18n.lookup(key, { ignoreMissing: true })) {
37+
return I18n.t(key);
38+
}
39+
return "";
40+
}
41+
42+
sanitizedTranslationKey(id) {
43+
return id.replace(/\./g, "-");
44+
}
45+
46+
get hasLlmElements() {
1947
return this.args.llms.length !== 0;
2048
}
2149

50+
get preconfiguredTitle() {
51+
if (this.hasLlmElements) {
52+
return "discourse_ai.llms.preconfigured.title";
53+
} else {
54+
return "discourse_ai.llms.preconfigured.title_no_llms";
55+
}
56+
}
57+
58+
get preConfiguredLlms() {
59+
const options = [
60+
{
61+
id: "none",
62+
name: I18n.t("discourse_ai.llms.preconfigured.fake"),
63+
provider: "fake",
64+
},
65+
];
66+
67+
const llmsContent = this.args.llms.content.map((llm) => ({
68+
provider: llm.provider,
69+
name: llm.name,
70+
}));
71+
72+
this.args.llms.resultSetMeta.presets.forEach((llm) => {
73+
if (llm.models) {
74+
llm.models.forEach((model) => {
75+
const id = `${llm.id}-${model.name}`;
76+
const isConfigured = llmsContent.some(
77+
(content) =>
78+
content.provider === llm.provider && content.name === model.name
79+
);
80+
81+
if (!isConfigured) {
82+
options.push({
83+
id,
84+
name: model.display_name,
85+
provider: llm.provider,
86+
});
87+
}
88+
});
89+
}
90+
});
91+
92+
return options;
93+
}
94+
95+
@action
96+
transitionToLlmEditor(llmTemplate) {
97+
this.router.transitionTo("adminPlugins.show.discourse-ai-llms.new", {
98+
queryParams: { llmTemplate },
99+
});
100+
}
101+
22102
@action
23103
async toggleEnabledChatBot(llm) {
24104
const oldValue = llm.enabled_chat_bot;
@@ -39,60 +119,92 @@ export default class AiLlmsListEditor extends Component {
39119
@path="/admin/plugins/{{this.adminPluginNavManager.currentPlugin.name}}/ai-llms"
40120
@label={{i18n "discourse_ai.llms.short_title"}}
41121
/>
42-
<section class="ai-llms-list-editor admin-detail pull-left">
43-
122+
<section class="ai-llm-list-editor admin-detail">
44123
{{#if @currentLlm}}
45-
<AiLlmEditor @model={{@currentLlm}} @llms={{@llms}} />
124+
<AiLlmEditor
125+
@model={{@currentLlm}}
126+
@llms={{@llms}}
127+
@llmTemplate={{@llmTemplate}}
128+
/>
46129
{{else}}
47-
<div class="ai-llms-list-editor__header">
48-
<h3>{{i18n "discourse_ai.llms.short_title"}}</h3>
49-
{{#unless @currentLlm.isNew}}
50-
<LinkTo
51-
@route="adminPlugins.show.discourse-ai-llms.new"
52-
class="btn btn-small btn-primary ai-llms-list-editor__new"
53-
>
54-
{{icon "plus"}}
55-
<span>{{I18n.t "discourse_ai.llms.new"}}</span>
56-
</LinkTo>
57-
{{/unless}}
58-
</div>
59-
60-
{{#if this.hasLLMElements}}
61-
<table class="content-list ai-persona-list-editor">
62-
<thead>
63-
<tr>
64-
<th>{{i18n "discourse_ai.llms.display_name"}}</th>
65-
<th>{{i18n "discourse_ai.llms.provider"}}</th>
66-
<th>{{i18n "discourse_ai.llms.enabled_chat_bot"}}</th>
67-
<th></th>
68-
</tr>
69-
</thead>
70-
<tbody>
71-
{{#each @llms as |llm|}}
72-
<tr data-persona-id={{llm.id}} class="ai-llm-list__row">
73-
<td><strong>{{llm.display_name}}</strong></td>
74-
<td>{{i18n
75-
(concat "discourse_ai.llms.providers." llm.provider)
76-
}}</td>
77-
<td>
78-
<DToggleSwitch
79-
@state={{llm.enabled_chat_bot}}
80-
{{on "click" (fn this.toggleEnabledChatBot llm)}}
81-
/>
82-
</td>
83-
<td>
84-
<LinkTo
85-
@route="adminPlugins.show.discourse-ai-llms.show"
86-
current-when="true"
87-
class="btn btn-text btn-small"
88-
@model={{llm}}
89-
>{{i18n "discourse_ai.llms.edit"}}</LinkTo>
90-
</td>
130+
{{#if this.hasLlmElements}}
131+
<section class="ai-llms-list-editor__configured">
132+
<AdminPageSubheader
133+
@titleLabel="discourse_ai.llms.configured.title"
134+
/>
135+
<table>
136+
<thead>
137+
<tr>
138+
<th>{{i18n "discourse_ai.llms.display_name"}}</th>
139+
<th>{{i18n "discourse_ai.llms.provider"}}</th>
140+
<th>{{i18n "discourse_ai.llms.enabled_chat_bot"}}</th>
141+
<th></th>
91142
</tr>
92-
{{/each}}
93-
</tbody>
94-
</table>
143+
</thead>
144+
<tbody>
145+
{{#each @llms as |llm|}}
146+
<tr data-persona-id={{llm.id}} class="ai-llm-list__row">
147+
<td class="column-name">
148+
<h3>{{llm.display_name}}</h3>
149+
<p>
150+
{{this.modelDescription llm}}
151+
</p>
152+
</td>
153+
<td>
154+
{{i18n
155+
(concat "discourse_ai.llms.providers." llm.provider)
156+
}}
157+
</td>
158+
<td>
159+
<DToggleSwitch
160+
@state={{llm.enabled_chat_bot}}
161+
{{on "click" (fn this.toggleEnabledChatBot llm)}}
162+
/>
163+
</td>
164+
<td class="column-edit">
165+
<LinkTo
166+
@route="adminPlugins.show.discourse-ai-llms.show"
167+
class="btn btn-default"
168+
@model={{llm.id}}
169+
>
170+
{{icon "wrench"}}
171+
<div class="d-button-label">
172+
{{i18n "discourse_ai.llms.edit"}}
173+
</div>
174+
</LinkTo>
175+
</td>
176+
</tr>
177+
{{/each}}
178+
</tbody>
179+
</table>
180+
</section>
95181
{{/if}}
182+
<section class="ai-llms-list-editor__templates">
183+
<AdminPageSubheader @titleLabel={{this.preconfiguredTitle}} />
184+
<div class="ai-llms-list-editor__templates-list">
185+
{{#each this.preConfiguredLlms as |llm|}}
186+
<div
187+
data-llm-id={{llm.id}}
188+
class="ai-llms-list-editor__templates-list-item"
189+
>
190+
<h4>
191+
{{i18n (concat "discourse_ai.llms.providers." llm.provider)}}
192+
</h4>
193+
<h3>
194+
{{llm.name}}
195+
</h3>
196+
<p>
197+
{{this.modelDescription llm}}
198+
</p>
199+
<DButton
200+
@action={{fn this.transitionToLlmEditor llm.id}}
201+
@icon="gear"
202+
@label="discourse_ai.llms.preconfigured.button"
203+
/>
204+
</div>
205+
{{/each}}
206+
</div>
207+
</section>
96208
{{/if}}
97209
</section>
98210
</template>

0 commit comments

Comments
 (0)