Make a failed AI send visible instead of hanging

GET reaches the endpoint and POST does not, with nothing in the request log,
no CSRF termination and no PHP error — so it fails in the browser before the
request goes out, and the only symptom was the pending indicator sitting
there. Reads the response as text before parsing so an empty body reports as
rejected-before-execution rather than a JSON error, wraps the synchronous
path, and surfaces script errors into the transcript.
This commit is contained in:
Gmer4Lfe
2026-08-02 18:00:53 -04:00
parent 563ea0eb78
commit e0925b9d38
+52 -11
View File
@@ -399,18 +399,44 @@
$('vv-ai-input').value = '';
addPending();
const fd = new FormData();
fd.append('action', 'ask');
fd.append('question', q);
fd.append('history', JSON.stringify(history.slice(-MAXTURN * 2)));
fd.append('kind', $('vv-ai-kind').value);
fd.append('think', $('vv-ai-think').checked ? '1' : '0');
// Wrapped: a synchronous throw here — from building the request, or from a fetch wrapper
// installed elsewhere on the page — would escape the promise chain entirely and leave the
// pending indicator up forever with nothing logged anywhere. A hang is the one failure
// that tells you nothing, so every path below has to end in a visible message.
let res;
try {
const fd = new FormData();
fd.append('action', 'ask');
fd.append('question', q);
fd.append('history', JSON.stringify(history.slice(-MAXTURN * 2)));
fd.append('kind', $('vv-ai-kind').value);
fd.append('think', $('vv-ai-think').checked ? '1' : '0');
res = fetch(API, { method: 'POST', body: fd });
} catch (e) {
addError('Could not send the request: ' + (e && e.message ? e.message : e)
+ ' — this failed in the browser before reaching the server.');
finish();
return;
}
fetch(API, { method: 'POST', body: fd }).then(r => r.json()).then(d => {
if (!d.ok) { addError(d.error || 'Failed to start'); finish(); return; }
history.push({ role: 'user', content: q });
poll(d.token, Date.now());
}).catch(e => { addError('Request failed: ' + e); finish(); });
res.then(r => r.text().then(t => ({ status: r.status, text: t })))
.then(({ status, text }) => {
if (!text.trim()) {
// The CSRF prepend terminates with an empty body, so this is the shape that failure
// takes. Naming it beats a bare JSON parse error.
addError('Empty response (HTTP ' + status + '). This usually means the request was '
+ 'rejected before the endpoint ran — check the CSRF token shim.');
finish(); return;
}
let d;
try { d = JSON.parse(text); }
catch (e) { addError('Unparseable response (HTTP ' + status + '): ' + text.slice(0, 160));
finish(); return; }
if (!d.ok) { addError(d.error || 'Failed to start'); finish(); return; }
history.push({ role: 'user', content: q });
poll(d.token, Date.now());
})
.catch(e => { addError('Request failed: ' + (e && e.message ? e.message : e)); finish(); });
}
function finish() { busy = false; $('vv-ai-send').disabled = false; }
@@ -467,6 +493,21 @@
});
document.addEventListener('keydown', e => { if (e.key === 'Escape') vvAiCloseView(); });
// Surface any script error on this tab into the transcript. Without it a throw anywhere in
// the page is invisible unless the console happens to be open, which is how a silent hang
// survives a diagnosis session.
window.addEventListener('error', e => {
if (!busy) return;
addError('Script error: ' + (e.message || 'unknown')
+ (e.filename ? ' (' + e.filename.split('/').pop() + ':' + e.lineno + ')' : ''));
finish();
});
window.addEventListener('unhandledrejection', e => {
if (!busy) return;
addError('Unhandled rejection: ' + (e.reason && e.reason.message ? e.reason.message : e.reason));
finish();
});
loadBanner();
setInterval(loadBanner, 30000);
})();