fix: videos disappear after auto-reset or localStorage quota failure
Browse filesRoot causes:
1. Auto-reset calls store.clearMessages(), wiping messages from
localStorage. Any subsequent loadConversation() (sidebar click,
new chat then back) read from the now-empty store, losing the
video.
2. Large video base64 dataUrls can exceed localStorage quota; the
message is never persisted, so it vanishes on any reload.
Fix: add this._sessionMessages (Map<convId, messages[]>) as an
in-memory cache for the current page session.
- Every user/assistant message is pushed to _sessionMessages in
addition to (or instead of, when quota fails) localStorage.
- _selectConversation and _loadCurrentConversation use
_sessionConvFor(conv) which substitutes the in-memory messages
when available, so media dataUrls survive auto-resets and quota
failures for the lifetime of the page.
- Auto-reset (store.clearMessages) does NOT clear _sessionMessages —
it only resets the API context, not the visual history.
- /clean explicitly calls _clearSessionMsgs so the user's manual
wipe still works correctly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- src/components/app.js +40 -8
|
@@ -21,6 +21,10 @@ export class App {
|
|
| 21 |
this.modelPicker = new ModelPicker();
|
| 22 |
this.settingsModal = new SettingsModal();
|
| 23 |
this._sidebarOpen = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
}
|
| 25 |
|
| 26 |
init() {
|
|
@@ -34,6 +38,25 @@ export class App {
|
|
| 34 |
this.inputBar.focus();
|
| 35 |
}
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
_render() {
|
| 38 |
this.root.className = 'flex h-screen overflow-hidden bg-[var(--c-bg)]';
|
| 39 |
|
|
@@ -155,7 +178,7 @@ export class App {
|
|
| 155 |
if (currentId) {
|
| 156 |
const conv = store.getCurrentConversation();
|
| 157 |
if (conv) {
|
| 158 |
-
this.chat.loadConversation(conv);
|
| 159 |
this.modelPicker.syncToConversation(conv);
|
| 160 |
this.inputBar.setModel(this.modelPicker.getModel());
|
| 161 |
this._updateContextInfo();
|
|
@@ -188,7 +211,7 @@ export class App {
|
|
| 188 |
this._updateContextInfo();
|
| 189 |
return;
|
| 190 |
}
|
| 191 |
-
this.chat.loadConversation(conv);
|
| 192 |
this.modelPicker.syncToConversation(conv);
|
| 193 |
this.inputBar.setModel(this.modelPicker.getModel());
|
| 194 |
this.sidebar.update();
|
|
@@ -206,6 +229,7 @@ export class App {
|
|
| 206 |
const convId = store.getCurrentConversationId();
|
| 207 |
if (convId) {
|
| 208 |
store.clearMessages(convId);
|
|
|
|
| 209 |
this.chat.clear();
|
| 210 |
this._updateContextInfo();
|
| 211 |
}
|
|
@@ -260,6 +284,7 @@ export class App {
|
|
| 260 |
try {
|
| 261 |
this.chat.clearError();
|
| 262 |
store.addMessage(convId, userMessage);
|
|
|
|
| 263 |
this.chat.appendUserMessage(userMessage);
|
| 264 |
this._updateContextInfo();
|
| 265 |
renderOk = true;
|
|
@@ -305,11 +330,13 @@ export class App {
|
|
| 305 |
|
| 306 |
// Finalize
|
| 307 |
this.chat.finalizeAssistantMessage(fullText);
|
| 308 |
-
|
| 309 |
role: 'assistant',
|
| 310 |
content: fullText,
|
| 311 |
timestamp: new Date().toISOString(),
|
| 312 |
-
}
|
|
|
|
|
|
|
| 313 |
this._updateContextInfo();
|
| 314 |
this.sidebar.update();
|
| 315 |
} catch (err) {
|
|
@@ -360,6 +387,7 @@ export class App {
|
|
| 360 |
async _handleAudioTask({ convId, model, settings, instruction, audio }) {
|
| 361 |
const userMessage = this._buildAudioUserMessage(audio, instruction);
|
| 362 |
store.addMessage(convId, userMessage);
|
|
|
|
| 363 |
this.chat.appendUserMessage(userMessage);
|
| 364 |
this._updateContextInfo();
|
| 365 |
|
|
@@ -380,11 +408,13 @@ export class App {
|
|
| 380 |
this.chat.hideTypingIndicator();
|
| 381 |
this.chat.startAssistantMessage();
|
| 382 |
this.chat.finalizeAssistantMessage(transcript);
|
| 383 |
-
|
| 384 |
role: 'assistant',
|
| 385 |
content: transcript,
|
| 386 |
timestamp: new Date().toISOString(),
|
| 387 |
-
}
|
|
|
|
|
|
|
| 388 |
this._updateContextInfo();
|
| 389 |
this.sidebar.update();
|
| 390 |
return;
|
|
@@ -418,11 +448,13 @@ export class App {
|
|
| 418 |
}
|
| 419 |
|
| 420 |
this.chat.finalizeAssistantMessage(fullText);
|
| 421 |
-
|
| 422 |
role: 'assistant',
|
| 423 |
content: fullText,
|
| 424 |
timestamp: new Date().toISOString(),
|
| 425 |
-
}
|
|
|
|
|
|
|
| 426 |
this._updateContextInfo();
|
| 427 |
this.sidebar.update();
|
| 428 |
} catch (err) {
|
|
|
|
| 21 |
this.modelPicker = new ModelPicker();
|
| 22 |
this.settingsModal = new SettingsModal();
|
| 23 |
this._sidebarOpen = false;
|
| 24 |
+
// In-memory message cache keyed by convId. Persists for the page session so
|
| 25 |
+
// that media (video/image dataUrls) remains visible even after an auto-reset
|
| 26 |
+
// clears localStorage, or when localStorage quota prevents persistence.
|
| 27 |
+
this._sessionMessages = new Map();
|
| 28 |
}
|
| 29 |
|
| 30 |
init() {
|
|
|
|
| 38 |
this.inputBar.focus();
|
| 39 |
}
|
| 40 |
|
| 41 |
+
// --- Session message cache helpers ---
|
| 42 |
+
|
| 43 |
+
_pushSessionMsg(convId, msg) {
|
| 44 |
+
if (!this._sessionMessages.has(convId)) this._sessionMessages.set(convId, []);
|
| 45 |
+
this._sessionMessages.get(convId).push(msg);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
_clearSessionMsgs(convId) {
|
| 49 |
+
this._sessionMessages.delete(convId);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Returns a conv-like object backed by in-memory messages when available,
|
| 53 |
+
// so that media dataUrls survive auto-resets and localStorage quota failures.
|
| 54 |
+
_sessionConvFor(conv) {
|
| 55 |
+
if (!conv) return conv;
|
| 56 |
+
const mem = this._sessionMessages.get(conv.id);
|
| 57 |
+
return mem?.length ? { ...conv, messages: mem } : conv;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
_render() {
|
| 61 |
this.root.className = 'flex h-screen overflow-hidden bg-[var(--c-bg)]';
|
| 62 |
|
|
|
|
| 178 |
if (currentId) {
|
| 179 |
const conv = store.getCurrentConversation();
|
| 180 |
if (conv) {
|
| 181 |
+
this.chat.loadConversation(this._sessionConvFor(conv));
|
| 182 |
this.modelPicker.syncToConversation(conv);
|
| 183 |
this.inputBar.setModel(this.modelPicker.getModel());
|
| 184 |
this._updateContextInfo();
|
|
|
|
| 211 |
this._updateContextInfo();
|
| 212 |
return;
|
| 213 |
}
|
| 214 |
+
this.chat.loadConversation(this._sessionConvFor(conv));
|
| 215 |
this.modelPicker.syncToConversation(conv);
|
| 216 |
this.inputBar.setModel(this.modelPicker.getModel());
|
| 217 |
this.sidebar.update();
|
|
|
|
| 229 |
const convId = store.getCurrentConversationId();
|
| 230 |
if (convId) {
|
| 231 |
store.clearMessages(convId);
|
| 232 |
+
this._clearSessionMsgs(convId);
|
| 233 |
this.chat.clear();
|
| 234 |
this._updateContextInfo();
|
| 235 |
}
|
|
|
|
| 284 |
try {
|
| 285 |
this.chat.clearError();
|
| 286 |
store.addMessage(convId, userMessage);
|
| 287 |
+
this._pushSessionMsg(convId, userMessage);
|
| 288 |
this.chat.appendUserMessage(userMessage);
|
| 289 |
this._updateContextInfo();
|
| 290 |
renderOk = true;
|
|
|
|
| 330 |
|
| 331 |
// Finalize
|
| 332 |
this.chat.finalizeAssistantMessage(fullText);
|
| 333 |
+
const assistantMsg = {
|
| 334 |
role: 'assistant',
|
| 335 |
content: fullText,
|
| 336 |
timestamp: new Date().toISOString(),
|
| 337 |
+
};
|
| 338 |
+
store.addMessage(convId, assistantMsg);
|
| 339 |
+
this._pushSessionMsg(convId, assistantMsg);
|
| 340 |
this._updateContextInfo();
|
| 341 |
this.sidebar.update();
|
| 342 |
} catch (err) {
|
|
|
|
| 387 |
async _handleAudioTask({ convId, model, settings, instruction, audio }) {
|
| 388 |
const userMessage = this._buildAudioUserMessage(audio, instruction);
|
| 389 |
store.addMessage(convId, userMessage);
|
| 390 |
+
this._pushSessionMsg(convId, userMessage);
|
| 391 |
this.chat.appendUserMessage(userMessage);
|
| 392 |
this._updateContextInfo();
|
| 393 |
|
|
|
|
| 408 |
this.chat.hideTypingIndicator();
|
| 409 |
this.chat.startAssistantMessage();
|
| 410 |
this.chat.finalizeAssistantMessage(transcript);
|
| 411 |
+
const assistantMsg = {
|
| 412 |
role: 'assistant',
|
| 413 |
content: transcript,
|
| 414 |
timestamp: new Date().toISOString(),
|
| 415 |
+
};
|
| 416 |
+
store.addMessage(convId, assistantMsg);
|
| 417 |
+
this._pushSessionMsg(convId, assistantMsg);
|
| 418 |
this._updateContextInfo();
|
| 419 |
this.sidebar.update();
|
| 420 |
return;
|
|
|
|
| 448 |
}
|
| 449 |
|
| 450 |
this.chat.finalizeAssistantMessage(fullText);
|
| 451 |
+
const assistantMsg = {
|
| 452 |
role: 'assistant',
|
| 453 |
content: fullText,
|
| 454 |
timestamp: new Date().toISOString(),
|
| 455 |
+
};
|
| 456 |
+
store.addMessage(convId, assistantMsg);
|
| 457 |
+
this._pushSessionMsg(convId, assistantMsg);
|
| 458 |
this._updateContextInfo();
|
| 459 |
this.sidebar.update();
|
| 460 |
} catch (err) {
|