🔍 refactor: Search & Message Retrieval (#6903)

* refactor: conversation search fetch

* refactor: Message and Convo fetch with paramters and search

* refactor: update search states and cleanup old store states

* refactor: re-enable search API; fix: search conversation

* fix: message's convo fetch

* fix: redirect when searching

* chore: use logger instead of console

* fix: search message loading

* feat: small optimizations

* feat(Message): remove cache for search path

* fix: handle delete of all archivedConversation and sharedLinks

* chore: cleanup

* fix: search messages

* style: update ConvoOptions styles

* refactor(SearchButtons): streamline conversation fetching and remove unused state

* fix: ensure messages are invalidated after fetching conversation data

* fix: add iconURL to conversation query selection

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Marco Beretta 2025-04-17 03:07:43 +02:00 committed by GitHub
parent 851938e7a6
commit 88f4ad7c47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 489 additions and 576 deletions

View file

@ -1,11 +1,7 @@
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 { TConversation, ConversationListResponse } from 'librechat-data-provider';
import type { InfiniteQueryObserverResult } from '@tanstack/react-query';
import {
useLocalize,
@ -17,8 +13,6 @@ import {
} 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';
@ -74,71 +68,48 @@ const Nav = memo(
permission: Permissions.USE,
});
const isSearchEnabled = useRecoilValue(store.isSearchEnabled);
const isSearchTyping = useRecoilValue(store.isSearchTyping);
const { searchQuery, searchQueryRes } = useSearchContext();
const search = useRecoilValue(store.search);
const { data, fetchNextPage, isFetchingNextPage, refetch } = useConversationsInfiniteQuery(
{
isArchived: false,
tags: tags.length === 0 ? undefined : tags,
},
{
enabled: isAuthenticated,
staleTime: 30000,
cacheTime: 300000,
},
);
const { data, fetchNextPage, isFetchingNextPage, isLoading, isFetching, refetch } =
useConversationsInfiniteQuery(
{
tags: tags.length === 0 ? undefined : tags,
search: search.debouncedQuery || undefined,
},
{
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) {
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]);
}, [data?.pages]);
const outerContainerRef = useRef<HTMLDivElement>(null);
const listRef = useRef<any>(null);
const { moveToTop } = useNavScrolling<
ConversationListResponse | SearchConversationListResponse
>({
const { moveToTop } = useNavScrolling<ConversationListResponse>({
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 fetchNextPage(options);
}
return Promise.resolve(
{} as InfiniteQueryObserverResult<
SearchConversationListResponse | ConversationListResponse,
unknown
>,
{} as InfiniteQueryObserverResult<ConversationListResponse, unknown>,
);
},
isFetchingNext: searchQuery
? (searchQueryRes?.isFetchingNextPage ?? false)
: isFetchingNextPage,
isFetchingNext: 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]);
}, [data]);
const toggleNavVisible = useCallback(() => {
setNavVisible((prev: boolean) => {
@ -183,7 +154,7 @@ const Nav = memo(
const subHeaders = useMemo(
() => (
<>
{isSearchEnabled === true && <SearchBar isSmallScreen={isSmallScreen} />}
{search.enabled === true && <SearchBar isSmallScreen={isSmallScreen} />}
{hasAccessToBookmarks && (
<>
<div className="mt-1.5" />
@ -194,14 +165,22 @@ const Nav = memo(
)}
</>
),
[isSearchEnabled, hasAccessToBookmarks, isSmallScreen, tags, setTags],
[search.enabled, hasAccessToBookmarks, isSmallScreen, tags, setTags],
);
const isSearchLoading =
!!searchQuery &&
(isSearchTyping ||
(searchQueryRes?.isLoading ?? false) ||
(searchQueryRes?.isFetching ?? false));
const [isSearchLoading, setIsSearchLoading] = useState(
!!search.query && (search.isTyping || isLoading || isFetching),
);
useEffect(() => {
if (search.isTyping) {
setIsSearchLoading(true);
} else if (!isLoading && !isFetching) {
setIsSearchLoading(false);
} else if (!!search.query && (isLoading || isFetching)) {
setIsSearchLoading(true);
}
}, [search.query, search.isTyping, isLoading, isFetching]);
return (
<>
@ -243,7 +222,7 @@ const Nav = memo(
toggleNav={itemToggleNav}
containerRef={listRef}
loadMoreConversations={loadMoreConversations}
isFetchingNextPage={isFetchingNextPage || showLoading}
isLoading={isFetchingNextPage || showLoading || isLoading}
isSearchLoading={isSearchLoading}
/>
</div>