Spaces:
Runtime error
Runtime error
| import { escapeHtml, renderRichText } from "/chatclient/richText.js"; | |
| export function renderAttachments(card, container, summary, attachments, onRemove, onPreview) { | |
| summary.textContent = buildAttachmentSummary(attachments); | |
| container.innerHTML = ""; | |
| if (attachments.length === 0) { | |
| container.innerHTML = '<p class="empty-state">Add an image or audio file or link to include it with the next request.</p>'; | |
| return; | |
| } | |
| for (const attachment of attachments) { | |
| const card = document.createElement("article"); | |
| card.className = "attachment-item"; | |
| card.appendChild(renderAttachmentPreview(attachment, onPreview)); | |
| card.appendChild(renderAttachmentMeta(attachment)); | |
| card.appendChild(renderRemoveButton(attachment.id, onRemove)); | |
| container.appendChild(card); | |
| } | |
| } | |
| export function renderResponse(container, requestPayload, responseBody) { | |
| const assistant = responseBody?.choices?.[0]?.message ?? {}; | |
| const text = extractAssistantText(assistant); | |
| const audioTranscript = extractAssistantAudioTranscript(assistant); | |
| const images = extractAssistantImages(assistant); | |
| const audioUrl = assistant?.audio?.url || null; | |
| const assistantBody = text || audioTranscript || "No assistant text returned."; | |
| container.innerHTML = ""; | |
| container.appendChild(renderInfoCard("Request", describeRequest(requestPayload))); | |
| container.appendChild(renderRichTextCard("Assistant", assistantBody)); | |
| if (audioTranscript && !sameText(text, audioTranscript)) { | |
| container.appendChild(renderRichTextCard("Audio Transcript", audioTranscript)); | |
| } | |
| for (const imageUrl of images) { | |
| const imageCard = renderInfoCard("Image", imageUrl); | |
| const image = document.createElement("img"); | |
| image.src = imageUrl; | |
| image.alt = "assistant output"; | |
| image.className = "response-image"; | |
| imageCard.appendChild(image); | |
| container.appendChild(imageCard); | |
| } | |
| if (audioUrl) { | |
| const audioCard = renderInfoCard("Audio", audioUrl); | |
| const audio = document.createElement("audio"); | |
| audio.controls = true; | |
| audio.src = audioUrl; | |
| audio.className = "response-audio"; | |
| audioCard.appendChild(audio); | |
| container.appendChild(audioCard); | |
| } | |
| } | |
| export function setStatus(statusLine, message, isOk = false) { | |
| statusLine.textContent = message; | |
| statusLine.classList.toggle("status-ok", isOk); | |
| statusLine.classList.toggle("status-busy", !isOk && /sending/i.test(message)); | |
| } | |
| export function showError(errorToast, message) { | |
| const code = `ERR-${Date.now().toString(36).toUpperCase()}`; | |
| errorToast.hidden = false; | |
| errorToast.textContent = `${message} Click to copy ${code}.`; | |
| errorToast.onclick = async () => { | |
| try { | |
| await navigator.clipboard.writeText(code); | |
| errorToast.textContent = `Copied ${code}.`; | |
| } catch (_error) { | |
| errorToast.textContent = `Copy failed. Error code: ${code}`; | |
| } | |
| }; | |
| window.clearTimeout(showError.timer); | |
| showError.timer = window.setTimeout(() => { | |
| errorToast.hidden = true; | |
| }, 10000); | |
| } | |
| showError.timer = 0; | |
| function buildAttachmentSummary(attachments) { | |
| if (attachments.length === 0) { | |
| return "No files added."; | |
| } | |
| const imageCount = attachments.filter((item) => item.kind === "image").length; | |
| const audioCount = attachments.length - imageCount; | |
| return `${attachments.length} file${attachments.length === 1 ? "" : "s"} ready | ${imageCount} image | ${audioCount} audio`; | |
| } | |
| function renderAttachmentPreview(attachment, onPreview) { | |
| const button = document.createElement("button"); | |
| button.type = "button"; | |
| button.className = "attachment-preview-trigger"; | |
| button.setAttribute("aria-label", `Open ${attachment.name}`); | |
| button.addEventListener("click", () => onPreview(attachment)); | |
| if (attachment.kind === "image") { | |
| const image = document.createElement("img"); | |
| image.src = attachment.previewUrl; | |
| image.alt = attachment.name; | |
| image.className = "attachment-thumb"; | |
| button.appendChild(image); | |
| return button; | |
| } | |
| button.innerHTML = ` | |
| <svg viewBox="0 0 24 24" aria-hidden="true"> | |
| <path d="M12 18V6"></path> | |
| <path d="M8 15a4 4 0 1 0 0-6"></path> | |
| <path d="M16 15a4 4 0 1 0 0-6"></path> | |
| </svg> | |
| <span>Open</span> | |
| `; | |
| button.classList.add("attachment-audio-tile"); | |
| return button; | |
| } | |
| function renderAttachmentMeta(attachment) { | |
| const meta = document.createElement("div"); | |
| meta.className = "attachment-meta"; | |
| meta.innerHTML = ` | |
| <strong>${escapeHtml(attachment.name)}</strong> | |
| <span>${escapeHtml(attachment.kind)} | ${escapeHtml(attachment.sourceType)} | ${escapeHtml(attachment.sizeLabel)}</span> | |
| `; | |
| return meta; | |
| } | |
| function renderRemoveButton(attachmentId, onRemove) { | |
| const button = document.createElement("button"); | |
| button.type = "button"; | |
| button.className = "attachment-remove"; | |
| button.textContent = "Remove"; | |
| button.addEventListener("click", () => onRemove(attachmentId)); | |
| return button; | |
| } | |
| function describeRequest(payload) { | |
| const userContent = payload.messages.at(-1)?.content ?? []; | |
| const textPart = userContent.find((part) => part.type === "text"); | |
| const attachmentCount = userContent.filter((part) => part.type !== "text").length; | |
| const audioOutput = payload.audio ? `Audio output: ${payload.audio.voice}` : "Audio output: off"; | |
| return `${textPart?.text || "Attachment-only request."}\n\nAttachments: ${attachmentCount}\n${audioOutput}`; | |
| } | |
| function extractAssistantText(message) { | |
| if (typeof message.content === "string") { | |
| return message.content; | |
| } | |
| if (!Array.isArray(message.content)) { | |
| return ""; | |
| } | |
| return message.content | |
| .map((part) => part.text || part.output_text || "") | |
| .filter(Boolean) | |
| .join("\n\n"); | |
| } | |
| function extractAssistantImages(message) { | |
| if (!Array.isArray(message.content)) { | |
| return []; | |
| } | |
| return message.content | |
| .map((part) => part?.image_url?.proxy_url || part?.image_url?.url) | |
| .filter(Boolean); | |
| } | |
| function extractAssistantAudioTranscript(message) { | |
| return typeof message?.audio?.transcript === "string" ? message.audio.transcript.trim() : ""; | |
| } | |
| function sameText(left, right) { | |
| return normalizeText(left) === normalizeText(right); | |
| } | |
| function normalizeText(value) { | |
| return typeof value === "string" ? value.trim().replaceAll(/\s+/g, " ") : ""; | |
| } | |
| function renderInfoCard(title, body) { | |
| const card = document.createElement("article"); | |
| card.className = "response-block"; | |
| card.innerHTML = `<strong>${escapeHtml(title)}</strong><p>${escapeHtml(body)}</p>`; | |
| return card; | |
| } | |
| function renderRichTextCard(title, body) { | |
| const card = document.createElement("article"); | |
| card.className = "response-block"; | |
| card.innerHTML = `<strong>${escapeHtml(title)}</strong>`; | |
| const bodyElement = document.createElement("div"); | |
| bodyElement.className = "rich-response"; | |
| renderRichText(bodyElement, body); | |
| card.appendChild(bodyElement); | |
| return card; | |
| } | |