feat: Refactor Artifacts component structure and add mobile/desktop variants for improved UI

This commit is contained in:
Marco Beretta 2025-10-25 17:47:43 +02:00
parent 6fb76c7d60
commit 3c02a7b2e8
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
6 changed files with 479 additions and 277 deletions

View file

@ -1,17 +1,14 @@
import { useRef, useState, useEffect } from 'react';
import * as Tabs from '@radix-ui/react-tabs';
import { Code, Play, RefreshCw, X } from 'lucide-react';
import { Code, Play } from 'lucide-react';
import { useSetRecoilState, useResetRecoilState } from 'recoil';
import { Button, Spinner, useMediaQuery, Radio } from '@librechat/client';
import { useMediaQuery } from '@librechat/client';
import type { SandpackPreviewRef, CodeEditorRef } from '@codesandbox/sandpack-react';
import useArtifacts from '~/hooks/Artifacts/useArtifacts';
import DownloadArtifact from './DownloadArtifact';
import ArtifactVersion from './ArtifactVersion';
import MobileArtifacts from './MobileArtifacts';
import DesktopArtifacts from './DesktopArtifacts';
import { useEditorContext } from '~/Providers';
import ArtifactTabs from './ArtifactTabs';
import { CopyCodeButton } from './Code';
import type { TabOption } from './ArtifactsTypes';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
import store from '~/store';
export default function Artifacts() {
@ -20,19 +17,12 @@ export default function Artifacts() {
const isMobile = useMediaQuery('(max-width: 868px)');
const editorRef = useRef<CodeEditorRef>();
const previewRef = useRef<SandpackPreviewRef>();
const [isVisible, setIsVisible] = useState(false);
const [isClosing, setIsClosing] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [isMounted, setIsMounted] = useState(false);
const [height, setHeight] = useState(90);
const [isDragging, setIsDragging] = useState(false);
const [blurAmount, setBlurAmount] = useState(0);
const dragStartY = useRef(0);
const dragStartHeight = useRef(90);
const [isRefreshing, setIsRefreshing] = useState(false);
const setArtifactsVisible = useSetRecoilState(store.artifactsVisibility);
const resetCurrentArtifactId = useResetRecoilState(store.currentArtifactId);
const tabOptions = [
const tabOptions: TabOption[] = [
{
value: 'code',
label: localize('com_ui_code'),
@ -45,35 +35,6 @@ export default function Artifacts() {
},
];
useEffect(() => {
setIsMounted(true);
const delay = isMobile ? 50 : 30;
const timer = setTimeout(() => setIsVisible(true), delay);
return () => {
clearTimeout(timer);
setIsMounted(false);
};
}, [isMobile]);
useEffect(() => {
if (!isMobile) {
setBlurAmount(0);
return;
}
const minHeightForBlur = 50;
const maxHeightForBlur = 100;
if (height <= minHeightForBlur) {
setBlurAmount(0);
} else if (height >= maxHeightForBlur) {
setBlurAmount(MAX_BLUR_AMOUNT);
} else {
const progress = (height - minHeightForBlur) / (maxHeightForBlur - minHeightForBlur);
setBlurAmount(Math.round(progress * MAX_BLUR_AMOUNT));
}
}, [height, isMobile]);
const {
activeTab,
setActiveTab,
@ -83,45 +44,10 @@ export default function Artifacts() {
setCurrentArtifactId,
} = useArtifacts();
const handleDragStart = (e: React.PointerEvent) => {
setIsDragging(true);
dragStartY.current = e.clientY;
dragStartHeight.current = height;
(e.target as HTMLElement).setPointerCapture(e.pointerId);
};
const handleDragMove = (e: React.PointerEvent) => {
if (!isDragging) return;
const deltaY = dragStartY.current - e.clientY;
const viewportHeight = window.innerHeight;
const deltaPercentage = (deltaY / viewportHeight) * 100;
const newHeight = Math.max(10, Math.min(100, dragStartHeight.current + deltaPercentage));
setHeight(newHeight);
};
const handleDragEnd = (e: React.PointerEvent) => {
if (!isDragging) return;
setIsDragging(false);
(e.target as HTMLElement).releasePointerCapture(e.pointerId);
// Snap to positions based on final height
if (height < 30) {
// Close if dragged down significantly
closeArtifacts();
} else if (height > 95) {
// Snap to full height if dragged near top
setHeight(100);
} else if (height < 60) {
// Snap to minimum if in lower range
setHeight(50);
} else {
// Snap to default
setHeight(90);
}
};
useEffect(() => {
setIsMounted(true);
return () => setIsMounted(false);
}, []);
if (!currentArtifact || !isMounted) {
return null;
@ -136,201 +62,30 @@ export default function Artifacts() {
setTimeout(() => setIsRefreshing(false), 750);
};
const closeArtifacts = () => {
const handleClose = () => {
if (isMobile) {
setIsClosing(true);
setIsVisible(false);
setTimeout(() => {
setArtifactsVisible(false);
setIsClosing(false);
setHeight(90);
}, 250);
setArtifactsVisible(false);
} else {
resetCurrentArtifactId();
setArtifactsVisible(false);
}
};
// Matches the maximum blur applied when the sheet is fully expanded
const MAX_BLUR_AMOUNT = 32;
// Ensures the backdrop caps at 30% opacity when blur reaches its maximum
const MAX_BACKDROP_OPACITY = 0.3;
const sharedProps = {
currentArtifact,
activeTab,
setActiveTab,
currentIndex,
orderedArtifactIds,
setCurrentArtifactId,
editorRef: editorRef as React.MutableRefObject<CodeEditorRef>,
previewRef: previewRef as React.MutableRefObject<SandpackPreviewRef>,
isMutating,
onClose: handleClose,
onRefresh: handleRefresh,
isRefreshing,
tabOptions,
};
const backdropOpacity =
blurAmount > 0
? (Math.min(blurAmount, MAX_BLUR_AMOUNT) / MAX_BLUR_AMOUNT) * MAX_BACKDROP_OPACITY
: 0;
return (
<Tabs.Root value={activeTab} onValueChange={setActiveTab} asChild>
<div className="flex h-full w-full flex-col">
{/* Mobile backdrop with dynamic blur */}
{isMobile && (
<div
className={cn(
'fixed inset-0 z-[99] bg-black will-change-[opacity,backdrop-filter]',
isVisible && !isClosing
? 'transition-all duration-300'
: 'pointer-events-none opacity-0 backdrop-blur-none transition-opacity duration-150',
blurAmount < 8 && isVisible && !isClosing ? 'pointer-events-none' : '',
)}
style={{
opacity: isVisible && !isClosing ? backdropOpacity : 0,
backdropFilter: isVisible && !isClosing ? `blur(${blurAmount}px)` : 'none',
WebkitBackdropFilter: isVisible && !isClosing ? `blur(${blurAmount}px)` : 'none',
}}
onClick={blurAmount >= 8 ? closeArtifacts : undefined}
aria-hidden="true"
/>
)}
<div
className={cn(
'flex w-full flex-col bg-surface-primary text-xl text-text-primary',
isMobile
? cn(
'fixed inset-x-0 bottom-0 z-[100] rounded-t-[20px] shadow-[0_-10px_60px_rgba(0,0,0,0.35)]',
isVisible && !isClosing
? 'translate-y-0 opacity-100'
: 'duration-250 translate-y-full opacity-0 transition-all',
isDragging ? '' : 'transition-all duration-300',
)
: cn(
'h-full shadow-2xl',
isVisible && !isClosing
? 'duration-350 translate-x-0 opacity-100 transition-all'
: 'translate-x-5 opacity-0 transition-all duration-300',
),
)}
style={isMobile ? { height: `${height}vh` } : { overflow: 'hidden' }}
>
{isMobile && (
<div
className="flex flex-shrink-0 cursor-grab items-center justify-center bg-surface-primary-alt pb-1.5 pt-2.5 active:cursor-grabbing"
onPointerDown={handleDragStart}
onPointerMove={handleDragMove}
onPointerUp={handleDragEnd}
onPointerCancel={handleDragEnd}
>
<div className="h-1 w-12 rounded-full bg-border-xheavy opacity-40 transition-all duration-200 active:opacity-60" />
</div>
)}
{/* 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',
isMobile ? 'justify-center' : 'overflow-hidden',
)}
>
{!isMobile && (
<div
className={cn(
'flex items-center transition-all duration-500',
isVisible && !isClosing
? 'translate-x-0 opacity-100'
: '-translate-x-2 opacity-0',
)}
>
<Radio
options={tabOptions}
value={activeTab}
onChange={setActiveTab}
disabled={isMutating && activeTab !== 'code'}
/>
</div>
)}
<div
className={cn(
'flex items-center gap-2 transition-all duration-500',
isMobile ? 'min-w-max' : '',
isVisible && !isClosing ? 'translate-x-0 opacity-100' : 'translate-x-2 opacity-0',
)}
>
{activeTab === 'preview' && (
<Button
size="icon"
variant="ghost"
onClick={handleRefresh}
disabled={isRefreshing}
aria-label={localize('com_ui_refresh')}
>
{isRefreshing ? (
<Spinner size={16} />
) : (
<RefreshCw size={16} className="transition-transform duration-200" />
)}
</Button>
)}
{activeTab !== 'preview' && isMutating && (
<RefreshCw size={16} className="animate-spin text-text-secondary" />
)}
{orderedArtifactIds.length > 1 && (
<ArtifactVersion
currentIndex={currentIndex}
totalVersions={orderedArtifactIds.length}
onVersionChange={(index) => {
const target = orderedArtifactIds[index];
if (target) {
setCurrentArtifactId(target);
}
}}
/>
)}
<CopyCodeButton content={currentArtifact.content ?? ''} />
<DownloadArtifact artifact={currentArtifact} />
<Button
size="icon"
variant="ghost"
onClick={closeArtifacts}
aria-label={localize('com_ui_close')}
>
<X size={16} />
</Button>
</div>
</div>
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden bg-surface-primary">
<div className="absolute inset-0 flex flex-col">
<ArtifactTabs
artifact={currentArtifact}
editorRef={editorRef as React.MutableRefObject<CodeEditorRef>}
previewRef={previewRef as React.MutableRefObject<SandpackPreviewRef>}
/>
</div>
<div
className={cn(
'absolute inset-0 z-[60] flex items-center justify-center bg-black/70 backdrop-blur-sm transition-opacity duration-300 ease-in-out',
isRefreshing ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0',
)}
aria-hidden={!isRefreshing}
role="status"
>
<div
className={cn(
'transition-transform duration-300 ease-in-out',
isRefreshing ? 'scale-100' : 'scale-95',
)}
>
<Spinner size={24} />
</div>
</div>
</div>
{isMobile && (
<div className="flex-shrink-0 border-t border-border-light bg-surface-primary-alt p-2">
<Radio
fullWidth
options={tabOptions}
value={activeTab}
onChange={setActiveTab}
disabled={isMutating && activeTab !== 'code'}
/>
</div>
)}
</div>
</div>
</Tabs.Root>
);
return isMobile ? <MobileArtifacts {...sharedProps} /> : <DesktopArtifacts {...sharedProps} />;
}

View file

@ -0,0 +1,104 @@
import { RefreshCw, X } from 'lucide-react';
import { Button, Spinner, Radio } from '@librechat/client';
import type { TabOption } from './ArtifactsTypes';
import DownloadArtifact from './DownloadArtifact';
import ArtifactVersion from './ArtifactVersion';
import { CopyCodeButton } from './Code';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
import type { Artifact } from '~/common';
interface ArtifactsHeaderProps {
activeTab: string;
setActiveTab: (tab: string) => void;
currentIndex: number;
orderedArtifactIds: string[];
setCurrentArtifactId: (id: string) => void;
currentArtifact: Artifact;
isMutating: boolean;
isRefreshing: boolean;
onRefresh: () => void;
onClose: () => void;
isMobile?: boolean;
tabOptions: TabOption[];
}
export default function ArtifactsHeader({
activeTab,
setActiveTab,
currentIndex,
orderedArtifactIds,
setCurrentArtifactId,
currentArtifact,
isMutating,
isRefreshing,
onRefresh,
onClose,
isMobile = false,
tabOptions,
}: ArtifactsHeaderProps) {
const localize = useLocalize();
return (
<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',
isMobile ? 'justify-center' : 'w-full overflow-hidden',
)}
>
{!isMobile && (
<div className="flex items-center transition-all duration-500">
<Radio
options={tabOptions}
value={activeTab}
onChange={setActiveTab}
disabled={isMutating && activeTab !== 'code'}
/>
</div>
)}
<div
className={cn(
'flex items-center gap-2 transition-all duration-500',
isMobile ? 'min-w-max' : '',
)}
>
{activeTab === 'preview' && (
<Button
size="icon"
variant="ghost"
onClick={onRefresh}
disabled={isRefreshing}
aria-label={localize('com_ui_refresh')}
>
{isRefreshing ? (
<Spinner size={16} />
) : (
<RefreshCw size={16} className="transition-transform duration-200" />
)}
</Button>
)}
{activeTab !== 'preview' && isMutating && (
<RefreshCw size={16} className="animate-spin text-text-secondary" />
)}
{orderedArtifactIds.length > 1 && (
<ArtifactVersion
currentIndex={currentIndex}
totalVersions={orderedArtifactIds.length}
onVersionChange={(index) => {
const target = orderedArtifactIds[index];
if (target) {
setCurrentArtifactId(target);
}
}}
/>
)}
<CopyCodeButton content={currentArtifact.content ?? ''} />
<DownloadArtifact artifact={currentArtifact} />
<Button size="icon" variant="ghost" onClick={onClose} aria-label={localize('com_ui_close')}>
<X size={16} />
</Button>
</div>
</div>
);
}

View file

@ -0,0 +1,23 @@
import type { SandpackPreviewRef, CodeEditorRef } from '@codesandbox/sandpack-react';
import type { Artifact } from '~/common';
export interface ArtifactsSharedProps {
currentArtifact: Artifact;
activeTab: string;
setActiveTab: (tab: string) => void;
currentIndex: number;
orderedArtifactIds: string[];
setCurrentArtifactId: (id: string) => void;
editorRef: React.MutableRefObject<CodeEditorRef>;
previewRef: React.MutableRefObject<SandpackPreviewRef>;
isMutating: boolean;
onClose: () => void;
onRefresh: () => void;
isRefreshing: boolean;
}
export interface TabOption {
value: string;
label: string;
icon: React.ReactNode;
}

View file

@ -0,0 +1,106 @@
import { useState, useEffect } from 'react';
import * as Tabs from '@radix-ui/react-tabs';
import { Spinner } from '@librechat/client';
import type { ArtifactsSharedProps, TabOption } from './ArtifactsTypes';
import ArtifactsHeader from './ArtifactsHeader';
import ArtifactTabs from './ArtifactTabs';
import { cn } from '~/utils';
interface DesktopArtifactsProps extends ArtifactsSharedProps {
tabOptions: TabOption[];
}
export default function DesktopArtifacts({
currentArtifact,
activeTab,
setActiveTab,
currentIndex,
orderedArtifactIds,
setCurrentArtifactId,
editorRef,
previewRef,
isMutating,
onClose,
onRefresh,
isRefreshing,
tabOptions,
}: DesktopArtifactsProps) {
const [isVisible, setIsVisible] = useState(false);
const [isClosing, setIsClosing] = useState(false);
useEffect(() => {
const timer = setTimeout(() => setIsVisible(true), 30);
return () => clearTimeout(timer);
}, []);
const handleClose = () => {
setIsClosing(true);
setIsVisible(false);
setTimeout(() => {
onClose();
setIsClosing(false);
}, 300);
};
return (
<Tabs.Root value={activeTab} onValueChange={setActiveTab} asChild>
<div
className={cn(
'flex h-full w-full flex-col bg-surface-primary text-xl text-text-primary shadow-2xl',
isVisible && !isClosing
? 'duration-350 translate-x-0 opacity-100 transition-all'
: 'translate-x-5 opacity-0 transition-all duration-300',
)}
style={{ overflow: 'hidden' }}
>
{/* Header */}
<div className={cn('flex items-center transition-all duration-500')}>
<ArtifactsHeader
activeTab={activeTab}
setActiveTab={setActiveTab}
currentIndex={currentIndex}
orderedArtifactIds={orderedArtifactIds}
setCurrentArtifactId={setCurrentArtifactId}
currentArtifact={currentArtifact}
isMutating={isMutating}
isRefreshing={isRefreshing}
onRefresh={onRefresh}
onClose={handleClose}
isMobile={false}
tabOptions={tabOptions}
/>
</div>
{/* Content */}
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden bg-surface-primary">
<div className="absolute inset-0 flex flex-col">
<ArtifactTabs
artifact={currentArtifact}
editorRef={editorRef}
previewRef={previewRef}
/>
</div>
{/* Refresh overlay */}
<div
className={cn(
'absolute inset-0 z-[60] flex items-center justify-center bg-black/70 backdrop-blur-sm transition-opacity duration-300 ease-in-out',
isRefreshing ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0',
)}
aria-hidden={!isRefreshing}
role="status"
>
<div
className={cn(
'transition-transform duration-300 ease-in-out',
isRefreshing ? 'scale-100' : 'scale-95',
)}
>
<Spinner size={24} />
</div>
</div>
</div>
</div>
</Tabs.Root>
);
}

View file

@ -0,0 +1,215 @@
import { useRef, useState, useEffect } from 'react';
import * as Tabs from '@radix-ui/react-tabs';
import { Spinner, Radio } from '@librechat/client';
import type { ArtifactsSharedProps, TabOption } from './ArtifactsTypes';
import ArtifactsHeader from './ArtifactsHeader';
import ArtifactTabs from './ArtifactTabs';
import { cn } from '~/utils';
const MAX_BLUR_AMOUNT = 32;
const MAX_BACKDROP_OPACITY = 0.3;
interface MobileArtifactsProps extends ArtifactsSharedProps {
tabOptions: TabOption[];
}
export default function MobileArtifacts({
currentArtifact,
activeTab,
setActiveTab,
currentIndex,
orderedArtifactIds,
setCurrentArtifactId,
editorRef,
previewRef,
isMutating,
onClose,
onRefresh,
isRefreshing,
tabOptions,
}: MobileArtifactsProps) {
const [isVisible, setIsVisible] = useState(false);
const [isClosing, setIsClosing] = useState(false);
const [height, setHeight] = useState(90);
const [isDragging, setIsDragging] = useState(false);
const [blurAmount, setBlurAmount] = useState(0);
const dragStartY = useRef(0);
const dragStartHeight = useRef(90);
useEffect(() => {
const timer = setTimeout(() => setIsVisible(true), 50);
return () => clearTimeout(timer);
}, []);
useEffect(() => {
const minHeightForBlur = 50;
const maxHeightForBlur = 100;
if (height <= minHeightForBlur) {
setBlurAmount(0);
} else if (height >= maxHeightForBlur) {
setBlurAmount(MAX_BLUR_AMOUNT);
} else {
const progress = (height - minHeightForBlur) / (maxHeightForBlur - minHeightForBlur);
setBlurAmount(Math.round(progress * MAX_BLUR_AMOUNT));
}
}, [height]);
const handleDragStart = (e: React.PointerEvent) => {
setIsDragging(true);
dragStartY.current = e.clientY;
dragStartHeight.current = height;
(e.target as HTMLElement).setPointerCapture(e.pointerId);
};
const handleDragMove = (e: React.PointerEvent) => {
if (!isDragging) {
return;
}
const deltaY = dragStartY.current - e.clientY;
const viewportHeight = window.innerHeight;
const deltaPercentage = (deltaY / viewportHeight) * 100;
const newHeight = Math.max(10, Math.min(100, dragStartHeight.current + deltaPercentage));
setHeight(newHeight);
};
const handleDragEnd = (e: React.PointerEvent) => {
if (!isDragging) {
return;
}
setIsDragging(false);
(e.target as HTMLElement).releasePointerCapture(e.pointerId);
// Snap to positions based on final height
if (height < 30) {
handleClose();
} else if (height > 95) {
setHeight(100);
} else if (height < 60) {
setHeight(50);
} else {
setHeight(90);
}
};
const handleClose = () => {
setIsClosing(true);
setIsVisible(false);
setTimeout(() => {
onClose();
setIsClosing(false);
setHeight(90);
}, 250);
};
const backdropOpacity =
blurAmount > 0
? (Math.min(blurAmount, MAX_BLUR_AMOUNT) / MAX_BLUR_AMOUNT) * MAX_BACKDROP_OPACITY
: 0;
return (
<Tabs.Root value={activeTab} onValueChange={setActiveTab} asChild>
<div className="flex h-full w-full flex-col">
{/* Mobile backdrop with dynamic blur */}
<div
className={cn(
'fixed inset-0 z-[99] bg-black will-change-[opacity,backdrop-filter]',
isVisible && !isClosing
? 'transition-all duration-300'
: 'pointer-events-none opacity-0 backdrop-blur-none transition-opacity duration-150',
blurAmount < 8 && isVisible && !isClosing ? 'pointer-events-none' : '',
)}
style={{
opacity: isVisible && !isClosing ? backdropOpacity : 0,
backdropFilter: isVisible && !isClosing ? `blur(${blurAmount}px)` : 'none',
WebkitBackdropFilter: isVisible && !isClosing ? `blur(${blurAmount}px)` : 'none',
}}
onClick={blurAmount >= 8 ? handleClose : undefined}
aria-hidden="true"
/>
<div
className={cn(
'fixed inset-x-0 bottom-0 z-[100] flex w-full flex-col rounded-t-[20px] bg-surface-primary text-xl text-text-primary shadow-[0_-10px_60px_rgba(0,0,0,0.35)]',
isVisible && !isClosing
? 'translate-y-0 opacity-100'
: 'duration-250 translate-y-full opacity-0 transition-all',
isDragging ? '' : 'transition-all duration-300',
)}
style={{ height: `${height}vh` }}
>
{/* Drag handle */}
<div
className="flex flex-shrink-0 cursor-grab items-center justify-center bg-surface-primary-alt pb-1.5 pt-2.5 active:cursor-grabbing"
onPointerDown={handleDragStart}
onPointerMove={handleDragMove}
onPointerUp={handleDragEnd}
onPointerCancel={handleDragEnd}
>
<div className="h-1 w-12 rounded-full bg-border-xheavy opacity-40 transition-all duration-200 active:opacity-60" />
</div>
{/* Header */}
<ArtifactsHeader
activeTab={activeTab}
setActiveTab={setActiveTab}
currentIndex={currentIndex}
orderedArtifactIds={orderedArtifactIds}
setCurrentArtifactId={setCurrentArtifactId}
currentArtifact={currentArtifact}
isMutating={isMutating}
isRefreshing={isRefreshing}
onRefresh={onRefresh}
onClose={handleClose}
isMobile={true}
tabOptions={tabOptions}
/>
{/* Content */}
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden bg-surface-primary">
<div className="absolute inset-0 flex flex-col">
<ArtifactTabs
artifact={currentArtifact}
editorRef={editorRef}
previewRef={previewRef}
/>
</div>
{/* Refresh overlay */}
<div
className={cn(
'absolute inset-0 z-[60] flex items-center justify-center bg-black/70 backdrop-blur-sm transition-opacity duration-300 ease-in-out',
isRefreshing ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0',
)}
aria-hidden={!isRefreshing}
role="status"
>
<div
className={cn(
'transition-transform duration-300 ease-in-out',
isRefreshing ? 'scale-100' : 'scale-95',
)}
>
<Spinner size={24} />
</div>
</div>
</div>
{/* Bottom tabs */}
<div className="flex-shrink-0 border-t border-border-light bg-surface-primary-alt p-2">
<Radio
fullWidth
options={tabOptions}
value={activeTab}
onChange={setActiveTab}
disabled={isMutating && activeTab !== 'code'}
/>
</div>
</div>
</div>
</Tabs.Root>
);
}

View file

@ -140,14 +140,14 @@ const SidePanelGroup = memo(
<ResizablePanelGroup
direction="horizontal"
onLayout={(sizes) => throttledSaveLayout(sizes)}
className="ease-[cubic-bezier(0.25,0.46,0.45,0.94)] relative h-full w-full flex-1 overflow-auto bg-presentation transition-all duration-500"
className="relative h-full w-full flex-1 overflow-auto bg-presentation"
>
<ResizablePanel
defaultSize={currentLayout[0]}
minSize={minSizeMain}
order={1}
id="messages-view"
className="ease-[cubic-bezier(0.25,0.46,0.45,0.94)] transition-all duration-500"
className="transition-width relative h-full w-full flex-1 overflow-auto bg-presentation"
>
{children}
</ResizablePanel>
@ -167,7 +167,6 @@ const SidePanelGroup = memo(
collapsedSize={0}
order={2}
id="artifacts-panel"
className="ease-[cubic-bezier(0.25,0.46,0.45,0.94)] transition-all duration-500"
>
<div className="h-full min-w-[400px] overflow-hidden">{artifacts}</div>
</ResizablePanel>