11import Component from " @glimmer/component" ;
22import { tracked } from " @glimmer/tracking" ;
3- import { fn } from " @ember/helper" ;
3+ import { hash } from " @ember/helper" ;
44import { action } from " @ember/object" ;
55import { service } from " @ember/service" ;
6- import { eq } from " truth-helpers" ;
76import DBreadcrumbsItem from " discourse/components/d-breadcrumbs-item" ;
87import DButton from " discourse/components/d-button" ;
98import 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" ;
1111import { i18n } from " discourse-i18n" ;
1212import AiFeaturesList from " ./ai-features-list" ;
1313
14+ const ALL = " all" ;
1415const CONFIGURED = " configured" ;
1516const UNCONFIGURED = " unconfigured" ;
1617
1718export 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