Spaces:
Runtime error
Runtime error
| const IMAGE_PREFIX = "image/"; | |
| const AUDIO_PREFIX = "audio/"; | |
| export async function createAttachments(fileList) { | |
| return Promise.all(Array.from(fileList, (file) => createFileAttachment(file))); | |
| } | |
| export function createLinkAttachment(kind, url) { | |
| const trimmedUrl = String(url || "").trim(); | |
| if (!trimmedUrl) { | |
| throw new Error("Enter an image or audio link before adding it."); | |
| } | |
| if (!/^https?:\/\//i.test(trimmedUrl)) { | |
| throw new Error("Attachment links must start with http:// or https://."); | |
| } | |
| return { | |
| id: crypto.randomUUID(), | |
| kind, | |
| name: inferNameFromUrl(trimmedUrl), | |
| sizeLabel: "link", | |
| previewUrl: trimmedUrl, | |
| sourceType: "link", | |
| url: trimmedUrl | |
| }; | |
| } | |
| export async function buildAttachmentParts(attachments) { | |
| return Promise.all(attachments.map((attachment) => buildAttachmentPart(attachment))); | |
| } | |
| export function releaseAttachment(attachment) { | |
| if (attachment?.sourceType === "file" && attachment.previewUrl?.startsWith("blob:")) { | |
| URL.revokeObjectURL(attachment.previewUrl); | |
| } | |
| } | |
| export function hydrateAttachment(savedAttachment) { | |
| if (savedAttachment?.sourceType === "link") { | |
| return savedAttachment; | |
| } | |
| if (savedAttachment?.sourceType === "file" && typeof savedAttachment.dataUrl === "string") { | |
| return { | |
| id: savedAttachment.id, | |
| kind: savedAttachment.kind, | |
| name: savedAttachment.name, | |
| sizeLabel: savedAttachment.sizeLabel, | |
| previewUrl: savedAttachment.dataUrl, | |
| sourceType: "file", | |
| dataUrl: savedAttachment.dataUrl, | |
| mimeType: savedAttachment.mimeType | |
| }; | |
| } | |
| return null; | |
| } | |
| async function createFileAttachment(file) { | |
| const kind = classifyFile(file); | |
| const dataUrl = await readFileAsDataUrl(file); | |
| return { | |
| id: crypto.randomUUID(), | |
| kind, | |
| name: file.name, | |
| sizeLabel: formatBytes(file.size), | |
| previewUrl: dataUrl, | |
| sourceType: "file", | |
| dataUrl, | |
| mimeType: file.type | |
| }; | |
| } | |
| async function buildAttachmentPart(attachment) { | |
| if (attachment.sourceType === "link") { | |
| return attachment.kind === "image" | |
| ? { type: "image_url", image_url: { url: attachment.url } } | |
| : { type: "input_audio", input_audio: { url: attachment.url } }; | |
| } | |
| if (attachment.kind === "image") { | |
| return { | |
| type: "image_url", | |
| image_url: { | |
| url: attachment.dataUrl ?? await readFileAsDataUrl(attachment.file) | |
| } | |
| }; | |
| } | |
| return { | |
| type: "input_audio", | |
| input_audio: { | |
| data: extractBase64(attachment.dataUrl ?? await readFileAsDataUrl(attachment.file)), | |
| format: inferAudioFormat(attachment.file ?? attachment) | |
| } | |
| }; | |
| } | |
| function classifyFile(file) { | |
| if (file.type.startsWith(IMAGE_PREFIX)) { | |
| return "image"; | |
| } | |
| if (file.type.startsWith(AUDIO_PREFIX)) { | |
| return "audio"; | |
| } | |
| throw new Error(`Unsupported file type: ${file.type || file.name}`); | |
| } | |
| function inferAudioFormat(file) { | |
| const mimeType = file?.type ?? file?.mimeType ?? ""; | |
| const name = (file?.name ?? "").toLowerCase(); | |
| if (mimeType === "audio/mp4" || mimeType === "audio/x-m4a" || name.endsWith(".m4a")) { | |
| return "m4a"; | |
| } | |
| if (mimeType === "audio/wav" || mimeType === "audio/x-wav" || name.endsWith(".wav")) { | |
| return "wav"; | |
| } | |
| return "mp3"; | |
| } | |
| function readFileAsDataUrl(file) { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onload = () => resolve(String(reader.result)); | |
| reader.onerror = () => reject(new Error("Failed to read the selected file.")); | |
| reader.readAsDataURL(file); | |
| }); | |
| } | |
| async function readFileAsBase64(file) { | |
| const dataUrl = await readFileAsDataUrl(file); | |
| return extractBase64(dataUrl); | |
| } | |
| function extractBase64(dataUrl) { | |
| return dataUrl.split(",")[1] || ""; | |
| } | |
| function formatBytes(value) { | |
| if (value < 1024) { | |
| return `${value} B`; | |
| } | |
| if (value < 1024 * 1024) { | |
| return `${(value / 1024).toFixed(1)} KB`; | |
| } | |
| return `${(value / (1024 * 1024)).toFixed(1)} MB`; | |
| } | |
| function inferNameFromUrl(url) { | |
| try { | |
| const { pathname, hostname } = new URL(url); | |
| const lastSegment = pathname.split("/").filter(Boolean).at(-1); | |
| return lastSegment || hostname; | |
| } catch (_error) { | |
| return "remote-file"; | |
| } | |
| } | |