import React, { useEffect, useState } from "react"; import type { Attachment } from "@copilotkit/shared"; import { formatFileSize, getSourceUrl, getDocumentIcon, } from "@copilotkit/shared"; import { Play } from "lucide-react"; import { cn } from "../../lib/utils"; import { Lightbox, useLightbox } from "./Lightbox"; interface CopilotChatAttachmentQueueProps { attachments: Attachment[]; onRemoveAttachment: (id: string) => void; className?: string; } export const CopilotChatAttachmentQueue: React.FC< CopilotChatAttachmentQueueProps > = ({ attachments, onRemoveAttachment, className }) => { if (attachments.length === 0) return null; return (
{attachments.map((attachment) => { const isMedia = attachment.type === "image" || attachment.type === "video"; return (
{attachment.status === "uploading" && }
); })}
); }; // --------------------------------------------------------------------------- // Shared // --------------------------------------------------------------------------- function UploadingOverlay() { return (
); } function AttachmentPreview({ attachment }: { attachment: Attachment }) { if (attachment.status === "uploading") { return
; } switch (attachment.type) { case "image": return ; case "audio": return ; case "video": return ; case "document": return ; } } // --------------------------------------------------------------------------- // Image // --------------------------------------------------------------------------- function ImagePreview({ attachment }: { attachment: Attachment }) { const src = getSourceUrl(attachment.source); const { thumbnailRef, vtName, open, openLightbox, closeLightbox } = useLightbox(); return ( <> } src={src} alt={attachment.filename || "Image attachment"} className="cpk:w-full cpk:h-full cpk:object-cover cpk:cursor-pointer" onClick={openLightbox} /> {open && ( {attachment.filename )} ); } // --------------------------------------------------------------------------- // Audio // --------------------------------------------------------------------------- function AudioPreview({ attachment }: { attachment: Attachment }) { const src = getSourceUrl(attachment.source); return (
); } // --------------------------------------------------------------------------- // Video โ€“ thumbnail with play button; click opens lightbox with full controls // --------------------------------------------------------------------------- function VideoPreview({ attachment }: { attachment: Attachment }) { const src = getSourceUrl(attachment.source); const { thumbnailRef, vtName, open, openLightbox, closeLightbox } = useLightbox(); return ( <>
} className="cpk:w-full cpk:h-full" > {attachment.thumbnail ? ( {attachment.filename ) : (
{open && ( )} ); } // --------------------------------------------------------------------------- // Document โ€“ click opens lightbox with PDF/text preview or info card // --------------------------------------------------------------------------- function isPdf(mimeType: string | undefined): boolean { return !!mimeType && mimeType.includes("pdf"); } function isText(mimeType: string | undefined): boolean { return !!mimeType && mimeType.startsWith("text/"); } function canPreviewInBrowser(mimeType: string | undefined): boolean { return isPdf(mimeType) || isText(mimeType); } /** * Convert a base64-encoded data source to a blob: URL that browsers will * render inside an iframe (data: URLs are blocked for PDFs in most browsers). */ function useBlobUrl(attachment: Attachment): string | null { const [url, setUrl] = useState(null); useEffect(() => { if (attachment.source.type !== "data") return; try { const binary = atob(attachment.source.value); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } const blob = new Blob([bytes], { type: attachment.source.mimeType || "application/octet-stream", }); const blobUrl = URL.createObjectURL(blob); setUrl(blobUrl); return () => URL.revokeObjectURL(blobUrl); } catch (error) { console.error("[CopilotKit] Failed to decode attachment data:", error); setUrl(null); } }, [ attachment.source.type, attachment.source.value, attachment.source.mimeType, ]); if (attachment.source.type === "url") return attachment.source.value; return url; } function DocumentLightboxContent({ attachment, vtName, }: { attachment: Attachment; vtName: string; }) { const mimeType = attachment.source.mimeType; const blobUrl = useBlobUrl(attachment); if (isPdf(mimeType)) { if (!blobUrl) return null; return (