Spaces:
Runtime error
Runtime error
import { useCallback, useEffect, useRef, useState } from 'react'; | |
import { useGetConversationsQuery, useSearchQuery } from '@librechat/data-provider'; | |
import { useRecoilValue, useSetRecoilState } from 'recoil'; | |
import Conversations from '../Conversations'; | |
import NavLinks from './NavLinks'; | |
import NewChat from './NewChat'; | |
import Pages from '../Conversations/Pages'; | |
import { Panel, Spinner } from '~/components'; | |
import { cn } from '~/utils/'; | |
import { useAuthContext, useDebounce } from '~/hooks'; | |
import store from '~/store'; | |
import { localize } from '~/localization/Translation'; | |
export default function Nav({ navVisible, setNavVisible }) { | |
const [isHovering, setIsHovering] = useState(false); | |
const { isAuthenticated } = useAuthContext(); | |
const containerRef = useRef(null); | |
const scrollPositionRef = useRef(null); | |
const lang = useRecoilValue(store.lang); | |
const [conversations, setConversations] = useState([]); | |
// current page | |
const [pageNumber, setPageNumber] = useState(1); | |
// total pages | |
const [pages, setPages] = useState(1); | |
// data provider | |
const getConversationsQuery = useGetConversationsQuery(pageNumber, { enabled: isAuthenticated }); | |
// search | |
const searchQuery = useRecoilValue(store.searchQuery); | |
const isSearchEnabled = useRecoilValue(store.isSearchEnabled); | |
const isSearching = useRecoilValue(store.isSearching); | |
const { newConversation, searchPlaceholderConversation } = store.useConversation(); | |
// current conversation | |
const conversation = useRecoilValue(store.conversation); | |
const { conversationId } = conversation || {}; | |
const setSearchResultMessages = useSetRecoilState(store.searchResultMessages); | |
const refreshConversationsHint = useRecoilValue(store.refreshConversationsHint); | |
const { refreshConversations } = store.useConversations(); | |
const [isFetching, setIsFetching] = useState(false); | |
const debouncedSearchTerm = useDebounce(searchQuery, 750); | |
const searchQueryFn = useSearchQuery(debouncedSearchTerm, pageNumber, { | |
enabled: | |
!!debouncedSearchTerm && debouncedSearchTerm.length > 0 && isSearchEnabled && isSearching, | |
}); | |
const onSearchSuccess = (data, expectedPage) => { | |
const res = data; | |
setConversations(res.conversations); | |
if (expectedPage) { | |
setPageNumber(expectedPage); | |
} | |
setPages(res.pages); | |
setIsFetching(false); | |
searchPlaceholderConversation(); | |
setSearchResultMessages(res.messages); | |
}; | |
useEffect(() => { | |
//we use isInitialLoading here instead of isLoading because query is disabled by default | |
if (searchQueryFn.isInitialLoading) { | |
setIsFetching(true); | |
} else if (searchQueryFn.data) { | |
onSearchSuccess(searchQueryFn.data); | |
} | |
}, [searchQueryFn.data, searchQueryFn.isInitialLoading]); | |
const clearSearch = () => { | |
setPageNumber(1); | |
refreshConversations(); | |
if (conversationId == 'search') { | |
newConversation(); | |
} | |
}; | |
const moveToTop = useCallback(() => { | |
const container = containerRef.current; | |
if (container) { | |
scrollPositionRef.current = container.scrollTop; | |
} | |
}, [containerRef, scrollPositionRef]); | |
const nextPage = async () => { | |
moveToTop(); | |
setPageNumber(pageNumber + 1); | |
}; | |
const previousPage = async () => { | |
moveToTop(); | |
setPageNumber(pageNumber - 1); | |
}; | |
useEffect(() => { | |
if (getConversationsQuery.data) { | |
if (isSearching) { | |
return; | |
} | |
let { conversations, pages } = getConversationsQuery.data; | |
if (pageNumber > pages) { | |
setPageNumber(pages); | |
} else { | |
if (!isSearching) { | |
conversations = conversations.sort( | |
(a, b) => new Date(b.createdAt) - new Date(a.createdAt), | |
); | |
} | |
setConversations(conversations); | |
setPages(pages); | |
} | |
} | |
}, [getConversationsQuery.isSuccess, getConversationsQuery.data, isSearching, pageNumber]); | |
useEffect(() => { | |
if (!isSearching) { | |
getConversationsQuery.refetch(); | |
} | |
// eslint-disable-next-line react-hooks/exhaustive-deps | |
}, [pageNumber, conversationId, refreshConversationsHint]); | |
const toggleNavVisible = () => { | |
setNavVisible((prev) => !prev); | |
}; | |
const containerClasses = | |
getConversationsQuery.isLoading && pageNumber === 1 | |
? 'flex flex-col gap-2 text-gray-100 text-sm h-full justify-center items-center' | |
: 'flex flex-col gap-2 text-gray-100 text-sm'; | |
return ( | |
<> | |
<div | |
className="nav active dark flex-shrink-0 overflow-x-hidden bg-gray-900 transition-all duration-200 ease-in-out" | |
style={{ | |
width: navVisible ? '260px' : '0px', | |
visibility: navVisible ? 'visible' : 'hidden', | |
}} | |
> | |
<div className="h-full w-[260px]"> | |
<div className="flex h-full min-h-0 flex-col "> | |
<div className="scrollbar-trigger relative flex h-full w-full flex-1 items-start border-white/20"> | |
<nav className="relative flex h-full flex-1 flex-col space-y-1 p-2"> | |
<div className="mb-2 flex h-11 flex-row"> | |
<NewChat /> | |
<button | |
type="button" | |
className={cn( | |
'nav-close-button inline-flex h-11 w-11 items-center justify-center rounded-md border border-white/20 text-white hover:bg-gray-500/10', | |
)} | |
onClick={toggleNavVisible} | |
> | |
<span className="sr-only">{localize(lang, 'com_nav_close_sidebar')}</span> | |
<Panel open={false} /> | |
</button> | |
</div> | |
<div | |
className={`flex-1 flex-col overflow-y-auto ${ | |
isHovering ? '' : 'scrollbar-transparent' | |
} border-b border-white/20`} | |
onMouseEnter={() => setIsHovering(true)} | |
onMouseLeave={() => setIsHovering(false)} | |
ref={containerRef} | |
> | |
<div className={containerClasses}> | |
{(getConversationsQuery.isLoading && pageNumber === 1) || isFetching ? ( | |
<Spinner /> | |
) : ( | |
<Conversations | |
conversations={conversations} | |
conversationId={conversationId} | |
moveToTop={moveToTop} | |
/> | |
)} | |
<Pages | |
pageNumber={pageNumber} | |
pages={pages} | |
nextPage={nextPage} | |
previousPage={previousPage} | |
/> | |
</div> | |
</div> | |
<NavLinks clearSearch={clearSearch} isSearchEnabled={isSearchEnabled} /> | |
</nav> | |
</div> | |
</div> | |
</div> | |
</div> | |
{!navVisible && ( | |
<div className="absolute left-2 top-2 z-10 hidden md:inline-block"> | |
<button | |
type="button" | |
className="nav-open-button flex h-11 cursor-pointer items-center gap-3 rounded-md border border-black/10 bg-white p-3 text-sm text-black transition-colors duration-200 hover:bg-gray-50 dark:border-white/20 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700" | |
onClick={toggleNavVisible} | |
> | |
<div className="flex items-center justify-center"> | |
<span className="sr-only">{localize(lang, 'com_nav_open_sidebar')}</span> | |
<Panel open={true} /> | |
</div> | |
</button> | |
</div> | |
)} | |
<div className={'nav-mask' + (navVisible ? ' active' : '')} onClick={toggleNavVisible}></div> | |
</> | |
); | |
} | |