1- import { useEffect , useState } from 'react' ;
1+ import { useEffect } from 'react' ;
22import { useTranslation } from 'react-i18next' ;
33import { useDispatch , useSelector } from 'react-redux' ;
44import { useNavigate } from 'react-router-dom' ;
55
6+ import Search from '../assets/search.svg' ;
67import Spinner from '../components/Spinner' ;
78import {
89 setConversation ,
910 updateConversationId ,
1011} from '../conversation/conversationSlice' ;
1112import {
1213 selectSelectedAgent ,
13- selectToken ,
1414 setSelectedAgent ,
1515} from '../preferences/preferenceSlice' ;
1616import AgentCard from './AgentCard' ;
17- import { agentSectionsConfig } from './agents.config' ;
17+ import { AgentSectionId , agentSectionsConfig } from './agents.config' ;
18+ import { AgentFilterTab , useAgentSearch } from './hooks/useAgentSearch' ;
19+ import { useAgentsFetch } from './hooks/useAgentsFetch' ;
1820import { Agent } from './types' ;
1921
22+ const FILTER_TABS : { id : AgentFilterTab ; labelKey : string } [ ] = [
23+ { id : 'all' , labelKey : 'agents.filters.all' } ,
24+ { id : 'template' , labelKey : 'agents.filters.byDocsGPT' } ,
25+ { id : 'user' , labelKey : 'agents.filters.byMe' } ,
26+ { id : 'shared' , labelKey : 'agents.filters.shared' } ,
27+ ] ;
28+
2029export default function AgentsList ( ) {
2130 const { t } = useTranslation ( ) ;
2231 const dispatch = useDispatch ( ) ;
23- const token = useSelector ( selectToken ) ;
2432 const selectedAgent = useSelector ( selectSelectedAgent ) ;
2533
34+ const { isLoading } = useAgentsFetch ( ) ;
35+
36+ const {
37+ searchQuery,
38+ setSearchQuery,
39+ activeFilter,
40+ setActiveFilter,
41+ filteredAgentsBySection,
42+ totalAgentsBySection,
43+ hasAnyAgents,
44+ hasFilteredResults,
45+ isDataLoaded,
46+ } = useAgentSearch ( ) ;
47+
2648 useEffect ( ( ) => {
2749 dispatch ( setConversation ( [ ] ) ) ;
2850 dispatch (
@@ -31,57 +53,150 @@ export default function AgentsList() {
3153 } ) ,
3254 ) ;
3355 if ( selectedAgent ) dispatch ( setSelectedAgent ( null ) ) ;
34- } , [ token ] ) ;
56+ } , [ ] ) ;
57+
58+ const visibleSections = agentSectionsConfig . filter ( ( config ) => {
59+ if ( activeFilter !== 'all' ) {
60+ return config . id === activeFilter ;
61+ }
62+ const sectionId = config . id as AgentSectionId ;
63+ const hasAgentsInSection = totalAgentsBySection [ sectionId ] > 0 ;
64+ const hasFilteredAgents = filteredAgentsBySection [ sectionId ] . length > 0 ;
65+ const sectionDataLoaded = isDataLoaded [ sectionId ] ;
66+
67+ if ( ! sectionDataLoaded ) return true ;
68+ if ( searchQuery ) return hasFilteredAgents ;
69+ if ( config . id === 'user' ) return true ;
70+ return hasAgentsInSection ;
71+ } ) ;
72+
73+ const showSearchEmptyState =
74+ searchQuery &&
75+ hasAnyAgents &&
76+ ! hasFilteredResults &&
77+ activeFilter === 'all' ;
78+
3579 return (
3680 < div className = "p-4 md:p-12" >
3781 < h1 className = "text-eerie-black mb-0 text-[32px] font-bold lg:text-[40px] dark:text-[#E0E0E0]" >
3882 { t ( 'agents.title' ) }
3983 </ h1 >
40- < p className = "dark:text-gray-4000 mt-5 text-[15px] text-[#71717A]" >
84+ < p className = "dark:text-gray-4000 mt-5 max-w-lg text-[15px] leading-6 text-[#71717A]" >
4185 { t ( 'agents.description' ) }
4286 </ p >
43- { agentSectionsConfig . map ( ( sectionConfig ) => (
44- < AgentSection key = { sectionConfig . id } config = { sectionConfig } />
87+
88+ < div className = "mt-6 flex flex-col gap-4 pb-4" >
89+ < div className = "relative w-full max-w-md" >
90+ < img
91+ src = { Search }
92+ alt = ""
93+ className = "absolute top-1/2 left-4 h-5 w-5 -translate-y-1/2 opacity-40"
94+ />
95+ < input
96+ type = "text"
97+ value = { searchQuery }
98+ onChange = { ( e ) => setSearchQuery ( e . target . value ) }
99+ placeholder = { t ( 'agents.searchPlaceholder' ) }
100+ className = "h-[44px] w-full rounded-full border border-[#E5E5E5] bg-white py-2 pr-5 pl-11 text-sm shadow-[0_1px_4px_rgba(0,0,0,0.06)] transition-shadow outline-none placeholder:text-[#9CA3AF] focus:shadow-[0_2px_8px_rgba(0,0,0,0.1)] dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white dark:shadow-none dark:placeholder:text-[#6B7280]"
101+ />
102+ </ div >
103+
104+ < div className = "flex flex-wrap gap-2" >
105+ { FILTER_TABS . map ( ( tab ) => (
106+ < button
107+ key = { tab . id }
108+ onClick = { ( ) => setActiveFilter ( tab . id ) }
109+ className = { `rounded-full px-4 py-2 text-sm transition-colors ${
110+ activeFilter === tab . id
111+ ? 'bg-[#E0E0E0] text-[#18181B] dark:bg-[#4A4A4A] dark:text-white'
112+ : 'bg-transparent text-[#71717A] hover:bg-[#F5F5F5] dark:text-[#949494] dark:hover:bg-[#383838]/50'
113+ } `}
114+ >
115+ { t ( tab . labelKey ) }
116+ </ button >
117+ ) ) }
118+ </ div >
119+ </ div >
120+
121+ { visibleSections . map ( ( sectionConfig ) => (
122+ < AgentSection
123+ key = { sectionConfig . id }
124+ config = { sectionConfig }
125+ filteredAgents = {
126+ filteredAgentsBySection [ sectionConfig . id as AgentSectionId ]
127+ }
128+ totalAgents = { totalAgentsBySection [ sectionConfig . id as AgentSectionId ] }
129+ searchQuery = { searchQuery }
130+ isFilteredView = { activeFilter !== 'all' }
131+ isLoading = { isLoading [ sectionConfig . id as AgentSectionId ] }
132+ />
45133 ) ) }
134+
135+ { showSearchEmptyState && (
136+ < div className = "mt-12 flex flex-col items-center justify-center gap-2 text-[#71717A]" >
137+ < p className = "text-lg" > { t ( 'agents.noSearchResults' ) } </ p >
138+ < p className = "text-sm" > { t ( 'agents.tryDifferentSearch' ) } </ p >
139+ </ div >
140+ ) }
46141 </ div >
47142 ) ;
48143}
49144
145+ interface AgentSectionProps {
146+ config : ( typeof agentSectionsConfig ) [ number ] ;
147+ filteredAgents : Agent [ ] ;
148+ totalAgents : number ;
149+ searchQuery : string ;
150+ isFilteredView : boolean ;
151+ isLoading : boolean ;
152+ }
153+
50154function AgentSection ( {
51155 config,
52- } : {
53- config : ( typeof agentSectionsConfig ) [ number ] ;
54- } ) {
156+ filteredAgents,
157+ totalAgents,
158+ searchQuery,
159+ isFilteredView,
160+ isLoading,
161+ } : AgentSectionProps ) {
55162 const { t } = useTranslation ( ) ;
56163 const navigate = useNavigate ( ) ;
57164 const dispatch = useDispatch ( ) ;
58- const token = useSelector ( selectToken ) ;
59- const agents = useSelector ( config . selectData ) ;
60-
61- const [ loading , setLoading ] = useState ( true ) ;
165+ const allAgents = useSelector ( config . selectData ) ;
62166
63167 const updateAgents = ( updatedAgents : Agent [ ] ) => {
64168 dispatch ( config . updateAction ( updatedAgents ) ) ;
65169 } ;
66170
67- useEffect ( ( ) => {
68- const getAgents = async ( ) => {
69- setLoading ( true ) ;
70- try {
71- const response = await config . fetchAgents ( token ) ;
72- if ( ! response . ok )
73- throw new Error ( `Failed to fetch ${ config . id } agents` ) ;
74- const data = await response . json ( ) ;
75- dispatch ( config . updateAction ( data ) ) ;
76- } catch ( error ) {
77- console . error ( `Error fetching ${ config . id } agents:` , error ) ;
78- dispatch ( config . updateAction ( [ ] ) ) ;
79- } finally {
80- setLoading ( false ) ;
81- }
82- } ;
83- getAgents ( ) ;
84- } , [ token , config , dispatch ] ) ;
171+ const hasNoAgentsAtAll = ! isLoading && totalAgents === 0 ;
172+ const isSearchingWithNoResults =
173+ ! isLoading && searchQuery && filteredAgents . length === 0 && totalAgents > 0 ;
174+
175+ if ( isFilteredView && isSearchingWithNoResults ) {
176+ return (
177+ < div className = "mt-12 flex flex-col items-center justify-center gap-2 text-[#71717A]" >
178+ < p className = "text-lg" > { t ( 'agents.noSearchResults' ) } </ p >
179+ < p className = "text-sm" > { t ( 'agents.tryDifferentSearch' ) } </ p >
180+ </ div >
181+ ) ;
182+ }
183+
184+ if ( isFilteredView && hasNoAgentsAtAll ) {
185+ return (
186+ < div className = "mt-12 flex flex-col items-center justify-center gap-3 text-[#71717A]" >
187+ < p > { t ( `agents.sections.${ config . id } .emptyState` ) } </ p >
188+ { config . showNewAgentButton && (
189+ < button
190+ className = "bg-purple-30 hover:bg-violets-are-blue rounded-full px-4 py-2 text-sm text-white"
191+ onClick = { ( ) => navigate ( '/agents/new' ) }
192+ >
193+ { t ( 'agents.newAgent' ) }
194+ </ button >
195+ ) }
196+ </ div >
197+ ) ;
198+ }
199+
85200 return (
86201 < div className = "mt-8 flex flex-col gap-4" >
87202 < div className = "flex w-full items-center justify-between" >
@@ -103,24 +218,24 @@ function AgentSection({
103218 ) }
104219 </ div >
105220 < div >
106- { loading ? (
107- < div className = "flex h-72 w-full items-center justify-center" >
221+ { isLoading ? (
222+ < div className = "flex h-40 w-full items-center justify-center" >
108223 < Spinner />
109224 </ div >
110- ) : agents && agents . length > 0 ? (
225+ ) : filteredAgents . length > 0 ? (
111226 < div className = "grid grid-cols-1 gap-4 sm:flex sm:flex-wrap" >
112- { agents . map ( ( agent ) => (
227+ { filteredAgents . map ( ( agent ) => (
113228 < AgentCard
114229 key = { agent . id }
115230 agent = { agent }
116- agents = { agents }
231+ agents = { allAgents || [ ] }
117232 updateAgents = { updateAgents }
118233 section = { config . id }
119234 />
120235 ) ) }
121236 </ div >
122- ) : (
123- < div className = "flex h-72 w-full flex-col items-center justify-center gap-3 text-base text-[#18181B] dark:text-[#E0E0E0 ]" >
237+ ) : hasNoAgentsAtAll ? (
238+ < div className = "flex h-40 w-full flex-col items-center justify-center gap-3 text-[#71717A ]" >
124239 < p > { t ( `agents.sections.${ config . id } .emptyState` ) } </ p >
125240 { config . showNewAgentButton && (
126241 < button
@@ -131,7 +246,7 @@ function AgentSection({
131246 </ button >
132247 ) }
133248 </ div >
134- ) }
249+ ) : null }
135250 </ div >
136251 </ div >
137252 ) ;
0 commit comments