mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-03 14:27:20 +02:00
🔄 refactor: Migrate to react-resizable-panels v4 with Artifacts Header polish (#12356)
* chore: Update react-resizable-panels dependency to version 4.7.4 - Upgraded the "react-resizable-panels" package in package-lock.json, package.json, and client package.json files to ensure compatibility with the latest features and improvements. - Adjusted peer dependencies for React and ReactDOM to align with the new version requirements. * refactor: Update Share and SidePanel components to `react-resizable-panels` v4 - Refactored the ShareArtifactsContainer to utilize a new layout change handler, enhancing artifact panel resizing functionality. - Updated ArtifactsPanel to use the new `usePanelRef` hook, improving panel reference management. - Simplified SidePanelGroup by removing unnecessary layout normalization and integrating default layout handling with localStorage. - Removed the deprecated `normalizeLayout` utility function to streamline the codebase. - Adjusted Resizable components to ensure consistent sizing and layout behavior across panels. * style: Enhance scrollbar appearance across application - Added custom scrollbar styles to both artifacts and markdown files, improving aesthetics and user experience. - Implemented dark mode adjustments for scrollbar visibility, ensuring consistency across different color schemes. * style: Standardize button sizes and layout in Artifacts components - Updated button dimensions to a consistent height of 9 units across various components including Artifacts, Code, and DownloadArtifact. - Adjusted padding and layout properties in the Artifacts header for improved visual consistency. - Enhanced the Radio component to accept a new `buttonClassName` prop for better customization of button styles. * chore: import order
This commit is contained in:
parent
733a9364c0
commit
676641f3da
14 changed files with 92 additions and 118 deletions
|
|
@ -216,7 +216,7 @@ export default function Artifacts() {
|
|||
{/* Header */}
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-shrink-0 items-center justify-between gap-2 border-b border-border-light bg-surface-primary-alt px-3 py-2 transition-all duration-300',
|
||||
'flex h-[52px] flex-shrink-0 items-center justify-between gap-2 border-b border-border-light bg-surface-primary-alt p-2 transition-all duration-300',
|
||||
isMobile ? 'justify-center' : 'overflow-hidden',
|
||||
)}
|
||||
>
|
||||
|
|
@ -234,6 +234,7 @@ export default function Artifacts() {
|
|||
value={activeTab}
|
||||
onChange={setActiveTab}
|
||||
disabled={isMutating && activeTab !== 'code'}
|
||||
buttonClassName="h-9 px-3 gap-1.5"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -249,6 +250,7 @@ export default function Artifacts() {
|
|||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-9 w-9"
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
aria-label={localize('com_ui_refresh')}
|
||||
|
|
@ -284,6 +286,7 @@ export default function Artifacts() {
|
|||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-9 w-9"
|
||||
onClick={closeArtifacts}
|
||||
aria-label={localize('com_ui_close')}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export const CopyCodeButton: React.FC<{ content: string }> = ({ content }) => {
|
|||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-9 w-9"
|
||||
onClick={handleCopy}
|
||||
aria-label={isCopied ? localize('com_ui_copied') : localize('com_ui_copy_code')}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ const DownloadArtifact = ({ artifact }: { artifact: Artifact }) => {
|
|||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-9 w-9"
|
||||
onClick={handleDownload}
|
||||
aria-label={localize('com_ui_download_artifact')}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -83,14 +83,9 @@ export function ShareArtifactsContainer({
|
|||
|
||||
const normalizedArtifactSize = Math.min(60, Math.max(20, artifactPanelSize));
|
||||
|
||||
/**
|
||||
* Handles artifact panel resize and persists size to localStorage
|
||||
*/
|
||||
const handleLayoutChange = (sizes: number[]) => {
|
||||
if (sizes.length < 2) {
|
||||
return;
|
||||
}
|
||||
const newSize = sizes[1];
|
||||
const handleLayoutChanged = (layout: Record<string, number | string>) => {
|
||||
const raw = layout['share-artifacts'];
|
||||
const newSize = typeof raw === 'string' ? parseFloat(raw) : raw;
|
||||
if (!Number.isFinite(newSize)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -115,24 +110,22 @@ export function ShareArtifactsContainer({
|
|||
|
||||
return (
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
className="flex h-full w-full"
|
||||
onLayout={handleLayoutChange}
|
||||
orientation="horizontal"
|
||||
className="h-full w-full"
|
||||
onLayoutChanged={handleLayoutChanged}
|
||||
>
|
||||
<ResizablePanel
|
||||
defaultSize={100 - normalizedArtifactSize}
|
||||
minSize={35}
|
||||
order={1}
|
||||
defaultSize={`${100 - normalizedArtifactSize}`}
|
||||
minSize="35"
|
||||
id="share-content"
|
||||
>
|
||||
{mainContent}
|
||||
</ResizablePanel>
|
||||
<ResizableHandleAlt withHandle className="bg-border-medium text-text-primary" />
|
||||
<ResizablePanel
|
||||
defaultSize={normalizedArtifactSize}
|
||||
minSize={20}
|
||||
maxSize={60}
|
||||
order={2}
|
||||
defaultSize={`${normalizedArtifactSize}`}
|
||||
minSize="20"
|
||||
maxSize="60"
|
||||
id="share-artifacts"
|
||||
>
|
||||
<ShareArtifactsPanel contextValue={artifactsContextValue} />
|
||||
|
|
|
|||
|
|
@ -1,27 +1,21 @@
|
|||
import { useRef, useEffect, memo } from 'react';
|
||||
import { useEffect, memo } from 'react';
|
||||
import { usePanelRef } from 'react-resizable-panels';
|
||||
import { ResizableHandleAlt, ResizablePanel } from '@librechat/client';
|
||||
import type { ImperativePanelHandle } from 'react-resizable-panels';
|
||||
|
||||
interface ArtifactsPanelProps {
|
||||
artifacts: React.ReactNode | null;
|
||||
currentLayout: number[];
|
||||
minSizeMain: number;
|
||||
minSizeMain: string;
|
||||
shouldRender: boolean;
|
||||
onRenderChange: (shouldRender: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* ArtifactsPanel component - memoized to prevent unnecessary re-renders
|
||||
* Only re-renders when artifacts visibility or layout changes
|
||||
*/
|
||||
const ArtifactsPanel = memo(function ArtifactsPanel({
|
||||
artifacts,
|
||||
currentLayout,
|
||||
minSizeMain,
|
||||
shouldRender,
|
||||
onRenderChange,
|
||||
}: ArtifactsPanelProps) {
|
||||
const artifactsPanelRef = useRef<ImperativePanelHandle>(null);
|
||||
const artifactsPanelRef = usePanelRef();
|
||||
|
||||
useEffect(() => {
|
||||
if (artifacts != null) {
|
||||
|
|
@ -34,7 +28,7 @@ const ArtifactsPanel = memo(function ArtifactsPanel({
|
|||
} else if (shouldRender) {
|
||||
onRenderChange(false);
|
||||
}
|
||||
}, [artifacts, shouldRender, onRenderChange]);
|
||||
}, [artifacts, shouldRender, onRenderChange, artifactsPanelRef]);
|
||||
|
||||
if (!shouldRender) {
|
||||
return null;
|
||||
|
|
@ -46,13 +40,12 @@ const ArtifactsPanel = memo(function ArtifactsPanel({
|
|||
<ResizableHandleAlt withHandle className="bg-border-medium text-text-primary" />
|
||||
)}
|
||||
<ResizablePanel
|
||||
ref={artifactsPanelRef}
|
||||
defaultSize={artifacts != null ? currentLayout[1] : 0}
|
||||
minSize={minSizeMain}
|
||||
maxSize={70}
|
||||
defaultSize="50"
|
||||
maxSize="70"
|
||||
collapsedSize="0"
|
||||
collapsible={true}
|
||||
collapsedSize={0}
|
||||
order={2}
|
||||
minSize={minSizeMain}
|
||||
panelRef={artifactsPanelRef}
|
||||
id="artifacts-panel"
|
||||
>
|
||||
<div className="h-full min-w-[400px] overflow-hidden">{artifacts}</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { useState, useEffect, useMemo, memo } from 'react';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { useState, memo } from 'react';
|
||||
import { useDefaultLayout } from 'react-resizable-panels';
|
||||
import { ResizablePanel, ResizablePanelGroup, useMediaQuery } from '@librechat/client';
|
||||
import ArtifactsPanel from './ArtifactsPanel';
|
||||
import { normalizeLayout } from '~/utils';
|
||||
|
||||
const PANEL_IDS_SINGLE = ['messages-view'];
|
||||
const PANEL_IDS_SPLIT = ['messages-view', 'artifacts-panel'];
|
||||
|
||||
interface SidePanelProps {
|
||||
artifacts?: React.ReactNode;
|
||||
|
|
@ -13,46 +15,29 @@ const SidePanelGroup = memo(({ artifacts, children }: SidePanelProps) => {
|
|||
const [shouldRenderArtifacts, setShouldRenderArtifacts] = useState(artifacts != null);
|
||||
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
||||
|
||||
const currentLayout = useMemo(() => {
|
||||
if (artifacts == null) {
|
||||
return [100];
|
||||
}
|
||||
return normalizeLayout([50, 50]);
|
||||
}, [artifacts]);
|
||||
const { defaultLayout, onLayoutChanged } = useDefaultLayout({
|
||||
id: 'side-panel-layout',
|
||||
panelIds: artifacts != null ? PANEL_IDS_SPLIT : PANEL_IDS_SINGLE,
|
||||
storage: localStorage,
|
||||
});
|
||||
|
||||
const throttledSaveLayout = useMemo(
|
||||
() =>
|
||||
throttle((sizes: number[]) => {
|
||||
const normalizedSizes = normalizeLayout(sizes);
|
||||
localStorage.setItem('react-resizable-panels:layout', JSON.stringify(normalizedSizes));
|
||||
}, 350),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => () => throttledSaveLayout.cancel(), [throttledSaveLayout]);
|
||||
|
||||
const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]);
|
||||
const minSizeMain = artifacts != null ? '15' : '30';
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
onLayout={(sizes) => throttledSaveLayout(sizes)}
|
||||
className="relative h-full w-full flex-1 overflow-auto bg-presentation"
|
||||
orientation="horizontal"
|
||||
defaultLayout={defaultLayout}
|
||||
onLayoutChanged={onLayoutChanged}
|
||||
className="relative flex-1 bg-presentation"
|
||||
>
|
||||
<ResizablePanel
|
||||
defaultSize={currentLayout[0]}
|
||||
minSize={minSizeMain}
|
||||
order={1}
|
||||
id="messages-view"
|
||||
>
|
||||
<ResizablePanel defaultSize="50" minSize={minSizeMain} id="messages-view">
|
||||
{children}
|
||||
</ResizablePanel>
|
||||
|
||||
{!isSmallScreen && (
|
||||
<ArtifactsPanel
|
||||
artifacts={artifacts}
|
||||
currentLayout={currentLayout}
|
||||
minSizeMain={minSizeMain}
|
||||
shouldRender={shouldRenderArtifacts}
|
||||
onRenderChange={setShouldRenderArtifacts}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue