"use client"; import type { ImageMessagePartComponent } from "@assistant-ui/react"; import { cva, type VariantProps } from "class-variance-authority"; import { ImageIcon, ImageOffIcon } from "lucide-react"; import NextImage from "next/image"; import { memo, type PropsWithChildren, useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { Button } from "@/components/ui/button"; 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< React.ComponentProps<"img">, "children" | "height" | "onError" | "onLoad" | "src" | "width" > & { containerClassName?: string; onError?: React.ReactEventHandler; onLoad?: React.ReactEventHandler; src?: string; }; function ImagePreview({ className, containerClassName, onLoad, onError, alt = "Image content", src, ...props }: ImagePreviewProps) { const [loadedSrc, setLoadedSrc] = useState(undefined); const [errorSrc, setErrorSrc] = useState(undefined); const imageSrc = src ?? ""; const loaded = imageSrc !== "" && loadedSrc === imageSrc; const error = imageSrc === "" || errorSrc === imageSrc; useEffect(() => { setLoadedSrc((current) => (current === imageSrc ? current : undefined)); setErrorSrc((current) => (current === imageSrc ? current : undefined)); }, [imageSrc]); return (
{!loaded && !error && (
)} {error ? (
) : ( { setLoadedSrc(imageSrc); onLoad?.(event); }} onError={(event) => { setErrorSrc(imageSrc); onError?.(event); }} unoptimized={isDataOrBlobUrl(imageSrc)} {...props} /> )}
); } function ImageFilename({ className, children, ...props }: React.ComponentProps<"span">) { if (!children) return null; return ( {children} ); } type ImageZoomProps = PropsWithChildren<{ src: string; alt?: string; }>; function isDataOrBlobUrl(src: string | undefined): boolean { if (!src || typeof src !== "string") return false; return src.startsWith("data:") || src.startsWith("blob:"); } 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 };