"use client"; import { ExternalLinkIcon, ImageIcon, Loader2 } from "lucide-react"; import NextImage from "next/image"; import { Component, type ReactNode, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Card } from "@/components/ui/card"; import { cn } from "@/lib/utils"; /** * Aspect ratio options for images */ type AspectRatio = "1:1" | "4:3" | "16:9" | "9:16" | "auto"; /** * Image fit options */ type ImageFit = "cover" | "contain"; /** * Source attribution */ interface ImageSource { label: string; iconUrl?: string; url?: string; } /** * Props for the Image component */ export interface ImageProps { id: string; assetId: string; src: string; alt: string; title?: string; description?: string; href?: string; domain?: string; ratio?: AspectRatio; fit?: ImageFit; source?: ImageSource; maxWidth?: string; className?: string; } /** * Serializable schema for Image props (for tool results) */ export interface SerializableImage { id: string; assetId: string; src: string; alt: string; title?: string; description?: string; href?: string; domain?: string; ratio?: AspectRatio; source?: ImageSource; } /** * Parse and validate serializable image from tool result */ export function parseSerializableImage(result: unknown): SerializableImage { if (typeof result !== "object" || result === null) { throw new Error("Invalid image result: expected object"); } const obj = result as Record; // Validate required fields if (typeof obj.id !== "string") { throw new Error("Invalid image: missing id"); } if (typeof obj.assetId !== "string") { throw new Error("Invalid image: missing assetId"); } if (typeof obj.src !== "string") { throw new Error("Invalid image: missing src"); } if (typeof obj.alt !== "string") { throw new Error("Invalid image: missing alt"); } return { id: obj.id, assetId: obj.assetId, src: obj.src, alt: obj.alt, title: typeof obj.title === "string" ? obj.title : undefined, description: typeof obj.description === "string" ? obj.description : undefined, href: typeof obj.href === "string" ? obj.href : undefined, domain: typeof obj.domain === "string" ? obj.domain : undefined, ratio: typeof obj.ratio === "string" ? (obj.ratio as AspectRatio) : undefined, source: typeof obj.source === "object" && obj.source !== null ? (obj.source as ImageSource) : undefined, }; } /** * Get aspect ratio class based on ratio prop */ function getAspectRatioClass(ratio?: AspectRatio): string { switch (ratio) { case "1:1": return "aspect-square"; case "4:3": return "aspect-[4/3]"; case "16:9": return "aspect-video"; case "9:16": return "aspect-[9/16]"; case "auto": default: return "aspect-[4/3]"; } } /** * Error boundary for Image component */ interface ImageErrorBoundaryState { hasError: boolean; error?: Error; } export class ImageErrorBoundary extends Component< { children: ReactNode }, ImageErrorBoundaryState > { constructor(props: { children: ReactNode }) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error: Error): ImageErrorBoundaryState { return { hasError: true, error }; } render() { if (this.state.hasError) { return (

Failed to load image

); } return this.props.children; } } /** * Loading skeleton for Image */ export function ImageSkeleton({ maxWidth = "420px" }: { maxWidth?: string }) { return (
); } /** * Image Loading State */ export function ImageLoading({ title = "Loading image..." }: { title?: string }) { return (

{title}

); } /** * Image Component * * Display images with metadata and attribution. * Features hover overlay with title and source attribution. */ export function Image({ id, src, alt, title, description, href, domain, ratio = "4:3", fit = "cover", source, maxWidth = "420px", className, }: ImageProps) { const [isHovered, setIsHovered] = useState(false); const [imageError, setImageError] = useState(false); const aspectRatioClass = getAspectRatioClass(ratio); const displayDomain = domain || source?.label; const handleClick = () => { const targetUrl = href || source?.url || src; if (targetUrl) { window.open(targetUrl, "_blank", "noopener,noreferrer"); } }; if (imageError) { return (

Image not available

); } return ( setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleClick(); } }} role="button" tabIndex={0} >
{/* Image */} setImageError(true)} /> {/* Hover overlay - appears on hover */}
{/* Content at bottom */}
{/* Title */} {title && (

{title}

)} {/* Description */} {description && (

{description}

)} {/* Source attribution */} {displayDomain && (
{source?.iconUrl ? ( ) : ( )} {displayDomain}
)}
{/* Always visible domain badge (bottom right, shown when NOT hovered) */} {displayDomain && !isHovered && (
{displayDomain}
)}
); }