| import { useState, useCallback, useMemo, memo } from 'react'; | |
| import { getEndpointField } from 'librechat-data-provider'; | |
| import { useUserKeyQuery } from 'librechat-data-provider/react-query'; | |
| import { ResizableHandleAlt, ResizablePanel, useMediaQuery } from '@librechat/client'; | |
| import type { TEndpointsConfig, TInterfaceConfig } from 'librechat-data-provider'; | |
| import type { ImperativePanelHandle } from 'react-resizable-panels'; | |
| import useSideNavLinks from '~/hooks/Nav/useSideNavLinks'; | |
| import { useLocalStorage, useLocalize } from '~/hooks'; | |
| import { useGetEndpointsQuery } from '~/data-provider'; | |
| import NavToggle from '~/components/Nav/NavToggle'; | |
| import { useSidePanelContext } from '~/Providers'; | |
| import { cn } from '~/utils'; | |
| import Nav from './Nav'; | |
| const defaultMinSize = 20; | |
| const SidePanel = ({ | |
| defaultSize, | |
| panelRef, | |
| navCollapsedSize = 3, | |
| hasArtifacts, | |
| minSize, | |
| setMinSize, | |
| collapsedSize, | |
| setCollapsedSize, | |
| isCollapsed, | |
| setIsCollapsed, | |
| fullCollapse, | |
| setFullCollapse, | |
| interfaceConfig, | |
| }: { | |
| defaultSize?: number; | |
| hasArtifacts: boolean; | |
| navCollapsedSize?: number; | |
| minSize: number; | |
| setMinSize: React.Dispatch<React.SetStateAction<number>>; | |
| collapsedSize: number; | |
| setCollapsedSize: React.Dispatch<React.SetStateAction<number>>; | |
| isCollapsed: boolean; | |
| setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>; | |
| fullCollapse: boolean; | |
| setFullCollapse: React.Dispatch<React.SetStateAction<boolean>>; | |
| panelRef: React.RefObject<ImperativePanelHandle>; | |
| interfaceConfig: TInterfaceConfig; | |
| }) => { | |
| const localize = useLocalize(); | |
| const { endpoint } = useSidePanelContext(); | |
| const [isHovering, setIsHovering] = useState(false); | |
| const [newUser, setNewUser] = useLocalStorage('newUser', true); | |
| const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery(); | |
| const isSmallScreen = useMediaQuery('(max-width: 767px)'); | |
| const { data: keyExpiry = { expiresAt: undefined } } = useUserKeyQuery(endpoint ?? ''); | |
| const defaultActive = useMemo(() => { | |
| const activePanel = localStorage.getItem('side:active-panel'); | |
| return typeof activePanel === 'string' ? activePanel : undefined; | |
| }, []); | |
| const endpointType = useMemo( | |
| () => getEndpointField(endpointsConfig, endpoint, 'type'), | |
| [endpoint, endpointsConfig], | |
| ); | |
| const userProvidesKey = useMemo( | |
| () => !!(endpointsConfig?.[endpoint ?? '']?.userProvide ?? false), | |
| [endpointsConfig, endpoint], | |
| ); | |
| const keyProvided = useMemo( | |
| () => (userProvidesKey ? !!(keyExpiry.expiresAt ?? '') : true), | |
| [keyExpiry.expiresAt, userProvidesKey], | |
| ); | |
| const hidePanel = useCallback(() => { | |
| setIsCollapsed(true); | |
| setCollapsedSize(0); | |
| setMinSize(defaultMinSize); | |
| setFullCollapse(true); | |
| localStorage.setItem('fullPanelCollapse', 'true'); | |
| panelRef.current?.collapse(); | |
| }, [panelRef, setMinSize, setIsCollapsed, setFullCollapse, setCollapsedSize]); | |
| const Links = useSideNavLinks({ | |
| endpoint, | |
| hidePanel, | |
| keyProvided, | |
| endpointType, | |
| interfaceConfig, | |
| endpointsConfig, | |
| }); | |
| const toggleNavVisible = useCallback(() => { | |
| if (newUser) { | |
| setNewUser(false); | |
| } | |
| setIsCollapsed((prev: boolean) => { | |
| if (prev) { | |
| setMinSize(defaultMinSize); | |
| setCollapsedSize(navCollapsedSize); | |
| setFullCollapse(false); | |
| localStorage.setItem('fullPanelCollapse', 'false'); | |
| } | |
| return !prev; | |
| }); | |
| if (!isCollapsed) { | |
| panelRef.current?.collapse(); | |
| } else { | |
| panelRef.current?.expand(); | |
| } | |
| }, [ | |
| newUser, | |
| panelRef, | |
| setNewUser, | |
| setMinSize, | |
| isCollapsed, | |
| setIsCollapsed, | |
| setFullCollapse, | |
| setCollapsedSize, | |
| navCollapsedSize, | |
| ]); | |
| return ( | |
| <> | |
| <div | |
| onMouseEnter={() => setIsHovering(true)} | |
| onMouseLeave={() => setIsHovering(false)} | |
| className="relative flex w-px items-center justify-center" | |
| > | |
| <NavToggle | |
| navVisible={!isCollapsed} | |
| isHovering={isHovering} | |
| onToggle={toggleNavVisible} | |
| setIsHovering={setIsHovering} | |
| className={cn( | |
| 'fixed top-1/2', | |
| (isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse | |
| ? 'mr-9' | |
| : 'mr-16', | |
| )} | |
| translateX={false} | |
| side="right" | |
| /> | |
| </div> | |
| {(!isCollapsed || minSize > 0) && !isSmallScreen && !fullCollapse && ( | |
| <ResizableHandleAlt withHandle className="bg-transparent text-text-primary" /> | |
| )} | |
| <ResizablePanel | |
| tagName="nav" | |
| id="controls-nav" | |
| order={hasArtifacts ? 3 : 2} | |
| aria-label={localize('com_ui_controls')} | |
| role="navigation" | |
| collapsedSize={collapsedSize} | |
| defaultSize={defaultSize} | |
| collapsible={true} | |
| minSize={minSize} | |
| maxSize={40} | |
| ref={panelRef} | |
| style={{ | |
| overflowY: 'auto', | |
| transition: 'width 0.2s ease, visibility 0s linear 0.2s', | |
| }} | |
| onExpand={() => { | |
| if (isCollapsed && (fullCollapse || collapsedSize === 0)) { | |
| return; | |
| } | |
| setIsCollapsed(false); | |
| localStorage.setItem('react-resizable-panels:collapsed', 'false'); | |
| }} | |
| onCollapse={() => { | |
| setIsCollapsed(true); | |
| localStorage.setItem('react-resizable-panels:collapsed', 'true'); | |
| }} | |
| className={cn( | |
| 'sidenav hide-scrollbar border-l border-border-light bg-background py-1 transition-opacity', | |
| isCollapsed ? 'min-w-[50px]' : 'min-w-[340px] sm:min-w-[352px]', | |
| (isSmallScreen && isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse | |
| ? 'hidden min-w-0' | |
| : 'opacity-100', | |
| )} | |
| > | |
| <Nav | |
| resize={panelRef.current?.resize} | |
| isCollapsed={isCollapsed} | |
| defaultActive={defaultActive} | |
| links={Links} | |
| /> | |
| </ResizablePanel> | |
| </> | |
| ); | |
| }; | |
| export default memo(SidePanel); | |