/** * `File → ChatAttachment` — the engine-side producer that was missing. * * Before this, every host re-implemented the conversion (see the fake * stub in `apps/storybook/.../SpeechAndAttachments`). It lives here so * the picker, drag-drop and paste paths all mint identical attachments. */ import { createId } from '../core/ids'; import { getAssetTypeFromMime } from '../../forms/Uploader'; import type { ChatAttachment } from '../types'; /** Map a MIME type to the `ChatAttachment.type` discriminant. */ function attachmentTypeFromMime(mime: string): ChatAttachment['type'] { if (mime.startsWith('image/')) return 'image'; if (mime.startsWith('audio/')) return 'audio'; if (mime.startsWith('video/')) return 'video'; // getAssetTypeFromMime returns 'document' for everything else; the // ChatAttachment union has no 'document' member — collapse to 'file'. const asset = getAssetTypeFromMime(mime); return asset === 'document' ? 'file' : asset; } /** * Convert a picked/dropped/pasted `File` into a `ChatAttachment`. * * The `url` is an object-URL (`URL.createObjectURL`) — instantly usable * for previews. When a host uploads the file via `uploadFn`, it replaces * `url` with the remote URL and flips `status` to `'ready'`. * * `status` defaults to `'ready'`: with no `uploadFn` the attachment is * immediately usable (current behaviour). The attach pipeline overrides * it to `'uploading'` when an upload is in flight. */ export function fileToAttachment(file: File): ChatAttachment { const mime = file.type || 'application/octet-stream'; return { id: createId('att'), type: attachmentTypeFromMime(mime), url: URL.createObjectURL(file), name: file.name, mimeType: mime, sizeBytes: file.size, status: 'ready', }; } /** Revoke an attachment's object-URL, if it holds one. Call on remove. */ export function revokeAttachmentUrl(attachment: Pick): void { if (attachment.url.startsWith('blob:')) { URL.revokeObjectURL(attachment.url); } }