diff --git a/frontend/src/agents/AgentsList.tsx b/frontend/src/agents/AgentsList.tsx
index bdccb0f1..0c3c1d0e 100644
--- a/frontend/src/agents/AgentsList.tsx
+++ b/frontend/src/agents/AgentsList.tsx
@@ -1,8 +1,9 @@
-import { useEffect, useState } from 'react';
+import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
+import Search from '../assets/search.svg';
import Spinner from '../components/Spinner';
import {
setConversation,
@@ -10,19 +11,40 @@ import {
} from '../conversation/conversationSlice';
import {
selectSelectedAgent,
- selectToken,
setSelectedAgent,
} from '../preferences/preferenceSlice';
import AgentCard from './AgentCard';
-import { agentSectionsConfig } from './agents.config';
+import { AgentSectionId, agentSectionsConfig } from './agents.config';
+import { AgentFilterTab, useAgentSearch } from './hooks/useAgentSearch';
+import { useAgentsFetch } from './hooks/useAgentsFetch';
import { Agent } from './types';
+const FILTER_TABS: { id: AgentFilterTab; labelKey: string }[] = [
+ { id: 'all', labelKey: 'agents.filters.all' },
+ { id: 'template', labelKey: 'agents.filters.byDocsGPT' },
+ { id: 'user', labelKey: 'agents.filters.byMe' },
+ { id: 'shared', labelKey: 'agents.filters.shared' },
+];
+
export default function AgentsList() {
const { t } = useTranslation();
const dispatch = useDispatch();
- const token = useSelector(selectToken);
const selectedAgent = useSelector(selectSelectedAgent);
+ const { isLoading } = useAgentsFetch();
+
+ const {
+ searchQuery,
+ setSearchQuery,
+ activeFilter,
+ setActiveFilter,
+ filteredAgentsBySection,
+ totalAgentsBySection,
+ hasAnyAgents,
+ hasFilteredResults,
+ isDataLoaded,
+ } = useAgentSearch();
+
useEffect(() => {
dispatch(setConversation([]));
dispatch(
@@ -31,57 +53,150 @@ export default function AgentsList() {
}),
);
if (selectedAgent) dispatch(setSelectedAgent(null));
- }, [token]);
+ }, []);
+
+ const visibleSections = agentSectionsConfig.filter((config) => {
+ if (activeFilter !== 'all') {
+ return config.id === activeFilter;
+ }
+ const sectionId = config.id as AgentSectionId;
+ const hasAgentsInSection = totalAgentsBySection[sectionId] > 0;
+ const hasFilteredAgents = filteredAgentsBySection[sectionId].length > 0;
+ const sectionDataLoaded = isDataLoaded[sectionId];
+
+ if (!sectionDataLoaded) return true;
+ if (searchQuery) return hasFilteredAgents;
+ if (config.id === 'user') return true;
+ return hasAgentsInSection;
+ });
+
+ const showSearchEmptyState =
+ searchQuery &&
+ hasAnyAgents &&
+ !hasFilteredResults &&
+ activeFilter === 'all';
+
return (
{t('agents.title')}
-
+
{t('agents.description')}
- {agentSectionsConfig.map((sectionConfig) => (
-
+
+
+
+

+
setSearchQuery(e.target.value)}
+ placeholder={t('agents.searchPlaceholder')}
+ 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]"
+ />
+
+
+
+ {FILTER_TABS.map((tab) => (
+
+ ))}
+
+
+
+ {visibleSections.map((sectionConfig) => (
+
))}
+
+ {showSearchEmptyState && (
+
+
{t('agents.noSearchResults')}
+
{t('agents.tryDifferentSearch')}
+
+ )}
);
}
+interface AgentSectionProps {
+ config: (typeof agentSectionsConfig)[number];
+ filteredAgents: Agent[];
+ totalAgents: number;
+ searchQuery: string;
+ isFilteredView: boolean;
+ isLoading: boolean;
+}
+
function AgentSection({
config,
-}: {
- config: (typeof agentSectionsConfig)[number];
-}) {
+ filteredAgents,
+ totalAgents,
+ searchQuery,
+ isFilteredView,
+ isLoading,
+}: AgentSectionProps) {
const { t } = useTranslation();
const navigate = useNavigate();
const dispatch = useDispatch();
- const token = useSelector(selectToken);
- const agents = useSelector(config.selectData);
-
- const [loading, setLoading] = useState(true);
+ const allAgents = useSelector(config.selectData);
const updateAgents = (updatedAgents: Agent[]) => {
dispatch(config.updateAction(updatedAgents));
};
- useEffect(() => {
- const getAgents = async () => {
- setLoading(true);
- try {
- const response = await config.fetchAgents(token);
- if (!response.ok)
- throw new Error(`Failed to fetch ${config.id} agents`);
- const data = await response.json();
- dispatch(config.updateAction(data));
- } catch (error) {
- console.error(`Error fetching ${config.id} agents:`, error);
- dispatch(config.updateAction([]));
- } finally {
- setLoading(false);
- }
- };
- getAgents();
- }, [token, config, dispatch]);
+ const hasNoAgentsAtAll = !isLoading && totalAgents === 0;
+ const isSearchingWithNoResults =
+ !isLoading && searchQuery && filteredAgents.length === 0 && totalAgents > 0;
+
+ if (isFilteredView && isSearchingWithNoResults) {
+ return (
+
+
{t('agents.noSearchResults')}
+
{t('agents.tryDifferentSearch')}
+
+ );
+ }
+
+ if (isFilteredView && hasNoAgentsAtAll) {
+ return (
+
+
{t(`agents.sections.${config.id}.emptyState`)}
+ {config.showNewAgentButton && (
+
+ )}
+
+ );
+ }
+
return (
@@ -103,24 +218,24 @@ function AgentSection({
)}
- {loading ? (
-
+ {isLoading ? (
+
- ) : agents && agents.length > 0 ? (
+ ) : filteredAgents.length > 0 ? (
- {agents.map((agent) => (
+ {filteredAgents.map((agent) => (
))}
- ) : (
-
+ ) : hasNoAgentsAtAll ? (
+
{t(`agents.sections.${config.id}.emptyState`)}
{config.showNewAgentButton && (
- )}
+ ) : null}
);
diff --git a/frontend/src/agents/agents.config.ts b/frontend/src/agents/agents.config.ts
index b6be5015..3569600e 100644
--- a/frontend/src/agents/agents.config.ts
+++ b/frontend/src/agents/agents.config.ts
@@ -1,16 +1,18 @@
import userService from '../api/services/userService';
import {
selectAgents,
- selectTemplateAgents,
selectSharedAgents,
+ selectTemplateAgents,
setAgents,
- setTemplateAgents,
setSharedAgents,
+ setTemplateAgents,
} from '../preferences/preferenceSlice';
+export type AgentSectionId = 'template' | 'user' | 'shared';
+
export const agentSectionsConfig = [
{
- id: 'template',
+ id: 'template' as const,
title: 'By DocsGPT',
description: 'Agents provided by DocsGPT',
showNewAgentButton: false,
@@ -20,7 +22,7 @@ export const agentSectionsConfig = [
updateAction: setTemplateAgents,
},
{
- id: 'user',
+ id: 'user' as const,
title: 'By me',
description: 'Agents created or published by you',
showNewAgentButton: true,
@@ -30,7 +32,7 @@ export const agentSectionsConfig = [
updateAction: setAgents,
},
{
- id: 'shared',
+ id: 'shared' as const,
title: 'Shared with me',
description: 'Agents imported by using a public link',
showNewAgentButton: false,
diff --git a/frontend/src/agents/hooks/useAgentSearch.ts b/frontend/src/agents/hooks/useAgentSearch.ts
new file mode 100644
index 00000000..7a5edb9e
--- /dev/null
+++ b/frontend/src/agents/hooks/useAgentSearch.ts
@@ -0,0 +1,122 @@
+import { useCallback, useMemo, useState } from 'react';
+import { useSelector } from 'react-redux';
+
+import {
+ selectAgents,
+ selectSharedAgents,
+ selectTemplateAgents,
+} from '../../preferences/preferenceSlice';
+import { AgentSectionId } from '../agents.config';
+import { Agent } from '../types';
+
+export type AgentFilterTab = 'all' | AgentSectionId;
+
+export type AgentsBySection = Record
;
+
+interface UseAgentSearchResult {
+ searchQuery: string;
+ setSearchQuery: (query: string) => void;
+ activeFilter: AgentFilterTab;
+ setActiveFilter: (filter: AgentFilterTab) => void;
+ filteredAgentsBySection: AgentsBySection;
+ totalAgentsBySection: Record;
+ hasAnyAgents: boolean;
+ hasFilteredResults: boolean;
+ isDataLoaded: Record;
+}
+
+const filterAgentsByQuery = (
+ agents: Agent[] | null,
+ query: string,
+): Agent[] => {
+ if (!agents) return [];
+ if (!query.trim()) return agents;
+
+ const normalizedQuery = query.toLowerCase().trim();
+ return agents.filter(
+ (agent) =>
+ agent.name.toLowerCase().includes(normalizedQuery) ||
+ agent.description?.toLowerCase().includes(normalizedQuery),
+ );
+};
+
+export function useAgentSearch(): UseAgentSearchResult {
+ const [searchQuery, setSearchQuery] = useState('');
+ const [activeFilter, setActiveFilter] = useState('all');
+
+ const templateAgents = useSelector(selectTemplateAgents);
+ const userAgents = useSelector(selectAgents);
+ const sharedAgents = useSelector(selectSharedAgents);
+
+ const handleSearchChange = useCallback((query: string) => {
+ setSearchQuery(query);
+ }, []);
+
+ const handleFilterChange = useCallback((filter: AgentFilterTab) => {
+ setActiveFilter(filter);
+ }, []);
+
+ const isDataLoaded = useMemo(
+ (): Record => ({
+ template: templateAgents !== null,
+ user: userAgents !== null,
+ shared: sharedAgents !== null,
+ }),
+ [templateAgents, userAgents, sharedAgents],
+ );
+
+ const totalAgentsBySection = useMemo(
+ (): Record => ({
+ template: templateAgents?.length ?? 0,
+ user: userAgents?.length ?? 0,
+ shared: sharedAgents?.length ?? 0,
+ }),
+ [templateAgents, userAgents, sharedAgents],
+ );
+
+ const filteredAgentsBySection = useMemo((): AgentsBySection => {
+ const filtered = {
+ template: filterAgentsByQuery(templateAgents, searchQuery),
+ user: filterAgentsByQuery(userAgents, searchQuery),
+ shared: filterAgentsByQuery(sharedAgents, searchQuery),
+ };
+
+ if (activeFilter === 'all') {
+ return filtered;
+ }
+
+ return {
+ template: activeFilter === 'template' ? filtered.template : [],
+ user: activeFilter === 'user' ? filtered.user : [],
+ shared: activeFilter === 'shared' ? filtered.shared : [],
+ };
+ }, [templateAgents, userAgents, sharedAgents, searchQuery, activeFilter]);
+
+ const hasAnyAgents = useMemo(() => {
+ return (
+ totalAgentsBySection.template > 0 ||
+ totalAgentsBySection.user > 0 ||
+ totalAgentsBySection.shared > 0
+ );
+ }, [totalAgentsBySection]);
+
+ const hasFilteredResults = useMemo(() => {
+ return (
+ filteredAgentsBySection.template.length > 0 ||
+ filteredAgentsBySection.user.length > 0 ||
+ filteredAgentsBySection.shared.length > 0
+ );
+ }, [filteredAgentsBySection]);
+
+ return {
+ searchQuery,
+ setSearchQuery: handleSearchChange,
+ activeFilter,
+ setActiveFilter: handleFilterChange,
+ filteredAgentsBySection,
+ totalAgentsBySection,
+ hasAnyAgents,
+ hasFilteredResults,
+ isDataLoaded,
+ };
+}
diff --git a/frontend/src/agents/hooks/useAgentsFetch.ts b/frontend/src/agents/hooks/useAgentsFetch.ts
new file mode 100644
index 00000000..8f5367d6
--- /dev/null
+++ b/frontend/src/agents/hooks/useAgentsFetch.ts
@@ -0,0 +1,83 @@
+import { useCallback, useEffect, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+
+import userService from '../../api/services/userService';
+import {
+ selectToken,
+ setAgents,
+ setSharedAgents,
+ setTemplateAgents,
+} from '../../preferences/preferenceSlice';
+import { AgentSectionId } from '../agents.config';
+
+interface UseAgentsFetchResult {
+ isLoading: Record;
+ isAllLoaded: boolean;
+}
+
+export function useAgentsFetch(): UseAgentsFetchResult {
+ const dispatch = useDispatch();
+ const token = useSelector(selectToken);
+
+ const [isLoading, setIsLoading] = useState>({
+ template: true,
+ user: true,
+ shared: true,
+ });
+
+ const fetchTemplateAgents = useCallback(async () => {
+ try {
+ const response = await userService.getTemplateAgents(token);
+ if (!response.ok) throw new Error('Failed to fetch template agents');
+ const data = await response.json();
+ dispatch(setTemplateAgents(data));
+ } catch (error) {
+ dispatch(setTemplateAgents([]));
+ } finally {
+ setIsLoading((prev) => ({ ...prev, template: false }));
+ }
+ }, [token, dispatch]);
+
+ const fetchUserAgents = useCallback(async () => {
+ try {
+ const response = await userService.getAgents(token);
+ if (!response.ok) throw new Error('Failed to fetch user agents');
+ const data = await response.json();
+ dispatch(setAgents(data));
+ } catch (error) {
+ dispatch(setAgents([]));
+ } finally {
+ setIsLoading((prev) => ({ ...prev, user: false }));
+ }
+ }, [token, dispatch]);
+
+ const fetchSharedAgents = useCallback(async () => {
+ try {
+ const response = await userService.getSharedAgents(token);
+ if (!response.ok) throw new Error('Failed to fetch shared agents');
+ const data = await response.json();
+ dispatch(setSharedAgents(data));
+ } catch (error) {
+ dispatch(setSharedAgents([]));
+ } finally {
+ setIsLoading((prev) => ({ ...prev, shared: false }));
+ }
+ }, [token, dispatch]);
+
+ useEffect(() => {
+ setIsLoading({ template: true, user: true, shared: true });
+ Promise.all([
+ fetchTemplateAgents(),
+ fetchUserAgents(),
+ fetchSharedAgents(),
+ ]);
+ }, [fetchTemplateAgents, fetchUserAgents, fetchSharedAgents]);
+
+ const isAllLoaded =
+ !isLoading.template && !isLoading.user && !isLoading.shared;
+
+ return {
+ isLoading,
+ isAllLoaded,
+ };
+}
diff --git a/frontend/src/locale/de.json b/frontend/src/locale/de.json
index a4b10003..c4b58a3e 100644
--- a/frontend/src/locale/de.json
+++ b/frontend/src/locale/de.json
@@ -491,6 +491,15 @@
"description": "Entdecke und erstelle benutzerdefinierte Versionen von DocsGPT, die Anweisungen, zusätzliches Wissen und beliebige Kombinationen von Fähigkeiten kombinieren",
"newAgent": "Neuer Agent",
"backToAll": "Zurück zu allen Agenten",
+ "searchPlaceholder": "Suchen...",
+ "noSearchResults": "Keine Agenten gefunden",
+ "tryDifferentSearch": "Versuche einen anderen Suchbegriff",
+ "filters": {
+ "all": "Alle",
+ "byDocsGPT": "Von DocsGPT",
+ "byMe": "Von mir",
+ "shared": "Mit mir geteilt"
+ },
"sections": {
"template": {
"title": "Von DocsGPT",
diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json
index 69bde744..dbb8bdbe 100644
--- a/frontend/src/locale/en.json
+++ b/frontend/src/locale/en.json
@@ -491,6 +491,15 @@
"description": "Discover and create custom versions of DocsGPT that combine instructions, extra knowledge, and any combination of skills",
"newAgent": "New Agent",
"backToAll": "Back to all agents",
+ "searchPlaceholder": "Search...",
+ "noSearchResults": "No agents found",
+ "tryDifferentSearch": "Try a different search term",
+ "filters": {
+ "all": "All",
+ "byDocsGPT": "By DocsGPT",
+ "byMe": "By Me",
+ "shared": "Shared With Me"
+ },
"sections": {
"template": {
"title": "By DocsGPT",
diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json
index 65eea4c9..49f09344 100644
--- a/frontend/src/locale/es.json
+++ b/frontend/src/locale/es.json
@@ -491,6 +491,15 @@
"description": "Descubre y crea versiones personalizadas de DocsGPT que combinan instrucciones, conocimiento adicional y cualquier combinación de habilidades",
"newAgent": "Nuevo Agente",
"backToAll": "Volver a todos los agentes",
+ "searchPlaceholder": "Buscar...",
+ "noSearchResults": "No se encontraron agentes",
+ "tryDifferentSearch": "Prueba con un término de búsqueda diferente",
+ "filters": {
+ "all": "Todos",
+ "byDocsGPT": "Por DocsGPT",
+ "byMe": "Por mí",
+ "shared": "Compartidos conmigo"
+ },
"sections": {
"template": {
"title": "Por DocsGPT",
diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json
index 6be92551..b8458d2c 100644
--- a/frontend/src/locale/jp.json
+++ b/frontend/src/locale/jp.json
@@ -491,6 +491,15 @@
"description": "指示、追加知識、スキルの組み合わせを含むDocsGPTのカスタムバージョンを発見して作成します",
"newAgent": "新しいエージェント",
"backToAll": "すべてのエージェントに戻る",
+ "searchPlaceholder": "検索...",
+ "noSearchResults": "エージェントが見つかりません",
+ "tryDifferentSearch": "別の検索語をお試しください",
+ "filters": {
+ "all": "すべて",
+ "byDocsGPT": "DocsGPT提供",
+ "byMe": "自分の",
+ "shared": "共有された"
+ },
"sections": {
"template": {
"title": "DocsGPT提供",
diff --git a/frontend/src/locale/ru.json b/frontend/src/locale/ru.json
index fe136ee7..103f3627 100644
--- a/frontend/src/locale/ru.json
+++ b/frontend/src/locale/ru.json
@@ -491,6 +491,15 @@
"description": "Откройте и создайте пользовательские версии DocsGPT, которые объединяют инструкции, дополнительные знания и любую комбинацию навыков",
"newAgent": "Новый Агент",
"backToAll": "Вернуться ко всем агентам",
+ "searchPlaceholder": "Поиск...",
+ "noSearchResults": "Агенты не найдены",
+ "tryDifferentSearch": "Попробуйте другой поисковый запрос",
+ "filters": {
+ "all": "Все",
+ "byDocsGPT": "От DocsGPT",
+ "byMe": "Мои",
+ "shared": "Поделились со мной"
+ },
"sections": {
"template": {
"title": "От DocsGPT",
diff --git a/frontend/src/locale/zh-TW.json b/frontend/src/locale/zh-TW.json
index 24c8d273..47c0b236 100644
--- a/frontend/src/locale/zh-TW.json
+++ b/frontend/src/locale/zh-TW.json
@@ -491,6 +491,15 @@
"description": "探索並創建結合指令、額外知識和任意技能組合的DocsGPT自訂版本",
"newAgent": "新建代理",
"backToAll": "返回所有代理",
+ "searchPlaceholder": "搜尋...",
+ "noSearchResults": "未找到代理",
+ "tryDifferentSearch": "請嘗試不同的搜尋詞",
+ "filters": {
+ "all": "全部",
+ "byDocsGPT": "由DocsGPT提供",
+ "byMe": "我的",
+ "shared": "與我共享"
+ },
"sections": {
"template": {
"title": "由DocsGPT提供",
diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json
index c14c4e74..12b867fa 100644
--- a/frontend/src/locale/zh.json
+++ b/frontend/src/locale/zh.json
@@ -491,6 +491,15 @@
"description": "发现并创建结合指令、额外知识和任意技能组合的DocsGPT自定义版本",
"newAgent": "新建代理",
"backToAll": "返回所有代理",
+ "searchPlaceholder": "搜索...",
+ "noSearchResults": "未找到代理",
+ "tryDifferentSearch": "请尝试不同的搜索词",
+ "filters": {
+ "all": "全部",
+ "byDocsGPT": "由DocsGPT提供",
+ "byMe": "我的",
+ "shared": "与我共享"
+ },
"sections": {
"template": {
"title": "由DocsGPT提供",