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 = '
Add an image or audio file or link to include it with the next request.
'; 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 = ` Open `; button.classList.add("attachment-audio-tile"); return button; } function renderAttachmentMeta(attachment) { const meta = document.createElement("div"); meta.className = "attachment-meta"; meta.innerHTML = ` ${escapeHtml(attachment.name)} ${escapeHtml(attachment.kind)} | ${escapeHtml(attachment.sourceType)} | ${escapeHtml(attachment.sizeLabel)} `; 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 = `${escapeHtml(title)}${escapeHtml(body)}
`; return card; } function renderRichTextCard(title, body) { const card = document.createElement("article"); card.className = "response-block"; card.innerHTML = `${escapeHtml(title)}`; const bodyElement = document.createElement("div"); bodyElement.className = "rich-response"; renderRichText(bodyElement, body); card.appendChild(bodyElement); return card; }