🔍 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,14 +1,19 @@
import React, { useState, useEffect } from 'react';
import { Outlet } from 'react-router-dom';
import type { ContextType } from '~/common';
import {
useAuthContext,
useAssistantsMap,
useAgentsMap,
useFileMap,
useSearchEnabled,
} from '~/hooks';
import {
AgentsMapContext,
AssistantsMapContext,
FileMapContext,
SearchContext,
SetConvoProvider,
} from '~/Providers';
import { useAuthContext, useAssistantsMap, useAgentsMap, useFileMap, useSearch } from '~/hooks';
import TermsAndConditionsModal from '~/components/ui/TermsAndConditionsModal';
import { useUserTermsQuery, useGetStartupConfig } from '~/data-provider';
import { Nav, MobileNav } from '~/components/Nav';
@ -26,13 +31,14 @@ export default function Root() {
const assistantsMap = useAssistantsMap({ isAuthenticated });
const agentsMap = useAgentsMap({ isAuthenticated });
const fileMap = useFileMap({ isAuthenticated });
const search = useSearch({ isAuthenticated });
const { data: config } = useGetStartupConfig();
const { data: termsData } = useUserTermsQuery({
enabled: isAuthenticated && config?.interface?.termsOfService?.modalAcceptance === true,
});
useSearchEnabled(isAuthenticated);
useEffect(() => {
if (termsData) {
setShowTerms(!termsData.termsAccepted);
@ -43,7 +49,6 @@ export default function Root() {
setShowTerms(false);
};
// Pass the desired redirect parameter to logout
const handleDeclineTerms = () => {
setShowTerms(false);
logout('/login?redirect=false');
@ -55,34 +60,32 @@ export default function Root() {
return (
<SetConvoProvider>
<SearchContext.Provider value={search}>
<FileMapContext.Provider value={fileMap}>
<AssistantsMapContext.Provider value={assistantsMap}>
<AgentsMapContext.Provider value={agentsMap}>
<Banner onHeightChange={setBannerHeight} />
<div className="flex" style={{ height: `calc(100dvh - ${bannerHeight}px)` }}>
<div className="relative z-0 flex h-full w-full overflow-hidden">
<Nav navVisible={navVisible} setNavVisible={setNavVisible} />
<div className="relative flex h-full max-w-full flex-1 flex-col overflow-hidden">
<MobileNav setNavVisible={setNavVisible} />
<Outlet context={{ navVisible, setNavVisible } satisfies ContextType} />
</div>
<FileMapContext.Provider value={fileMap}>
<AssistantsMapContext.Provider value={assistantsMap}>
<AgentsMapContext.Provider value={agentsMap}>
<Banner onHeightChange={setBannerHeight} />
<div className="flex" style={{ height: `calc(100dvh - ${bannerHeight}px)` }}>
<div className="relative z-0 flex h-full w-full overflow-hidden">
<Nav navVisible={navVisible} setNavVisible={setNavVisible} />
<div className="relative flex h-full max-w-full flex-1 flex-col overflow-hidden">
<MobileNav setNavVisible={setNavVisible} />
<Outlet context={{ navVisible, setNavVisible } satisfies ContextType} />
</div>
</div>
</AgentsMapContext.Provider>
{config?.interface?.termsOfService?.modalAcceptance === true && (
<TermsAndConditionsModal
open={showTerms}
onOpenChange={setShowTerms}
onAccept={handleAcceptTerms}
onDecline={handleDeclineTerms}
title={config.interface.termsOfService.modalTitle}
modalContent={config.interface.termsOfService.modalContent}
/>
)}
</AssistantsMapContext.Provider>
</FileMapContext.Provider>
</SearchContext.Provider>
</div>
</AgentsMapContext.Provider>
{config?.interface?.termsOfService?.modalAcceptance === true && (
<TermsAndConditionsModal
open={showTerms}
onOpenChange={setShowTerms}
onAccept={handleAcceptTerms}
onDecline={handleDeclineTerms}
title={config.interface.termsOfService.modalTitle}
modalContent={config.interface.termsOfService.modalContent}
/>
)}
</AssistantsMapContext.Provider>
</FileMapContext.Provider>
</SetConvoProvider>
);
}

View file

@ -1,47 +1,62 @@
import { useEffect, useMemo } from 'react';
import type { FetchNextPageOptions } from '@tanstack/react-query';
import { useToastContext, useSearchContext, useFileMapContext } from '~/Providers';
import { useRecoilValue } from 'recoil';
import MinimalMessagesWrapper from '~/components/Chat/Messages/MinimalMessages';
import { useNavScrolling, useLocalize, useAuthContext } from '~/hooks';
import SearchMessage from '~/components/Chat/Messages/SearchMessage';
import { useNavScrolling, useLocalize } from '~/hooks';
import { useToastContext, useFileMapContext } from '~/Providers';
import { useMessagesInfiniteQuery } from '~/data-provider';
import { Spinner } from '~/components';
import { buildTree } from '~/utils';
import { useRecoilValue } from 'recoil';
import store from '~/store';
export default function Search() {
const localize = useLocalize();
const fileMap = useFileMapContext();
const { showToast } = useToastContext();
const { searchQuery, searchQueryRes } = useSearchContext();
const isSearchTyping = useRecoilValue(store.isSearchTyping);
const { isAuthenticated } = useAuthContext();
const search = useRecoilValue(store.search);
const searchQuery = search.debouncedQuery;
const {
data: searchMessages,
isLoading,
isError,
fetchNextPage,
isFetchingNextPage,
hasNextPage,
} = useMessagesInfiniteQuery(
{
search: searchQuery || undefined,
},
{
enabled: isAuthenticated && !!searchQuery,
staleTime: 30000,
cacheTime: 300000,
},
);
const { containerRef } = useNavScrolling({
nextCursor: searchQueryRes?.data?.pages[searchQueryRes.data.pages.length - 1]?.nextCursor,
nextCursor: searchMessages?.pages[searchMessages.pages.length - 1]?.nextCursor,
setShowLoading: () => ({}),
fetchNextPage: searchQueryRes?.fetchNextPage
? (options?: FetchNextPageOptions) => searchQueryRes.fetchNextPage(options)
: undefined,
isFetchingNext: searchQueryRes?.isFetchingNextPage ?? false,
fetchNextPage: fetchNextPage,
isFetchingNext: isFetchingNextPage,
});
useEffect(() => {
if (searchQueryRes?.error) {
showToast({ message: 'An error occurred during search', status: 'error' });
}
}, [searchQueryRes?.error, showToast]);
const messages = useMemo(() => {
const msgs = searchQueryRes?.data?.pages.flatMap((page) => page.messages) || [];
const msgs = searchMessages?.pages.flatMap((page) => page.messages) || [];
const dataTree = buildTree({ messages: msgs, fileMap });
return dataTree?.length === 0 ? null : (dataTree ?? null);
}, [fileMap, searchQueryRes?.data?.pages]);
}, [fileMap, searchMessages?.pages]);
if (!searchQuery || !searchQueryRes?.data) {
return null;
}
useEffect(() => {
if (isError && searchQuery) {
showToast({ message: 'An error occurred during search', status: 'error' });
}
}, [isError, searchQuery, showToast]);
if (isSearchTyping || searchQueryRes.isInitialLoading || searchQueryRes.isLoading) {
const isSearchLoading = search.isTyping || isLoading || isFetchingNextPage;
if (isSearchLoading) {
return (
<div className="absolute inset-0 flex items-center justify-center">
<Spinner className="text-text-primary" />
@ -49,9 +64,13 @@ export default function Search() {
);
}
if (!searchQuery) {
return null;
}
return (
<MinimalMessagesWrapper ref={containerRef} className="relative flex h-full pt-4">
{(messages && messages.length == 0) || messages == null ? (
{(messages && messages.length === 0) || messages == null ? (
<div className="absolute inset-0 flex items-center justify-center">
<div className="rounded-lg bg-white p-6 text-lg text-gray-500 dark:border-gray-800/50 dark:bg-gray-800 dark:text-gray-300">
{localize('com_ui_nothing_found')}
@ -62,7 +81,7 @@ export default function Search() {
{messages.map((msg) => (
<SearchMessage key={msg.messageId} message={msg} />
))}
{searchQueryRes.isFetchingNextPage && (
{isFetchingNextPage && (
<div className="flex justify-center py-4">
<Spinner className="text-text-primary" />
</div>