mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 01:10:14 +01:00
feat: Implement error handling for favorites limit and consolidate marketplace access logic
This commit is contained in:
parent
1ed0ae9de4
commit
92f80f3aaa
8 changed files with 80 additions and 72 deletions
|
|
@ -17,7 +17,11 @@ const updateFavoritesController = async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (favorites.length > MAX_FAVORITES) {
|
if (favorites.length > MAX_FAVORITES) {
|
||||||
return res.status(400).json({ message: `Maximum ${MAX_FAVORITES} favorites allowed` });
|
return res.status(400).json({
|
||||||
|
code: 'MAX_FAVORITES_EXCEEDED',
|
||||||
|
message: `Maximum ${MAX_FAVORITES} favorites allowed`,
|
||||||
|
limit: MAX_FAVORITES,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const fav of favorites) {
|
for (const fav of favorites) {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { useMemo, memo, type FC, useCallback, useEffect, useRef, useContext } from 'react';
|
import { useMemo, memo, type FC, useCallback, useEffect, useRef } from 'react';
|
||||||
import throttle from 'lodash/throttle';
|
import throttle from 'lodash/throttle';
|
||||||
import { ChevronRight } from 'lucide-react';
|
import { ChevronRight } from 'lucide-react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Spinner, useMediaQuery } from '@librechat/client';
|
import { Spinner, useMediaQuery } from '@librechat/client';
|
||||||
import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from 'react-virtualized';
|
import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from 'react-virtualized';
|
||||||
import { TConversation, PermissionTypes, Permissions } from 'librechat-data-provider';
|
import { TConversation } from 'librechat-data-provider';
|
||||||
import { useLocalize, TranslationKeys, useFavorites, useHasAccess, AuthContext } from '~/hooks';
|
import { useLocalize, TranslationKeys, useFavorites, useShowMarketplace } from '~/hooks';
|
||||||
import FavoritesList from '~/components/Nav/Favorites/FavoritesList';
|
import FavoritesList from '~/components/Nav/Favorites/FavoritesList';
|
||||||
import { groupConversationsByDate, cn } from '~/utils';
|
import { groupConversationsByDate, cn } from '~/utils';
|
||||||
import Convo from './Convo';
|
import Convo from './Convo';
|
||||||
|
|
@ -90,29 +90,11 @@ const Conversations: FC<ConversationsProps> = ({
|
||||||
setIsChatsExpanded,
|
setIsChatsExpanded,
|
||||||
}) => {
|
}) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const authContext = useContext(AuthContext);
|
|
||||||
const search = useRecoilValue(store.search);
|
const search = useRecoilValue(store.search);
|
||||||
const { favorites, isLoading: isFavoritesLoading } = useFavorites();
|
const { favorites, isLoading: isFavoritesLoading } = useFavorites();
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||||
const convoHeight = isSmallScreen ? 44 : 34;
|
const convoHeight = isSmallScreen ? 44 : 34;
|
||||||
|
const showAgentMarketplace = useShowMarketplace();
|
||||||
const hasAccessToAgents = useHasAccess({
|
|
||||||
permissionType: PermissionTypes.AGENTS,
|
|
||||||
permission: Permissions.USE,
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasAccessToMarketplace = useHasAccess({
|
|
||||||
permissionType: PermissionTypes.MARKETPLACE,
|
|
||||||
permission: Permissions.USE,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if auth is ready (avoid race conditions)
|
|
||||||
const authReady =
|
|
||||||
authContext?.isAuthenticated !== undefined &&
|
|
||||||
(authContext?.isAuthenticated === false || authContext?.user !== undefined);
|
|
||||||
|
|
||||||
// Show agent marketplace when marketplace permission is enabled, auth is ready, and user has access to agents
|
|
||||||
const showAgentMarketplace = authReady && hasAccessToAgents && hasAccessToMarketplace;
|
|
||||||
|
|
||||||
// Determine if FavoritesList will render content
|
// Determine if FavoritesList will render content
|
||||||
const shouldShowFavorites =
|
const shouldShowFavorites =
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import React, { useCallback, useContext } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { LayoutGrid } from 'lucide-react';
|
import { LayoutGrid } from 'lucide-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { TooltipAnchor, Button } from '@librechat/client';
|
import { TooltipAnchor, Button } from '@librechat/client';
|
||||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
import { useLocalize, useShowMarketplace } from '~/hooks';
|
||||||
import { useLocalize, useHasAccess, AuthContext } from '~/hooks';
|
|
||||||
|
|
||||||
interface AgentMarketplaceButtonProps {
|
interface AgentMarketplaceButtonProps {
|
||||||
isSmallScreen?: boolean;
|
isSmallScreen?: boolean;
|
||||||
|
|
@ -16,17 +15,7 @@ export default function AgentMarketplaceButton({
|
||||||
}: AgentMarketplaceButtonProps) {
|
}: AgentMarketplaceButtonProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const authContext = useContext(AuthContext);
|
const showAgentMarketplace = useShowMarketplace();
|
||||||
|
|
||||||
const hasAccessToAgents = useHasAccess({
|
|
||||||
permissionType: PermissionTypes.AGENTS,
|
|
||||||
permission: Permissions.USE,
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasAccessToMarketplace = useHasAccess({
|
|
||||||
permissionType: PermissionTypes.MARKETPLACE,
|
|
||||||
permission: Permissions.USE,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleAgentMarketplace = useCallback(() => {
|
const handleAgentMarketplace = useCallback(() => {
|
||||||
navigate('/agents');
|
navigate('/agents');
|
||||||
|
|
@ -35,14 +24,6 @@ export default function AgentMarketplaceButton({
|
||||||
}
|
}
|
||||||
}, [navigate, isSmallScreen, toggleNav]);
|
}, [navigate, isSmallScreen, toggleNav]);
|
||||||
|
|
||||||
// Check if auth is ready (avoid race conditions)
|
|
||||||
const authReady =
|
|
||||||
authContext?.isAuthenticated !== undefined &&
|
|
||||||
(authContext?.isAuthenticated === false || authContext?.user !== undefined);
|
|
||||||
|
|
||||||
// Show agent marketplace when marketplace permission is enabled, auth is ready, and user has access to agents
|
|
||||||
const showAgentMarketplace = authReady && hasAccessToAgents && hasAccessToMarketplace;
|
|
||||||
|
|
||||||
if (!showAgentMarketplace) {
|
if (!showAgentMarketplace) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import React, { useRef, useCallback, useMemo, useEffect, useContext } from 'react';
|
import React, { useRef, useCallback, useMemo, useEffect } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { LayoutGrid } from 'lucide-react';
|
import { LayoutGrid } from 'lucide-react';
|
||||||
import { useDrag, useDrop } from 'react-dnd';
|
import { useDrag, useDrop } from 'react-dnd';
|
||||||
import { Skeleton } from '@librechat/client';
|
import { Skeleton } from '@librechat/client';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { QueryKeys, dataService } from 'librechat-data-provider';
|
||||||
import { useQueries, useQueryClient } from '@tanstack/react-query';
|
import { useQueries, useQueryClient } from '@tanstack/react-query';
|
||||||
import { QueryKeys, dataService, PermissionTypes, Permissions } from 'librechat-data-provider';
|
|
||||||
import type { InfiniteData } from '@tanstack/react-query';
|
import type { InfiniteData } from '@tanstack/react-query';
|
||||||
import type t from 'librechat-data-provider';
|
import type t from 'librechat-data-provider';
|
||||||
import { useFavorites, useLocalize, useHasAccess, AuthContext } from '~/hooks';
|
import { useFavorites, useLocalize, useShowMarketplace } from '~/hooks';
|
||||||
import FavoriteItem from './FavoriteItem';
|
import FavoriteItem from './FavoriteItem';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
|
|
@ -119,27 +119,9 @@ export default function FavoritesList({
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const authContext = useContext(AuthContext);
|
|
||||||
const search = useRecoilValue(store.search);
|
const search = useRecoilValue(store.search);
|
||||||
const { favorites, reorderFavorites, isLoading: isFavoritesLoading } = useFavorites();
|
const { favorites, reorderFavorites, isLoading: isFavoritesLoading } = useFavorites();
|
||||||
|
const showAgentMarketplace = useShowMarketplace();
|
||||||
const hasAccessToAgents = useHasAccess({
|
|
||||||
permissionType: PermissionTypes.AGENTS,
|
|
||||||
permission: Permissions.USE,
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasAccessToMarketplace = useHasAccess({
|
|
||||||
permissionType: PermissionTypes.MARKETPLACE,
|
|
||||||
permission: Permissions.USE,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if auth is ready (avoid race conditions)
|
|
||||||
const authReady =
|
|
||||||
authContext?.isAuthenticated !== undefined &&
|
|
||||||
(authContext?.isAuthenticated === false || authContext?.user !== undefined);
|
|
||||||
|
|
||||||
// Show agent marketplace when marketplace permission is enabled, auth is ready, and user has access to agents
|
|
||||||
const showAgentMarketplace = authReady && hasAccessToAgents && hasAccessToMarketplace;
|
|
||||||
|
|
||||||
const handleAgentMarketplace = useCallback(() => {
|
const handleAgentMarketplace = useCallback(() => {
|
||||||
navigate('/agents');
|
navigate('/agents');
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
export { default as useNavScrolling } from './useNavScrolling';
|
export { default as useNavScrolling } from './useNavScrolling';
|
||||||
|
export { default as useShowMarketplace } from './useShowMarketplace';
|
||||||
export * from './useNavHelpers';
|
export * from './useNavHelpers';
|
||||||
|
|
|
||||||
37
client/src/hooks/Nav/useShowMarketplace.ts
Normal file
37
client/src/hooks/Nav/useShowMarketplace.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { useContext, useMemo } from 'react';
|
||||||
|
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||||
|
import { useHasAccess, AuthContext } from '~/hooks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to determine if the Agent Marketplace should be shown.
|
||||||
|
* Consolidates the logic for checking:
|
||||||
|
* - Auth readiness (avoid race conditions)
|
||||||
|
* - Access to Agents permission
|
||||||
|
* - Access to Marketplace permission
|
||||||
|
*
|
||||||
|
* @returns Whether the Agent Marketplace should be displayed
|
||||||
|
*/
|
||||||
|
export default function useShowMarketplace(): boolean {
|
||||||
|
const authContext = useContext(AuthContext);
|
||||||
|
|
||||||
|
const hasAccessToAgents = useHasAccess({
|
||||||
|
permissionType: PermissionTypes.AGENTS,
|
||||||
|
permission: Permissions.USE,
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasAccessToMarketplace = useHasAccess({
|
||||||
|
permissionType: PermissionTypes.MARKETPLACE,
|
||||||
|
permission: Permissions.USE,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if auth is ready (avoid race conditions)
|
||||||
|
const authReady = useMemo(
|
||||||
|
() =>
|
||||||
|
authContext?.isAuthenticated !== undefined &&
|
||||||
|
(authContext?.isAuthenticated === false || authContext?.user !== undefined),
|
||||||
|
[authContext?.isAuthenticated, authContext?.user],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show agent marketplace when marketplace permission is enabled, auth is ready, and user has access to agents
|
||||||
|
return authReady && hasAccessToAgents && hasAccessToMarketplace;
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,9 @@ import { favoritesAtom } from '~/store';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { logger } from '~/utils';
|
import { logger } from '~/utils';
|
||||||
|
|
||||||
|
/** Maximum number of favorites allowed (must match backend MAX_FAVORITES) */
|
||||||
|
const MAX_FAVORITES = 50;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook for managing user favorites (pinned agents and models).
|
* Hook for managing user favorites (pinned agents and models).
|
||||||
*
|
*
|
||||||
|
|
@ -61,6 +64,23 @@ export default function useFavorites() {
|
||||||
}
|
}
|
||||||
}, [getFavoritesQuery.data, setFavorites, updateFavoritesMutation.isLoading]);
|
}, [getFavoritesQuery.data, setFavorites, updateFavoritesMutation.isLoading]);
|
||||||
|
|
||||||
|
const getErrorMessage = useCallback(
|
||||||
|
(error: unknown): string => {
|
||||||
|
if (error && typeof error === 'object' && 'response' in error) {
|
||||||
|
const axiosError = error as {
|
||||||
|
response?: { data?: { code?: string; limit?: number } };
|
||||||
|
};
|
||||||
|
const { code, limit } = axiosError.response?.data ?? {};
|
||||||
|
|
||||||
|
if (code === 'MAX_FAVORITES_EXCEEDED') {
|
||||||
|
return localize('com_ui_max_favorites_reached', { 0: String(limit ?? MAX_FAVORITES) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return localize('com_ui_error');
|
||||||
|
},
|
||||||
|
[localize],
|
||||||
|
);
|
||||||
|
|
||||||
const saveFavorites = useCallback(
|
const saveFavorites = useCallback(
|
||||||
async (newFavorites: typeof favorites) => {
|
async (newFavorites: typeof favorites) => {
|
||||||
const cleaned = cleanFavorites(newFavorites);
|
const cleaned = cleanFavorites(newFavorites);
|
||||||
|
|
@ -70,7 +90,7 @@ export default function useFavorites() {
|
||||||
await updateFavoritesMutation.mutateAsync(cleaned);
|
await updateFavoritesMutation.mutateAsync(cleaned);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error updating favorites:', error);
|
logger.error('Error updating favorites:', error);
|
||||||
showToast({ message: localize('com_ui_error'), status: 'error' });
|
showToast({ message: getErrorMessage(error), status: 'error' });
|
||||||
// Refetch to resync state with server
|
// Refetch to resync state with server
|
||||||
getFavoritesQuery.refetch();
|
getFavoritesQuery.refetch();
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -81,7 +101,7 @@ export default function useFavorites() {
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setFavorites, updateFavoritesMutation, showToast, localize, getFavoritesQuery],
|
[setFavorites, updateFavoritesMutation, showToast, getErrorMessage, getFavoritesQuery],
|
||||||
);
|
);
|
||||||
|
|
||||||
const addFavoriteAgent = (agentId: string) => {
|
const addFavoriteAgent = (agentId: string) => {
|
||||||
|
|
@ -147,8 +167,8 @@ export default function useFavorites() {
|
||||||
try {
|
try {
|
||||||
await updateFavoritesMutation.mutateAsync(cleaned);
|
await updateFavoritesMutation.mutateAsync(cleaned);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error reordering favorites:', error);
|
logger.error('Error reordering favorites:', error);
|
||||||
showToast({ message: localize('com_ui_error'), status: 'error' });
|
showToast({ message: getErrorMessage(error), status: 'error' });
|
||||||
// Refetch to resync state with server
|
// Refetch to resync state with server
|
||||||
getFavoritesQuery.refetch();
|
getFavoritesQuery.refetch();
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -158,7 +178,7 @@ export default function useFavorites() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setFavorites, updateFavoritesMutation, showToast, localize, getFavoritesQuery],
|
[setFavorites, updateFavoritesMutation, showToast, getErrorMessage, getFavoritesQuery],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1034,6 +1034,7 @@
|
||||||
"com_ui_manage": "Manage",
|
"com_ui_manage": "Manage",
|
||||||
"com_ui_marketplace": "Marketplace",
|
"com_ui_marketplace": "Marketplace",
|
||||||
"com_ui_marketplace_allow_use": "Allow using Marketplace",
|
"com_ui_marketplace_allow_use": "Allow using Marketplace",
|
||||||
|
"com_ui_max_favorites_reached": "Maximum pinned items reached ({{0}}). Unpin an item to add more.",
|
||||||
"com_ui_max_file_size": "PNG, JPG or JPEG (max {{0}})",
|
"com_ui_max_file_size": "PNG, JPG or JPEG (max {{0}})",
|
||||||
"com_ui_max_tags": "Maximum number allowed is {{0}}, using latest values.",
|
"com_ui_max_tags": "Maximum number allowed is {{0}}, using latest values.",
|
||||||
"com_ui_mcp_authenticated_success": "MCP server '{{0}}' authenticated successfully",
|
"com_ui_mcp_authenticated_success": "MCP server '{{0}}' authenticated successfully",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue