import { useMemo, useState, type DragEvent, type ReactNode } from "react"; import { useQuery } from "@tanstack/react-query"; import type { IssueAttachment } from "@paperclipai/shared"; import { Download, ExternalLink, FileText, Paperclip, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { FoldCurtain } from "./FoldCurtain"; import { MarkdownBody } from "./MarkdownBody"; import { OutputFileTile } from "./issue-output/OutputFileTile"; import { OutputVideoPlayer } from "./issue-output/OutputVideoPlayer"; import { formatBytes } from "@/lib/issue-output"; import { attachmentDownloadPath, attachmentFilename, attachmentOpenPath, isImageAttachment, isMarkdownAttachment, isVideoAttachment, } from "@/lib/issue-attachments"; import { queryKeys } from "@/lib/queryKeys"; import { cn } from "@/lib/utils"; interface IssueAttachmentsSectionProps { attachments: IssueAttachment[]; uploadButton?: ReactNode; error?: string | null; dragActive?: boolean; deletePending?: boolean; onDelete: (attachmentId: string) => void; onImageClick: (attachment: IssueAttachment) => void; onDragEnter?: (evt: DragEvent) => void; onDragOver?: (evt: DragEvent) => void; onDragLeave?: (evt: DragEvent) => void; onDrop?: (evt: DragEvent) => void; } async function fetchAttachmentText(attachment: IssueAttachment) { const response = await fetch(attachment.contentPath, { headers: { Accept: "text/markdown,text/plain;q=0.9,*/*;q=0.1" }, }); if (!response.ok) { throw new Error(`Unable to load attachment preview (${response.status})`); } return response.text(); } function AttachmentActions({ attachment, onDelete, deletePending, }: { attachment: IssueAttachment; onDelete: (attachmentId: string) => void; deletePending?: boolean; }) { const filename = attachmentFilename(attachment); return (
); } function AttachmentMeta({ attachment }: { attachment: IssueAttachment }) { return (

Attachment · {attachment.contentType} · {formatBytes(attachment.byteSize)}

); } function MarkdownAttachmentCard({ attachment, onDelete, deletePending, }: { attachment: IssueAttachment; onDelete: (attachmentId: string) => void; deletePending?: boolean; }) { const filename = attachmentFilename(attachment); const { data, isLoading, error } = useQuery({ queryKey: queryKeys.issues.attachmentPreview(attachment.id), queryFn: () => fetchAttachmentText(attachment), }); return (
{filename}
{isLoading ? (

Loading preview...

) : error ? (

Could not load markdown preview.

) : ( {data ?? ""} )}
); } function VideoAttachmentCard({ attachment, onDelete, deletePending, }: { attachment: IssueAttachment; onDelete: (attachmentId: string) => void; deletePending?: boolean; }) { const filename = attachmentFilename(attachment); return (

{filename}

); } function GenericAttachmentRow({ attachment, onDelete, deletePending, }: { attachment: IssueAttachment; onDelete: (attachmentId: string) => void; deletePending?: boolean; }) { const filename = attachmentFilename(attachment); return (
{filename}

Attachment · {attachment.contentType} · {formatBytes(attachment.byteSize)}

); } export function IssueAttachmentsSection({ attachments, uploadButton, error, dragActive = false, deletePending = false, onDelete, onImageClick, onDragEnter, onDragOver, onDragLeave, onDrop, }: IssueAttachmentsSectionProps) { const [confirmDeleteId, setConfirmDeleteId] = useState(null); const { imageAttachments, markdownAttachments, videoAttachments, genericAttachments } = useMemo(() => { const images: IssueAttachment[] = []; const markdown: IssueAttachment[] = []; const videos: IssueAttachment[] = []; const generic: IssueAttachment[] = []; for (const attachment of attachments) { if (isImageAttachment(attachment)) images.push(attachment); else if (isMarkdownAttachment(attachment)) markdown.push(attachment); else if (isVideoAttachment(attachment)) videos.push(attachment); else generic.push(attachment); } return { imageAttachments: images, markdownAttachments: markdown, videoAttachments: videos, genericAttachments: generic, }; }, [attachments]); const requestDelete = (attachmentId: string) => setConfirmDeleteId(attachmentId); const confirmDelete = (attachmentId: string) => { onDelete(attachmentId); setConfirmDeleteId(null); }; return (
{uploadButton}
{error && (

{error}

)} {imageAttachments.length > 0 && (
{imageAttachments.map((attachment) => (
onImageClick(attachment)} > {attachment.originalFilename
{confirmDeleteId === attachment.id ? (
event.stopPropagation()} >

Delete?

) : ( )}
))}
)} {markdownAttachments.length > 0 && (
{markdownAttachments.map((attachment) => ( ))}
)} {videoAttachments.length > 0 && (
{videoAttachments.map((attachment) => ( ))}
)} {genericAttachments.length > 0 && (
{genericAttachments.map((attachment) => ( ))}
)} {confirmDeleteId && !imageAttachments.some((attachment) => attachment.id === confirmDeleteId) ? (

Delete this attachment? This cannot be undone.

) : null}
); }