mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 09:20:15 +01:00
♻️ refactor: SidePanel Context to Optimize on ChatView Rerender (#8509)
This commit is contained in:
parent
0b1b0af741
commit
dfef7c31d2
5 changed files with 180 additions and 139 deletions
31
client/src/Providers/SidePanelContext.tsx
Normal file
31
client/src/Providers/SidePanelContext.tsx
Normal file
|
|
@ -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<SidePanelContextValue | undefined>(undefined);
|
||||||
|
|
||||||
|
export function SidePanelProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const { conversation } = useChatContext();
|
||||||
|
|
||||||
|
/** Context value only created when endpoint changes */
|
||||||
|
const contextValue = useMemo<SidePanelContextValue>(
|
||||||
|
() => ({
|
||||||
|
endpoint: conversation?.endpoint,
|
||||||
|
}),
|
||||||
|
[conversation?.endpoint],
|
||||||
|
);
|
||||||
|
|
||||||
|
return <SidePanelContext.Provider value={contextValue}>{children}</SidePanelContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSidePanelContext() {
|
||||||
|
const context = useContext(SidePanelContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useSidePanelContext must be used within SidePanelProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
@ -24,4 +24,5 @@ export * from './ToolCallsMapContext';
|
||||||
export * from './SetConvoContext';
|
export * from './SetConvoContext';
|
||||||
export * from './SearchContext';
|
export * from './SearchContext';
|
||||||
export * from './BadgeRowContext';
|
export * from './BadgeRowContext';
|
||||||
|
export * from './SidePanelContext';
|
||||||
export { default as BadgeRowProvider } from './BadgeRowContext';
|
export { default as BadgeRowProvider } from './BadgeRowContext';
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import { FileSources, LocalStorageKeys } from 'librechat-data-provider';
|
||||||
import type { ExtendedFile } from '~/common';
|
import type { ExtendedFile } from '~/common';
|
||||||
import { useDeleteFilesMutation } from '~/data-provider';
|
import { useDeleteFilesMutation } from '~/data-provider';
|
||||||
import DragDropWrapper from '~/components/Chat/Input/Files/DragDropWrapper';
|
import DragDropWrapper from '~/components/Chat/Input/Files/DragDropWrapper';
|
||||||
|
import { EditorProvider, SidePanelProvider } from '~/Providers';
|
||||||
import Artifacts from '~/components/Artifacts/Artifacts';
|
import Artifacts from '~/components/Artifacts/Artifacts';
|
||||||
import { SidePanelGroup } from '~/components/SidePanel';
|
import { SidePanelGroup } from '~/components/SidePanel';
|
||||||
import { useSetFilesToDelete } from '~/hooks';
|
import { useSetFilesToDelete } from '~/hooks';
|
||||||
import { EditorProvider } from '~/Providers';
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function Presentation({ children }: { children: React.ReactNode }) {
|
export default function Presentation({ children }: { children: React.ReactNode }) {
|
||||||
|
|
@ -59,22 +59,24 @@ export default function Presentation({ children }: { children: React.ReactNode }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation">
|
<DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation">
|
||||||
<SidePanelGroup
|
<SidePanelProvider>
|
||||||
defaultLayout={defaultLayout}
|
<SidePanelGroup
|
||||||
fullPanelCollapse={fullCollapse}
|
defaultLayout={defaultLayout}
|
||||||
defaultCollapsed={defaultCollapsed}
|
fullPanelCollapse={fullCollapse}
|
||||||
artifacts={
|
defaultCollapsed={defaultCollapsed}
|
||||||
artifactsVisibility === true && Object.keys(artifacts ?? {}).length > 0 ? (
|
artifacts={
|
||||||
<EditorProvider>
|
artifactsVisibility === true && Object.keys(artifacts ?? {}).length > 0 ? (
|
||||||
<Artifacts />
|
<EditorProvider>
|
||||||
</EditorProvider>
|
<Artifacts />
|
||||||
) : null
|
</EditorProvider>
|
||||||
}
|
) : null
|
||||||
>
|
}
|
||||||
<main className="flex h-full flex-col overflow-y-auto" role="main">
|
>
|
||||||
{children}
|
<main className="flex h-full flex-col overflow-y-auto" role="main">
|
||||||
</main>
|
{children}
|
||||||
</SidePanelGroup>
|
</main>
|
||||||
|
</SidePanelGroup>
|
||||||
|
</SidePanelProvider>
|
||||||
</DragDropWrapper>
|
</DragDropWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import { useMediaQuery, useLocalStorage, useLocalize } from '~/hooks';
|
||||||
import useSideNavLinks from '~/hooks/Nav/useSideNavLinks';
|
import useSideNavLinks from '~/hooks/Nav/useSideNavLinks';
|
||||||
import { useGetEndpointsQuery } from '~/data-provider';
|
import { useGetEndpointsQuery } from '~/data-provider';
|
||||||
import NavToggle from '~/components/Nav/NavToggle';
|
import NavToggle from '~/components/Nav/NavToggle';
|
||||||
|
import { useSidePanelContext } from '~/Providers';
|
||||||
import { cn, getEndpointField } from '~/utils';
|
import { cn, getEndpointField } from '~/utils';
|
||||||
import { useChatContext } from '~/Providers';
|
|
||||||
import Nav from './Nav';
|
import Nav from './Nav';
|
||||||
|
|
||||||
const defaultMinSize = 20;
|
const defaultMinSize = 20;
|
||||||
|
|
@ -43,13 +43,13 @@ const SidePanel = ({
|
||||||
interfaceConfig: TInterfaceConfig;
|
interfaceConfig: TInterfaceConfig;
|
||||||
}) => {
|
}) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
const { endpoint } = useSidePanelContext();
|
||||||
const [isHovering, setIsHovering] = useState(false);
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
const [newUser, setNewUser] = useLocalStorage('newUser', true);
|
const [newUser, setNewUser] = useLocalStorage('newUser', true);
|
||||||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||||
|
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
||||||
const { conversation } = useChatContext();
|
|
||||||
const { endpoint } = conversation ?? {};
|
|
||||||
const { data: keyExpiry = { expiresAt: undefined } } = useUserKeyQuery(endpoint ?? '');
|
const { data: keyExpiry = { expiresAt: undefined } } = useUserKeyQuery(endpoint ?? '');
|
||||||
|
|
||||||
const defaultActive = useMemo(() => {
|
const defaultActive = useMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -22,132 +22,139 @@ interface SidePanelProps {
|
||||||
const defaultMinSize = 20;
|
const defaultMinSize = 20;
|
||||||
const defaultInterface = getConfigDefaults().interface;
|
const defaultInterface = getConfigDefaults().interface;
|
||||||
|
|
||||||
const SidePanelGroup = ({
|
const SidePanelGroup = memo(
|
||||||
defaultLayout = [97, 3],
|
({
|
||||||
defaultCollapsed = false,
|
defaultLayout = [97, 3],
|
||||||
fullPanelCollapse = false,
|
defaultCollapsed = false,
|
||||||
navCollapsedSize = 3,
|
fullPanelCollapse = false,
|
||||||
artifacts,
|
navCollapsedSize = 3,
|
||||||
children,
|
artifacts,
|
||||||
}: SidePanelProps) => {
|
children,
|
||||||
const { data: startupConfig } = useGetStartupConfig();
|
}: SidePanelProps) => {
|
||||||
const interfaceConfig = useMemo(
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
() => startupConfig?.interface ?? defaultInterface,
|
const interfaceConfig = useMemo(
|
||||||
[startupConfig],
|
() => startupConfig?.interface ?? defaultInterface,
|
||||||
);
|
[startupConfig],
|
||||||
|
);
|
||||||
|
|
||||||
const panelRef = useRef<ImperativePanelHandle>(null);
|
const panelRef = useRef<ImperativePanelHandle>(null);
|
||||||
const [minSize, setMinSize] = useState(defaultMinSize);
|
const [minSize, setMinSize] = useState(defaultMinSize);
|
||||||
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
||||||
const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse);
|
const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse);
|
||||||
const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize);
|
const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize);
|
||||||
|
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
||||||
const hideSidePanel = useRecoilValue(store.hideSidePanel);
|
const hideSidePanel = useRecoilValue(store.hideSidePanel);
|
||||||
|
|
||||||
const calculateLayout = useCallback(() => {
|
const calculateLayout = useCallback(() => {
|
||||||
if (artifacts == null) {
|
if (artifacts == null) {
|
||||||
const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2];
|
const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2];
|
||||||
return [100 - navSize, navSize];
|
return [100 - navSize, navSize];
|
||||||
} else {
|
} else {
|
||||||
const navSize = 0;
|
const navSize = 0;
|
||||||
const remainingSpace = 100 - navSize;
|
const remainingSpace = 100 - navSize;
|
||||||
const newMainSize = Math.floor(remainingSpace / 2);
|
const newMainSize = Math.floor(remainingSpace / 2);
|
||||||
const artifactsSize = remainingSpace - newMainSize;
|
const artifactsSize = remainingSpace - newMainSize;
|
||||||
return [newMainSize, artifactsSize, navSize];
|
return [newMainSize, artifactsSize, navSize];
|
||||||
}
|
}
|
||||||
}, [artifacts, defaultLayout]);
|
}, [artifacts, defaultLayout]);
|
||||||
|
|
||||||
const currentLayout = useMemo(() => normalizeLayout(calculateLayout()), [calculateLayout]);
|
const currentLayout = useMemo(() => normalizeLayout(calculateLayout()), [calculateLayout]);
|
||||||
|
|
||||||
const throttledSaveLayout = useMemo(
|
const throttledSaveLayout = useMemo(
|
||||||
() =>
|
() =>
|
||||||
throttle((sizes: number[]) => {
|
throttle((sizes: number[]) => {
|
||||||
const normalizedSizes = normalizeLayout(sizes);
|
const normalizedSizes = normalizeLayout(sizes);
|
||||||
localStorage.setItem('react-resizable-panels:layout', JSON.stringify(normalizedSizes));
|
localStorage.setItem('react-resizable-panels:layout', JSON.stringify(normalizedSizes));
|
||||||
}, 350),
|
}, 350),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSmallScreen) {
|
if (isSmallScreen) {
|
||||||
setIsCollapsed(true);
|
setIsCollapsed(true);
|
||||||
setCollapsedSize(0);
|
setCollapsedSize(0);
|
||||||
setMinSize(defaultMinSize);
|
setMinSize(defaultMinSize);
|
||||||
setFullCollapse(true);
|
setFullCollapse(true);
|
||||||
localStorage.setItem('fullPanelCollapse', '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();
|
panelRef.current?.collapse();
|
||||||
return;
|
}, []);
|
||||||
} else {
|
|
||||||
setIsCollapsed(defaultCollapsed);
|
|
||||||
setCollapsedSize(navCollapsedSize);
|
|
||||||
setMinSize(defaultMinSize);
|
|
||||||
}
|
|
||||||
}, [isSmallScreen, defaultCollapsed, navCollapsedSize, fullPanelCollapse]);
|
|
||||||
|
|
||||||
const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]);
|
return (
|
||||||
|
<>
|
||||||
return (
|
<ResizablePanelGroup
|
||||||
<>
|
direction="horizontal"
|
||||||
<ResizablePanelGroup
|
onLayout={(sizes) => throttledSaveLayout(sizes)}
|
||||||
direction="horizontal"
|
className="transition-width relative h-full w-full flex-1 overflow-auto bg-presentation"
|
||||||
onLayout={(sizes) => throttledSaveLayout(sizes)}
|
|
||||||
className="transition-width relative h-full w-full flex-1 overflow-auto bg-presentation"
|
|
||||||
>
|
|
||||||
<ResizablePanel
|
|
||||||
defaultSize={currentLayout[0]}
|
|
||||||
minSize={minSizeMain}
|
|
||||||
order={1}
|
|
||||||
id="messages-view"
|
|
||||||
>
|
>
|
||||||
{children}
|
<ResizablePanel
|
||||||
</ResizablePanel>
|
defaultSize={currentLayout[0]}
|
||||||
{artifacts != null && (
|
minSize={minSizeMain}
|
||||||
<>
|
order={1}
|
||||||
<ResizableHandleAlt withHandle className="ml-3 bg-border-medium text-text-primary" />
|
id="messages-view"
|
||||||
<ResizablePanel
|
>
|
||||||
defaultSize={currentLayout[1]}
|
{children}
|
||||||
minSize={minSizeMain}
|
</ResizablePanel>
|
||||||
order={2}
|
{artifacts != null && (
|
||||||
id="artifacts-panel"
|
<>
|
||||||
>
|
<ResizableHandleAlt withHandle className="ml-3 bg-border-medium text-text-primary" />
|
||||||
{artifacts}
|
<ResizablePanel
|
||||||
</ResizablePanel>
|
defaultSize={currentLayout[1]}
|
||||||
</>
|
minSize={minSizeMain}
|
||||||
)}
|
order={2}
|
||||||
{!hideSidePanel && interfaceConfig.sidePanel === true && (
|
id="artifacts-panel"
|
||||||
<SidePanel
|
>
|
||||||
panelRef={panelRef}
|
{artifacts}
|
||||||
minSize={minSize}
|
</ResizablePanel>
|
||||||
setMinSize={setMinSize}
|
</>
|
||||||
isCollapsed={isCollapsed}
|
)}
|
||||||
setIsCollapsed={setIsCollapsed}
|
{!hideSidePanel && interfaceConfig.sidePanel === true && (
|
||||||
collapsedSize={collapsedSize}
|
<SidePanel
|
||||||
setCollapsedSize={setCollapsedSize}
|
panelRef={panelRef}
|
||||||
fullCollapse={fullCollapse}
|
minSize={minSize}
|
||||||
setFullCollapse={setFullCollapse}
|
setMinSize={setMinSize}
|
||||||
defaultSize={currentLayout[currentLayout.length - 1]}
|
isCollapsed={isCollapsed}
|
||||||
hasArtifacts={artifacts != null}
|
setIsCollapsed={setIsCollapsed}
|
||||||
interfaceConfig={interfaceConfig}
|
collapsedSize={collapsedSize}
|
||||||
/>
|
setCollapsedSize={setCollapsedSize}
|
||||||
)}
|
fullCollapse={fullCollapse}
|
||||||
</ResizablePanelGroup>
|
setFullCollapse={setFullCollapse}
|
||||||
<button
|
defaultSize={currentLayout[currentLayout.length - 1]}
|
||||||
aria-label="Close right side panel"
|
hasArtifacts={artifacts != null}
|
||||||
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
|
interfaceConfig={interfaceConfig}
|
||||||
onClick={() => {
|
/>
|
||||||
setIsCollapsed(() => {
|
)}
|
||||||
localStorage.setItem('fullPanelCollapse', 'true');
|
</ResizablePanelGroup>
|
||||||
setFullCollapse(true);
|
<button
|
||||||
setCollapsedSize(0);
|
aria-label="Close right side panel"
|
||||||
setMinSize(0);
|
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
|
||||||
return false;
|
onClick={handleClosePanel}
|
||||||
});
|
/>
|
||||||
panelRef.current?.collapse();
|
</>
|
||||||
}}
|
);
|
||||||
/>
|
},
|
||||||
</>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(SidePanelGroup);
|
SidePanelGroup.displayName = 'SidePanelGroup';
|
||||||
|
|
||||||
|
export default SidePanelGroup;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue