import React, { useMemo } from 'react'; import { Button, Spinner } from '@librechat/client'; import { PERMISSION_BITS } from 'librechat-data-provider'; import type t from 'librechat-data-provider'; import { useMarketplaceAgentsInfiniteQuery } from '~/data-provider/Agents'; import { useAgentCategories } from '~/hooks/Agents'; import useLocalize from '~/hooks/useLocalize'; import { useHasData } from './SmartLoader'; import ErrorDisplay from './ErrorDisplay'; import AgentCard from './AgentCard'; import { cn } from '~/utils'; interface AgentGridProps { category: string; // Currently selected category searchQuery: string; // Current search query onSelectAgent: (agent: t.Agent) => void; // Callback when agent is selected } /** * Component for displaying a grid of agent cards */ const AgentGrid: React.FC = ({ category, searchQuery, onSelectAgent }) => { const localize = useLocalize(); // Get category data from API const { categories } = useAgentCategories(); // Build query parameters based on current state const queryParams = useMemo(() => { const params: { requiredPermission: number; category?: string; search?: string; limit: number; promoted?: 0 | 1; } = { requiredPermission: PERMISSION_BITS.VIEW, // View permission for marketplace viewing limit: 6, }; // Handle search if (searchQuery) { params.search = searchQuery; // Include category filter for search if it's not 'all' or 'promoted' if (category !== 'all' && category !== 'promoted') { params.category = category; } } else { // Handle category-based queries if (category === 'promoted') { params.promoted = 1; } else if (category !== 'all') { params.category = category; } // For 'all' category, no additional filters needed } return params; }, [category, searchQuery]); // Use infinite query for marketplace agents const { data, isLoading, error, isFetching, fetchNextPage, hasNextPage, refetch, isFetchingNextPage, } = useMarketplaceAgentsInfiniteQuery(queryParams); // Flatten all pages into a single array of agents const currentAgents = useMemo(() => { if (!data?.pages) return []; return data.pages.flatMap((page) => page.data || []); }, [data?.pages]); // Check if we have meaningful data to prevent unnecessary loading states const hasData = useHasData(data?.pages?.[0]); /** * Get category display name from API data or use fallback */ const getCategoryDisplayName = (categoryValue: string) => { const categoryData = categories.find((cat) => cat.value === categoryValue); if (categoryData) { return categoryData.label; } // Fallback for special categories or unknown categories if (categoryValue === 'promoted') { return localize('com_agents_top_picks'); } if (categoryValue === 'all') { return 'All'; } // Simple capitalization for unknown categories return categoryValue.charAt(0).toUpperCase() + categoryValue.slice(1); }; /** * Load more agents when "See More" button is clicked */ const handleLoadMore = () => { if (hasNextPage && !isFetching) { fetchNextPage(); } }; /** * Get the appropriate title for the agents grid based on current state */ const getGridTitle = () => { if (searchQuery) { return localize('com_agents_results_for', { query: searchQuery }); } return getCategoryDisplayName(category); }; // Loading skeleton component const loadingSkeleton = (
{Array(6) .fill(0) .map((_, index) => (
))}
); // Handle error state with enhanced error display if (error) { return ( refetch()} context={{ searchQuery, category, }} /> ); } // Main content component with proper semantic structure const mainContent = (
{/* Grid title - only show for search results */} {searchQuery && (

{getGridTitle()}

)} {/* Handle empty results with enhanced accessibility */} {(!currentAgents || currentAgents.length === 0) && !isLoading && !isFetching ? (

{searchQuery ? localize('com_agents_search_empty_heading') : localize('com_agents_empty_state_heading')}

{searchQuery ? localize('com_agents_no_results') : localize('com_agents_none_in_category')}

) : ( <> {/* Announcement for screen readers */}
{localize('com_agents_grid_announcement', { count: currentAgents?.length || 0, category: getCategoryDisplayName(category), })}
{/* Agent grid - 2 per row with proper semantic structure */} {currentAgents && currentAgents.length > 0 && (
{currentAgents.map((agent: t.Agent, index: number) => (
onSelectAgent(agent)} />
))}
)} {/* Loading indicator when fetching more with accessibility */} {isFetching && hasNextPage && (
{localize('com_agents_loading')}
)} {/* Load more button with enhanced accessibility */} {hasNextPage && !isFetching && (
)} )}
); if (isLoading || (isFetching && !isFetchingNextPage)) { return loadingSkeleton; } return mainContent; }; export default AgentGrid;