mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-20 10:20:15 +01:00
- 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:
parent
6ebcfdf3e2
commit
3f6d7ab7c7
9 changed files with 143 additions and 128 deletions
|
|
@ -389,6 +389,16 @@ const deleteAgentHandler = async (req, res) => {
|
|||
const getListAgentsHandler = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
if (!req.query.requiredPermission) {
|
||||
req.query.requiredPermission = PermissionBits.VIEW;
|
||||
} else if (typeof req.query.requiredPermission === 'string') {
|
||||
req.query.requiredPermission = parseInt(req.query.requiredPermission, 10);
|
||||
if (isNaN(req.query.requiredPermission)) {
|
||||
req.query.requiredPermission = PermissionBits.VIEW;
|
||||
}
|
||||
} else if (typeof req.query.requiredPermission !== 'number') {
|
||||
req.query.requiredPermission = PermissionBits.VIEW;
|
||||
}
|
||||
const requiredPermission = req.query.requiredPermission || PermissionBits.VIEW;
|
||||
// Get agent IDs the user has VIEW access to via ACL
|
||||
const accessibleIds = await findAccessibleResources({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -1,2 +1,5 @@
|
|||
export * from './queries';
|
||||
export * from './mutations';
|
||||
|
||||
// Re-export specific marketplace queries for easier imports
|
||||
export { useGetAgentCategoriesQuery, useMarketplaceAgentsInfiniteQuery } from './queries';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { QueryKeys, dataService, EModelEndpoint, defaultOrderQuery } from 'librechat-data-provider';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import type { QueryObserverResult, UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useQuery, useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import type {
|
||||
QueryObserverResult,
|
||||
UseQueryOptions,
|
||||
UseInfiniteQueryOptions,
|
||||
InfiniteData,
|
||||
} from '@tanstack/react-query';
|
||||
import type t from 'librechat-data-provider';
|
||||
|
||||
/**
|
||||
|
|
@ -98,3 +103,60 @@ export const useGetExpandedAgentByIdQuery = (
|
|||
},
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* MARKETPLACE
|
||||
*/
|
||||
/**
|
||||
* Hook for getting agent categories for marketplace tabs
|
||||
*/
|
||||
export const useGetAgentCategoriesQuery = (
|
||||
config?: UseQueryOptions<t.TMarketplaceCategory[]>,
|
||||
): QueryObserverResult<t.TMarketplaceCategory[]> => {
|
||||
return useQuery<t.TMarketplaceCategory[]>(
|
||||
[QueryKeys.agentCategories],
|
||||
() => dataService.getAgentCategories(),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
...config,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for infinite loading of marketplace agents with cursor-based pagination
|
||||
*/
|
||||
export const useMarketplaceAgentsInfiniteQuery = (
|
||||
params: {
|
||||
requiredPermission: number;
|
||||
category?: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
promoted?: 0 | 1;
|
||||
cursor?: string; // For pagination
|
||||
},
|
||||
config?: UseInfiniteQueryOptions<t.AgentListResponse, unknown>,
|
||||
) => {
|
||||
return useInfiniteQuery<t.AgentListResponse>({
|
||||
queryKey: [QueryKeys.marketplaceAgents, params],
|
||||
queryFn: ({ pageParam }) => {
|
||||
const queryParams = { ...params };
|
||||
if (pageParam) {
|
||||
queryParams.cursor = pageParam.toString();
|
||||
}
|
||||
return dataService.getMarketplaceAgents(queryParams);
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage?.after ?? undefined,
|
||||
enabled: !!params.requiredPermission,
|
||||
keepPreviousData: true,
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
cacheTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
...config,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useMemo } from 'react';
|
||||
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import { useGetAgentCategoriesQuery } from 'librechat-data-provider/react-query';
|
||||
import { useGetAgentCategoriesQuery } from '~/data-provider/Agents';
|
||||
import { EMPTY_AGENT_CATEGORY } from '~/constants/agentCategories';
|
||||
|
||||
// This interface matches the structure used by the ControlCombobox component
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { TAgentsMap } from 'librechat-data-provider';
|
||||
import { PERMISSION_BITS, TAgentsMap } from 'librechat-data-provider';
|
||||
import { useMemo } from 'react';
|
||||
import { useListAgentsQuery } from '~/data-provider';
|
||||
import { mapAgents } from '~/utils';
|
||||
|
|
@ -8,10 +8,15 @@ export default function useAgentsMap({
|
|||
}: {
|
||||
isAuthenticated: boolean;
|
||||
}): TAgentsMap | undefined {
|
||||
const { data: agentsList = null } = useListAgentsQuery(undefined, {
|
||||
select: (res) => mapAgents(res.data),
|
||||
enabled: isAuthenticated,
|
||||
});
|
||||
const { data: agentsList = null } = useListAgentsQuery(
|
||||
{
|
||||
requiredPermission: PERMISSION_BITS.EDIT,
|
||||
},
|
||||
{
|
||||
select: (res) => mapAgents(res.data),
|
||||
enabled: isAuthenticated,
|
||||
},
|
||||
);
|
||||
|
||||
const agents = useMemo<TAgentsMap | undefined>(() => {
|
||||
return agentsList !== null ? agentsList : undefined;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import * as q from '../types/queries';
|
|||
import { QueryKeys } from '../keys';
|
||||
import * as s from '../schemas';
|
||||
import * as t from '../types';
|
||||
import * as a from '../types/assistants';
|
||||
import * as permissions from '../accessPermissions';
|
||||
|
||||
export { hasPermissions } from '../accessPermissions';
|
||||
|
|
@ -451,52 +450,3 @@ export const useGetEffectivePermissionsQuery = (
|
|||
...config,
|
||||
});
|
||||
};
|
||||
|
||||
/* Marketplace */
|
||||
|
||||
/**
|
||||
* Get agent categories with counts for marketplace tabs
|
||||
*/
|
||||
export const useGetAgentCategoriesQuery = (
|
||||
config?: UseQueryOptions<t.TMarketplaceCategory[]>,
|
||||
): QueryObserverResult<t.TMarketplaceCategory[]> => {
|
||||
return useQuery<t.TMarketplaceCategory[]>(
|
||||
[QueryKeys.agentCategories],
|
||||
() => dataService.getAgentCategories(),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
...config,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Unified marketplace agents query with query string controls
|
||||
*/
|
||||
export const useGetMarketplaceAgentsQuery = (
|
||||
params: {
|
||||
requiredPermission: number;
|
||||
category?: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
promoted?: 0 | 1;
|
||||
},
|
||||
config?: UseQueryOptions<a.AgentListResponse>,
|
||||
): QueryObserverResult<a.AgentListResponse> => {
|
||||
return useQuery<a.AgentListResponse>(
|
||||
[QueryKeys.marketplaceAgents, params],
|
||||
() => dataService.getMarketplaceAgents(params),
|
||||
{
|
||||
enabled: !!params.requiredPermission,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
staleTime: 2 * 60 * 1000, // Cache for 2 minutes
|
||||
...config,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue