import React from 'react'; export interface MessageAttachment { url?: string; display_url?: string; name?: string; kind?: 'image'; } function dataUrlToBlobUrl(dataUrl: string): string | null { const separatorIndex = dataUrl.indexOf(','); if (!dataUrl.startsWith('data:') || separatorIndex < 0) return null; const header = dataUrl.slice(0, separatorIndex); const payload = dataUrl.slice(separatorIndex + 1); const mime = /^data:([^;,]+)/.exec(header)?.[1] ?? 'application/octet-stream'; const bytes = header.includes(';base64') ? Uint8Array.from(atob(payload), char => char.charCodeAt(0)) : new TextEncoder().encode(decodeURIComponent(payload)); return URL.createObjectURL(new Blob([bytes], { type: mime })); } function openAttachmentUrl(event: React.MouseEvent, url: string) { if (!url.startsWith('data:')) return; event.preventDefault(); const blobUrl = dataUrlToBlobUrl(url); if (!blobUrl) return; window.open(blobUrl, '_blank', 'noopener,noreferrer'); window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000); } /** * Read-only attachments rendered inside a message (and reused for the model's * "rendered output" preview). Wire message data into this once messages carry * attachments. */ const MessageAttachments: React.FC<{ attachments?: MessageAttachment[] }> = ({ attachments }) => { if (!attachments || attachments.length === 0) return null; return (
{attachments.map((attachment, index) => { const imageUrl = attachment.display_url ?? attachment.url; const key = `${imageUrl ?? attachment.name ?? 'attachment'}-${index}`; return ( imageUrl ? ( openAttachmentUrl(event, imageUrl)} className="block overflow-hidden rounded-lg border border-app-border transition-colors hover:border-brand/40 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand" > {attachment.name ) : (
{attachment.name ?? 'Image attachment'}
) ); })}
); }; export default MessageAttachments;