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 && (
)}
>
);
}
// ---------------------------------------------------------------------------
// Audio
// ---------------------------------------------------------------------------
function AudioPreview({ attachment }: { attachment: Attachment }) {
const src = getSourceUrl(attachment.source);
return (
{attachment.filename && (
{attachment.filename}
)}
);
}
// ---------------------------------------------------------------------------
// 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 ? (

) : (
)}
{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 (
);
}
if (isText(mimeType)) {
// Decode base64 text content for display
const textContent =
attachment.source.type === "data"
? (() => {
try {
return atob(attachment.source.value);
} catch {
return attachment.source.value;
}
})()
: null;
return (
{attachment.filename && (
{attachment.filename}
)}
{textContent ? (
{textContent}
) : blobUrl ? (
) : null}
);
}
// Fallback: info card for non-previewable documents
return (
{getDocumentIcon(mimeType ?? "")}
{attachment.filename || "Document"}
{mimeType || "Unknown type"}
{attachment.size != null && ` ยท ${formatFileSize(attachment.size)}`}
No preview available for this file type
);
}
function DocumentPreview({ attachment }: { attachment: Attachment }) {
const { thumbnailRef, vtName, open, openLightbox, closeLightbox } =
useLightbox();
const mimeType = attachment.source.mimeType;
const previewable = canPreviewInBrowser(mimeType);
return (
<>
}
className={cn(
"cpk:flex cpk:items-center cpk:gap-2",
previewable && "cpk:cursor-pointer",
)}
onClick={previewable ? openLightbox : undefined}
>
{getDocumentIcon(mimeType ?? "")}
{attachment.filename || "Document"}
{attachment.size != null && (
{formatFileSize(attachment.size)}
)}
{open && (
)}
>
);
}