diff --git a/client/src/components/Conversations/Conversations.tsx b/client/src/components/Conversations/Conversations.tsx index 604c702a33..63ee52ee9b 100644 --- a/client/src/components/Conversations/Conversations.tsx +++ b/client/src/components/Conversations/Conversations.tsx @@ -1,16 +1,26 @@ import { useMemo, memo, type FC, useCallback, useEffect, useRef } from 'react'; import throttle from 'lodash/throttle'; -import { ChevronRight } from 'lucide-react'; +import { ChevronDown } 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 type { 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'; +export type CellPosition = { + columnIndex: number; + rowIndex: number; +}; + +export type MeasuredCellParent = { + invalidateCellSizeAfterRender?: ((cell: CellPosition) => void) | undefined; + recomputeGridSize?: ((cell: CellPosition) => void) | undefined; +}; + interface ConversationsProps { conversations: Array; moveToTop: () => void; @@ -23,6 +33,30 @@ interface ConversationsProps { setIsChatsExpanded: (expanded: boolean) => void; } +interface MeasuredRowProps { + cache: CellMeasurerCache; + rowKey: string; + parent: MeasuredCellParent; + index: number; + style: React.CSSProperties; + children: React.ReactNode; +} + +/** Reusable wrapper for virtualized row measurement */ +const MeasuredRow: FC = memo( + ({ cache, rowKey, parent, index, style, children }) => ( + + {({ registerChild }) => ( +
} style={style}> + {children} +
+ )} +
+ ), +); + +MeasuredRow.displayName = 'MeasuredRow'; + const LoadingSpinner = memo(() => { const localize = useLocalize(); @@ -36,6 +70,30 @@ const LoadingSpinner = memo(() => { LoadingSpinner.displayName = 'LoadingSpinner'; +interface ChatsHeaderProps { + isExpanded: boolean; + onToggle: () => void; +} + +/** Collapsible header for the Chats section */ +const ChatsHeader: FC = memo(({ isExpanded, onToggle }) => { + const localize = useLocalize(); + return ( + + ); +}); + +ChatsHeader.displayName = 'ChatsHeader'; + const DateLabel: FC<{ groupName: string; isFirst?: boolean }> = memo(({ groupName, isFirst }) => { const localize = useLocalize(); return ( @@ -188,62 +246,60 @@ const Conversations: FC = ({ const rowRenderer = useCallback( ({ index, key, parent, style }) => { const item = flattenedItems[index]; + const rowProps = { cache, rowKey: key, parent, index, style }; + 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') { + } + + if (item.type === 'chats-header') { + return ( + + setIsChatsExpanded(!isChatsExpanded)} + /> + + ); + } + + 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 ( + + + ); } - return ( - - {({ registerChild }) => ( -
- {rendering} -
- )} -
- ); + + if (item.type === 'convo') { + return ( + + + + ); + } + + return null; }, [ cache, @@ -253,7 +309,6 @@ const Conversations: FC = ({ clearFavoritesCache, isSmallScreen, isChatsExpanded, - localize, setIsChatsExpanded, shouldShowFavorites, ], @@ -300,10 +355,10 @@ const Conversations: FC = ({ overscanRowCount={10} aria-readonly={false} className="outline-none" - style={{ outline: 'none' }} aria-label="Conversations" onRowsRendered={handleRowsRendered} tabIndex={-1} + style={{ outline: 'none', scrollbarGutter: 'stable' }} /> )} diff --git a/client/src/hooks/Conversations/useSearchEnabled.ts b/client/src/hooks/Conversations/useSearchEnabled.ts index d643833426..1b60869097 100644 --- a/client/src/hooks/Conversations/useSearchEnabled.ts +++ b/client/src/hooks/Conversations/useSearchEnabled.ts @@ -10,9 +10,12 @@ export default function useSearchEnabled(isAuthenticated: boolean) { useEffect(() => { if (searchEnabledQuery.data === true) { - setSearch((prev) => ({ ...prev, enabled: searchEnabledQuery.data })); + setSearch((prev) => ({ ...prev, enabled: true })); + } else if (searchEnabledQuery.data === false) { + setSearch((prev) => ({ ...prev, enabled: false })); } else if (searchEnabledQuery.isError) { logger.error('Failed to get search enabled: ', searchEnabledQuery.error); + setSearch((prev) => ({ ...prev, enabled: false })); } }, [searchEnabledQuery.data, searchEnabledQuery.error, searchEnabledQuery.isError, setSearch]);