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

Commit 054ff5f

Browse files
committed
UX: add filter to features page, update styles
1 parent b35f9bc commit 054ff5f

File tree

4 files changed

+278
-68
lines changed

4 files changed

+278
-68
lines changed

assets/javascripts/discourse/components/ai-features-list.gjs

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,19 @@ class ExpandableList extends Component {
4444
this.isExpanded = !this.isExpanded;
4545
}
4646

47+
@action
48+
isLastItem(index) {
49+
return index === this.visibleItems.length - 1;
50+
}
51+
4752
<template>
4853
{{#each this.visibleItems as |item index|}}
49-
{{yield item index}}
54+
{{yield item index this.isLastItem}}
5055
{{/each}}
5156

5257
{{#if this.hasMore}}
5358
<DButton
54-
class="btn-flat btn-small ai-expanded-list__toggle-button"
59+
class="btn-flat ai-expanded-list__toggle-button"
5560
@translatedLabel={{this.expandToggleLabel}}
5661
@action={{this.toggleExpanded}}
5762
/>
@@ -61,11 +66,11 @@ class ExpandableList extends Component {
6166

6267
export default class AiFeaturesList extends Component {
6368
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+
if (!this.args.modules || !this.args.modules.length) {
70+
return [];
71+
}
72+
73+
return this.args.modules.sortBy("module_name");
6974
}
7075

7176
@action
@@ -136,62 +141,74 @@ export default class AiFeaturesList extends Component {
136141
{{/unless}}
137142
</div>
138143
<div class="ai-feature-card__persona">
139-
<span>{{i18n
144+
<span class="ai-feature-card__label">
145+
{{i18n
140146
"discourse_ai.features.persona"
141147
count=feature.personas.length
142-
}}</span>
148+
}}
149+
</span>
143150
{{#if feature.personas}}
144151
<ExpandableList
145152
@items={{feature.personas}}
146153
@maxItemsToShow={{5}}
147-
as |persona|
154+
as |persona index isLastItem|
148155
>
149156
<DButton
150-
class="btn-flat btn-small ai-feature-card__persona-button"
151-
@translatedLabel={{persona.name}}
157+
class="btn-flat ai-feature-card__persona-button btn-text"
158+
@translatedLabel={{concat persona.name (unless (isLastItem index) ", ")}}
152159
@route="adminPlugins.show.discourse-ai-personas.edit"
153160
@routeModels={{persona.id}}
154161
/>
155162
</ExpandableList>
156163
{{else}}
157-
{{i18n "discourse_ai.features.no_persona"}}
164+
<span class="ai-feature-card__label">
165+
{{i18n "discourse_ai.features.no_persona"}}
166+
</span>
158167
{{/if}}
159168
</div>
160169
<div class="ai-feature-card__llm">
161170
{{#if feature.llm_models}}
162-
<span>{{i18n
171+
<span class="ai-feature-card__label">
172+
{{i18n
163173
"discourse_ai.features.llm"
164174
count=feature.llm_models.length
165-
}}</span>
175+
}}
176+
</span>
166177
{{/if}}
167178
{{#if feature.llm_models}}
168179
<ExpandableList
169180
@items={{feature.llm_models}}
170181
@maxItemsToShow={{5}}
171-
as |llm|
182+
as |llm index isLastItem|
172183
>
173184
<DButton
174-
class="btn-flat btn-small ai-feature-card__llm-button"
175-
@translatedLabel={{llm.name}}
185+
class="btn-flat ai-feature-card__llm-button"
186+
@translatedLabel={{concat llm.name (unless (isLastItem index) ", ")}}
176187
@route="adminPlugins.show.discourse-ai-llms.edit"
177188
@routeModels={{llm.id}}
178189
/>
179190
</ExpandableList>
180191
{{else}}
181-
{{i18n "discourse_ai.features.no_llm"}}
192+
<span class="ai-feature-card__label">
193+
{{i18n "discourse_ai.features.no_llm"}}
194+
</span>
182195
{{/if}}
183196
</div>
184197
{{#if feature.personas}}
185198
<div class="ai-feature-card__groups">
186-
<span>{{i18n "discourse_ai.features.groups"}}</span>
199+
<span class="ai-feature-card__label">
200+
{{i18n "discourse_ai.features.groups"}}
201+
</span>
187202
{{#if (this.hasGroups feature)}}
188203
<ul class="ai-feature-card__item-groups">
189204
{{#each (this.groupList feature) as |group|}}
190205
<li>{{group.name}}</li>
191206
{{/each}}
192207
</ul>
193208
{{else}}
194-
{{i18n "discourse_ai.features.no_groups"}}
209+
<span class="ai-feature-card__label">
210+
{{i18n "discourse_ai.features.no_groups"}}
211+
</span>
195212
{{/if}}
196213
</div>
197214
{{/if}}

assets/javascripts/discourse/components/ai-features.gjs

Lines changed: 166 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,170 @@
11
import Component from "@glimmer/component";
22
import { tracked } from "@glimmer/tracking";
3-
import { fn } from "@ember/helper";
3+
import { hash } from "@ember/helper";
44
import { action } from "@ember/object";
55
import { service } from "@ember/service";
6-
import { eq } from "truth-helpers";
76
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
87
import DButton from "discourse/components/d-button";
98
import DPageSubheader from "discourse/components/d-page-subheader";
10-
import concatClass from "discourse/helpers/concat-class";
9+
import DSelect from "discourse/components/d-select";
10+
import FilterInput from "discourse/components/filter-input";
1111
import { i18n } from "discourse-i18n";
1212
import AiFeaturesList from "./ai-features-list";
1313

14+
const ALL = "all";
1415
const CONFIGURED = "configured";
1516
const UNCONFIGURED = "unconfigured";
1617

1718
export default class AiFeatures extends Component {
1819
@service adminPluginNavManager;
1920

21+
@tracked filterValue = "";
2022
@tracked selectedFeatureGroup = CONFIGURED;
2123

2224
constructor() {
2325
super(...arguments);
2426

25-
if (this.configuredFeatures.length === 0) {
26-
this.selectedFeatureGroup = UNCONFIGURED;
27+
// if there are features but none are configured, show unconfigured
28+
if (this.args.features?.length > 0) {
29+
const configuredCount = this.args.features.filter(
30+
(f) => f.module_enabled === true
31+
).length;
32+
if (configuredCount === 0) {
33+
this.selectedFeatureGroup = UNCONFIGURED;
34+
}
2735
}
2836
}
2937

30-
get featureGroups() {
38+
get featureGroupOptions() {
3139
return [
32-
{ id: CONFIGURED, label: "discourse_ai.features.nav.configured" },
33-
{ id: UNCONFIGURED, label: "discourse_ai.features.nav.unconfigured" },
40+
{ value: ALL, label: i18n("discourse_ai.features.filters.all") },
41+
{
42+
value: CONFIGURED,
43+
label: i18n("discourse_ai.features.nav.configured"),
44+
},
45+
{
46+
value: UNCONFIGURED,
47+
label: i18n("discourse_ai.features.nav.unconfigured"),
48+
},
3449
];
3550
}
3651

37-
get configuredFeatures() {
38-
return this.args.features.filter(
39-
(feature) => feature.module_enabled === true
40-
);
52+
get filteredFeatures() {
53+
if (!this.args.features || this.args.features.length === 0) {
54+
return [];
55+
}
56+
57+
let features = this.args.features;
58+
59+
if (this.selectedFeatureGroup === CONFIGURED) {
60+
features = features.filter((feature) => feature.module_enabled === true);
61+
} else if (this.selectedFeatureGroup === UNCONFIGURED) {
62+
features = features.filter((feature) => feature.module_enabled === false);
63+
}
64+
65+
if (this.filterValue && this.filterValue.trim() !== "") {
66+
const term = this.filterValue.toLowerCase().trim();
67+
68+
const featureMatches = (module, feature) => {
69+
try {
70+
const featureName = i18n(
71+
`discourse_ai.features.${module.module_name}.${feature.name}`
72+
).toLowerCase();
73+
if (featureName.includes(term)) {
74+
return true;
75+
}
76+
77+
const personaMatches = feature.personas?.some((persona) =>
78+
persona.name?.toLowerCase().includes(term)
79+
);
80+
81+
const llmMatches = feature.llm_models?.some((llm) =>
82+
llm.name?.toLowerCase().includes(term)
83+
);
84+
85+
const groupMatches = feature.personas?.some((persona) =>
86+
persona.allowed_groups?.some((group) =>
87+
group.name?.toLowerCase().includes(term)
88+
)
89+
);
90+
91+
return personaMatches || llmMatches || groupMatches;
92+
} catch (error) {
93+
// eslint-disable-next-line no-console
94+
console.error(`Error filtering features`, error);
95+
return false;
96+
}
97+
};
98+
99+
// Filter modules by name or features
100+
features = features.filter((module) => {
101+
try {
102+
const moduleName = i18n(
103+
`discourse_ai.features.${module.module_name}.name`
104+
).toLowerCase();
105+
if (moduleName.includes(term)) {
106+
return true;
107+
}
108+
109+
return (module.features || []).some((feature) =>
110+
featureMatches(module, feature)
111+
);
112+
} catch (error) {
113+
// eslint-disable-next-line no-console
114+
console.error(`Error filtering features`, error);
115+
return false;
116+
}
117+
});
118+
119+
// For modules that don't match by name, filter features
120+
features = features
121+
.map((module) => {
122+
try {
123+
const moduleName = i18n(
124+
`discourse_ai.features.${module.module_name}.name`
125+
).toLowerCase();
126+
127+
// if name matches
128+
if (moduleName.includes(term)) {
129+
return module;
130+
}
131+
132+
// if no name match
133+
const matchingFeatures = (module.features || []).filter((feature) =>
134+
featureMatches(module, feature)
135+
);
136+
137+
// recreate with matching features
138+
return Object.assign({}, module, {
139+
features: matchingFeatures,
140+
});
141+
} catch (error) {
142+
// eslint-disable-next-line no-console
143+
console.error(`Error filtering features`, error);
144+
return module;
145+
}
146+
})
147+
.filter((module) => module.features && module.features.length > 0);
148+
}
149+
150+
return features;
41151
}
42152

43-
get unconfiguredFeatures() {
44-
return this.args.features.filter(
45-
(feature) => feature.module_enabled === false
46-
);
153+
@action
154+
onFilterChange(event) {
155+
this.filterValue = event.target?.value || "";
47156
}
48157

49158
@action
50-
selectFeatureGroup(groupId) {
51-
this.selectedFeatureGroup = groupId;
159+
onFeatureGroupChange(value) {
160+
this.selectedFeatureGroup = value;
161+
}
162+
163+
@action
164+
resetAndFocus() {
165+
this.filterValue = "";
166+
this.selectedFeatureGroup = CONFIGURED;
167+
document.querySelector(".admin-filter__input").focus();
52168
}
53169

54170
<template>
@@ -63,27 +179,41 @@ export default class AiFeatures extends Component {
63179
@learnMoreUrl="todo"
64180
/>
65181

66-
<div class="ai-feature-groups">
67-
{{#each this.featureGroups as |groupData|}}
68-
<DButton
69-
class={{concatClass
70-
groupData.id
71-
(if
72-
(eq this.selectedFeatureGroup groupData.id)
73-
"btn-primary"
74-
"btn-default"
75-
)
76-
}}
77-
@action={{fn this.selectFeatureGroup groupData.id}}
78-
@label={{groupData.label}}
79-
/>
80-
{{/each}}
182+
<div class="ai-features__controls">
183+
<DSelect
184+
@value={{this.selectedFeatureGroup}}
185+
@includeNone={{false}}
186+
@onChange={{this.onFeatureGroupChange}}
187+
as |select|
188+
>
189+
{{#each this.featureGroupOptions as |option|}}
190+
<select.Option @value={{option.value}}>
191+
{{option.label}}
192+
</select.Option>
193+
{{/each}}
194+
</DSelect>
195+
196+
<FilterInput
197+
placeholder={{i18n "discourse_ai.features.filters.text"}}
198+
@filterAction={{this.onFilterChange}}
199+
@value={{this.filterValue}}
200+
class="admin-filter__input"
201+
@icons={{hash left="magnifying-glass"}}
202+
/>
81203
</div>
82204

83-
{{#if (eq this.selectedFeatureGroup "configured")}}
84-
<AiFeaturesList @modules={{this.configuredFeatures}} />
205+
{{#if this.filteredFeatures.length}}
206+
<AiFeaturesList @modules={{this.filteredFeatures}} />
85207
{{else}}
86-
<AiFeaturesList @modules={{this.unconfiguredFeatures}} />
208+
<div class="ai-features__no-results">
209+
<h3>{{i18n "discourse_ai.features.filters.no_results"}}</h3>
210+
<DButton
211+
@icon="arrow-rotate-left"
212+
@label="discourse_ai.features.filters.reset"
213+
@action={{this.resetAndFocus}}
214+
class="btn-default"
215+
/>
216+
</div>
87217
{{/if}}
88218
</section>
89219
</template>

0 commit comments

Comments
 (0)