File size: 6,888 Bytes
adb34c2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2671e04
 
8b5482f
adb34c2
2671e04
8b5482f
2671e04
adb34c2
 
8b5482f
 
 
 
 
adb34c2
 
 
 
 
 
 
 
 
2671e04
 
 
adb34c2
2671e04
 
 
adb34c2
2671e04
adb34c2
2671e04
 
 
 
 
 
adb34c2
2671e04
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
adb34c2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2671e04
 
 
 
 
 
 
 
 
 
 
 
 
 
 
adb34c2
2671e04
adb34c2
2671e04
 
adb34c2
 
 
2671e04
 
8b5482f
 
 
 
 
 
 
 
 
 
 
 
adb34c2
2671e04
adb34c2
2671e04
 
 
 
adb34c2
 
 
 
 
 
 
 
 
 
2671e04
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
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;
}