import { useConvexAccessToken } from "@code/auth/web"; import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentGroup, AttachmentMedia, AttachmentTitle, } from "@code/ui/components/attachment"; import { ImageIcon, X } from "lucide-react"; import { useEffect, useState } from "react"; import type { PendingChatImage } from "@/lib/chat/attachments"; import type { ConversationPart } from "@/lib/chat/types"; type FilePart = Extract; const isDirectlyRenderableUrl = (url: string): boolean => url.startsWith("blob:") || url.startsWith("data:"); const useAuthenticatedImageSource = (url?: string): string | undefined => { const resolveAccessToken = useConvexAccessToken(); const [loaded, setLoaded] = useState<{ readonly source: string; readonly url: string; }>(); useEffect(() => { if (!url || isDirectlyRenderableUrl(url)) { return; } let active = true; let objectUrl: string | undefined; const load = async (): Promise => { try { const accessToken = await resolveAccessToken(); const response = await fetch(url, { headers: accessToken ? { authorization: `Bearer ${accessToken}` } : undefined, }); if (!response.ok) { return; } objectUrl = URL.createObjectURL(await response.blob()); if (active) { setLoaded({ source: objectUrl, url }); } } catch { // The attachment remains represented by its filename and media type. } }; void load(); return () => { active = false; if (objectUrl) { URL.revokeObjectURL(objectUrl); } }; }, [resolveAccessToken, url]); if (url && isDirectlyRenderableUrl(url)) { return url; } return loaded && loaded.url === url ? loaded.source : undefined; }; const ImagePreview = ({ alt, mediaType, url, }: { readonly alt: string; readonly mediaType: string; readonly url?: string; }) => { const source = useAuthenticatedImageSource(url); return ( {source && mediaType.startsWith("image/") ? ( {alt} ) : ( )} ); }; export const PendingChatAttachments = ({ images, onRemove, }: { readonly images: readonly PendingChatImage[]; readonly onRemove: (id: string) => void; }) => ( {images.map((image) => ( {image.file.name} onRemove(image.id)} > ))} ); export const MessageAttachments = ({ parts, }: { readonly parts: readonly FilePart[]; }) => { if (parts.length === 0) { return null; } return ( {parts.map((part, index) => { const title = part.filename ?? `Image ${index + 1}`; return ( {title} {part.mediaType} ); })} ); };