mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-20 18:30:15 +01:00
🔍 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:
parent
851938e7a6
commit
88f4ad7c47
30 changed files with 489 additions and 576 deletions
|
|
@ -1,5 +0,0 @@
|
|||
import { createContext, useContext } from 'react';
|
||||
import { UseSearchMessagesResult } from '~/hooks/Conversations/useSearch';
|
||||
|
||||
export const SearchContext = createContext<UseSearchMessagesResult>({} as UseSearchMessagesResult);
|
||||
export const useSearchContext = () => useContext(SearchContext);
|
||||
|
|
@ -4,7 +4,6 @@ export { default as AgentsProvider } from './AgentsContext';
|
|||
export * from './ChatContext';
|
||||
export * from './ShareContext';
|
||||
export * from './ToastContext';
|
||||
export * from './SearchContext';
|
||||
export * from './FileMapContext';
|
||||
export * from './AddedChatContext';
|
||||
export * from './EditorContext';
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
|||
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false);
|
||||
const [backupBadges, setBackupBadges] = useState<Pick<BadgeItem, 'id'>[]>([]);
|
||||
|
||||
const isSearching = useRecoilValue(store.isSearching);
|
||||
const search = useRecoilValue(store.search);
|
||||
const SpeechToText = useRecoilValue(store.speechToText);
|
||||
const TextToSpeech = useRecoilValue(store.textToSpeech);
|
||||
const chatDirection = useRecoilValue(store.chatDirection);
|
||||
|
|
@ -151,10 +151,10 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
|||
const textValue = useWatch({ control: methods.control, name: 'text' });
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSearching && textAreaRef.current && !disableInputs) {
|
||||
if (!search.isSearching && textAreaRef.current && !disableInputs) {
|
||||
textAreaRef.current.focus();
|
||||
}
|
||||
}, [isSearching, disableInputs]);
|
||||
}, [search.isSearching, disableInputs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (textAreaRef.current) {
|
||||
|
|
|
|||
|
|
@ -1,34 +1,63 @@
|
|||
import { Link } from 'lucide-react';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { QueryKeys } from 'librechat-data-provider';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import type { TMessage, TConversation } from 'librechat-data-provider';
|
||||
import type { InfiniteData } from '@tanstack/react-query';
|
||||
import type { ConversationCursorData } from '~/utils';
|
||||
import { useLocalize, useNavigateToConvo } from '~/hooks';
|
||||
import { findConversationInInfinite } from '~/utils';
|
||||
import { useSearchContext } from '~/Providers';
|
||||
import store from '~/store';
|
||||
|
||||
export default function SearchButtons({ message }: { message: TMessage }) {
|
||||
const localize = useLocalize();
|
||||
const { searchQueryRes } = useSearchContext();
|
||||
const queryClient = useQueryClient();
|
||||
const search = useRecoilValue(store.search);
|
||||
const { navigateWithLastTools } = useNavigateToConvo();
|
||||
const conversationId = message.conversationId ?? '';
|
||||
|
||||
const clickHandler = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
if (!conversationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let title = message.title ?? '';
|
||||
let cachedConvo = queryClient.getQueryData<TConversation>([
|
||||
QueryKeys.conversation,
|
||||
conversationId,
|
||||
]);
|
||||
const convos = queryClient.getQueryData<InfiniteData<ConversationCursorData>>([
|
||||
QueryKeys.allConversations,
|
||||
{ search: search.debouncedQuery },
|
||||
]);
|
||||
if (!cachedConvo && convos) {
|
||||
cachedConvo = findConversationInInfinite(convos, conversationId);
|
||||
}
|
||||
if (!title) {
|
||||
title = cachedConvo?.title ?? '';
|
||||
}
|
||||
|
||||
document.title = title;
|
||||
navigateWithLastTools(
|
||||
cachedConvo ??
|
||||
({
|
||||
conversationId,
|
||||
title,
|
||||
} as TConversation),
|
||||
true,
|
||||
true,
|
||||
);
|
||||
};
|
||||
|
||||
if (!conversationId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const clickHandler = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const conversation = findConversationInInfinite(searchQueryRes?.data, conversationId);
|
||||
if (!conversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.title = message.title ?? '';
|
||||
navigateWithLastTools(conversation, true, true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="visible mt-0 flex items-center justify-center gap-1 self-end text-text-secondary lg:justify-start">
|
||||
<button
|
||||
type="button"
|
||||
className="ml-0 flex cursor-pointer items-center gap-1.5 rounded-md p-1 text-xs hover:text-text-primary hover:underline"
|
||||
onClick={clickHandler}
|
||||
title={localize('com_ui_go_to_conversation')}
|
||||
|
|
|
|||
|
|
@ -14,13 +14,20 @@ interface ConversationsProps {
|
|||
toggleNav: () => void;
|
||||
containerRef: React.RefObject<HTMLDivElement | List>;
|
||||
loadMoreConversations: () => void;
|
||||
isFetchingNextPage: boolean;
|
||||
isLoading: boolean;
|
||||
isSearchLoading: boolean;
|
||||
}
|
||||
|
||||
const LoadingSpinner = memo(() => (
|
||||
<Spinner className="m-1 mx-auto mb-4 h-4 w-4 text-text-primary" />
|
||||
));
|
||||
const LoadingSpinner = memo(() => {
|
||||
const localize = useLocalize();
|
||||
|
||||
return (
|
||||
<div className="mx-auto mt-2 flex items-center justify-center gap-2">
|
||||
<Spinner className="h-4 w-4 text-text-primary" />
|
||||
<span className="animate-pulse text-text-primary">{localize('com_ui_loading')}</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const DateLabel: FC<{ groupName: string }> = memo(({ groupName }) => {
|
||||
const localize = useLocalize();
|
||||
|
|
@ -35,7 +42,8 @@ DateLabel.displayName = 'DateLabel';
|
|||
|
||||
type FlattenedItem =
|
||||
| { type: 'header'; groupName: string }
|
||||
| { type: 'convo'; convo: TConversation };
|
||||
| { type: 'convo'; convo: TConversation }
|
||||
| { type: 'loading' };
|
||||
|
||||
const MemoizedConvo = memo(
|
||||
({
|
||||
|
|
@ -74,7 +82,7 @@ const Conversations: FC<ConversationsProps> = ({
|
|||
toggleNav,
|
||||
containerRef,
|
||||
loadMoreConversations,
|
||||
isFetchingNextPage,
|
||||
isLoading,
|
||||
isSearchLoading,
|
||||
}) => {
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
|
|
@ -103,8 +111,12 @@ const Conversations: FC<ConversationsProps> = ({
|
|||
items.push({ type: 'header', groupName });
|
||||
items.push(...convos.map((convo) => ({ type: 'convo' as const, convo })));
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
items.push({ type: 'loading' } as any);
|
||||
}
|
||||
return items;
|
||||
}, [groupedConversations]);
|
||||
}, [groupedConversations, isLoading]);
|
||||
|
||||
const cache = useMemo(
|
||||
() =>
|
||||
|
|
@ -113,7 +125,16 @@ const Conversations: FC<ConversationsProps> = ({
|
|||
defaultHeight: convoHeight,
|
||||
keyMapper: (index) => {
|
||||
const item = flattenedItems[index];
|
||||
return item.type === 'header' ? `header-${index}` : `convo-${item.convo.conversationId}`;
|
||||
if (item.type === 'header') {
|
||||
return `header-${index}`;
|
||||
}
|
||||
if (item.type === 'convo') {
|
||||
return `convo-${item.convo.conversationId}`;
|
||||
}
|
||||
if (item.type === 'loading') {
|
||||
return `loading-${index}`;
|
||||
}
|
||||
return `unknown-${index}`;
|
||||
},
|
||||
}),
|
||||
[flattenedItems, convoHeight],
|
||||
|
|
@ -122,20 +143,31 @@ const Conversations: FC<ConversationsProps> = ({
|
|||
const rowRenderer = useCallback(
|
||||
({ index, key, parent, style }) => {
|
||||
const item = flattenedItems[index];
|
||||
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>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<CellMeasurer cache={cache} columnIndex={0} key={key} parent={parent} rowIndex={index}>
|
||||
{({ registerChild }) => (
|
||||
<div ref={registerChild} style={style}>
|
||||
{item.type === 'header' ? (
|
||||
<DateLabel groupName={item.groupName} />
|
||||
) : (
|
||||
) : item.type === 'convo' ? (
|
||||
<MemoizedConvo
|
||||
conversation={item.convo}
|
||||
retainView={moveToTop}
|
||||
toggleNav={toggleNav}
|
||||
isLatestConvo={item.convo.conversationId === firstTodayConvoId}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</CellMeasurer>
|
||||
|
|
@ -156,7 +188,7 @@ const Conversations: FC<ConversationsProps> = ({
|
|||
|
||||
const handleRowsRendered = useCallback(
|
||||
({ stopIndex }: { stopIndex: number }) => {
|
||||
if (stopIndex >= flattenedItems.length - 2) {
|
||||
if (stopIndex >= flattenedItems.length - 8) {
|
||||
throttledLoadMore();
|
||||
}
|
||||
},
|
||||
|
|
@ -193,11 +225,6 @@ const Conversations: FC<ConversationsProps> = ({
|
|||
</AutoSizer>
|
||||
</div>
|
||||
)}
|
||||
{isFetchingNextPage && !isSearchLoading && (
|
||||
<div className="mt-2">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ export function DeleteConversationDialog({
|
|||
<OGDialogContent
|
||||
title={localize('com_ui_delete_confirm') + ' ' + title}
|
||||
className="w-11/12 max-w-md"
|
||||
showCloseButton={false}
|
||||
>
|
||||
<OGDialogHeader>
|
||||
<OGDialogTitle>{localize('com_ui_delete_conversation')}</OGDialogTitle>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ import { cn } from '~/utils';
|
|||
import store from '~/store';
|
||||
|
||||
const NewChatButtonIcon = React.memo(({ conversation }: { conversation: TConversation | null }) => {
|
||||
const searchQuery = useRecoilValue(store.searchQuery);
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
const search = useRecoilValue(store.search);
|
||||
const searchQuery = search.debouncedQuery;
|
||||
|
||||
const computedIcon = useMemo(() => {
|
||||
if (searchQuery) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { forwardRef, useState, useCallback, useMemo, useEffect, Ref } from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { Search, X } from 'lucide-react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import { QueryKeys } from 'librechat-data-provider';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { forwardRef, useState, useCallback, useMemo, Ref } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useLocalize, useNewConvo } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
|
@ -17,29 +17,34 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
|||
const localize = useLocalize();
|
||||
const location = useLocation();
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const { isSmallScreen } = props;
|
||||
|
||||
const [text, setText] = useState('');
|
||||
const [showClearIcon, setShowClearIcon] = useState(false);
|
||||
|
||||
const { newConversation } = useNewConvo();
|
||||
const clearConvoState = store.useClearConvoState();
|
||||
const setSearchQuery = useSetRecoilState(store.searchQuery);
|
||||
const setIsSearching = useSetRecoilState(store.isSearching);
|
||||
const setIsSearchTyping = useSetRecoilState(store.isSearchTyping);
|
||||
const setSearchState = useSetRecoilState(store.search);
|
||||
const search = useRecoilValue(store.search);
|
||||
|
||||
const clearSearch = useCallback(() => {
|
||||
if (location.pathname.includes('/search')) {
|
||||
newConversation({ disableFocus: true });
|
||||
navigate('/c/new', { replace: true });
|
||||
}
|
||||
}, [newConversation, location.pathname]);
|
||||
}, [newConversation, location.pathname, navigate]);
|
||||
|
||||
const clearText = useCallback(() => {
|
||||
setShowClearIcon(false);
|
||||
setSearchQuery('');
|
||||
clearSearch();
|
||||
setText('');
|
||||
}, [setSearchQuery, clearSearch]);
|
||||
setSearchState((prev) => ({
|
||||
...prev,
|
||||
query: '',
|
||||
debouncedQuery: '',
|
||||
isTyping: false,
|
||||
}));
|
||||
clearSearch();
|
||||
}, [setSearchState, clearSearch]);
|
||||
|
||||
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const { value } = e.target as HTMLInputElement;
|
||||
|
|
@ -50,34 +55,48 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
|||
|
||||
const sendRequest = useCallback(
|
||||
(value: string) => {
|
||||
setSearchQuery(value);
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
queryClient.invalidateQueries([QueryKeys.messages]);
|
||||
clearConvoState();
|
||||
},
|
||||
[queryClient, clearConvoState, setSearchQuery],
|
||||
[queryClient],
|
||||
);
|
||||
|
||||
const debouncedSendRequest = useMemo(
|
||||
const debouncedSetDebouncedQuery = useMemo(
|
||||
() =>
|
||||
debounce((value: string) => {
|
||||
setSearchState((prev) => ({ ...prev, debouncedQuery: value, isTyping: false }));
|
||||
sendRequest(value);
|
||||
}, 350),
|
||||
[sendRequest, setIsSearchTyping],
|
||||
}, 500),
|
||||
[setSearchState, sendRequest],
|
||||
);
|
||||
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setShowClearIcon(value.length > 0);
|
||||
setText(value);
|
||||
setSearchQuery(value);
|
||||
setIsSearchTyping(true);
|
||||
// debounce only the API call
|
||||
debouncedSendRequest(value);
|
||||
setSearchState((prev) => ({
|
||||
...prev,
|
||||
query: value,
|
||||
isTyping: true,
|
||||
}));
|
||||
debouncedSetDebouncedQuery(value);
|
||||
if (value.length > 0 && location.pathname !== '/search') {
|
||||
navigate('/search', { replace: true });
|
||||
} else if (value.length === 0 && location.pathname === '/search') {
|
||||
navigate('/c/new', { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
// Automatically set isTyping to false when loading is done and debouncedQuery matches query
|
||||
// (prevents stuck loading state if input is still focused)
|
||||
useEffect(() => {
|
||||
if (search.isTyping && !search.isSearching && search.debouncedQuery === search.query) {
|
||||
setSearchState((prev) => ({ ...prev, isTyping: false }));
|
||||
}
|
||||
}, [search.isTyping, search.isSearching, search.debouncedQuery, search.query, setSearchState]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
|
|
@ -98,8 +117,8 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
|||
aria-label={localize('com_nav_search_placeholder')}
|
||||
placeholder={localize('com_nav_search_placeholder')}
|
||||
onKeyUp={handleKeyUp}
|
||||
onFocus={() => setIsSearching(true)}
|
||||
onBlur={() => setIsSearching(true)}
|
||||
onFocus={() => setSearchState((prev) => ({ ...prev, isSearching: true }))}
|
||||
onBlur={() => setSearchState((prev) => ({ ...prev, isSearching: false }))}
|
||||
autoComplete="off"
|
||||
dir="auto"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -272,7 +272,6 @@ export default function ArchivedChatsTable({
|
|||
isFetchingNextPage={isFetchingNextPage}
|
||||
isLoading={isLoading}
|
||||
showCheckboxes={false}
|
||||
manualSorting={true} // Ensures server-side sorting
|
||||
/>
|
||||
|
||||
<OGDialog open={isDeleteOpen} onOpenChange={onOpenChange}>
|
||||
|
|
|
|||
|
|
@ -216,8 +216,8 @@ export default function DataTable<TData, TValue>({
|
|||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const search = useRecoilValue(store.search);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const isSearchEnabled = useRecoilValue(store.isSearchEnabled);
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
const [sorting, setSorting] = useState<SortingState>(defaultSort);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
|
|
@ -331,7 +331,6 @@ export default function DataTable<TData, TValue>({
|
|||
const itemsToDelete = table.getFilteredSelectedRowModel().rows.map((r) => r.original);
|
||||
await onDelete(itemsToDelete);
|
||||
setRowSelection({});
|
||||
// await fetchNextPage?.({ pageParam: lastPage?.nextCursor });
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
|
|
@ -375,7 +374,7 @@ export default function DataTable<TData, TValue>({
|
|||
localize={localize}
|
||||
/>
|
||||
)}
|
||||
{filterColumn !== undefined && table.getColumn(filterColumn) && isSearchEnabled && (
|
||||
{filterColumn !== undefined && table.getColumn(filterColumn) && search.enabled && (
|
||||
<div className="relative flex-1">
|
||||
<AnimatedSearchInput
|
||||
value={searchTerm}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,10 @@ import type t from 'librechat-data-provider';
|
|||
import type {
|
||||
Action,
|
||||
TPreset,
|
||||
TPlugin,
|
||||
ConversationListResponse,
|
||||
ConversationListParams,
|
||||
SearchConversationListResponse,
|
||||
SearchConversationListParams,
|
||||
MessagesListParams,
|
||||
MessagesListResponse,
|
||||
Assistant,
|
||||
AssistantListParams,
|
||||
AssistantListResponse,
|
||||
|
|
@ -30,6 +29,7 @@ import type {
|
|||
SharedLinksListParams,
|
||||
SharedLinksResponse,
|
||||
} from 'librechat-data-provider';
|
||||
import type { ConversationCursorData } from '~/utils/convos';
|
||||
|
||||
export const useGetPresetsQuery = (
|
||||
config?: UseQueryOptions<TPreset[]>,
|
||||
|
|
@ -68,9 +68,9 @@ export const useGetConvoIdQuery = (
|
|||
[QueryKeys.conversation, id],
|
||||
() => {
|
||||
// Try to find in all fetched infinite pages
|
||||
const convosQuery = queryClient.getQueryData<
|
||||
InfiniteData<import('~/utils').ConversationCursorData>
|
||||
>([QueryKeys.allConversations]);
|
||||
const convosQuery = queryClient.getQueryData<InfiniteData<ConversationCursorData>>([
|
||||
QueryKeys.allConversations,
|
||||
]);
|
||||
const found = convosQuery?.pages
|
||||
.flatMap((page) => page.conversations)
|
||||
.find((c) => c.conversationId === id);
|
||||
|
|
@ -90,30 +90,6 @@ export const useGetConvoIdQuery = (
|
|||
);
|
||||
};
|
||||
|
||||
export const useSearchInfiniteQuery = (
|
||||
params?: SearchConversationListParams,
|
||||
config?: UseInfiniteQueryOptions<SearchConversationListResponse, unknown>,
|
||||
) => {
|
||||
return useInfiniteQuery<SearchConversationListResponse, unknown>(
|
||||
[QueryKeys.searchConversations, params],
|
||||
({ pageParam = null }) =>
|
||||
dataService
|
||||
.listConversations({
|
||||
...params,
|
||||
search: params?.search ?? '',
|
||||
cursor: pageParam?.toString(),
|
||||
})
|
||||
.then((res) => ({ ...res })) as Promise<SearchConversationListResponse>,
|
||||
{
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
...config,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const useConversationsInfiniteQuery = (
|
||||
params: ConversationListParams,
|
||||
config?: UseInfiniteQueryOptions<ConversationListResponse, unknown>,
|
||||
|
|
@ -134,7 +110,36 @@ export const useConversationsInfiniteQuery = (
|
|||
search,
|
||||
cursor: pageParam?.toString(),
|
||||
}),
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
|
||||
getNextPageParam: (lastPage) => lastPage?.nextCursor ?? undefined,
|
||||
keepPreviousData: true,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
cacheTime: 30 * 60 * 1000, // 30 minutes
|
||||
...config,
|
||||
});
|
||||
};
|
||||
|
||||
export const useMessagesInfiniteQuery = (
|
||||
params: MessagesListParams,
|
||||
config?: UseInfiniteQueryOptions<MessagesListResponse, unknown>,
|
||||
) => {
|
||||
const { sortBy, sortDirection, pageSize, conversationId, messageId, search } = params;
|
||||
|
||||
return useInfiniteQuery<MessagesListResponse>({
|
||||
queryKey: [
|
||||
QueryKeys.messages,
|
||||
{ sortBy, sortDirection, pageSize, conversationId, messageId, search },
|
||||
],
|
||||
queryFn: ({ pageParam }) =>
|
||||
dataService.listMessages({
|
||||
sortBy,
|
||||
sortDirection,
|
||||
pageSize,
|
||||
conversationId,
|
||||
messageId,
|
||||
search,
|
||||
cursor: pageParam?.toString(),
|
||||
}),
|
||||
getNextPageParam: (lastPage) => lastPage?.nextCursor ?? undefined,
|
||||
keepPreviousData: true,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
cacheTime: 30 * 60 * 1000, // 30 minutes
|
||||
|
|
@ -159,7 +164,7 @@ export const useSharedLinksQuery = (
|
|||
sortBy,
|
||||
sortDirection,
|
||||
}),
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
|
||||
getNextPageParam: (lastPage) => lastPage?.nextCursor ?? undefined,
|
||||
keepPreviousData: true,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
cacheTime: 30 * 60 * 1000, // 30 minutes
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export { default as useSearch } from './useSearch';
|
||||
export { default as usePresets } from './usePresets';
|
||||
export { default as useGetSender } from './useGetSender';
|
||||
export { default as useDefaultConvo } from './useDefaultConvo';
|
||||
export { default as useSearchEnabled } from './useSearchEnabled';
|
||||
export { default as useGenerateConvo } from './useGenerateConvo';
|
||||
export { default as useDebouncedInput } from './useDebouncedInput';
|
||||
export { default as useBookmarkSuccess } from './useBookmarkSuccess';
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ const useNavigateToConvo = (index = 0) => {
|
|||
dataService.getConversationById(conversationId),
|
||||
);
|
||||
logger.log('conversation', 'Fetched fresh conversation data', data);
|
||||
await queryClient.invalidateQueries([QueryKeys.messages, conversationId]);
|
||||
setConversation(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching conversation data on navigation', error);
|
||||
|
|
@ -38,6 +39,7 @@ const useNavigateToConvo = (index = 0) => {
|
|||
const navigateToConvo = (
|
||||
conversation?: TConversation | null,
|
||||
_resetLatestMessage = true,
|
||||
/** Likely need to remove this since it happens after fetching conversation data */
|
||||
invalidateMessages = false,
|
||||
) => {
|
||||
if (!conversation) {
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
import { useEffect, useCallback, useState } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import type { UseInfiniteQueryResult } from '@tanstack/react-query';
|
||||
import type { SearchConversationListResponse } from 'librechat-data-provider';
|
||||
import { useSearchInfiniteQuery, useGetSearchEnabledQuery } from '~/data-provider';
|
||||
import useNewConvo from '~/hooks/useNewConvo';
|
||||
import store from '~/store';
|
||||
|
||||
export interface UseSearchMessagesResult {
|
||||
searchQuery: string;
|
||||
searchQueryRes: UseInfiniteQueryResult<SearchConversationListResponse, unknown> | undefined;
|
||||
}
|
||||
|
||||
export default function useSearchMessages({
|
||||
isAuthenticated,
|
||||
}: {
|
||||
isAuthenticated: boolean;
|
||||
}): UseSearchMessagesResult {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { switchToConversation } = useNewConvo();
|
||||
const searchPlaceholderConversation = useCallback(() => {
|
||||
switchToConversation({
|
||||
conversationId: 'search',
|
||||
title: 'Search',
|
||||
endpoint: null,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
});
|
||||
}, [switchToConversation]);
|
||||
|
||||
const searchQuery = useRecoilValue(store.searchQuery);
|
||||
const setIsSearchEnabled = useSetRecoilState(store.isSearchEnabled);
|
||||
|
||||
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(searchQuery);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedSearchQuery(searchQuery);
|
||||
}, 350); // 350ms debounce
|
||||
return () => clearTimeout(handler);
|
||||
}, [searchQuery]);
|
||||
|
||||
const searchEnabledQuery = useGetSearchEnabledQuery({ enabled: isAuthenticated });
|
||||
const searchQueryRes = useSearchInfiniteQuery(
|
||||
{ nextCursor: null, search: debouncedSearchQuery, pageSize: 20 },
|
||||
{ enabled: isAuthenticated && !!debouncedSearchQuery },
|
||||
) as UseInfiniteQueryResult<SearchConversationListResponse, unknown> | undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (searchQuery && searchQuery.length > 0) {
|
||||
navigate('/search', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (location.pathname && location.pathname.includes('/c/')) {
|
||||
return;
|
||||
}
|
||||
navigate('/c/new', { replace: true });
|
||||
/* Disabled eslint rule because we don't want to run this effect when location changes */
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [navigate, searchQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchEnabledQuery.data === true) {
|
||||
setIsSearchEnabled(searchEnabledQuery.data);
|
||||
} else if (searchEnabledQuery.isError) {
|
||||
console.error('Failed to get search enabled', searchEnabledQuery.error);
|
||||
}
|
||||
}, [
|
||||
searchEnabledQuery.data,
|
||||
searchEnabledQuery.error,
|
||||
searchEnabledQuery.isError,
|
||||
setIsSearchEnabled,
|
||||
]);
|
||||
|
||||
const onSearchSuccess = useCallback(
|
||||
() => searchPlaceholderConversation(),
|
||||
[searchPlaceholderConversation],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// we use isInitialLoading here instead of isLoading because query is disabled by default
|
||||
if (searchQueryRes?.data) {
|
||||
onSearchSuccess();
|
||||
}
|
||||
}, [searchQueryRes?.data, searchQueryRes?.isInitialLoading, onSearchSuccess]);
|
||||
|
||||
const setIsSearchTyping = useSetRecoilState(store.isSearchTyping);
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchQueryRes?.isLoading && !searchQueryRes?.isFetching) {
|
||||
setIsSearchTyping(false);
|
||||
}
|
||||
}, [searchQueryRes?.isLoading, searchQueryRes?.isFetching, setIsSearchTyping]);
|
||||
|
||||
return {
|
||||
searchQuery,
|
||||
searchQueryRes,
|
||||
};
|
||||
}
|
||||
20
client/src/hooks/Conversations/useSearchEnabled.ts
Normal file
20
client/src/hooks/Conversations/useSearchEnabled.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useGetSearchEnabledQuery } from '~/data-provider';
|
||||
import { logger } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
export default function useSearchEnabled(isAuthenticated: boolean) {
|
||||
const setSearch = useSetRecoilState(store.search);
|
||||
const searchEnabledQuery = useGetSearchEnabledQuery({ enabled: isAuthenticated });
|
||||
|
||||
useEffect(() => {
|
||||
if (searchEnabledQuery.data === true) {
|
||||
setSearch((prev) => ({ ...prev, enabled: searchEnabledQuery.data }));
|
||||
} else if (searchEnabledQuery.isError) {
|
||||
logger.error('Failed to get search enabled: ', searchEnabledQuery.error);
|
||||
}
|
||||
}, [searchEnabledQuery.data, searchEnabledQuery.error, searchEnabledQuery.isError, setSearch]);
|
||||
|
||||
return searchEnabledQuery;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,28 +1,24 @@
|
|||
import { atom } from 'recoil';
|
||||
|
||||
const isSearchEnabled = atom<boolean | null>({
|
||||
key: 'isSearchEnabled',
|
||||
default: null,
|
||||
});
|
||||
export type SearchState = {
|
||||
enabled: boolean | null;
|
||||
query: string;
|
||||
debouncedQuery: string;
|
||||
isSearching: boolean;
|
||||
isTyping: boolean;
|
||||
};
|
||||
|
||||
const searchQuery = atom({
|
||||
key: 'searchQuery',
|
||||
default: '',
|
||||
});
|
||||
|
||||
const isSearching = atom({
|
||||
key: 'isSearching',
|
||||
default: false,
|
||||
});
|
||||
|
||||
const isSearchTyping = atom({
|
||||
key: 'isSearchTyping',
|
||||
default: false,
|
||||
export const search = atom<SearchState>({
|
||||
key: 'search',
|
||||
default: {
|
||||
enabled: null,
|
||||
query: '',
|
||||
debouncedQuery: '',
|
||||
isSearching: false,
|
||||
isTyping: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
isSearchEnabled,
|
||||
searchQuery,
|
||||
isSearching,
|
||||
isSearchTyping,
|
||||
search,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2509,7 +2509,6 @@ html {
|
|||
}
|
||||
|
||||
.popover-ui {
|
||||
/* z-index: 1000; */
|
||||
display: flex;
|
||||
max-height: min(var(--popover-available-height, 1700px), 1700px);
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { QueryClient } from '@tanstack/react-query';
|
||||
import type { TConversation, InfiniteData } from 'librechat-data-provider';
|
||||
import { QueryClient, InfiniteData } from '@tanstack/react-query';
|
||||
import type { TConversation } from 'librechat-data-provider';
|
||||
import {
|
||||
dateKeys,
|
||||
storeEndpointSettings,
|
||||
|
|
@ -14,7 +14,6 @@ import {
|
|||
removeConvoFromAllQueries,
|
||||
addConversationToAllConversationsQueries,
|
||||
} from './convos';
|
||||
import { convoData } from './convos.fakeData';
|
||||
import { normalizeData } from './collection';
|
||||
|
||||
jest.mock('date-fns', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue