diff --git a/client/src/components/Artifacts/Artifacts.tsx b/client/src/components/Artifacts/Artifacts.tsx index 5d53a7e35e..2697d4a7f3 100644 --- a/client/src/components/Artifacts/Artifacts.tsx +++ b/client/src/components/Artifacts/Artifacts.tsx @@ -2,7 +2,7 @@ import { useRef, useState, useEffect } from 'react'; import * as Tabs from '@radix-ui/react-tabs'; import { Code, Play, RefreshCw, X } from 'lucide-react'; import { useSetRecoilState, useResetRecoilState } from 'recoil'; -import { Button, Spinner, useMediaQuery } from '@librechat/client'; +import { Button, Spinner, useMediaQuery, Radio } from '@librechat/client'; import type { SandpackPreviewRef, CodeEditorRef } from '@codesandbox/sandpack-react'; import useArtifacts from '~/hooks/Artifacts/useArtifacts'; import DownloadArtifact from './DownloadArtifact'; @@ -24,14 +24,27 @@ export default function Artifacts() { const [isClosing, setIsClosing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false); const [isMounted, setIsMounted] = useState(false); - const [height, setHeight] = useState(90); // Height as percentage of viewport + const [height, setHeight] = useState(90); const [isDragging, setIsDragging] = useState(false); - const [blurAmount, setBlurAmount] = useState(0); // Dynamic blur amount + const [blurAmount, setBlurAmount] = useState(0); const dragStartY = useRef(0); const dragStartHeight = useRef(90); const setArtifactsVisible = useSetRecoilState(store.artifactsVisibility); const resetCurrentArtifactId = useResetRecoilState(store.currentArtifactId); + const tabOptions = [ + { + value: 'code', + label: localize('com_ui_code'), + icon: , + }, + { + value: 'preview', + label: localize('com_ui_preview'), + icon: , + }, + ]; + useEffect(() => { setIsMounted(true); const delay = isMobile ? 50 : 30; @@ -42,26 +55,22 @@ export default function Artifacts() { }; }, [isMobile]); - // Dynamic blur based on height - more blur when taking up more screen useEffect(() => { if (!isMobile) { setBlurAmount(0); return; } - // Calculate blur amount based on how much screen is covered - // 50% height = no blur, 100% height = full blur const minHeightForBlur = 50; const maxHeightForBlur = 100; if (height <= minHeightForBlur) { setBlurAmount(0); } else if (height >= maxHeightForBlur) { - setBlurAmount(32); // Increased from 16 to 32 for stronger blur + setBlurAmount(32); } else { - // Linear interpolation between 0 and 32px blur const progress = (height - minHeightForBlur) / (maxHeightForBlur - minHeightForBlur); - setBlurAmount(Math.round(progress * 32)); // Changed from 16 to 32 + setBlurAmount(Math.round(progress * 32)); } }, [height, isMobile]); @@ -142,7 +151,6 @@ export default function Artifacts() { } }; - // Calculate backdrop opacity based on blur amount const backdropOpacity = blurAmount > 0 ? Math.min(0.3, blurAmount / 53.33) : 0; return ( @@ -156,7 +164,6 @@ export default function Artifacts() { isVisible && !isClosing ? 'transition-all duration-300' : 'pointer-events-none opacity-0 backdrop-blur-none transition-opacity duration-150', - // Allow pointer events when not fully blurred so chat is scrollable blurAmount < 8 && isVisible && !isClosing ? 'pointer-events-none' : '', )} style={{ @@ -199,6 +206,7 @@ export default function Artifacts() {
)} + {/* Header */}
- -
- - - {localize('com_ui_code')} - - - - {localize('com_ui_preview')} - - +
)} @@ -257,7 +246,6 @@ export default function Artifacts() { onClick={handleRefresh} disabled={isRefreshing} aria-label={localize('com_ui_refresh')} - className="transition-all duration-150 active:scale-90" > {isRefreshing ? ( @@ -288,14 +276,12 @@ export default function Artifacts() { variant="ghost" onClick={closeArtifacts} aria-label={localize('com_ui_close')} - className="h-8 w-8 transition-all duration-150 hover:scale-105 active:scale-90" >
- {/* Content Area - Fixed positioning to prevent layout shifts */}
} />
+ +
+
+ +
+
- {/* Mobile Tab Switcher with iOS-style animation */} {isMobile && ( -
- -
- - - {localize('com_ui_code')} - - - - {localize('com_ui_preview')} - - +
+
)}
diff --git a/packages/client/src/components/Radio.tsx b/packages/client/src/components/Radio.tsx index b419f78e6d..2e770775ea 100644 --- a/packages/client/src/components/Radio.tsx +++ b/packages/client/src/components/Radio.tsx @@ -4,6 +4,7 @@ import { useLocalize } from '~/hooks'; interface Option { value: string; label: string; + icon?: React.ReactNode; } interface RadioProps { @@ -11,9 +12,18 @@ interface RadioProps { value?: string; onChange?: (value: string) => void; disabled?: boolean; + className?: string; + fullWidth?: boolean; } -const Radio = memo(function Radio({ options, value, onChange, disabled = false }: RadioProps) { +const Radio = memo(function Radio({ + options, + value, + onChange, + disabled = false, + className = '', + fullWidth = false, +}: RadioProps) { const localize = useLocalize(); const [currentValue, setCurrentValue] = useState(value ?? ''); const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]); @@ -67,7 +77,10 @@ const Radio = memo(function Radio({ options, value, onChange, disabled = false } const selectedIndex = options.findIndex((opt) => opt.value === currentValue); return ( -
+
{selectedIndex >= 0 && (
handleChange(option.value)} disabled={disabled} - className={`relative z-10 flex h-[34px] items-center justify-center rounded-md px-4 text-sm font-medium transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${ + className={`relative z-10 flex h-[34px] items-center justify-center gap-2 rounded-md px-4 text-sm font-medium transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${ currentValue === option.value ? 'text-foreground' : 'text-foreground' - } ${disabled ? 'cursor-not-allowed opacity-50' : ''}`} + } ${disabled ? 'cursor-not-allowed opacity-50' : ''} ${fullWidth ? 'flex-1' : ''}`} > + {option.icon && {option.icon}} {option.label} ))}