import { useMemo, memo, type FC, useCallback, useEffect, useRef } from 'react'; import throttle from 'lodash/throttle'; import { ChevronRight } from 'lucide-react'; import { useRecoilValue } from 'recoil'; import { Spinner, useMediaQuery } from '@librechat/client'; import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from 'react-virtualized'; import { TConversation } from 'librechat-data-provider'; import { useLocalize, TranslationKeys, useFavorites, useShowMarketplace } from '~/hooks'; import FavoritesList from '~/components/Nav/Favorites/FavoritesList'; import { groupConversationsByDate, cn } from '~/utils'; import Convo from './Convo'; import store from '~/store'; interface ConversationsProps { conversations: Array; moveToTop: () => void; toggleNav: () => void; containerRef: React.RefObject; loadMoreConversations: () => void; isLoading: boolean; isSearchLoading: boolean; isChatsExpanded: boolean; setIsChatsExpanded: (expanded: boolean) => void; } const LoadingSpinner = memo(() => { const localize = useLocalize(); return (
{localize('com_ui_loading')}
); }); LoadingSpinner.displayName = 'LoadingSpinner'; const DateLabel: FC<{ groupName: string; isFirst?: boolean }> = memo(({ groupName, isFirst }) => { const localize = useLocalize(); return (

{localize(groupName as TranslationKeys) || groupName}

); }); DateLabel.displayName = 'DateLabel'; type FlattenedItem = | { type: 'favorites' } | { type: 'chats-header' } | { type: 'header'; groupName: string } | { type: 'convo'; convo: TConversation } | { type: 'loading' }; const MemoizedConvo = memo( ({ conversation, retainView, toggleNav, }: { conversation: TConversation; retainView: () => void; toggleNav: () => void; }) => { return ; }, (prevProps, nextProps) => { return ( prevProps.conversation.conversationId === nextProps.conversation.conversationId && prevProps.conversation.title === nextProps.conversation.title && prevProps.conversation.endpoint === nextProps.conversation.endpoint ); }, ); const Conversations: FC = ({ conversations: rawConversations, moveToTop, toggleNav, containerRef, loadMoreConversations, isLoading, isSearchLoading, isChatsExpanded, setIsChatsExpanded, }) => { const localize = useLocalize(); const search = useRecoilValue(store.search); const { favorites, isLoading: isFavoritesLoading } = useFavorites(); const isSmallScreen = useMediaQuery('(max-width: 768px)'); const convoHeight = isSmallScreen ? 44 : 34; const showAgentMarketplace = useShowMarketplace(); // Determine if FavoritesList will render content const shouldShowFavorites = !search.query && (isFavoritesLoading || favorites.length > 0 || showAgentMarketplace); const filteredConversations = useMemo( () => rawConversations.filter(Boolean) as TConversation[], [rawConversations], ); const groupedConversations = useMemo( () => groupConversationsByDate(filteredConversations), [filteredConversations], ); const flattenedItems = useMemo(() => { const items: FlattenedItem[] = []; // Only include favorites row if FavoritesList will render content if (shouldShowFavorites) { items.push({ type: 'favorites' }); } items.push({ type: 'chats-header' }); if (isChatsExpanded) { groupedConversations.forEach(([groupName, convos]) => { items.push({ type: 'header', groupName }); items.push(...convos.map((convo) => ({ type: 'convo' as const, convo }))); }); if (isLoading) { items.push({ type: 'loading' } as any); } } return items; }, [groupedConversations, isLoading, isChatsExpanded, shouldShowFavorites]); // Store flattenedItems in a ref for keyMapper to access without recreating cache const flattenedItemsRef = useRef(flattenedItems); flattenedItemsRef.current = flattenedItems; // Create a stable cache that doesn't depend on flattenedItems const cache = useMemo( () => new CellMeasurerCache({ fixedWidth: true, defaultHeight: convoHeight, keyMapper: (index) => { const item = flattenedItemsRef.current[index]; if (!item) { return `unknown-${index}`; } if (item.type === 'favorites') { return 'favorites'; } if (item.type === 'chats-header') { return 'chats-header'; } if (item.type === 'header') { return `header-${item.groupName}`; } if (item.type === 'convo') { return `convo-${item.convo.conversationId}`; } if (item.type === 'loading') { return 'loading'; } return `unknown-${index}`; }, }), [convoHeight], ); // Debounced function to clear cache and recompute heights const clearFavoritesCache = useCallback(() => { if (cache) { cache.clear(0, 0); if (containerRef.current && 'recomputeRowHeights' in containerRef.current) { containerRef.current.recomputeRowHeights(0); } } }, [cache, containerRef]); // Clear cache when favorites change useEffect(() => { const frameId = requestAnimationFrame(() => { clearFavoritesCache(); }); return () => cancelAnimationFrame(frameId); }, [favorites.length, isFavoritesLoading, clearFavoritesCache]); const rowRenderer = useCallback( ({ index, key, parent, style }) => { const item = flattenedItems[index]; if (item.type === 'loading') { return ( {({ registerChild }) => (
)}
); } let rendering: JSX.Element; if (item.type === 'favorites') { rendering = ( ); } else if (item.type === 'chats-header') { rendering = ( ); } else if (item.type === 'header') { // First date header index depends on whether favorites row is included // With favorites: [favorites, chats-header, first-header] → index 2 // Without favorites: [chats-header, first-header] → index 1 const firstHeaderIndex = shouldShowFavorites ? 2 : 1; rendering = ; } else if (item.type === 'convo') { rendering = ( ); } return ( {({ registerChild }) => (
{rendering}
)}
); }, [ cache, flattenedItems, moveToTop, toggleNav, clearFavoritesCache, isSmallScreen, isChatsExpanded, localize, setIsChatsExpanded, shouldShowFavorites, ], ); const getRowHeight = useCallback( ({ index }: { index: number }) => cache.getHeight(index, 0), [cache], ); const throttledLoadMore = useMemo( () => throttle(loadMoreConversations, 300), [loadMoreConversations], ); const handleRowsRendered = useCallback( ({ stopIndex }: { stopIndex: number }) => { if (stopIndex >= flattenedItems.length - 8) { throttledLoadMore(); } }, [flattenedItems.length, throttledLoadMore], ); return (
{isSearchLoading ? (
{localize('com_ui_loading')}
) : (
{({ width, height }) => ( )}
)}
); }; export default memo(Conversations);