feat: Optimize cache handling in Conversations and enhance FavoritesList to notify height changes on loading completion

This commit is contained in:
Marco Beretta
2025-11-30 18:23:44 +01:00
parent 5d329b8dfa
commit 73fbed992e
2 changed files with 49 additions and 10 deletions

View File

@@ -1,4 +1,4 @@
import { useMemo, memo, type FC, useCallback, useEffect } from 'react';
import { useMemo, memo, type FC, useCallback, useEffect, useRef } from 'react';
import throttle from 'lodash/throttle';
import { ChevronRight } from 'lucide-react';
import { Spinner, useMediaQuery } from '@librechat/client';
@@ -122,13 +122,21 @@ const Conversations: FC<ConversationsProps> = ({
return items;
}, [groupedConversations, isLoading, isChatsExpanded]);
// 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 = flattenedItems[index];
const item = flattenedItemsRef.current[index];
if (!item) {
return `unknown-${index}`;
}
if (item.type === 'favorites') {
return 'favorites';
}
@@ -136,29 +144,37 @@ const Conversations: FC<ConversationsProps> = ({
return 'chats-header';
}
if (item.type === 'header') {
return `header-${index}`;
return `header-${item.groupName}`;
}
if (item.type === 'convo') {
return `convo-${item.convo.conversationId}`;
}
if (item.type === 'loading') {
return `loading-${index}`;
return 'loading';
}
return `unknown-${index}`;
},
}),
[flattenedItems, convoHeight],
[convoHeight],
);
useEffect(() => {
// Debounced function to clear cache and recompute heights
const clearFavoritesCache = useCallback(() => {
if (cache) {
// Clear the favorites row when favorites change or finish loading
cache.clear(0, 0);
if (containerRef.current && 'recomputeRowHeights' in containerRef.current) {
containerRef.current.recomputeRowHeights(0);
}
}
}, [favorites.length, isFavoritesLoading, cache, containerRef]);
}, [cache, containerRef]);
// Clear cache when favorites change - use requestAnimationFrame for smoother updates
useEffect(() => {
const frameId = requestAnimationFrame(() => {
clearFavoritesCache();
});
return () => cancelAnimationFrame(frameId);
}, [favorites.length, isFavoritesLoading, clearFavoritesCache]);
const rowRenderer = useCallback(
({ index, key, parent, style }) => {
@@ -176,7 +192,13 @@ const Conversations: FC<ConversationsProps> = ({
}
let rendering: JSX.Element;
if (item.type === 'favorites') {
rendering = <FavoritesList isSmallScreen={isSmallScreen} toggleNav={toggleNav} />;
rendering = (
<FavoritesList
isSmallScreen={isSmallScreen}
toggleNav={toggleNav}
onHeightChange={clearFavoritesCache}
/>
);
} else if (item.type === 'chats-header') {
rendering = (
<button
@@ -210,7 +232,7 @@ const Conversations: FC<ConversationsProps> = ({
</CellMeasurer>
);
},
[cache, flattenedItems, moveToTop, toggleNav],
[cache, flattenedItems, moveToTop, toggleNav, clearFavoritesCache, isSmallScreen],
);
const getRowHeight = useCallback(

View File

@@ -2,6 +2,7 @@ import React, { useRef, useCallback, useMemo, useContext, useEffect } from 'reac
import { LayoutGrid } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useDrag, useDrop } from 'react-dnd';
import { useRecoilValue } from 'recoil';
import { Skeleton } from '@librechat/client';
import { QueryKeys, dataService, PermissionTypes, Permissions } from 'librechat-data-provider';
import { useQueries, useQueryClient } from '@tanstack/react-query';
@@ -9,6 +10,7 @@ import type { InfiniteData } from '@tanstack/react-query';
import type t from 'librechat-data-provider';
import { useFavorites, useLocalize, useHasAccess, AuthContext } from '~/hooks';
import FavoriteItem from './FavoriteItem';
import store from '~/store';
/** Skeleton placeholder for a favorite item while loading */
const FavoriteItemSkeleton = () => (
@@ -109,14 +111,18 @@ const DraggableFavoriteItem = ({
export default function FavoritesList({
isSmallScreen,
toggleNav,
onHeightChange,
}: {
isSmallScreen?: boolean;
toggleNav?: () => void;
/** Callback when the list height might have changed (e.g., agents finished loading) */
onHeightChange?: () => void;
}) {
const navigate = useNavigate();
const localize = useLocalize();
const queryClient = useQueryClient();
const authContext = useContext(AuthContext);
const search = useRecoilValue(store.search);
const { favorites, reorderFavorites, isLoading: isFavoritesLoading } = useFavorites();
const hasAccessToAgents = useHasAccess({
@@ -157,6 +163,12 @@ 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();
}
}, [isAgentsLoading, onHeightChange]);
const agentsMap = useMemo(() => {
const map: Record<string, t.Agent> = {};
@@ -214,6 +226,11 @@ 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;