LibreChat/client/src/routes/Search.tsx
Dustin Healy 0cf7bb2d0e
🔊 fix: Conversation Search Result Announcement (#11449)
* fix: search result announcements

* chore: address Copilot comments

* chore: no nested ternary statements allowed

* chore: memoize results announcement
2026-01-22 19:36:46 -05:00

118 lines
3.7 KiB
TypeScript

import { useEffect, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { Spinner, useToastContext } from '@librechat/client';
import MinimalMessagesWrapper from '~/components/Chat/Messages/MinimalMessages';
import { useNavScrolling, useLocalize, useAuthContext } from '~/hooks';
import SearchMessage from '~/components/Chat/Messages/SearchMessage';
import { useMessagesInfiniteQuery } from '~/data-provider';
import { useFileMapContext } from '~/Providers';
import store from '~/store';
export default function Search() {
const localize = useLocalize();
const fileMap = useFileMapContext();
const { showToast } = useToastContext();
const { isAuthenticated } = useAuthContext();
const search = useRecoilValue(store.search);
const searchQuery = search.debouncedQuery;
const {
data: searchMessages,
isLoading,
isError,
fetchNextPage,
isFetchingNextPage,
hasNextPage: _hasNextPage,
} = useMessagesInfiniteQuery(
{
search: searchQuery || undefined,
},
{
enabled: isAuthenticated && !!searchQuery,
staleTime: 30000,
cacheTime: 300000,
},
);
const { containerRef } = useNavScrolling({
nextCursor: searchMessages?.pages[searchMessages.pages.length - 1]?.nextCursor,
setShowLoading: () => ({}),
fetchNextPage: fetchNextPage,
isFetchingNext: isFetchingNextPage,
});
const messages = useMemo(() => {
const msgs =
searchMessages?.pages.flatMap((page) =>
page.messages.map((message) => {
if (!message.files || !fileMap) {
return message;
}
return {
...message,
files: message.files.map((file) => fileMap[file.file_id ?? ''] ?? file),
};
}),
) || [];
return msgs.length === 0 ? null : msgs;
}, [fileMap, searchMessages?.pages]);
useEffect(() => {
if (isError && searchQuery) {
showToast({ message: 'An error occurred during search', status: 'error' });
}
}, [isError, searchQuery, showToast]);
const resultsCount = messages?.length ?? 0;
const resultsAnnouncement = useMemo(() => {
if (resultsCount === 0) {
return localize('com_ui_nothing_found');
}
if (resultsCount === 1) {
return localize('com_ui_result_found', { count: resultsCount });
}
return localize('com_ui_results_found', { count: resultsCount });
}, [resultsCount, localize]);
const isSearchLoading = search.isTyping || isLoading || isFetchingNextPage;
if (isSearchLoading) {
return (
<div className="absolute inset-0 flex items-center justify-center">
<Spinner className="text-text-primary" />
</div>
);
}
if (!searchQuery) {
return null;
}
return (
<MinimalMessagesWrapper ref={containerRef} className="relative flex h-full pt-4">
<div className="sr-only" role="alert" aria-atomic="true">
{resultsAnnouncement}
</div>
{(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')}
</div>
</div>
) : (
<>
{messages.map((msg) => (
<SearchMessage key={msg.messageId} message={msg} />
))}
{isFetchingNextPage && (
<div className="flex justify-center py-4">
<Spinner className="text-text-primary" />
</div>
)}
</>
)}
<div className="absolute bottom-0 left-0 right-0 h-[5%] bg-gradient-to-t from-gray-50 to-transparent dark:from-gray-800" />
</MinimalMessagesWrapper>
);
}