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

Commit 73768ce

Browse files
authored
FEATURE: Display bot in feature list (#1466)
- allows features to have multiple llms and multiple personas - sorts module list - adds Bot as a first class module - fixes issue where search module was always configured - some tests
1 parent a40e2d3 commit 73768ce

File tree

11 files changed

+419
-134
lines changed

11 files changed

+419
-134
lines changed

admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-features-edit.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { action } from "@ember/object";
12
import { ajax } from "discourse/lib/ajax";
23
import DiscourseRoute from "discourse/routes/discourse";
34
import SiteSetting from "admin/models/site-setting";
@@ -24,4 +25,11 @@ export default class AdminPluginsShowDiscourseAiFeaturesEdit extends DiscourseRo
2425

2526
return currentFeature;
2627
}
28+
29+
@action
30+
willTransition() {
31+
// site settings may amend if a feature is enabled or disabled, so refresh the model
32+
// even on back button
33+
this.router.refresh("adminPlugins.show.discourse-ai-features");
34+
}
2735
}

app/controllers/discourse_ai/admin/ai_features_controller.rb

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ def serialize_module(a_module)
3737
def serialize_feature(feature)
3838
{
3939
name: feature.name,
40-
persona: serialize_persona(persona_id_obj_hash[feature.persona_id]),
41-
llm_model: {
42-
id: feature.llm_model&.id,
43-
name: feature.llm_model&.name,
44-
},
40+
personas: feature.persona_ids.map { |id| serialize_persona(persona_id_obj_hash[id]) },
41+
llm_models:
42+
feature.llm_models.map do |llm_model|
43+
{ id: llm_model.id, name: llm_model.display_name }
44+
end,
4545
enabled: feature.enabled?,
4646
}
4747
end
@@ -57,9 +57,7 @@ def serialize_persona(persona)
5757
def persona_id_obj_hash
5858
@persona_id_obj_hash ||=
5959
begin
60-
setting_names = DiscourseAi::Configuration::Feature.all_persona_setting_names
61-
ids = setting_names.map { |sn| SiteSetting.public_send(sn) }
62-
60+
ids = DiscourseAi::Configuration::Feature.all.map(&:persona_ids).flatten.uniq
6361
AiPersona.where(id: ids).index_by(&:id)
6462
end
6563
end
Lines changed: 195 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,206 @@
1+
import Component from "@glimmer/component";
2+
import { tracked } from "@glimmer/tracking";
13
import { concat } from "@ember/helper";
2-
import { gt } from "truth-helpers";
4+
import { action } from "@ember/object";
35
import DButton from "discourse/components/d-button";
46
import { i18n } from "discourse-i18n";
57

