umer-wasim commited on
Commit
053057f
·
verified ·
1 Parent(s): b42d57c

Chat: save before the AI writes, detect a page killed mid-answer, no 1 GB model on phones

Browse files
Files changed (1) hide show
  1. chat/chat.js +30 -12
chat/chat.js CHANGED
@@ -28,6 +28,11 @@ function newChat() {
28
  const AI_CONSENT = 'devseis.aiConsent';
29
  // Set when the AI wrote unreadable text on this device (a faulty graphics chip or driver); holds the model version, so a new one is tried again.
30
  const AI_FAULT = 'devseis.aiFault';
 
 
 
 
 
31
  const FIRST_WORDS_LIMIT = 60000, ANSWER_LIMIT = 180000;
32
  let webllmPromise = null;
33
  const loadWebllm = () => (webllmPromise ||= import(WEBLLM_URL));
@@ -43,9 +48,11 @@ function chatModel() {
43
  }
44
  // A broken-AI flag is kept per model build, so a new build gets a fresh try.
45
  function faultKey() { const c = state.config?.chat || {}; return [c.version || 'base', c.quant].filter(Boolean).join('-'); }
46
- async function needs16Bit() {
47
- if (chatModel().quant !== 'q0f16') return false;
48
- try { const a = await navigator.gpu.requestAdapter(); return !a || !a.features.has('shader-f16'); } catch { return true; }
 
 
49
  }
50
 
51
  async function modelChoice() {
@@ -231,8 +238,9 @@ async function ask(question) {
231
  if (state.engine) {
232
  // Typing dots until the AI's first words arrive; the knowledge-base answer is only the fallback if the AI fails.
233
  reply.writing = true; reply.note = '✨ Writing answer…';
234
- render();
235
- await aiAnswer(chat, reply, messages, passages);
 
236
  } else {
237
  reply.content = quickAnswer(passages);
238
  reply.cited = true; reply.quick = true;
@@ -301,11 +309,11 @@ function fallback(reply, passages, note) {
301
  }
302
 
303
  // The AI wrote junk: free the graphics chip (a broken driver can hang the whole computer) and stay on the knowledge base here.
304
- function turnOffBrokenAi() {
305
  const engine = state.engine;
306
  state.engine = null; state.enginePromise = null; state.loading = false; state.kbOnly = true; state.aiFault = true;
307
- state.modelLabel = 'Turned off: it wrote unreadable text on this device'; $('modelName').textContent = 'Knowledge base only';
308
- try { localStorage.setItem(AI_FAULT, faultKey()); } catch { /* ignore */ }
309
  if (engine && engine.unload) engine.unload().catch(() => {});
310
  aiChip();
311
  }
@@ -315,7 +323,8 @@ async function rewriteWithAi(index) {
315
  if (!reply || !state.engine || state.busy) return;
316
  state.busy = true; updateSend();
317
  const messages = chatMessages(reply.question, reply.passages, auditBrief(chat.audit), reply.history || [], chat.audit ? null : deviceBrief(activeDevice()));
318
- try { await aiAnswer(chat, reply, messages, reply.passages); } catch (error) { reply.note = `The AI could not answer: ${error.message}`; }
 
319
  state.busy = false; saveChats(); updateSend(); render();
320
  }
321
 
@@ -453,7 +462,7 @@ function renderDevicePanel() {
453
  const d = activeDevice();
454
  const row = (label, value, hint = '') => value ? `<div class="drow"><dt>${label}</dt><dd>${esc(value)}${hint ? `<small>${esc(hint)}</small>` : ''}</dd></div>` : '';
455
  const unlock = real.platform_authenticator === true ? 'Available (fingerprint, face or PIN)' : real.platform_authenticator === false ? 'None found by the browser' : '';
456
- const ai = state.aiFault ? 'Turned off: it wrote unreadable text on this device\'s graphics chip' : state.engine ? `Ready · ${state.modelLabel}` : state.loading ? `Downloading · ${Math.round((state.progress || 0) * 100)}%`
457
  : 'gpu' in navigator && state.engine !== false ? 'Supported (WebGPU)' : 'Not supported here: answers come from the knowledge base';
458
  let html;
459
  if (audit) {
@@ -545,6 +554,9 @@ $('themeToggle').addEventListener('click', () => {
545
  async function init() {
546
  try { const t = localStorage.getItem('devseis.theme'); if (t) document.documentElement.dataset.theme = t; } catch { /* ignore */ }
547
  state.chats = loadChats();
 
 
 
548
  state.current = state.chats[0] || null;
549
  const [kb, device, config] = await Promise.all([
550
  getJson('library/knowledge.json'),
@@ -567,8 +579,14 @@ async function init() {
567
  if ('gpu' in navigator) {
568
  let consent = null; try { consent = localStorage.getItem(AI_CONSENT); } catch { /* ignore */ }
569
  let fault = null; try { fault = localStorage.getItem(AI_FAULT); } catch { /* ignore */ }
570
- if (fault && fault === faultKey()) { state.kbOnly = true; state.aiFault = true; state.modelLabel = 'Turned off: it wrote unreadable text on this device'; $('modelName').textContent = 'Knowledge base only'; aiChip(); }
571
- else needs16Bit().then(no => no ? (() => { state.engine = false; state.modelLabel = 'Not available: this graphics chip has no 16-bit support'; $('modelName').textContent = 'Knowledge base only'; aiChip(); render(); updateSend(); })() : modelCached().then(cached => {
 
 
 
 
 
 
572
  if (cached || consent === 'yes') { state.cachedStart = cached; getEngine(); }
573
  else if (consent === 'later') { state.kbOnly = true; aiChip(); }
574
  else {
 
28
  const AI_CONSENT = 'devseis.aiConsent';
29
  // Set when the AI wrote unreadable text on this device (a faulty graphics chip or driver); holds the model version, so a new one is tried again.
30
  const AI_FAULT = 'devseis.aiFault';
31
+ const AI_FAULT_REASON = 'devseis.aiFaultReason';
32
+ // Set while the AI loads or writes. Still set when the page starts again = the browser killed the page (out of memory:
33
+ // iOS reloads a tab that uses too much), so the AI is turned off on this device instead of crashing every question.
34
+ const AI_RUNNING = 'devseis.aiRunning';
35
+ function aiRunning(on) { try { if (on) localStorage.setItem(AI_RUNNING, faultKey()); else localStorage.removeItem(AI_RUNNING); } catch { /* ignore */ } }
36
  const FIRST_WORDS_LIMIT = 60000, ANSWER_LIMIT = 180000;
37
  let webllmPromise = null;
38
  const loadWebllm = () => (webllmPromise ||= import(WEBLLM_URL));
 
48
  }
49
  // A broken-AI flag is kept per model build, so a new build gets a fresh try.
50
  function faultKey() { const c = state.config?.chat || {}; return [c.version || 'base', c.quant].filter(Boolean).join('-'); }
51
+ // Why the AI cannot run here ('' = it can): the unrounded build is too big for a phone tab and needs 16-bit graphics.
52
+ async function aiUnsuitable() {
53
+ if (chatModel().quant !== 'q0f16') return '';
54
+ if (state.device && state.device.desktop === false) return 'too large for phones until the smaller version is ready';
55
+ try { const a = await navigator.gpu.requestAdapter(); return !a || !a.features.has('shader-f16') ? 'this graphics chip has no 16-bit support' : ''; } catch { return 'no graphics adapter'; }
56
  }
57
 
58
  async function modelChoice() {
 
238
  if (state.engine) {
239
  // Typing dots until the AI's first words arrive; the knowledge-base answer is only the fallback if the AI fails.
240
  reply.writing = true; reply.note = '✨ Writing answer…';
241
+ render(); saveChats();
242
+ aiRunning(true);
243
+ try { await aiAnswer(chat, reply, messages, passages); } finally { aiRunning(false); }
244
  } else {
245
  reply.content = quickAnswer(passages);
246
  reply.cited = true; reply.quick = true;
 
309
  }
310
 
311
  // The AI wrote junk: free the graphics chip (a broken driver can hang the whole computer) and stay on the knowledge base here.
312
+ function turnOffBrokenAi(reason = 'it wrote unreadable text on this device') {
313
  const engine = state.engine;
314
  state.engine = null; state.enginePromise = null; state.loading = false; state.kbOnly = true; state.aiFault = true;
315
+ state.modelLabel = `Turned off: ${reason}`; $('modelName').textContent = 'Knowledge base only';
316
+ try { localStorage.setItem(AI_FAULT, faultKey()); localStorage.setItem(AI_FAULT_REASON, reason); } catch { /* ignore */ }
317
  if (engine && engine.unload) engine.unload().catch(() => {});
318
  aiChip();
319
  }
 
323
  if (!reply || !state.engine || state.busy) return;
324
  state.busy = true; updateSend();
325
  const messages = chatMessages(reply.question, reply.passages, auditBrief(chat.audit), reply.history || [], chat.audit ? null : deviceBrief(activeDevice()));
326
+ saveChats(); aiRunning(true);
327
+ try { await aiAnswer(chat, reply, messages, reply.passages); } catch (error) { reply.note = `The AI could not answer: ${error.message}`; } finally { aiRunning(false); }
328
  state.busy = false; saveChats(); updateSend(); render();
329
  }
330
 
 
462
  const d = activeDevice();
463
  const row = (label, value, hint = '') => value ? `<div class="drow"><dt>${label}</dt><dd>${esc(value)}${hint ? `<small>${esc(hint)}</small>` : ''}</dd></div>` : '';
464
  const unlock = real.platform_authenticator === true ? 'Available (fingerprint, face or PIN)' : real.platform_authenticator === false ? 'None found by the browser' : '';
465
+ const ai = state.aiFault ? state.modelLabel : state.engine ? `Ready · ${state.modelLabel}` : state.loading ? `Downloading · ${Math.round((state.progress || 0) * 100)}%`
466
  : 'gpu' in navigator && state.engine !== false ? 'Supported (WebGPU)' : 'Not supported here: answers come from the knowledge base';
467
  let html;
468
  if (audit) {
 
554
  async function init() {
555
  try { const t = localStorage.getItem('devseis.theme'); if (t) document.documentElement.dataset.theme = t; } catch { /* ignore */ }
556
  state.chats = loadChats();
557
+ // An answer still being written when the page was saved: the page was killed mid-answer. Show the knowledge-base one.
558
+ for (const c of state.chats) for (const m of c.messages) if (m.role === 'assistant' && (m.pending || m.writing))
559
+ fallback(m, m.passages || [], 'The page reloaded while the AI was writing this (the device likely ran out of memory), so this answer comes from the knowledge base.');
560
  state.current = state.chats[0] || null;
561
  const [kb, device, config] = await Promise.all([
562
  getJson('library/knowledge.json'),
 
579
  if ('gpu' in navigator) {
580
  let consent = null; try { consent = localStorage.getItem(AI_CONSENT); } catch { /* ignore */ }
581
  let fault = null; try { fault = localStorage.getItem(AI_FAULT); } catch { /* ignore */ }
582
+ let crashed = null; try { crashed = localStorage.getItem(AI_RUNNING); } catch { /* ignore */ }
583
+ if (crashed) {
584
+ aiRunning(false);
585
+ if (crashed === faultKey()) { turnOffBrokenAi('the page reloaded while the AI was running (this device ran out of memory)'); fault = faultKey(); }
586
+ }
587
+ let reason = 'it wrote unreadable text on this device'; try { reason = localStorage.getItem(AI_FAULT_REASON) || reason; } catch { /* ignore */ }
588
+ if (fault && fault === faultKey()) { state.kbOnly = true; state.aiFault = true; state.modelLabel = `Turned off: ${reason}`; $('modelName').textContent = 'Knowledge base only'; aiChip(); }
589
+ else aiUnsuitable().then(no => no ? (() => { state.engine = false; state.modelLabel = `Not available: ${no}`; $('modelName').textContent = 'Knowledge base only'; aiChip(); render(); updateSend(); })() : modelCached().then(cached => {
590
  if (cached || consent === 'yes') { state.cachedStart = cached; getEngine(); }
591
  else if (consent === 'later') { state.kbOnly = true; aiChip(); }
592
  else {