Skip to content

Commit 9b9f957

Browse files
authored
feat: agent search functionality with filters and loading states (#2179)
* feat: implement agent search functionality with filters and loading states * style: improve layout and styling of agent search input and description
1 parent 3352d42 commit 9b9f957

11 files changed

Lines changed: 431 additions & 46 deletions

File tree

frontend/src/agents/AgentsList.tsx

Lines changed: 156 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,50 @@
1-
import { useEffect, useState } from 'react';
1+
import { useEffect } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { useDispatch, useSelector } from 'react-redux';
44
import { useNavigate } from 'react-router-dom';
55

6+
import Search from '../assets/search.svg';
67
import Spinner from '../components/Spinner';
78
import {
89
setConversation,
910
updateConversationId,
1011
} from '../conversation/conversationSlice';
1112
import {
1213
selectSelectedAgent,
13-
selectToken,
1414
setSelectedAgent,
1515
} from '../preferences/preferenceSlice';
1616
import 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';
1820
import { 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+
2029
export 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+
50154
function 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
);

frontend/src/agents/agents.config.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import userService from '../api/services/userService';
22
import {
33
selectAgents,
4-
selectTemplateAgents,
54
selectSharedAgents,
5+
selectTemplateAgents,
66
setAgents,
7-
setTemplateAgents,
87
setSharedAgents,
8+
setTemplateAgents,
99
} from '../preferences/preferenceSlice';
1010

11+
export type AgentSectionId = 'template' | 'user' | 'shared';
12+
1113
export const agentSectionsConfig = [
1214
{
13-
id: 'template',
15+
id: 'template' as const,
1416
title: 'By DocsGPT',
1517
description: 'Agents provided by DocsGPT',
1618
showNewAgentButton: false,
@@ -20,7 +22,7 @@ export const agentSectionsConfig = [
2022
updateAction: setTemplateAgents,
2123
},
2224
{
23-
id: 'user',
25+
id: 'user' as const,
2426
title: 'By me',
2527
description: 'Agents created or published by you',
2628
showNewAgentButton: true,
@@ -30,7 +32,7 @@ export const agentSectionsConfig = [
3032
updateAction: setAgents,
3133
},
3234
{
33-
id: 'shared',
35+
id: 'shared' as const,
3436
title: 'Shared with me',
3537
description: 'Agents imported by using a public link',
3638
showNewAgentButton: false,

0 commit comments

Comments
 (0)