6-
const AiFeaturesList = <template>
7-
<div class="ai-features-list">
8-
{{#each @modules as |module|}}
9-
<div class="ai-module" data-module-name={{module.module_name}}>
10-
<div class="ai-module__header">
11-
<div class="ai-module__module-title">
12-
<h3>{{i18n
13-
(concat "discourse_ai.features." module.module_name ".name")
14-
}}</h3>
15-
<DButton
16-
class="edit"
17-
@label="discourse_ai.features.edit"
18-
@route="adminPlugins.show.discourse-ai-features.edit"
19-
@routeModels={{module.id}}
20-
/>
8+
class ExpandableList extends Component {
9+
@tracked isExpanded = false;
10+
11+
get maxItemsToShow() {
12+
return this.args.maxItemsToShow ?? 5;
13+
}
14+
15+
get hasMore() {
16+
return this.args.items?.length > this.maxItemsToShow;
17+
}
18+
19+
get visibleItems() {
20+
if (!this.args.items) {
21+
return [];
22+
}
23+
return this.isExpanded
24+
? this.args.items
25+
: this.args.items.slice(0, this.maxItemsToShow);
26+
}
27+
28+
get remainingCount() {
29+
return this.args.items?.length - this.maxItemsToShow;
30+
}
31+
32+
get expandToggleLabel() {
33+
if (this.isExpanded) {
34+
return i18n("discourse_ai.features.collapse_list");
35+
} else {
36+
return i18n("discourse_ai.features.expand_list", {
37+
count: this.remainingCount,
38+
});
39+
}
40+
}
41+
42+
@action
43+
toggleExpanded() {
44+
this.isExpanded = !this.isExpanded;
45+
}
46+
47+
<template>
48+
{{#each this.visibleItems as |item index|}}
49+
{{yield item index}}
50+
{{/each}}
51+
52+
{{#if this.hasMore}}
53+
<DButton
54+
class="btn-flat btn-small ai-expanded-list__toggle-button"
55+
@translatedLabel={{this.expandToggleLabel}}
56+
@action={{this.toggleExpanded}}
57+
/>
58+
{{/if}}
59+
</template>
60+
}
61+
62+
export default class AiFeaturesList extends Component {
63+
get sortedModules() {
64+
return this.args.modules.sort((a, b) => {
65+
const nameA = i18n(`discourse_ai.features.${a.module_name}.name`);
66+
const nameB = i18n(`discourse_ai.features.${b.module_name}.name`);
67+
return nameA.localeCompare(nameB);
68+
});
69+
}
70+
71+
@action
72+
hasGroups(feature) {
73+
return this.groupList(feature).length > 0;
74+
}
75+
76+
@action
77+
groupList(feature) {
78+
const groups = [];
79+
const groupIds = new Set();
80+
if (feature.personas) {
81+
feature.personas.forEach((persona) => {
82+
if (persona.allowed_groups) {
83+
persona.allowed_groups.forEach((group) => {
84+
if (!groupIds.has(group.id)) {
85+
groupIds.add(group.id);
86+
groups.push(group);
87+
}
88+
});
89+
}
90+
});
91+
}
92+
return groups;
93+
}
94+
95+
<template>
96+
<div class="ai-features-list">
97+
{{#each this.sortedModules as |module|}}
98+
<div class="ai-module" data-module-name={{module.module_name}}>
99+
<div class="ai-module__header">
100+
<div class="ai-module__module-title">
101+
<h3>{{i18n
102+
(concat "discourse_ai.features." module.module_name ".name")
103+
}}</h3>
104+
<DButton
105+
class="edit"
106+
@label="discourse_ai.features.edit"
107+
@route="adminPlugins.show.discourse-ai-features.edit"
108+
@routeModels={{module.id}}
109+
/>
110+
</div>
111+
<div>{{i18n
112+
(concat
113+
"discourse_ai.features." module.module_name ".description"
114+
)
115+
}}</div>
21116
</div>
22-
<div>{{i18n
23-
(concat
24-
"discourse_ai.features." module.module_name ".description"
25-
)
26-
}}</div>
27-
</div>
28117

29-
<div class="admin-section-landing-wrapper ai-feature-cards">
30-
{{#each module.features as |feature|}}
31-
<div
32-
class="admin-section-landing-item ai-feature-card"
33-
data-feature-name={{feature.name}}
34-
>
35-
<div class="admin-section-landing-item__content">
36-
<div class="ai-feature-card__feature-name">
37-
{{i18n
38-
(concat
39-
"discourse_ai.features."
40-
module.module_name
41-
"."
42-
feature.name
43-
)
44-
}}
45-
{{#unless feature.enabled}}
46-
<span>{{i18n "discourse_ai.features.disabled"}}</span>
47-
{{/unless}}
48-
</div>
49-
<div class="ai-feature-card__persona">
50-
<span>{{i18n "discourse_ai.features.persona"}}</span>
51-
{{#if feature.persona}}
52-
<DButton
53-
class="btn-flat btn-small ai-feature-card__persona-button"
54-
@translatedLabel={{feature.persona.name}}
55-
@route="adminPlugins.show.discourse-ai-personas.edit"
56-
@routeModels={{feature.persona.id}}
57-
/>
58-
{{else}}
59-
{{i18n "discourse_ai.features.no_persona"}}
60-
{{/if}}
61-
</div>
62-
<div class="ai-feature-card__llm">
63-
<span>{{i18n "discourse_ai.features.llm"}}</span>
64-
{{#if feature.llm_model.name}}
65-
<DButton
66-
class="btn-flat btn-small ai-feature-card__llm-button"
67-
@translatedLabel={{feature.llm_model.name}}
68-
@route="adminPlugins.show.discourse-ai-llms.edit"
69-
@routeModels={{feature.llm_model.id}}
70-
/>
71-
{{else}}
72-
{{i18n "discourse_ai.features.no_llm"}}
73-
{{/if}}
74-
</div>
75-
{{#if feature.persona}}
76-
<div class="ai-feature-card__groups">
77-
<span>{{i18n "discourse_ai.features.groups"}}</span>
78-
{{#if (gt feature.persona.allowed_groups.length 0)}}
79-
<ul class="ai-feature-card__item-groups">
80-
{{#each feature.persona.allowed_groups as |group|}}
81-
<li>{{group.name}}</li>
82-
{{/each}}
83-
</ul>
118+
<div class="admin-section-landing-wrapper ai-feature-cards">
119+
{{#each module.features as |feature|}}
120+
<div
121+
class="admin-section-landing-item ai-feature-card"
122+
data-feature-name={{feature.name}}
123+
>
124+
<div class="admin-section-landing-item__content">
125+
<div class="ai-feature-card__feature-name">
126+
{{i18n
127+
(concat
128+
"discourse_ai.features."
129+
module.module_name
130+
"."
131+
feature.name
132+
)
133+
}}
134+
{{#unless feature.enabled}}
135+
<span>{{i18n "discourse_ai.features.disabled"}}</span>
136+
{{/unless}}
137+
</div>
138+
<div class="ai-feature-card__persona">
139+
<span>{{i18n
140+
"discourse_ai.features.persona"
141+
count=feature.personas.length
142+
}}</span>
143+
{{#if feature.personas}}
144+
<ExpandableList
145+
@items={{feature.personas}}
146+
@maxItemsToShow={{5}}
147+
as |persona|
148+
>
149+
<DButton
150+
class="btn-flat btn-small ai-feature-card__persona-button"
151+
@translatedLabel={{persona.name}}
152+
@route="adminPlugins.show.discourse-ai-personas.edit"
153+
@routeModels={{persona.id}}
154+
/>
155+
</ExpandableList>
84156
{{else}}
85-
{{i18n "discourse_ai.features.no_groups"}}
157+
{{i18n "discourse_ai.features.no_persona"}}
86158
{{/if}}
87159
</div>
88-
{{/if}}
160+
<div class="ai-feature-card__llm">
161+
{{#if feature.llm_models}}
162+
<span>{{i18n
163+
"discourse_ai.features.llm"
164+
count=feature.llm_models.length
165+
}}</span>
166+
{{/if}}
167+
{{#if feature.llm_models}}
168+
<ExpandableList
169+
@items={{feature.llm_models}}
170+
@maxItemsToShow={{5}}
171+
as |llm|
172+
>
173+
<DButton
174+
class="btn-flat btn-small ai-feature-card__llm-button"
175+
@translatedLabel={{llm.name}}
176+
@route="adminPlugins.show.discourse-ai-llms.edit"
177+
@routeModels={{llm.id}}
178+
/>
179+
</ExpandableList>
180+
{{else}}
181+
{{i18n "discourse_ai.features.no_llm"}}
182+
{{/if}}
183+
</div>
184+
{{#if feature.personas}}
185+
<div class="ai-feature-card__groups">
186+
<span>{{i18n "discourse_ai.features.groups"}}</span>
187+
{{#if (this.hasGroups feature)}}
188+
<ul class="ai-feature-card__item-groups">
189+
{{#each (this.groupList feature) as |group|}}
190+
<li>{{group.name}}</li>
191+
{{/each}}
192+
</ul>
193+
{{else}}
194+
{{i18n "discourse_ai.features.no_groups"}}
195+
{{/if}}
196+
</div>
197+
{{/if}}
198+
</div>
89199
</div>
90-
</div>
91-
{{/each}}
200+
{{/each}}
201+
</div>
92202
</div>
93-
</div>
94-
{{/each}}
95-
</div>
96-
</template>;
97-
98-
export default AiFeaturesList;
203+
{{/each}}
204+
</div>
205+
</template>
206+
}

assets/stylesheets/common/ai-features.scss

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,18 @@
2222
background: var(--primary-very-low);
2323
border: 1px solid var(--primary-low);
2424
padding: 0.5rem;
25-
display: block;
25+
display: flex;
26+
flex-direction: column;
2627

2728
&__llm,
2829
&__persona,
2930
&__groups {
3031
font-size: var(--font-down-1-rem);
32+
display: flex;
33+
flex-flow: row wrap;
34+
gap: 0.1em;
35+
margin-top: 0.5rem;
36+
align-items: center;
3137
}
3238

3339
&__persona {
@@ -36,7 +42,7 @@
3642

3743
&__persona-button,
3844
&__llm-button {
39-
padding-left: 0;
45+
padding-left: 0.2em;
4046
}
4147

4248
&__groups {

config/locales/client.en.yml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,13 +186,25 @@ en:
186186
description: "These are the AI features available to visitors on your site. These can be configured to use specific personas and LLMs, and can be access controlled by groups."
187187
back: "Back"
188188
disabled: "(disabled)"
189-
persona: "Persona:"
189+
persona:
190+
one: "Persona:"
191+
other: "Personas:"
190192
groups: "Groups:"
191-
llm: "LLM:"
193+
llm:
194+
one: "LLM:"
195+
other: "LLMs:"
192196
no_llm: "No LLM selected"
193197
no_persona: "Not set"
194198
no_groups: "None"
195199
edit: "Edit"
200+
expand_list:
201+
one: "(%{count} more)"
202+
other: "(%{count} more)"
203+
collapse_list: "(show less)"
204+
bot:
205+
bot: "Chatbot"
206+
name: "Bot"
207+
description: "A chat bot that can answer questions and assist users in private messagges, forum and in chat"
196208
nav:
197209
configured: "Configured"
198210
unconfigured: "Unconfigured"

0 commit comments

Comments
 (0)