"use client"; import { Button } from "@/components/ui/button"; import { HoverCard, HoverCardContent, HoverCardTrigger, } from "@/components/ui/hover-card"; import { cn } from "@/lib/utils"; import type { FileUIPart, SourceDocumentUIPart } from "ai"; import { FileTextIcon, GlobeIcon, ImageIcon, Music2Icon, PaperclipIcon, VideoIcon, XIcon, } from "lucide-react"; import type { ComponentProps, HTMLAttributes, ReactNode } from "react"; import { createContext, useCallback, useContext, useMemo } from "react"; // ============================================================================ // Types // ============================================================================ export type AttachmentData = | (FileUIPart & { id: string }) | (SourceDocumentUIPart & { id: string }); export type AttachmentMediaCategory = | "image" | "video" | "audio" | "document" | "source" | "unknown"; export type AttachmentVariant = "grid" | "inline" | "list"; const mediaCategoryIcons: Record = { audio: Music2Icon, document: FileTextIcon, image: ImageIcon, source: GlobeIcon, unknown: PaperclipIcon, video: VideoIcon, }; // ============================================================================ // Utility Functions // ============================================================================ export const getMediaCategory = ( data: AttachmentData ): AttachmentMediaCategory => { if (data.type === "source-document") { return "source"; } const mediaType = data.mediaType ?? ""; if (mediaType.startsWith("image/")) { return "image"; } if (mediaType.startsWith("video/")) { return "video"; } if (mediaType.startsWith("audio/")) { return "audio"; } if (mediaType.startsWith("application/") || mediaType.startsWith("text/")) { return "document"; } return "unknown"; }; export const getAttachmentLabel = (data: AttachmentData): string => { if (data.type === "source-document") { return data.title || data.filename || "Source"; } const category = getMediaCategory(data); return data.filename || (category === "image" ? "Image" : "Attachment"); }; const renderAttachmentImage = ( url: string, filename: string | undefined, isGrid: boolean ) => isGrid ? ( {filename ) : ( {filename ); // ============================================================================ // Contexts // ============================================================================ interface AttachmentsContextValue { variant: AttachmentVariant; } const AttachmentsContext = createContext(null); interface AttachmentContextValue { data: AttachmentData; mediaCategory: AttachmentMediaCategory; onRemove?: () => void; variant: AttachmentVariant; } const AttachmentContext = createContext(null); // ============================================================================ // Hooks // ============================================================================ export const useAttachmentsContext = () => useContext(AttachmentsContext) ?? { variant: "grid" as const }; export const useAttachmentContext = () => { const ctx = useContext(AttachmentContext); if (!ctx) { throw new Error("Attachment components must be used within "); } return ctx; }; // ============================================================================ // Attachments - Container // ============================================================================ export type AttachmentsProps = HTMLAttributes & { variant?: AttachmentVariant; }; export const Attachments = ({ variant = "grid", className, children, ...props }: AttachmentsProps) => { const contextValue = useMemo(() => ({ variant }), [variant]); return (
{children}
); }; // ============================================================================ // Attachment - Item // ============================================================================ export type AttachmentProps = HTMLAttributes & { data: AttachmentData; onRemove?: () => void; }; export const Attachment = ({ data, onRemove, className, children, ...props }: AttachmentProps) => { const { variant } = useAttachmentsContext(); const mediaCategory = getMediaCategory(data); const contextValue = useMemo( () => ({ data, mediaCategory, onRemove, variant }), [data, mediaCategory, onRemove, variant] ); return (
{children}
); }; // ============================================================================ // AttachmentPreview - Media preview // ============================================================================ export type AttachmentPreviewProps = HTMLAttributes & { fallbackIcon?: ReactNode; }; export const AttachmentPreview = ({ fallbackIcon, className, ...props }: AttachmentPreviewProps) => { const { data, mediaCategory, variant } = useAttachmentContext(); const iconSize = variant === "inline" ? "size-3" : "size-4"; const renderIcon = (Icon: typeof ImageIcon) => ( ); const renderContent = () => { if (mediaCategory === "image" && data.type === "file" && data.url) { return renderAttachmentImage(data.url, data.filename, variant === "grid"); } if (mediaCategory === "video" && data.type === "file" && data.url) { return