| | import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react'; |
| | import throttle from 'lodash/throttle'; |
| | import { useRecoilValue } from 'recoil'; |
| | import { getConfigDefaults } from 'librechat-data-provider'; |
| | import { ResizablePanel, ResizablePanelGroup, useMediaQuery } from '@librechat/client'; |
| | import type { ImperativePanelHandle } from 'react-resizable-panels'; |
| | import { useGetStartupConfig } from '~/data-provider'; |
| | import ArtifactsPanel from './ArtifactsPanel'; |
| | import { normalizeLayout } from '~/utils'; |
| | import SidePanel from './SidePanel'; |
| | import store from '~/store'; |
| |
|
| | 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 SidePanelGroup = memo( |
| | ({ |
| | defaultLayout = [97, 3], |
| | defaultCollapsed = false, |
| | fullPanelCollapse = false, |
| | navCollapsedSize = 3, |
| | artifacts, |
| | children, |
| | }: SidePanelProps) => { |
| | const { data: startupConfig } = useGetStartupConfig(); |
| | const interfaceConfig = useMemo( |
| | () => startupConfig?.interface ?? defaultInterface, |
| | [startupConfig], |
| | ); |
| |
|
| | const panelRef = useRef<ImperativePanelHandle>(null); |
| | const [minSize, setMinSize] = useState(defaultMinSize); |
| | const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed); |
| | const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse); |
| | const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize); |
| | const [shouldRenderArtifacts, setShouldRenderArtifacts] = useState(artifacts != null); |
| |
|
| | const isSmallScreen = useMediaQuery('(max-width: 767px)'); |
| | const hideSidePanel = useRecoilValue(store.hideSidePanel); |
| |
|
| | const calculateLayout = useCallback(() => { |
| | if (artifacts == null) { |
| | const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2]; |
| | return [100 - navSize, navSize]; |
| | } else { |
| | const navSize = 0; |
| | const remainingSpace = 100 - navSize; |
| | const newMainSize = Math.floor(remainingSpace / 2); |
| | const artifactsSize = remainingSpace - newMainSize; |
| | return [newMainSize, artifactsSize, navSize]; |
| | } |
| | }, [artifacts, defaultLayout]); |
| |
|
| | const currentLayout = useMemo(() => normalizeLayout(calculateLayout()), [calculateLayout]); |
| |
|
| | const throttledSaveLayout = useMemo( |
| | () => |
| | 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 minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]); |
| |
|
| | |
| | const handleClosePanel = useCallback(() => { |
| | setIsCollapsed(() => { |
| | localStorage.setItem('fullPanelCollapse', 'true'); |
| | setFullCollapse(true); |
| | setCollapsedSize(0); |
| | setMinSize(0); |
| | return false; |
| | }); |
| | panelRef.current?.collapse(); |
| | }, []); |
| |
|
| | return ( |
| | <> |
| | <ResizablePanelGroup |
| | direction="horizontal" |
| | onLayout={(sizes) => throttledSaveLayout(sizes)} |
| | className="relative h-full w-full flex-1 overflow-auto bg-presentation" |
| | > |
| | <ResizablePanel |
| | defaultSize={currentLayout[0]} |
| | minSize={minSizeMain} |
| | order={1} |
| | id="messages-view" |
| | > |
| | {children} |
| | </ResizablePanel> |
| | |
| | {!isSmallScreen && ( |
| | <ArtifactsPanel |
| | artifacts={artifacts} |
| | currentLayout={currentLayout} |
| | minSizeMain={minSizeMain} |
| | shouldRender={shouldRenderArtifacts} |
| | onRenderChange={setShouldRenderArtifacts} |
| | /> |
| | )} |
| | |
| | {!hideSidePanel && interfaceConfig.sidePanel === true && ( |
| | <SidePanel |
| | panelRef={panelRef} |
| | minSize={minSize} |
| | setMinSize={setMinSize} |
| | isCollapsed={isCollapsed} |
| | setIsCollapsed={setIsCollapsed} |
| | collapsedSize={collapsedSize} |
| | setCollapsedSize={setCollapsedSize} |
| | fullCollapse={fullCollapse} |
| | setFullCollapse={setFullCollapse} |
| | interfaceConfig={interfaceConfig} |
| | hasArtifacts={shouldRenderArtifacts} |
| | defaultSize={currentLayout[currentLayout.length - 1]} |
| | /> |
| | )} |
| | </ResizablePanelGroup> |
| | {artifacts != null && isSmallScreen && ( |
| | <div className="fixed inset-0 z-[100]">{artifacts}</div> |
| | )} |
| | {!hideSidePanel && interfaceConfig.sidePanel === true && ( |
| | <button |
| | aria-label="Close right side panel" |
| | className={`nav-mask ${!isCollapsed ? 'active' : ''}`} |
| | onClick={handleClosePanel} |
| | /> |
| | )} |
| | </> |
| | ); |
| | }, |
| | ); |
| |
|
| | SidePanelGroup.displayName = 'SidePanelGroup'; |
| |
|
| | export default SidePanelGroup; |
| |
|