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