"use client"; import { memo, useState, useEffect, useRef, type PropsWithChildren, } from "react"; import { createPortal } from "react-dom"; import { cva, type VariantProps } from "class-variance-authority"; import { ImageIcon, ImageOffIcon } from "lucide-react"; import type { ImageMessagePartComponent } from "@assistant-ui/react"; import { cn } from "@/lib/utils"; const imageVariants = cva( "aui-image-root relative overflow-hidden rounded-lg", { variants: { variant: { outline: "border border-border", ghost: "", muted: "bg-muted/50", }, size: { sm: "max-w-64", default: "max-w-96", lg: "max-w-[512px]", full: "w-full", }, }, defaultVariants: { variant: "outline", size: "default", }, }, ); export type ImageRootProps = React.ComponentProps<"div"> & VariantProps; function ImageRoot({ className, variant, size, children, ...props }: ImageRootProps) { return (
{children}
); } type ImagePreviewProps = Omit, "children"> & { containerClassName?: string; }; function ImagePreview({ className, containerClassName, onLoad, onError, alt = "Image content", src, ...props }: ImagePreviewProps) { const imgRef = useRef(null); const [loadedSrc, setLoadedSrc] = useState(undefined); const [errorSrc, setErrorSrc] = useState(undefined); const loaded = loadedSrc === src; const error = errorSrc === src; useEffect(() => { if ( typeof src === "string" && imgRef.current?.complete && imgRef.current.naturalWidth > 0 ) { setLoadedSrc(src); } }, [src]); return (
{!loaded && !error && (
)} {error ? (
) : ( // biome-ignore lint/performance/noImgElement: intentional for dynamic external URLs {alt} { if (typeof src === "string") setLoadedSrc(src); onLoad?.(e); }} onError={(e) => { if (typeof src === "string") setErrorSrc(src); onError?.(e); }} {...props} /> )}
); } function ImageFilename({ className, children, ...props }: React.ComponentProps<"span">) { if (!children) return null; return ( {children} ); } type ImageZoomProps = PropsWithChildren<{ src: string; alt?: string; }>; function ImageZoom({ src, alt = "Image preview", children }: ImageZoomProps) { const [isMounted, setIsMounted] = useState(false); const [isOpen, setIsOpen] = useState(false); useEffect(() => { setIsMounted(true); }, []); const handleOpen = () => setIsOpen(true); const handleClose = () => setIsOpen(false); useEffect(() => { if (!isOpen) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") setIsOpen(false); }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [isOpen]); useEffect(() => { if (!isOpen) return; const originalOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = originalOverflow; }; }, [isOpen]); return ( <> {isMounted && isOpen && createPortal( , document.body, )} ); } const ImageImpl: ImageMessagePartComponent = ({ image, filename }) => { return ( {filename} ); }; const Image = memo(ImageImpl) as unknown as ImageMessagePartComponent & { Root: typeof ImageRoot; Preview: typeof ImagePreview; Filename: typeof ImageFilename; Zoom: typeof ImageZoom; }; Image.displayName = "Image"; Image.Root = ImageRoot; Image.Preview = ImagePreview; Image.Filename = ImageFilename; Image.Zoom = ImageZoom; export { Image, ImageRoot, ImagePreview, ImageFilename, ImageZoom, imageVariants, };