1- import React , { useMemo } from 'react' ;
2- import { Dropdown } from '@librechat/client ' ;
1+ import React , { useMemo , useState } from 'react' ;
2+ import * as Ariakit from '@ariakit/react ' ;
33import { useTranslation } from 'react-i18next' ;
4- import { useFormContext , Controller } from 'react-hook-form ' ;
4+ import { DropdownPopup } from '@librechat/client ' ;
55import { LocalStorageKeys } from 'librechat-data-provider' ;
6+ import { useFormContext , Controller } from 'react-hook-form' ;
7+ import type { MenuItemProps } from '@librechat/client' ;
68import type { ReactNode } from 'react' ;
79import { useCategories } from '~/hooks' ;
10+ import { cn } from '~/utils' ;
811
912interface CategorySelectorProps {
1013 currentCategory ?: string ;
@@ -20,10 +23,11 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
2023 const { t } = useTranslation ( ) ;
2124 const formContext = useFormContext ( ) ;
2225 const { categories, emptyCategory } = useCategories ( ) ;
26+ const [ isOpen , setIsOpen ] = useState ( false ) ;
2327
24- const control = formContext . control ;
25- const watch = formContext . watch ;
26- const setValue = formContext . setValue ;
28+ const control = formContext ? .control ;
29+ const watch = formContext ? .watch ;
30+ const setValue = formContext ? .setValue ;
2731
2832 const watchedCategory = watch ? watch ( 'category' ) : currentCategory ;
2933
@@ -46,53 +50,71 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
4650 return categoryOption ;
4751 } , [ categoryOption , t ] ) ;
4852
53+ const menuItems : MenuItemProps [ ] = useMemo ( ( ) => {
54+ if ( ! categories ) return [ ] ;
55+
56+ return categories . map ( ( category ) => ( {
57+ id : category . value ,
58+ label : category . label ,
59+ icon : 'icon' in category ? category . icon : undefined ,
60+ onClick : ( ) => {
61+ const value = category . value || '' ;
62+ if ( formContext && setValue ) {
63+ setValue ( 'category' , value , { shouldDirty : false } ) ;
64+ }
65+ localStorage . setItem ( LocalStorageKeys . LAST_PROMPT_CATEGORY , value ) ;
66+ onValueChange ?.( value ) ;
67+ setIsOpen ( false ) ;
68+ } ,
69+ } ) ) ;
70+ } , [ categories , formContext , setValue , onValueChange ] ) ;
71+
72+ const trigger = (
73+ < Ariakit . MenuButton
74+ className = { cn (
75+ 'focus:ring-offset-ring-offset relative inline-flex items-center justify-between rounded-xl border border-input bg-background px-3 py-2 text-sm text-text-primary transition-all duration-200 ease-in-out hover:bg-accent hover:text-accent-foreground focus:ring-ring-primary' ,
76+ 'w-fit gap-2' ,
77+ className ,
78+ ) }
79+ onClick = { ( ) => setIsOpen ( ! isOpen ) }
80+ aria-label = "Prompt's category selector"
81+ aria-labelledby = "category-selector-label"
82+ >
83+ < div className = "flex items-center space-x-2" >
84+ { 'icon' in displayCategory && displayCategory . icon != null && (
85+ < span > { displayCategory . icon as ReactNode } </ span >
86+ ) }
87+ < span > { displayCategory . value ? displayCategory . label : t ( 'com_ui_category' ) } </ span >
88+ </ div >
89+ < Ariakit . MenuButtonArrow />
90+ </ Ariakit . MenuButton >
91+ ) ;
92+
4993 return formContext ? (
5094 < Controller
5195 name = "category"
5296 control = { control }
5397 render = { ( ) => (
54- < Dropdown
55- value = { displayCategory . value ?? '' }
56- label = { displayCategory . value ? undefined : t ( 'com_ui_category' ) }
57- onChange = { ( value : string ) => {
58- setValue ( 'category' , value , { shouldDirty : false } ) ;
59- localStorage . setItem ( LocalStorageKeys . LAST_PROMPT_CATEGORY , value ) ;
60- onValueChange ?.( value ) ;
61- } }
62- aria-labelledby = "category-selector-label"
63- ariaLabel = "Prompt's category selector"
64- className = { className }
65- options = { categories || [ ] }
66- renderValue = { ( ) => (
67- < div className = "flex items-center space-x-2" >
68- { 'icon' in displayCategory && displayCategory . icon != null && (
69- < span > { displayCategory . icon as ReactNode } </ span >
70- ) }
71- < span > { displayCategory . label } </ span >
72- </ div >
73- ) }
98+ < DropdownPopup
99+ trigger = { trigger }
100+ items = { menuItems }
101+ isOpen = { isOpen }
102+ setIsOpen = { setIsOpen }
103+ menuId = "category-selector-menu"
104+ className = "mt-2"
105+ portal = { true }
74106 />
75107 ) }
76108 />
77109 ) : (
78- < Dropdown
79- value = { currentCategory ?? '' }
80- onChange = { ( value : string ) => {
81- localStorage . setItem ( LocalStorageKeys . LAST_PROMPT_CATEGORY , value ) ;
82- onValueChange ?.( value ) ;
83- } }
84- aria-labelledby = "category-selector-label"
85- ariaLabel = "Prompt's category selector"
86- className = { className }
87- options = { categories || [ ] }
88- renderValue = { ( ) => (
89- < div className = "flex items-center space-x-2" >
90- { 'icon' in displayCategory && displayCategory . icon != null && (
91- < span > { displayCategory . icon as ReactNode } </ span >
92- ) }
93- < span > { displayCategory . label } </ span >
94- </ div >
95- ) }
110+ < DropdownPopup
111+ trigger = { trigger }
112+ items = { menuItems }
113+ isOpen = { isOpen }
114+ setIsOpen = { setIsOpen }
115+ menuId = "category-selector-menu"
116+ className = "mt-2"
117+ portal = { true }
96118 />
97119 ) ;
98120} ;
0 commit comments