- Add useMarketplaceAgentsInfiniteQuery and useGetAgentCategoriesQuery to client/src/data-provider/Agents/

- Replace manual pagination in AgentGrid with infinite query pattern
  - Update imports to use local data provider instead of librechat-data-provider
  - Add proper permission handling with PERMISSION_BITS.VIEW/EDIT constants
  - Improve agent access control by adding requiredPermission validation in backend
  - Remove manual cursor/state management in favor of infinite query built-ins
  - Maintain existing search and category filtering functionality
This commit is contained in:
Danny Avila 2025-06-23 11:44:12 -04:00
parent 6ebcfdf3e2
commit 3f6d7ab7c7
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
9 changed files with 143 additions and 128 deletions

View file

@ -1,8 +1,8 @@
import React, { useState } from 'react';
import React, { useMemo } from 'react';
import type t from 'librechat-data-provider';
import { useGetMarketplaceAgentsQuery } from 'librechat-data-provider/react-query';
import { useMarketplaceAgentsInfiniteQuery } from '~/data-provider/Agents';
import { useAgentCategories } from '~/hooks/Agents';
import useLocalize from '~/hooks/useLocalize';
import { Button } from '~/components/ui';
@ -11,7 +11,7 @@ import { SmartLoader, useHasData } from './SmartLoader';
import ErrorDisplay from './ErrorDisplay';
import AgentCard from './AgentCard';
import { cn } from '~/utils';
import { PERMISSION_BITS } from 'librechat-data-provider';
interface AgentGridProps {
category: string; // Currently selected category
searchQuery: string; // Current search query
@ -23,30 +23,23 @@ interface AgentGridProps {
*/
const AgentGrid: React.FC<AgentGridProps> = ({ category, searchQuery, onSelectAgent }) => {
const localize = useLocalize();
const [cursor, setCursor] = useState<string | undefined>(undefined);
const [allAgents, setAllAgents] = useState<t.Agent[]>([]);
// Get category data from API
const { categories } = useAgentCategories();
// Build query parameters based on current state
const queryParams = React.useMemo(() => {
const queryParams = useMemo(() => {
const params: {
requiredPermission: number;
category?: string;
search?: string;
limit: number;
cursor?: string;
promoted?: 0 | 1;
} = {
requiredPermission: 1, // Read permission for marketplace viewing
requiredPermission: PERMISSION_BITS.VIEW, // View permission for marketplace viewing
limit: 6,
};
if (cursor) {
params.cursor = cursor;
}
// Handle search
if (searchQuery) {
params.search = searchQuery;
@ -65,29 +58,28 @@ const AgentGrid: React.FC<AgentGridProps> = ({ category, searchQuery, onSelectAg
}
return params;
}, [category, searchQuery, cursor]);
}, [category, searchQuery]);
// Single unified query that handles all cases
const { data, isLoading, error, isFetching, refetch } = useGetMarketplaceAgentsQuery(queryParams);
// Use infinite query for marketplace agents
const {
data,
isLoading,
error,
isFetching,
fetchNextPage,
hasNextPage,
refetch,
isFetchingNextPage,
} = useMarketplaceAgentsInfiniteQuery(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 || [];
// 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);
const hasData = useHasData(data?.pages?.[0]);
/**
* Get category display name from API data or use fallback
@ -114,19 +106,11 @@ const AgentGrid: React.FC<AgentGridProps> = ({ category, searchQuery, onSelectAg
* Load more agents when "See More" button is clicked
*/
const handleLoadMore = () => {
if (data?.after) {
setCursor(data.after);
if (hasNextPage && !isFetching) {
fetchNextPage();
}
};
/**
* 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
*/
@ -257,7 +241,7 @@ const AgentGrid: React.FC<AgentGridProps> = ({ category, searchQuery, onSelectAg
)}
{/* Loading indicator when fetching more with accessibility */}
{isFetching && cursor && (
{isFetching && hasNextPage && (
<div
className="flex justify-center py-4"
role="status"
@ -270,7 +254,7 @@ const AgentGrid: React.FC<AgentGridProps> = ({ category, searchQuery, onSelectAg
)}
{/* Load more button with enhanced accessibility */}
{data?.has_more && !isFetching && (
{hasNextPage && !isFetching && (
<div className="mt-8 flex justify-center">
<Button
variant="outline"
@ -294,18 +278,13 @@ const AgentGrid: React.FC<AgentGridProps> = ({ category, searchQuery, onSelectAg
)}
</div>
);
// Use SmartLoader to prevent unnecessary loading flashes
return (
<SmartLoader
isLoading={isLoading}
hasData={hasData}
delay={200} // Show loading only after 200ms delay
loadingComponent={loadingSkeleton}
>
{mainContent}
</SmartLoader>
);
console.log('isLoading', isLoading);
console.log('isFetching', isFetching);
console.log('isFetchingNextPage', isFetchingNextPage);
if (isLoading || (isFetching && !isFetchingNextPage)) {
return loadingSkeleton;
}
return mainContent;
};
export default AgentGrid;

View file

@ -6,8 +6,7 @@ import { useSetRecoilState, useRecoilValue } from 'recoil';
import type t from 'librechat-data-provider';
import type { ContextType } from '~/common';
import { useGetEndpointsQuery } from '~/data-provider';
import { useGetAgentCategoriesQuery } from 'librechat-data-provider/react-query';
import { useGetEndpointsQuery, useGetAgentCategoriesQuery } from '~/data-provider';
import { useDocumentTitle } from '~/hooks';
import useLocalize from '~/hooks/useLocalize';
import { TooltipAnchor, Button } from '~/components/ui';

View file

@ -1,7 +1,11 @@
import { EarthIcon } from 'lucide-react';
import { useCallback, useEffect, useRef } from 'react';
import { useFormContext, Controller } from 'react-hook-form';
import { AgentCapabilities, defaultAgentFormValues } from 'librechat-data-provider';
import {
AgentCapabilities,
defaultAgentFormValues,
PERMISSION_BITS,
} from 'librechat-data-provider';
import type { UseMutationResult, QueryObserverResult } from '@tanstack/react-query';
import type { Agent, AgentCreateParams } from 'librechat-data-provider';
import type { TAgentCapabilities, AgentForm } from '~/common';
@ -28,18 +32,21 @@ export default function AgentSelect({
const { control, reset } = useFormContext();
const { data: startupConfig } = useGetStartupConfig();
const { data: agents = null } = useListAgentsQuery(undefined, {
select: (res) =>
res.data.map((agent) =>
processAgentOption({
agent: {
...agent,
name: agent.name || agent.id,
},
instanceProjectId: startupConfig?.instanceProjectId,
}),
),
});
const { data: agents = null } = useListAgentsQuery(
{ requiredPermission: PERMISSION_BITS.EDIT },
{
select: (res) =>
res.data.map((agent) =>
processAgentOption({
agent: {
...agent,
name: agent.name || agent.id,
},
instanceProjectId: startupConfig?.instanceProjectId,
}),
),
},
);
const resetAgentForm = useCallback(
(fullAgent: Agent) => {