amaniquery-frontend / src /components /chat /ImagePreview.tsx
Benaah's picture
Upload folder using huggingface_hub
6a059d3 verified
Raw
History Blame Contribute Delete
8.23 kB
"use client"
import { useState, useEffect } from "react"
import Image from "next/image"
import { X, ZoomIn, Loader2, RefreshCw, AlertCircle } from "lucide-react"
import { Button } from "@/components/ui/button"
interface ImagePreviewProps {
src: string
alt: string
className?: string
}
export function ImagePreview({ src, alt, className = "" }: ImagePreviewProps) {
const [isOpen, setIsOpen] = useState(false)
const [imageError, setImageError] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const [lightboxLoading, setLightboxLoading] = useState(true)
const [lightboxError, setLightboxError] = useState(false)
const [retryKey, setRetryKey] = useState(0)
// Handle ESC key to close modal
useEffect(() => {
if (!isOpen) return
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") {
setIsOpen(false)
}
}
// Prevent body scroll when modal is open
document.body.style.overflow = "hidden"
window.addEventListener("keydown", handleEscape)
return () => {
window.removeEventListener("keydown", handleEscape)
document.body.style.overflow = "unset"
}
}, [isOpen])
const handleImageLoad = () => {
setIsLoading(false)
}
const handleImageError = () => {
setImageError(true)
setIsLoading(false)
}
const handleLightboxImageLoad = () => {
setLightboxLoading(false)
}
const handleLightboxImageError = () => {
setLightboxError(true)
setLightboxLoading(false)
}
const handleRetry = () => {
setImageError(false)
setIsLoading(true)
setLightboxError(false)
setLightboxLoading(true)
// Force reload by updating retry key which will change the src
setRetryKey(prev => prev + 1)
}
// Add cache-busting parameter for retry
const imageSrc = retryKey > 0
? (src.includes("?") ? `${src}&retry=${retryKey}` : `${src}?retry=${retryKey}`)
: src
const handleOpen = () => {
setIsOpen(true)
setLightboxLoading(true)
setLightboxError(false)
}
const handleClose = () => {
setIsOpen(false)
}
// Error state for thumbnail
if (imageError) {
return (
<div className={`flex flex-col items-center justify-center w-full h-32 md:h-48 bg-muted/50 rounded-lg border border-white/10 ${className}`}>
<AlertCircle className="w-5 h-5 md:w-6 md:h-6 text-muted-foreground mb-2" />
<p className="text-xs md:text-sm text-muted-foreground mb-2 px-2 text-center">Failed to load image</p>
<Button
variant="ghost"
size="sm"
onClick={handleRetry}
className="h-7 md:h-8 text-xs"
>
<RefreshCw className="w-3 h-3 md:w-4 md:h-4 mr-1" />
Retry
</Button>
</div>
)
}
return (
<>
{/* Thumbnail */}
<div
className={`relative group cursor-pointer overflow-hidden rounded-lg border border-white/10 bg-white/5 hover:border-white/20 transition-all ${className}`}
onClick={handleOpen}
>
<div className="relative w-full h-32 md:h-48 min-h-[128px]">
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-muted/30">
<Loader2 className="w-5 h-5 md:w-6 md:h-6 text-muted-foreground animate-spin" />
</div>
)}
<Image
key={retryKey}
src={imageSrc}
alt={alt}
fill
className={`object-cover transition-opacity duration-300 ${isLoading ? "opacity-0" : "opacity-100"}`}
onLoad={handleImageLoad}
onError={handleImageError}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
unoptimized={imageSrc.startsWith("data:") || imageSrc.startsWith("blob:")}
/>
{/* Overlay on hover */}
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/30 transition-colors flex items-center justify-center">
<ZoomIn className="w-5 h-5 md:w-6 md:h-6 text-white opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</div>
</div>
{/* Lightbox Modal */}
{isOpen && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/95 backdrop-blur-sm"
onClick={handleClose}
>
<div className="relative w-full h-full p-2 md:p-4 flex items-center justify-center">
{/* Close button */}
<Button
variant="ghost"
size="icon"
className="absolute top-2 right-2 md:top-4 md:right-4 z-10 h-9 w-9 md:h-10 md:w-10 bg-black/70 hover:bg-black/90 text-white rounded-full"
onClick={handleClose}
aria-label="Close image"
>
<X className="w-5 h-5 md:w-6 md:h-6" />
</Button>
{/* Full-size image */}
<div
className="relative w-full h-full max-w-[95vw] max-h-[95vh] md:max-w-[90vw] md:max-h-[90vh] flex items-center justify-center"
onClick={(e) => e.stopPropagation()}
>
{lightboxLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-black/50">
<Loader2 className="w-8 h-8 md:w-10 md:h-10 text-white animate-spin" />
</div>
)}
{lightboxError ? (
<div className="flex flex-col items-center justify-center p-8 bg-black/50 rounded-lg">
<AlertCircle className="w-8 h-8 md:w-10 md:h-10 text-white mb-4" />
<p className="text-sm md:text-base text-white mb-4 text-center">Failed to load image</p>
<Button
variant="default"
size="sm"
onClick={handleRetry}
className="bg-white text-black hover:bg-white/90"
>
<RefreshCw className="w-4 h-4 mr-2" />
Retry
</Button>
</div>
) : (
<Image
key={retryKey}
src={imageSrc}
alt={alt}
fill
className={`object-contain transition-opacity duration-300 ${lightboxLoading ? "opacity-0" : "opacity-100"}`}
onLoad={handleLightboxImageLoad}
onError={handleLightboxImageError}
sizes="100vw"
unoptimized={imageSrc.startsWith("data:") || imageSrc.startsWith("blob:")}
priority
/>
)}
</div>
</div>
</div>
)}
</>
)
}
interface ImagePreviewGridProps {
images: Array<{ src: string; alt: string }>
className?: string
}
export function ImagePreviewGrid({ images, className = "" }: ImagePreviewGridProps) {
if (images.length === 0) return null
if (images.length === 1) {
return <ImagePreview src={images[0].src} alt={images[0].alt} className={className} />
}
// Responsive grid layout
const getGridClasses = () => {
if (images.length === 2) {
return "grid-cols-1 sm:grid-cols-2"
} else if (images.length === 3) {
return "grid-cols-1 sm:grid-cols-2"
} else if (images.length === 4) {
return "grid-cols-1 sm:grid-cols-2"
} else {
return "grid-cols-1 sm:grid-cols-2 md:grid-cols-3"
}
}
return (
<div className={`grid gap-2 ${getGridClasses()} ${className}`}>
{images.map((image, index) => {
// Special layout for 3 images: first image spans 2 rows on larger screens
const rowSpanClass = index === 0 && images.length === 3
? "sm:row-span-2"
: ""
return (
<ImagePreview
key={index}
src={image.src}
alt={image.alt}
className={rowSpanClass}
/>
)
})}
</div>
)
}