import React, { useState } from 'react'; import type t from 'librechat-data-provider'; import { useGetMarketplaceAgentsQuery } from 'librechat-data-provider/react-query'; import { useAgentCategories } from '~/hooks/Agents'; import useLocalize from '~/hooks/useLocalize'; import { Button } from '~/components/ui'; import { Spinner } from '~/components/svg'; import { SmartLoader, 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(); const [cursor, setCursor] = useState(undefined); const [allAgents, setAllAgents] = useState([]); // Get category data from API const { categories } = useAgentCategories(); // Build query parameters based on current state const queryParams = React.useMemo(() => { const params: { requiredPermission: number; category?: string; search?: string; limit: number; cursor?: string; promoted?: 0 | 1; } = { requiredPermission: 1, // Read permission for marketplace viewing limit: 6, }; if (cursor) { params.cursor = cursor; } // 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, cursor]); // Single unified query that handles all cases const { data, isLoading, error, isFetching, refetch } = useGetMarketplaceAgentsQuery(queryParams); // Handle data accumulation for pagination React.useEffect(() => { if (data?.data) { if (cursor) { // Append new data for pagination setAllAgents((prev) => [...prev, ...data.data]); } else { // Replace data for new queries setAllAgents(data.data); } } }, [data, cursor]); // Get current agents to display const currentAgents = cursor ? allAgents : data?.data || []; // Check if we have meaningful data to prevent unnecessary loading states const hasData = useHasData(data); /** * 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 (data?.after) { setCursor(data.after); } }; /** * Reset cursor and agents when category or search changes */ React.useEffect(() => { setCursor(undefined); setAllAgents([]); }, [category, searchQuery]); /** * 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 && cursor && (
{localize('com_agents_loading')}
)} {/* Load more button with enhanced accessibility */} {data?.has_more && !isFetching && (
)} )}
); // Use SmartLoader to prevent unnecessary loading flashes return ( {mainContent} ); }; export default AgentGrid;