mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 09:50:15 +01:00
feat: Refactor FavoritesController validation, streamline ModelSelector component, and enhance EndpointModelItem with selection state
This commit is contained in:
parent
5292c4ea73
commit
e48813acfe
7 changed files with 65 additions and 70 deletions
|
|
@ -9,7 +9,6 @@ const updateFavoritesController = async (req, res) => {
|
|||
return res.status(400).json({ message: 'Favorites data is required' });
|
||||
}
|
||||
|
||||
// Validate favorites structure
|
||||
if (!Array.isArray(favorites)) {
|
||||
return res.status(400).json({ message: 'Favorites must be an array' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ interface EndpointModelItemProps {
|
|||
isSelected: boolean;
|
||||
}
|
||||
|
||||
export function EndpointModelItem({ modelId, endpoint }: EndpointModelItemProps) {
|
||||
export function EndpointModelItem({ modelId, endpoint, isSelected }: EndpointModelItemProps) {
|
||||
const { handleSelectModel } = useModelSelectorContext();
|
||||
const { isFavoriteModel, toggleFavoriteModel, isFavoriteAgent, toggleFavoriteAgent } =
|
||||
useFavorites();
|
||||
|
|
@ -43,34 +43,42 @@ export function EndpointModelItem({ modelId, endpoint }: EndpointModelItemProps)
|
|||
|
||||
const handleFavoriteClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (modelId) {
|
||||
if (isAgent) {
|
||||
toggleFavoriteAgent(modelId);
|
||||
} else {
|
||||
toggleFavoriteModel({ model: modelId, endpoint: endpoint.value });
|
||||
}
|
||||
if (!modelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAgent) {
|
||||
toggleFavoriteAgent(modelId);
|
||||
} else {
|
||||
toggleFavoriteModel({ model: modelId, endpoint: endpoint.value });
|
||||
}
|
||||
};
|
||||
|
||||
const renderAvatar = () => {
|
||||
if (avatarUrl) {
|
||||
return (
|
||||
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center overflow-hidden rounded-full">
|
||||
<img src={avatarUrl} alt={modelName ?? ''} className="h-full w-full object-cover" />
|
||||
</div>
|
||||
);
|
||||
const isAgentOrAssistant =
|
||||
isAgentsEndpoint(endpoint.value) || isAssistantsEndpoint(endpoint.value);
|
||||
const showEndpointIcon = isAgentOrAssistant && endpoint.icon;
|
||||
|
||||
const getContent = () => {
|
||||
if (avatarUrl) {
|
||||
return <img src={avatarUrl} alt={modelName ?? ''} className="h-full w-full object-cover" />;
|
||||
}
|
||||
if (showEndpointIcon) {
|
||||
return endpoint.icon;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const content = getContent();
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
(isAgentsEndpoint(endpoint.value) || isAssistantsEndpoint(endpoint.value)) &&
|
||||
endpoint.icon
|
||||
) {
|
||||
return (
|
||||
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center overflow-hidden rounded-full">
|
||||
{endpoint.icon}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center overflow-hidden rounded-full">
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -97,6 +105,25 @@ export function EndpointModelItem({ modelId, endpoint }: EndpointModelItemProps)
|
|||
<Pin className="h-4 w-4 text-text-secondary" />
|
||||
)}
|
||||
</button>
|
||||
{isSelected && (
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="block"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM16.0755 7.93219C16.5272 8.25003 16.6356 8.87383 16.3178 9.32549L11.5678 16.0755C11.3931 16.3237 11.1152 16.4792 10.8123 16.4981C10.5093 16.517 10.2142 16.3973 10.0101 16.1727L7.51006 13.4227C7.13855 13.014 7.16867 12.3816 7.57733 12.0101C7.98598 11.6386 8.61843 11.6687 8.98994 12.0773L10.6504 13.9039L14.6822 8.17451C15 7.72284 15.6238 7.61436 16.0755 7.93219Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { useMemo, memo, type FC, useCallback, useEffect, useRef } from 'react';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { TConversation } from 'librechat-data-provider';
|
||||
import { Spinner, useMediaQuery } from '@librechat/client';
|
||||
import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from 'react-virtualized';
|
||||
import { TConversation } from 'librechat-data-provider';
|
||||
import FavoritesList from '~/components/Nav/Favorites/FavoritesList';
|
||||
import { useLocalize, TranslationKeys, useFavorites } from '~/hooks';
|
||||
import { groupConversationsByDate, cn } from '~/utils';
|
||||
import FavoritesList from '~/components/Nav/Favorites/FavoritesList';
|
||||
import Convo from './Convo';
|
||||
|
||||
interface ConversationsProps {
|
||||
|
|
@ -17,7 +17,6 @@ interface ConversationsProps {
|
|||
loadMoreConversations: () => void;
|
||||
isLoading: boolean;
|
||||
isSearchLoading: boolean;
|
||||
scrollElement?: HTMLElement | null;
|
||||
isChatsExpanded: boolean;
|
||||
setIsChatsExpanded: (expanded: boolean) => void;
|
||||
}
|
||||
|
|
@ -85,7 +84,6 @@ const Conversations: FC<ConversationsProps> = ({
|
|||
loadMoreConversations,
|
||||
isLoading,
|
||||
isSearchLoading,
|
||||
scrollElement,
|
||||
isChatsExpanded,
|
||||
setIsChatsExpanded,
|
||||
}) => {
|
||||
|
|
@ -168,7 +166,7 @@ const Conversations: FC<ConversationsProps> = ({
|
|||
}
|
||||
}, [cache, containerRef]);
|
||||
|
||||
// Clear cache when favorites change - use requestAnimationFrame for smoother updates
|
||||
// Clear cache when favorites change
|
||||
useEffect(() => {
|
||||
const frameId = requestAnimationFrame(() => {
|
||||
clearFavoritesCache();
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React, { useCallback, useContext } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { LayoutGrid } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import { TooltipAnchor, Button } from '@librechat/client';
|
||||
import { useLocalize, useHasAccess, AuthContext } from '~/hooks';
|
||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import { useLocalize, useHasAccess } from '~/hooks';
|
||||
|
||||
interface AgentMarketplaceButtonProps {
|
||||
isSmallScreen?: boolean;
|
||||
|
|
@ -16,7 +16,6 @@ export default function AgentMarketplaceButton({
|
|||
}: AgentMarketplaceButtonProps) {
|
||||
const navigate = useNavigate();
|
||||
const localize = useLocalize();
|
||||
const authContext = useContext(AuthContext);
|
||||
|
||||
const hasAccessToAgents = useHasAccess({
|
||||
permissionType: PermissionTypes.AGENTS,
|
||||
|
|
@ -35,13 +34,7 @@ export default function AgentMarketplaceButton({
|
|||
}
|
||||
}, [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;
|
||||
const showAgentMarketplace = hasAccessToAgents && hasAccessToMarketplace;
|
||||
|
||||
if (!showAgentMarketplace) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useRef, useCallback, useMemo, useContext, useEffect } from 'react';
|
||||
import React, { useRef, useCallback, useMemo, useEffect } from 'react';
|
||||
import { LayoutGrid } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
|
|
@ -8,11 +8,10 @@ import { QueryKeys, dataService, PermissionTypes, Permissions } from 'librechat-
|
|||
import { useQueries, useQueryClient } from '@tanstack/react-query';
|
||||
import type { InfiniteData } from '@tanstack/react-query';
|
||||
import type t from 'librechat-data-provider';
|
||||
import { useFavorites, useLocalize, useHasAccess, AuthContext } from '~/hooks';
|
||||
import { useFavorites, useLocalize, useHasAccess } from '~/hooks';
|
||||
import FavoriteItem from './FavoriteItem';
|
||||
import store from '~/store';
|
||||
|
||||
/** Skeleton placeholder for a favorite item while loading */
|
||||
const FavoriteItemSkeleton = () => (
|
||||
<div className="flex w-full items-center rounded-lg px-3 py-2">
|
||||
<Skeleton className="mr-2 h-5 w-5 rounded-full" />
|
||||
|
|
@ -20,7 +19,6 @@ const FavoriteItemSkeleton = () => (
|
|||
</div>
|
||||
);
|
||||
|
||||
/** Skeleton placeholder for the Agent Marketplace button while loading */
|
||||
const MarketplaceSkeleton = () => (
|
||||
<div className="flex w-full items-center rounded-lg px-3 py-2">
|
||||
<Skeleton className="mr-2 h-5 w-5" />
|
||||
|
|
@ -121,7 +119,6 @@ export default function FavoritesList({
|
|||
const navigate = useNavigate();
|
||||
const localize = useLocalize();
|
||||
const queryClient = useQueryClient();
|
||||
const authContext = useContext(AuthContext);
|
||||
const search = useRecoilValue(store.search);
|
||||
const { favorites, reorderFavorites, isLoading: isFavoritesLoading } = useFavorites();
|
||||
|
||||
|
|
@ -135,13 +132,8 @@ export default function FavoritesList({
|
|||
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;
|
||||
// Show agent marketplace when marketplace permission is enabled, and user has access to agents
|
||||
const showAgentMarketplace = hasAccessToAgents && hasAccessToMarketplace;
|
||||
|
||||
const handleAgentMarketplace = useCallback(() => {
|
||||
navigate('/agents');
|
||||
|
|
@ -160,10 +152,8 @@ export default function FavoritesList({
|
|||
})),
|
||||
});
|
||||
|
||||
// Check if any agent queries are still loading (not yet fetched)
|
||||
const isAgentsLoading = agentIds.length > 0 && agentQueries.some((q) => q.isLoading);
|
||||
|
||||
// Notify parent when agents finish loading (height might change)
|
||||
useEffect(() => {
|
||||
if (!isAgentsLoading && onHeightChange) {
|
||||
onHeightChange();
|
||||
|
|
@ -226,18 +216,14 @@ export default function FavoritesList({
|
|||
draggedFavoritesRef.current = favorites;
|
||||
}, [favorites]);
|
||||
|
||||
// Hide favorites when search is active
|
||||
if (search.query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If no favorites and no marketplace to show, and not loading, return null
|
||||
if (!isFavoritesLoading && favorites.length === 0 && !showAgentMarketplace) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// While favorites are initially loading, show a minimal placeholder
|
||||
// This prevents the "null to content" jump
|
||||
if (isFavoritesLoading) {
|
||||
return (
|
||||
<div className="mb-2 flex flex-col pb-2">
|
||||
|
|
|
|||
|
|
@ -25,16 +25,9 @@ const AccountSettings = lazy(() => import('./AccountSettings'));
|
|||
const NAV_WIDTH_DESKTOP = '260px';
|
||||
const NAV_WIDTH_MOBILE = '320px';
|
||||
|
||||
/** Skeleton placeholder for SearchBar while loading */
|
||||
const SearchBarSkeleton = memo(({ isSmallScreen }: { isSmallScreen: boolean }) => (
|
||||
<div
|
||||
className={cn(
|
||||
'mt-1 flex items-center gap-3 rounded-lg px-3 py-2',
|
||||
isSmallScreen ? 'mb-2 h-14 rounded-xl' : 'mb-1 h-10',
|
||||
)}
|
||||
>
|
||||
<Skeleton className="h-4 w-4 rounded" />
|
||||
<Skeleton className="h-4 flex-1" />
|
||||
const SearchBarSkeleton = memo(() => (
|
||||
<div className={cn('flex h-10 items-center py-2')}>
|
||||
<Skeleton className="h-10 w-full rounded-lg" />
|
||||
</div>
|
||||
));
|
||||
|
||||
|
|
|
|||
|
|
@ -114,7 +114,6 @@ export const renderAgentAvatar = (
|
|||
);
|
||||
}
|
||||
|
||||
// Fallback placeholder with Agent (Feather) icon - consistent with MessageEndpointIcon
|
||||
return (
|
||||
<div className={`relative flex items-center justify-center ${sizeClasses[size]} ${className}`}>
|
||||
<Feather className={`text-text-primary ${iconSizeClasses[size]}`} strokeWidth={1.5} />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue