import React, { useMemo, useEffect } from 'react'; import { Spinner } from '@librechat/client'; import { PermissionBits } from 'librechat-data-provider'; import type t from 'librechat-data-provider'; import { useMarketplaceAgentsInfiniteQuery } from '~/data-provider/Agents'; import { useAgentCategories, useLocalize } from '~/hooks'; import { useInfiniteScroll } from '~/hooks/useInfiniteScroll'; import { useHasData } from './SmartLoader'; import ErrorDisplay from './ErrorDisplay'; import AgentCard from './AgentCard'; interface AgentGridProps { category: string; // Currently selected category searchQuery: string; // Current search query onSelectAgent: (agent: t.Agent) => void; // Callback when agent is selected scrollElementRef?: React.RefObject; // Parent scroll container ref for infinite scroll } /** * Component for displaying a grid of agent cards */ const AgentGrid: React.FC = ({ category, searchQuery, onSelectAgent, scrollElementRef, }) => { 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: PermissionBits.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]); // Set up infinite scroll const { setScrollElement } = useInfiniteScroll({ hasNextPage, isLoading: isFetching || isFetchingNextPage, fetchNextPage: () => { if (hasNextPage && !isFetching) { fetchNextPage(); } }, threshold: 0.8, // Trigger when 80% scrolled throttleMs: 200, }); // Connect the scroll element when it's provided useEffect(() => { const scrollElement = scrollElementRef?.current; if (scrollElement) { setScrollElement(scrollElement); } }, [scrollElementRef, setScrollElement]); /** * 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); }; // Simple loading spinner const loadingSpinner = (
); // Handle error state with enhanced error display if (error) { return ( refetch()} context={{ searchQuery, category, }} /> ); } // Main content component with proper semantic structure const mainContent = (
{/* Handle empty results with enhanced accessibility */} {(!currentAgents || currentAgents.length === 0) && !isLoading && !isFetching ? (

{localize('com_agents_empty_state_heading')}

) : ( <> {/* 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 */} {isFetchingNextPage && (
{localize('com_agents_loading')}
)} {/* End of results indicator */} {!hasNextPage && currentAgents && currentAgents.length > 0 && (

{localize('com_agents_no_more_results')}

)} )}
); if (isLoading || (isFetching && !isFetchingNextPage)) { return loadingSpinner; } return mainContent; }; export default AgentGrid;