import { useState, useEffect, useCallback, useRef } from 'react'; import { Button } from '@librechat/client'; import { Maximize2, X } from 'lucide-react'; import { FileSources } from 'librechat-data-provider'; import * as DialogPrimitive from '@radix-ui/react-dialog'; import ProgressCircle from './ProgressCircle'; import SourceIcon from './SourceIcon'; import { cn } from '~/utils'; type styleProps = { backgroundImage?: string; backgroundSize?: string; backgroundPosition?: string; backgroundRepeat?: string; }; const ImagePreview = ({ imageBase64, url, progress = 1, className = '', source, alt = 'Preview image', }: { imageBase64?: string; url?: string; progress?: number; className?: string; source?: FileSources; alt?: string; }) => { const [isModalOpen, setIsModalOpen] = useState(false); const [isHovered, setIsHovered] = useState(false); const triggerRef = useRef(null); const imageRef = useRef(null); const closeButtonRef = useRef(null); const openModal = useCallback(() => { setIsModalOpen(true); }, []); const handleOpenChange = useCallback((open: boolean) => { setIsModalOpen(open); if (!open && triggerRef.current) { requestAnimationFrame(() => { triggerRef.current?.focus({ preventScroll: true }); }); } }, []); // Handle click on background areas to close (only if clicking the overlay/content directly) const handleBackgroundClick = useCallback( (e: React.MouseEvent) => { if (e.target === e.currentTarget) { handleOpenChange(false); } }, [handleOpenChange], ); useEffect(() => { if (isModalOpen) { document.body.style.overflow = 'hidden'; } else { document.body.style.overflow = 'unset'; } return () => { document.body.style.overflow = 'unset'; }; }, [isModalOpen]); // Handle escape key useEffect(() => { if (!isModalOpen) return; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') { handleOpenChange(false); } }; document.addEventListener('keydown', onKey); return () => document.removeEventListener('keydown', onKey); }, [isModalOpen, handleOpenChange]); const baseStyle: styleProps = { backgroundSize: 'cover', backgroundPosition: 'center', backgroundRepeat: 'no-repeat', }; const imageUrl = imageBase64 ?? url ?? ''; const style: styleProps = imageUrl ? { ...baseStyle, backgroundImage: `url(${imageUrl})`, } : baseStyle; if (typeof style.backgroundImage !== 'string' || style.backgroundImage.length === 0) { return null; } const radius = 55; const circumference = 2 * Math.PI * radius; const offset = circumference - progress * circumference; const circleCSSProperties = { transition: 'stroke-dashoffset 0.3s linear', }; return ( <> { e.preventDefault(); closeButtonRef.current?.focus(); }} onCloseAutoFocus={(e) => { e.preventDefault(); triggerRef.current?.focus(); }} onPointerDownOutside={(e) => e.preventDefault()} onClick={handleBackgroundClick} > {/* Close button */} {/* Image container */}
e.stopPropagation()}> {alt}
); }; export default ImagePreview;