blyon1995 Copilot commited on
Commit
c43b369
·
1 Parent(s): ca51841

fix: silent failure when sending image after video

Browse files

- formatMessagesForApi: strip video_url dataUrls from historical
messages (all but the latest user message), replacing them with
'[Video attachment]' text. Prevents oversized API payloads and
invalid content types reaching the backend.

- app.js: build apiMessages from preConv.messages + userMessage
directly (captured before the store write) instead of re-reading
from localStorage. The current message is always included even if
the localStorage save failed due to quota limits.

- app.js: wrap store.addMessage + appendUserMessage in try/catch so
any remaining failure is shown as a visible error instead of a
silent no-op (input cleared, no output).

- store.js: catch QuotaExceededError in save() to prevent an
unhandled exception when large video dataUrls fill localStorage.

- chat.js: fix video.map(() => videos[0]) bug — use the loop
variable (vid) instead of always referencing the first element.

All 103 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Files changed (4) hide show
  1. src/api.js +21 -2
  2. src/components/app.js +18 -7
  3. src/components/chat.js +2 -2
  4. src/store.js +9 -1
src/api.js CHANGED
@@ -209,8 +209,27 @@ export async function* streamCompletion(baseUrl, apiKey, model, messages, option
209
  }
210
 
211
  export function formatMessagesForApi(messages) {
212
- return messages.map(msg => {
 
 
 
 
 
 
 
213
  if (msg.role === 'assistant') return { role: 'assistant', content: msg.content };
214
- return { role: 'user', content: msg.content };
 
 
 
 
 
 
 
 
 
 
 
 
215
  });
216
  }
 
209
  }
210
 
211
  export function formatMessagesForApi(messages) {
212
+ // Find the index of the last user message so we can preserve its media dataUrls.
213
+ // Historical video messages are stripped (huge dataUrls, already processed by the model).
214
+ let lastUserIdx = -1;
215
+ for (let i = messages.length - 1; i >= 0; i--) {
216
+ if (messages[i].role === 'user') { lastUserIdx = i; break; }
217
+ }
218
+
219
+ return messages.map((msg, i) => {
220
  if (msg.role === 'assistant') return { role: 'assistant', content: msg.content };
221
+
222
+ // For the most recent user message keep content verbatim (includes any media dataUrl).
223
+ if (i === lastUserIdx || !Array.isArray(msg.content)) {
224
+ return { role: 'user', content: msg.content };
225
+ }
226
+
227
+ // For historical user messages: replace video_url parts with a text note so the
228
+ // API payload stays small and uses only standard content types.
229
+ const content = msg.content.map(part => {
230
+ if (part?.type === 'video_url') return { type: 'text', text: '[Video attachment]' };
231
+ return part;
232
+ });
233
+ return { role: 'user', content };
234
  });
235
  }
src/components/app.js CHANGED
@@ -255,10 +255,19 @@ export class App {
255
  );
256
  }
257
 
258
- // Add to store & render
259
- store.addMessage(convId, userMessage);
260
- this.chat.appendUserMessage(userMessage);
261
- this._updateContextInfo();
 
 
 
 
 
 
 
 
 
262
 
263
  // Update title if first message
264
  const conv = store.getCurrentConversation();
@@ -272,9 +281,11 @@ export class App {
272
  this.inputBar.setSending(true);
273
  this.chat.showTypingIndicator();
274
 
275
- // Get conversation history for API
276
- const currentConv = store.getCurrentConversation();
277
- const apiMessages = formatMessagesForApi(currentConv.messages);
 
 
278
 
279
  try {
280
  this.chat.startAssistantMessage();
 
255
  );
256
  }
257
 
258
+ // Add to store & render — wrapped so any localStorage/DOM error is caught
259
+ let renderOk = false;
260
+ try {
261
+ store.addMessage(convId, userMessage);
262
+ this.chat.appendUserMessage(userMessage);
263
+ this._updateContextInfo();
264
+ renderOk = true;
265
+ } catch (err) {
266
+ this.chat.showError(`Error: ${err.message}`);
267
+ this.inputBar.setSending(false);
268
+ this.inputBar.focus();
269
+ return;
270
+ }
271
 
272
  // Update title if first message
273
  const conv = store.getCurrentConversation();
 
281
  this.inputBar.setSending(true);
282
  this.chat.showTypingIndicator();
283
 
284
+ // Get conversation history for API.
285
+ // Use preConv (read before the store write) + userMessage directly so the
286
+ // current message is always included even if the localStorage save failed,
287
+ // and formatMessagesForApi can correctly identify it as the latest message.
288
+ const apiMessages = formatMessagesForApi([...(preConv?.messages || []), userMessage]);
289
 
290
  try {
291
  this.chat.startAssistantMessage();
src/components/chat.js CHANGED
@@ -155,8 +155,8 @@ export class Chat {
155
  <img src="${img.image_url?.url || ''}" alt="Attached image" class="max-w-xs max-h-48 rounded-xl border border-[var(--c-bd)] object-cover mb-2" />
156
  `).join('');
157
 
158
- const videoHtml = videos.map(() => `
159
- <video src="${escapeHtml(videos[0]?.video_url?.url || '')}" controls muted playsinline
160
  class="max-w-xs max-h-48 rounded-xl border border-[var(--c-bd)] bg-black mb-2"></video>
161
  `).join('');
162
 
 
155
  <img src="${img.image_url?.url || ''}" alt="Attached image" class="max-w-xs max-h-48 rounded-xl border border-[var(--c-bd)] object-cover mb-2" />
156
  `).join('');
157
 
158
+ const videoHtml = videos.map(vid => `
159
+ <video src="${escapeHtml(vid.video_url?.url || '')}" controls muted playsinline
160
  class="max-w-xs max-h-48 rounded-xl border border-[var(--c-bd)] bg-black mb-2"></video>
161
  `).join('');
162
 
src/store.js CHANGED
@@ -60,7 +60,15 @@ function load(key, fallback) {
60
  }
61
 
62
  function save(key, value) {
63
- localStorage.setItem(key, JSON.stringify(value));
 
 
 
 
 
 
 
 
64
  }
65
 
66
  function uuid() {
 
60
  }
61
 
62
  function save(key, value) {
63
+ try {
64
+ localStorage.setItem(key, JSON.stringify(value));
65
+ } catch (e) {
66
+ if (e?.name === 'QuotaExceededError' || e?.code === 22) {
67
+ console.warn('[store] localStorage quota exceeded — conversation not persisted');
68
+ } else {
69
+ throw e;
70
+ }
71
+ }
72
  }
73
 
74
  function uuid() {