mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 18:00:15 +01:00
* ✨ feat: improve Nav/Conversations/Convo/NewChat component performance * ✨ feat: implement cursor-based pagination for conversations API * 🔧 refactor: remove createdAt from conversation selection in API and type definitions * 🔧 refactor: include createdAt in conversation selection and update related types * ✨ fix: search functionality and bugs with loadMoreConversations * feat: move ArchivedChats to cursor and DataTable standard * 🔧 refactor: add InfiniteQueryObserverResult type import in Nav component * feat: enhance conversation listing with pagination, sorting, and search capabilities * 🔧 refactor: remove unnecessary comment regarding lodash/debounce in ArchivedChatsTable * 🔧 refactor: remove unused translation keys for archived chats and search results * 🔧 fix: Archived Chats, Delete Convo, Duplicate Convo * 🔧 refactor: improve conversation components with layout adjustments and new translations * 🔧 refactor: simplify archive conversation mutation and improve unarchive handling; fix: update fork mutation * 🔧 refactor: decode search query parameter in conversation route; improve error handling in unarchive mutation; clean up DataTable component styles * 🔧 refactor: remove unused translation key for empty archived chats * 🚀 fix: `archivedConversation` query key not updated correctly while archiving * 🧠 feat: Bedrock Anthropic Reasoning & Update Endpoint Handling (#6163) * feat: Add thinking and thinkingBudget parameters for Bedrock Anthropic models * chore: Update @librechat/agents to version 2.1.8 * refactor: change region order in params * refactor: Add maxTokens parameter to conversation preset schema * refactor: Update agent client to use bedrockInputSchema and improve error handling for model parameters * refactor: streamline/optimize llmConfig initialization and saving for bedrock * fix: ensure config titleModel is used for all endpoints * refactor: enhance OpenAIClient and agent initialization to support endpoint checks for OpenRouter * chore: bump @google/generative-ai * ✨ feat: improve Nav/Conversations/Convo/NewChat component performance * 🔧 refactor: remove unnecessary comment regarding lodash/debounce in ArchivedChatsTable * 🔧 refactor: update translation keys for clarity; simplify conversation query parameters and improve sorting functionality in SharedLinks component * 🔧 refactor: optimize conversation loading logic and improve search handling in Nav component * fix: package-lock * fix: package-lock 2 * fix: package lock 3 * refactor: remove unused utility files and exports to clean up the codebase * refactor: remove i18n and useAuthRedirect modules to streamline codebase * refactor: optimize Conversations component and remove unused ToggleContext * refactor(Convo): add RenameForm and ConvoLink components; enhance Conversations component with responsive design * fix: add missing @azure/storage-blob dependency in package.json * refactor(Search): add error handling with toast notification for search errors * refactor: make createdAt and updatedAt fields of tConvoUpdateSchema less restrictive if timestamps are missing * chore: update @azure/storage-blob dependency to version 12.27.0, ensure package-lock is correct * refactor(Search): improve conversation handling server side * fix: eslint warning and errors * refactor(Search): improved search loading state and overall UX * Refactors conversation cache management Centralizes conversation mutation logic into dedicated utility functions for adding, updating, and removing conversations from query caches. Improves reliability and maintainability by: - Consolidating duplicate cache manipulation code - Adding type safety for infinite query data structures - Implementing consistent cache update patterns across all conversation operations - Removing obsolete conversation helper functions in favor of standardized utilities * fix: conversation handling and SSE event processing - Optimizes conversation state management with useMemo and proper hook ordering - Improves SSE event handler documentation and error handling - Adds reset guard flag for conversation changes - Removes redundant navigation call - Cleans up cursor handling logic and document structure Improves code maintainability and prevents potential race conditions in conversation state updates * refactor: add type for SearchBar `onChange` * fix: type tags * style: rounded to xl all Header buttons * fix: activeConvo in Convo not working * style(Bookmarks): improved UI * a11y(AccountSettings): fixed hover style not visible when using light theme * style(SettingsTabs): improved tab switchers and dropdowns * feat: add translations keys for Speech * chore: fix package-lock * fix(mutations): legacy import after rebase * feat: refactor conversation navigation for accessibility * fix(search): convo and message create/update date not returned * fix(search): show correct iconURL and endpoint for searched messages * fix: small UI improvements * chore: console.log cleanup * chore: fix tests * fix(ChatForm): improve conversation ID handling and clean up useMemo dependencies * chore: improve typing * chore: improve typing * fix(useSSE): clear conversation ID on submission to prevent draft restoration * refactor(OpenAIClient): clean up abort handler * refactor(abortMiddleware): change handleAbort to use function expression * feat: add PENDING_CONVO constant and update conversation ID checks * fix: final event handling on abort * fix: improve title sync and query cache sync on final event * fix: prevent overwriting cached conversation data if it already exists --------- Co-authored-by: Danny Avila <danny@librechat.ai>
276 lines
8.8 KiB
TypeScript
276 lines
8.8 KiB
TypeScript
import { useCallback, useEffect, useState, useMemo, memo, lazy, Suspense, useRef } from 'react';
|
|
import { useRecoilValue } from 'recoil';
|
|
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
|
import type {
|
|
TConversation,
|
|
ConversationListResponse,
|
|
SearchConversationListResponse,
|
|
} from 'librechat-data-provider';
|
|
import type { InfiniteQueryObserverResult } from '@tanstack/react-query';
|
|
import {
|
|
useLocalize,
|
|
useHasAccess,
|
|
useMediaQuery,
|
|
useAuthContext,
|
|
useLocalStorage,
|
|
useNavScrolling,
|
|
} from '~/hooks';
|
|
import { useConversationsInfiniteQuery } from '~/data-provider';
|
|
import { Conversations } from '~/components/Conversations';
|
|
import { useSearchContext } from '~/Providers';
|
|
import { Spinner } from '~/components';
|
|
import NavToggle from './NavToggle';
|
|
import SearchBar from './SearchBar';
|
|
import NewChat from './NewChat';
|
|
import { cn } from '~/utils';
|
|
import store from '~/store';
|
|
|
|
const BookmarkNav = lazy(() => import('./Bookmarks/BookmarkNav'));
|
|
const AccountSettings = lazy(() => import('./AccountSettings'));
|
|
|
|
const NAV_WIDTH_DESKTOP = '260px';
|
|
const NAV_WIDTH_MOBILE = '320px';
|
|
|
|
const NavMask = memo(
|
|
({ navVisible, toggleNavVisible }: { navVisible: boolean; toggleNavVisible: () => void }) => (
|
|
<div
|
|
id="mobile-nav-mask-toggle"
|
|
role="button"
|
|
tabIndex={0}
|
|
className={`nav-mask ${navVisible ? 'active' : ''}`}
|
|
onClick={toggleNavVisible}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
toggleNavVisible();
|
|
}
|
|
}}
|
|
aria-label="Toggle navigation"
|
|
/>
|
|
),
|
|
);
|
|
|
|
const MemoNewChat = memo(NewChat);
|
|
|
|
const Nav = memo(
|
|
({
|
|
navVisible,
|
|
setNavVisible,
|
|
}: {
|
|
navVisible: boolean;
|
|
setNavVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
|
}) => {
|
|
const localize = useLocalize();
|
|
const { isAuthenticated } = useAuthContext();
|
|
|
|
const [navWidth, setNavWidth] = useState(NAV_WIDTH_DESKTOP);
|
|
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
|
const [newUser, setNewUser] = useLocalStorage('newUser', true);
|
|
const [isToggleHovering, setIsToggleHovering] = useState(false);
|
|
const [showLoading, setShowLoading] = useState(false);
|
|
const [tags, setTags] = useState<string[]>([]);
|
|
|
|
const hasAccessToBookmarks = useHasAccess({
|
|
permissionType: PermissionTypes.BOOKMARKS,
|
|
permission: Permissions.USE,
|
|
});
|
|
|
|
const isSearchEnabled = useRecoilValue(store.isSearchEnabled);
|
|
const isSearchTyping = useRecoilValue(store.isSearchTyping);
|
|
const { searchQuery, searchQueryRes } = useSearchContext();
|
|
|
|
const { data, fetchNextPage, isFetchingNextPage, refetch } = useConversationsInfiniteQuery(
|
|
{
|
|
isArchived: false,
|
|
tags: tags.length === 0 ? undefined : tags,
|
|
},
|
|
{
|
|
enabled: isAuthenticated,
|
|
staleTime: 30000,
|
|
cacheTime: 300000,
|
|
},
|
|
);
|
|
|
|
const computedHasNextPage = useMemo(() => {
|
|
if (searchQuery && searchQueryRes?.data) {
|
|
const pages = searchQueryRes.data.pages;
|
|
return pages[pages.length - 1]?.nextCursor !== null;
|
|
} else if (data?.pages && data.pages.length > 0) {
|
|
const lastPage: ConversationListResponse = data.pages[data.pages.length - 1];
|
|
return lastPage.nextCursor !== null;
|
|
}
|
|
return false;
|
|
}, [searchQuery, searchQueryRes?.data, data?.pages]);
|
|
|
|
const outerContainerRef = useRef<HTMLDivElement>(null);
|
|
const listRef = useRef<any>(null);
|
|
|
|
const { moveToTop } = useNavScrolling<
|
|
ConversationListResponse | SearchConversationListResponse
|
|
>({
|
|
setShowLoading,
|
|
fetchNextPage: async (options?) => {
|
|
if (computedHasNextPage) {
|
|
if (searchQuery && searchQueryRes) {
|
|
const pages = searchQueryRes.data?.pages;
|
|
if (pages && pages.length > 0 && pages[pages.length - 1]?.nextCursor !== null) {
|
|
return searchQueryRes.fetchNextPage(options);
|
|
}
|
|
} else {
|
|
return fetchNextPage(options);
|
|
}
|
|
}
|
|
return Promise.resolve(
|
|
{} as InfiniteQueryObserverResult<
|
|
SearchConversationListResponse | ConversationListResponse,
|
|
unknown
|
|
>,
|
|
);
|
|
},
|
|
isFetchingNext: searchQuery
|
|
? (searchQueryRes?.isFetchingNextPage ?? false)
|
|
: isFetchingNextPage,
|
|
});
|
|
|
|
const conversations = useMemo(() => {
|
|
if (searchQuery && searchQueryRes?.data) {
|
|
return searchQueryRes.data.pages.flatMap(
|
|
(page) => page.conversations ?? [],
|
|
) as TConversation[];
|
|
}
|
|
return data ? data.pages.flatMap((page) => page.conversations) : [];
|
|
}, [data, searchQuery, searchQueryRes?.data]);
|
|
|
|
const toggleNavVisible = useCallback(() => {
|
|
setNavVisible((prev: boolean) => {
|
|
localStorage.setItem('navVisible', JSON.stringify(!prev));
|
|
return !prev;
|
|
});
|
|
if (newUser) {
|
|
setNewUser(false);
|
|
}
|
|
}, [newUser, setNavVisible, setNewUser]);
|
|
|
|
const itemToggleNav = useCallback(() => {
|
|
if (isSmallScreen) {
|
|
toggleNavVisible();
|
|
}
|
|
}, [isSmallScreen, toggleNavVisible]);
|
|
|
|
useEffect(() => {
|
|
if (isSmallScreen) {
|
|
const savedNavVisible = localStorage.getItem('navVisible');
|
|
if (savedNavVisible === null) {
|
|
toggleNavVisible();
|
|
}
|
|
setNavWidth(NAV_WIDTH_MOBILE);
|
|
} else {
|
|
setNavWidth(NAV_WIDTH_DESKTOP);
|
|
}
|
|
}, [isSmallScreen, toggleNavVisible]);
|
|
|
|
useEffect(() => {
|
|
refetch();
|
|
}, [tags, refetch]);
|
|
|
|
const loadMoreConversations = useCallback(() => {
|
|
if (isFetchingNextPage || !computedHasNextPage) {
|
|
return;
|
|
}
|
|
|
|
fetchNextPage();
|
|
}, [isFetchingNextPage, computedHasNextPage, fetchNextPage]);
|
|
|
|
const subHeaders = useMemo(
|
|
() => (
|
|
<>
|
|
{isSearchEnabled === true && <SearchBar isSmallScreen={isSmallScreen} />}
|
|
{hasAccessToBookmarks && (
|
|
<>
|
|
<div className="mt-1.5" />
|
|
<Suspense fallback={null}>
|
|
<BookmarkNav tags={tags} setTags={setTags} isSmallScreen={isSmallScreen} />
|
|
</Suspense>
|
|
</>
|
|
)}
|
|
</>
|
|
),
|
|
[isSearchEnabled, hasAccessToBookmarks, isSmallScreen, tags, setTags],
|
|
);
|
|
|
|
const isSearchLoading =
|
|
!!searchQuery &&
|
|
(isSearchTyping ||
|
|
(searchQueryRes?.isLoading ?? false) ||
|
|
(searchQueryRes?.isFetching ?? false));
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
data-testid="nav"
|
|
className={cn(
|
|
'nav active max-w-[320px] flex-shrink-0 overflow-x-hidden bg-surface-primary-alt',
|
|
'md:max-w-[260px]',
|
|
)}
|
|
style={{
|
|
width: navVisible ? navWidth : '0px',
|
|
visibility: navVisible ? 'visible' : 'hidden',
|
|
transition: 'width 0.2s, visibility 0.2s',
|
|
}}
|
|
>
|
|
<div className="h-full w-[320px] md:w-[260px]">
|
|
<div className="flex h-full flex-col">
|
|
<div
|
|
className={cn(
|
|
'flex h-full flex-col transition-opacity',
|
|
isToggleHovering && !isSmallScreen ? 'opacity-50' : 'opacity-100',
|
|
)}
|
|
>
|
|
<div className="flex h-full flex-col">
|
|
<nav
|
|
id="chat-history-nav"
|
|
aria-label={localize('com_ui_chat_history')}
|
|
className="flex h-full flex-col px-3 pb-3.5"
|
|
>
|
|
<div className="flex flex-1 flex-col" ref={outerContainerRef}>
|
|
<MemoNewChat
|
|
toggleNav={itemToggleNav}
|
|
isSmallScreen={isSmallScreen}
|
|
subHeaders={subHeaders}
|
|
/>
|
|
<Conversations
|
|
conversations={conversations}
|
|
moveToTop={moveToTop}
|
|
toggleNav={itemToggleNav}
|
|
containerRef={listRef}
|
|
loadMoreConversations={loadMoreConversations}
|
|
isFetchingNextPage={isFetchingNextPage || showLoading}
|
|
isSearchLoading={isSearchLoading}
|
|
/>
|
|
</div>
|
|
<Suspense fallback={null}>
|
|
<AccountSettings />
|
|
</Suspense>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<NavToggle
|
|
isHovering={isToggleHovering}
|
|
setIsHovering={setIsToggleHovering}
|
|
onToggle={toggleNavVisible}
|
|
navVisible={navVisible}
|
|
className="fixed left-0 top-1/2 z-40 hidden md:flex"
|
|
/>
|
|
|
|
{isSmallScreen && <NavMask navVisible={navVisible} toggleNavVisible={toggleNavVisible} />}
|
|
</>
|
|
);
|
|
},
|
|
);
|
|
|
|
Nav.displayName = 'Nav';
|
|
|
|
export default Nav;
|