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}
))}