mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
📌 fix: Pin Agents and Models (#10808)
* fix(nav): handle search disabled/error states to stop skeleton loading * fix(ui): correct chevron direction for chats expand/collapse toggle * feat(Conversations): Introduce MeasuredRow and ChatsHeader components for improved rendering and layout --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
99f8bd2ce6
commit
e6288c379c
2 changed files with 106 additions and 48 deletions
|
|
@ -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<TConversation | null>;
|
||||
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<MeasuredRowProps> = memo(
|
||||
({ cache, rowKey, parent, index, style, children }) => (
|
||||
<CellMeasurer cache={cache} columnIndex={0} key={rowKey} parent={parent} rowIndex={index}>
|
||||
{({ registerChild }) => (
|
||||
<div ref={registerChild as React.LegacyRef<HTMLDivElement>} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</CellMeasurer>
|
||||
),
|
||||
);
|
||||
|
||||
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<ChatsHeaderProps> = memo(({ isExpanded, onToggle }) => {
|
||||
const localize = useLocalize();
|
||||
return (
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="group flex w-full items-center justify-between px-1 py-2 text-xs font-bold text-text-secondary"
|
||||
type="button"
|
||||
>
|
||||
<span className="select-none">{localize('com_ui_chats')}</span>
|
||||
<ChevronDown
|
||||
className={cn('h-3 w-3 transition-transform duration-200', isExpanded ? 'rotate-180' : '')}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
ChatsHeader.displayName = 'ChatsHeader';
|
||||
|
||||
const DateLabel: FC<{ groupName: string; isFirst?: boolean }> = memo(({ groupName, isFirst }) => {
|
||||
const localize = useLocalize();
|
||||
return (
|
||||
|
|
@ -188,62 +246,60 @@ const Conversations: FC<ConversationsProps> = ({
|
|||
const rowRenderer = useCallback(
|
||||
({ index, key, parent, style }) => {
|
||||
const item = flattenedItems[index];
|
||||
const rowProps = { cache, rowKey: key, parent, index, style };
|
||||
|
||||
if (item.type === 'loading') {
|
||||
return (
|
||||
<CellMeasurer cache={cache} columnIndex={0} key={key} parent={parent} rowIndex={index}>
|
||||
{({ registerChild }) => (
|
||||
<div ref={registerChild} style={style}>
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
)}
|
||||
</CellMeasurer>
|
||||
<MeasuredRow {...rowProps}>
|
||||
<LoadingSpinner />
|
||||
</MeasuredRow>
|
||||
);
|
||||
}
|
||||
let rendering: JSX.Element;
|
||||
|
||||
if (item.type === 'favorites') {
|
||||
rendering = (
|
||||
<FavoritesList
|
||||
isSmallScreen={isSmallScreen}
|
||||
toggleNav={toggleNav}
|
||||
onHeightChange={clearFavoritesCache}
|
||||
/>
|
||||
);
|
||||
} else if (item.type === 'chats-header') {
|
||||
rendering = (
|
||||
<button
|
||||
onClick={() => setIsChatsExpanded(!isChatsExpanded)}
|
||||
className="group flex w-full items-center justify-between px-1 py-2 text-xs font-bold text-text-secondary"
|
||||
type="button"
|
||||
>
|
||||
<span className="select-none">{localize('com_ui_chats')}</span>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'h-3 w-3 transition-transform duration-200',
|
||||
isChatsExpanded ? 'rotate-90' : '',
|
||||
)}
|
||||
return (
|
||||
<MeasuredRow {...rowProps}>
|
||||
<FavoritesList
|
||||
isSmallScreen={isSmallScreen}
|
||||
toggleNav={toggleNav}
|
||||
onHeightChange={clearFavoritesCache}
|
||||
/>
|
||||
</button>
|
||||
</MeasuredRow>
|
||||
);
|
||||
} else if (item.type === 'header') {
|
||||
}
|
||||
|
||||
if (item.type === 'chats-header') {
|
||||
return (
|
||||
<MeasuredRow {...rowProps}>
|
||||
<ChatsHeader
|
||||
isExpanded={isChatsExpanded}
|
||||
onToggle={() => setIsChatsExpanded(!isChatsExpanded)}
|
||||
/>
|
||||
</MeasuredRow>
|
||||
);
|
||||
}
|
||||
|
||||
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 = <DateLabel groupName={item.groupName} isFirst={index === firstHeaderIndex} />;
|
||||
} else if (item.type === 'convo') {
|
||||
rendering = (
|
||||
<MemoizedConvo conversation={item.convo} retainView={moveToTop} toggleNav={toggleNav} />
|
||||
return (
|
||||
<MeasuredRow {...rowProps}>
|
||||
<DateLabel groupName={item.groupName} isFirst={index === firstHeaderIndex} />
|
||||
</MeasuredRow>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<CellMeasurer cache={cache} columnIndex={0} key={key} parent={parent} rowIndex={index}>
|
||||
{({ registerChild }) => (
|
||||
<div ref={registerChild} style={style} className="">
|
||||
{rendering}
|
||||
</div>
|
||||
)}
|
||||
</CellMeasurer>
|
||||
);
|
||||
|
||||
if (item.type === 'convo') {
|
||||
return (
|
||||
<MeasuredRow {...rowProps}>
|
||||
<MemoizedConvo conversation={item.convo} retainView={moveToTop} toggleNav={toggleNav} />
|
||||
</MeasuredRow>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[
|
||||
cache,
|
||||
|
|
@ -253,7 +309,6 @@ const Conversations: FC<ConversationsProps> = ({
|
|||
clearFavoritesCache,
|
||||
isSmallScreen,
|
||||
isChatsExpanded,
|
||||
localize,
|
||||
setIsChatsExpanded,
|
||||
shouldShowFavorites,
|
||||
],
|
||||
|
|
@ -300,10 +355,10 @@ const Conversations: FC<ConversationsProps> = ({
|
|||
overscanRowCount={10}
|
||||
aria-readonly={false}
|
||||
className="outline-none"
|
||||
style={{ outline: 'none' }}
|
||||
aria-label="Conversations"
|
||||
onRowsRendered={handleRowsRendered}
|
||||
tabIndex={-1}
|
||||
style={{ outline: 'none', scrollbarGutter: 'stable' }}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue