diff --git a/client/src/Providers/SidePanelContext.tsx b/client/src/Providers/SidePanelContext.tsx new file mode 100644 index 0000000000..3ce7834ccc --- /dev/null +++ b/client/src/Providers/SidePanelContext.tsx @@ -0,0 +1,31 @@ +import React, { createContext, useContext, useMemo } from 'react'; +import type { EModelEndpoint } from 'librechat-data-provider'; +import { useChatContext } from './ChatContext'; + +interface SidePanelContextValue { + endpoint?: EModelEndpoint | null; +} + +const SidePanelContext = createContext(undefined); + +export function SidePanelProvider({ children }: { children: React.ReactNode }) { + const { conversation } = useChatContext(); + + /** Context value only created when endpoint changes */ + const contextValue = useMemo( + () => ({ + endpoint: conversation?.endpoint, + }), + [conversation?.endpoint], + ); + + return {children}; +} + +export function useSidePanelContext() { + const context = useContext(SidePanelContext); + if (!context) { + throw new Error('useSidePanelContext must be used within SidePanelProvider'); + } + return context; +} diff --git a/client/src/Providers/index.ts b/client/src/Providers/index.ts index b455cb3f1e..caf5b82b73 100644 --- a/client/src/Providers/index.ts +++ b/client/src/Providers/index.ts @@ -24,4 +24,5 @@ export * from './ToolCallsMapContext'; export * from './SetConvoContext'; export * from './SearchContext'; export * from './BadgeRowContext'; +export * from './SidePanelContext'; export { default as BadgeRowProvider } from './BadgeRowContext'; diff --git a/client/src/components/Chat/Presentation.tsx b/client/src/components/Chat/Presentation.tsx index 592ee3012a..f1456ae1e6 100644 --- a/client/src/components/Chat/Presentation.tsx +++ b/client/src/components/Chat/Presentation.tsx @@ -4,10 +4,10 @@ import { FileSources, LocalStorageKeys } from 'librechat-data-provider'; import type { ExtendedFile } from '~/common'; import { useDeleteFilesMutation } from '~/data-provider'; import DragDropWrapper from '~/components/Chat/Input/Files/DragDropWrapper'; +import { EditorProvider, SidePanelProvider } from '~/Providers'; import Artifacts from '~/components/Artifacts/Artifacts'; import { SidePanelGroup } from '~/components/SidePanel'; import { useSetFilesToDelete } from '~/hooks'; -import { EditorProvider } from '~/Providers'; import store from '~/store'; export default function Presentation({ children }: { children: React.ReactNode }) { @@ -59,22 +59,24 @@ export default function Presentation({ children }: { children: React.ReactNode } return ( - 0 ? ( - - - - ) : null - } - > -
- {children} -
-
+ + 0 ? ( + + + + ) : null + } + > +
+ {children} +
+
+
); } diff --git a/client/src/components/SidePanel/SidePanel.tsx b/client/src/components/SidePanel/SidePanel.tsx index d9b1256475..cf36ae6c54 100644 --- a/client/src/components/SidePanel/SidePanel.tsx +++ b/client/src/components/SidePanel/SidePanel.tsx @@ -7,8 +7,8 @@ import { useMediaQuery, useLocalStorage, useLocalize } from '~/hooks'; import useSideNavLinks from '~/hooks/Nav/useSideNavLinks'; import { useGetEndpointsQuery } from '~/data-provider'; import NavToggle from '~/components/Nav/NavToggle'; +import { useSidePanelContext } from '~/Providers'; import { cn, getEndpointField } from '~/utils'; -import { useChatContext } from '~/Providers'; import Nav from './Nav'; const defaultMinSize = 20; @@ -43,13 +43,13 @@ const SidePanel = ({ 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 { conversation } = useChatContext(); - const { endpoint } = conversation ?? {}; + const { data: keyExpiry = { expiresAt: undefined } } = useUserKeyQuery(endpoint ?? ''); const defaultActive = useMemo(() => { diff --git a/client/src/components/SidePanel/SidePanelGroup.tsx b/client/src/components/SidePanel/SidePanelGroup.tsx index 9ec1b10031..8c67d6bc4c 100644 --- a/client/src/components/SidePanel/SidePanelGroup.tsx +++ b/client/src/components/SidePanel/SidePanelGroup.tsx @@ -22,132 +22,139 @@ interface SidePanelProps { const defaultMinSize = 20; const defaultInterface = getConfigDefaults().interface; -const SidePanelGroup = ({ - defaultLayout = [97, 3], - defaultCollapsed = false, - fullPanelCollapse = false, - navCollapsedSize = 3, - artifacts, - children, -}: SidePanelProps) => { - const { data: startupConfig } = useGetStartupConfig(); - const interfaceConfig = useMemo( - () => startupConfig?.interface ?? defaultInterface, - [startupConfig], - ); +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(null); - const [minSize, setMinSize] = useState(defaultMinSize); - const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed); - const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse); - const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize); + const panelRef = useRef(null); + const [minSize, setMinSize] = useState(defaultMinSize); + const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed); + const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse); + const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize); - const isSmallScreen = useMediaQuery('(max-width: 767px)'); - const hideSidePanel = useRecoilValue(store.hideSidePanel); + 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 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 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), - [], - ); + 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'); + 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]); + + /** Memoized close button handler to prevent re-creating it */ + const handleClosePanel = useCallback(() => { + setIsCollapsed(() => { + localStorage.setItem('fullPanelCollapse', 'true'); + setFullCollapse(true); + setCollapsedSize(0); + setMinSize(0); + return false; + }); panelRef.current?.collapse(); - return; - } else { - setIsCollapsed(defaultCollapsed); - setCollapsedSize(navCollapsedSize); - setMinSize(defaultMinSize); - } - }, [isSmallScreen, defaultCollapsed, navCollapsedSize, fullPanelCollapse]); + }, []); - const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]); - - return ( - <> - throttledSaveLayout(sizes)} - className="transition-width relative h-full w-full flex-1 overflow-auto bg-presentation" - > - + throttledSaveLayout(sizes)} + className="transition-width relative h-full w-full flex-1 overflow-auto bg-presentation" > - {children} - - {artifacts != null && ( - <> - - - {artifacts} - - - )} - {!hideSidePanel && interfaceConfig.sidePanel === true && ( - - )} - -