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>
- src/api.js +21 -2
- src/components/app.js +18 -7
- src/components/chat.js +2 -2
- src/store.js +9 -1
|
@@ -209,8 +209,27 @@ export async function* streamCompletion(baseUrl, apiKey, model, messages, option
|
|
| 209 |
}
|
| 210 |
|
| 211 |
export function formatMessagesForApi(messages) {
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
if (msg.role === 'assistant') return { role: 'assistant', content: msg.content };
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
}
|
|
@@ -255,10 +255,19 @@ export class App {
|
|
| 255 |
);
|
| 256 |
}
|
| 257 |
|
| 258 |
-
// Add to store & render
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 277 |
-
|
|
|
|
|
|
|
| 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();
|
|
@@ -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(
|
| 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 |
|
|
@@ -60,7 +60,15 @@ function load(key, fallback) {
|
|
| 60 |
}
|
| 61 |
|
| 62 |
function save(key, value) {
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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() {
|