import throttle from 'lodash/throttle'; import { getConfigDefaults } from 'librechat-data-provider'; import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react'; import { useGetEndpointsQuery, useGetStartupConfig, useUserKeyQuery, } from 'librechat-data-provider/react-query'; import type { ImperativePanelHandle } from 'react-resizable-panels'; import type { TEndpointsConfig } from 'librechat-data-provider'; import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/components/ui/Resizable'; import { useMediaQuery, useLocalStorage, useLocalize } from '~/hooks'; import useSideNavLinks from '~/hooks/Nav/useSideNavLinks'; import NavToggle from '~/components/Nav/NavToggle'; import { useChatContext } from '~/Providers'; import Switcher from './Switcher'; import { cn } from '~/utils'; import Nav from './Nav'; interface SidePanelProps { defaultLayout?: number[] | undefined; defaultCollapsed?: boolean; navCollapsedSize?: number; fullPanelCollapse?: boolean; artifacts?: React.ReactNode; children: React.ReactNode; } const defaultMinSize = 20; const defaultInterface = getConfigDefaults().interface; const normalizeLayout = (layout: number[]) => { const sum = layout.reduce((acc, size) => acc + size, 0); if (Math.abs(sum - 100) < 0.01) { return layout.map((size) => Number(size.toFixed(2))); } const factor = 100 / sum; const normalizedLayout = layout.map((size) => Number((size * factor).toFixed(2))); const adjustedSum = normalizedLayout.reduce( (acc, size, index) => (index === layout.length - 1 ? acc : acc + size), 0, ); normalizedLayout[normalizedLayout.length - 1] = Number((100 - adjustedSum).toFixed(2)); return normalizedLayout; }; const SidePanel = ({ defaultLayout = [97, 3], defaultCollapsed = false, fullPanelCollapse = false, navCollapsedSize = 3, artifacts, children, }: SidePanelProps) => { const localize = useLocalize(); const [isHovering, setIsHovering] = useState(false); const [minSize, setMinSize] = useState(defaultMinSize); const [newUser, setNewUser] = useLocalStorage('newUser', true); const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed); const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse); const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize); const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery(); const { data: startupConfig } = useGetStartupConfig(); const interfaceConfig = useMemo( () => startupConfig?.interface ?? defaultInterface, [startupConfig], ); const isSmallScreen = useMediaQuery('(max-width: 767px)'); const { conversation } = useChatContext(); const { endpoint } = conversation ?? {}; const { data: keyExpiry = { expiresAt: undefined } } = useUserKeyQuery(endpoint ?? ''); const panelRef = useRef(null); const defaultActive = useMemo(() => { const activePanel = localStorage.getItem('side:active-panel'); return typeof activePanel === 'string' ? activePanel : undefined; }, []); const assistants = useMemo(() => endpointsConfig?.[endpoint ?? ''], [endpoint, endpointsConfig]); const agents = useMemo(() => endpointsConfig?.[endpoint ?? ''], [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(); }, []); const Links = useSideNavLinks({ agents, endpoint, hidePanel, assistants, keyProvided, interfaceConfig, }); const calculateLayout = useCallback(() => { if (!artifacts) { const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2]; return [100 - navSize, navSize]; } else { const navSize = Math.max(minSize, navCollapsedSize); const remainingSpace = 100 - navSize; const newMainSize = Math.floor(remainingSpace / 2); const artifactsSize = remainingSpace - newMainSize; return [newMainSize, artifactsSize, navSize]; } }, [artifacts, defaultLayout, minSize, navCollapsedSize]); const currentLayout = useMemo(() => normalizeLayout(calculateLayout()), [calculateLayout]); // eslint-disable-next-line react-hooks/exhaustive-deps const throttledSaveLayout = useCallback( throttle((sizes: number[]) => { const normalizedSizes = normalizeLayout(sizes); localStorage.setItem('react-resizable-panels:layout', JSON.stringify(normalizedSizes)); }, 350), [], ); useEffect(() => { if (isSmallScreen) { setIsCollapsed(true); setCollapsedSize(0); setMinSize(defaultMinSize); setFullCollapse(true); localStorage.setItem('fullPanelCollapse', 'true'); panelRef.current?.collapse(); return; } else { setIsCollapsed(defaultCollapsed); setCollapsedSize(navCollapsedSize); setMinSize(defaultMinSize); } }, [isSmallScreen, defaultCollapsed, navCollapsedSize, fullPanelCollapse]); 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(); } }, [isCollapsed, newUser, setNewUser, navCollapsedSize]); const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]); return ( <> throttledSaveLayout(sizes)} className="transition-width relative h-full w-full flex-1 overflow-auto bg-white dark:bg-gray-800" > {children} {artifacts != null && ( <> {artifacts} )}
setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} className="relative flex w-px items-center justify-center" >
{(!isCollapsed || minSize > 0) && !isSmallScreen && !fullCollapse && ( )} { 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-surface-primary-alt 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', )} > {interfaceConfig.modelSelect && (
)}