From 889b3f153f984cfca6dfeecb1a52d1e371749c77 Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Sun, 20 Sep 2026 01:47:51 +0530 Subject: [PATCH] fix(ui): label the offline fallback instead of faking calibrated telemetry A failed upstream call returned HTTP 200 with a hash-derived answer that the UI rendered with the same P(FAVORABLE) and CONFIDENCE figures as a real decision. The fallback is now badged OFFLINE with those derived numbers blanked out. Request errors also rendered "SIGNS POINT TO YES" and always reported a network timeout. Failures now show CANNOT PREDICT and the real HTTP status or backend message, and the stored history is validated before use so a malformed value cannot break the whole interface. --- public/css/style.css | 5 +++ public/index.html | 2 +- public/js/app.js | 87 ++++++++++++++++++++++++++++++++++---------- 3 files changed, 73 insertions(+), 21 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index 6480085..3f4b933 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -545,6 +545,11 @@ h1 { border-color: var(--red); background: rgba(239, 68, 68, 0.08); } +.telemetry-badge.offline { + color: var(--text-muted); + border-color: var(--text-dim); + background: rgba(255, 255, 255, 0.04); +} .telemetry-body { padding: 12px; diff --git a/public/index.html b/public/index.html index 7f0e86a..81adac7 100644 --- a/public/index.html +++ b/public/index.html @@ -91,7 +91,7 @@ id="questionInput" class="prompt-input" placeholder="ask the oracle a yes/no question..." - maxlength="200" + maxlength="300" autocomplete="off" spellcheck="false" /> diff --git a/public/js/app.js b/public/js/app.js index 0bc5aeb..4b04e80 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -170,13 +170,32 @@ const toast = document.getElementById('toast'); let isSubmitting = false; - let historyData = []; + let historyData = loadHistory(); - // Load history from localStorage - try { - const saved = localStorage.getItem('jev_history'); - if (saved) historyData = JSON.parse(saved); - } catch (e) {} + // Read the stored log defensively: a stale, hand-edited or truncated value must never + // break the interface, so anything that is not a well formed entry is dropped. + function loadHistory() { + const sentiments = ['affirmative', 'negative', 'neutral']; + try { + const saved = localStorage.getItem('jev_history'); + if (!saved) return []; + const parsed = JSON.parse(saved); + if (!Array.isArray(parsed)) return []; + return parsed + .filter( + (item) => item && typeof item.question === 'string' && typeof item.answer === 'string' + ) + .slice(0, 25) + .map((item) => ({ + question: item.question, + answer: item.answer, + sentiment: sentiments.includes(item.sentiment) ? item.sentiment : 'neutral', + timestamp: typeof item.timestamp === 'string' ? item.timestamp : '' + })); + } catch (e) { + return []; + } + } // ─── SOUND TOGGLE ─── function updateSoundUI() { @@ -296,7 +315,21 @@ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question }) - }).then(r => r.json()); + }).then(async (response) => { + let payload = null; + try { + payload = await response.json(); + } catch (e) { + payload = null; + } + // A non-OK status is only usable when the backend shipped a fallback with it. + if (!response.ok && !(payload && payload.fallback)) { + throw new Error( + (payload && payload.error) || `Oracle returned HTTP ${response.status}` + ); + } + return payload; + }); const [_, result] = await Promise.all([minRollTime, apiCall]); @@ -305,6 +338,10 @@ if (ballShadow) ballShadow.classList.remove('rolling-shadow'); ballSphere.style.transform = 'rotateX(0deg) rotateY(0deg)'; + if (!result) { + throw new Error('Oracle returned an empty response.'); + } + if (result.error && !result.fallback) { showToast(result.error || 'Decision failed'); dieText.innerHTML = 'CANNOT
PREDICT'; @@ -320,9 +357,12 @@ ballSphere.classList.remove('rolling'); if (ballShadow) ballShadow.classList.remove('rolling-shadow'); ballSphere.style.transform = 'rotateX(0deg) rotateY(0deg)'; - dieText.innerHTML = 'SIGNS POINT
TO YES'; + // Never show an affirmative prophecy for a request that failed. + dieText.innerHTML = 'CANNOT
PREDICT'; floatingDie.className = 'floating-die surfacing'; - showToast('Network timeout. Please retry.'); + telemetryBadge.className = 'telemetry-badge neutral'; + telemetryBadge.textContent = 'ERROR'; + showToast(err && err.message ? err.message : 'Request failed. Please retry.'); } finally { isSubmitting = false; submitBtn.disabled = false; @@ -334,7 +374,9 @@ } function displayOracleAnswer(data) { - const formattedText = data.answer.replace(/\s+/g, ' ').toUpperCase(); + const formattedText = String(data.answer || 'Cannot predict now') + .replace(/\s+/g, ' ') + .toUpperCase(); const words = formattedText.split(' '); let htmlAnswer = formattedText; if (words.length > 2) { @@ -348,19 +390,22 @@ // Audio chime audio.revealChime(data.sentiment); - // Update Telemetry - telemetryBadge.className = `telemetry-badge ${data.sentiment}`; - telemetryBadge.textContent = data.sentiment.toUpperCase(); + // Update Telemetry. An offline fallback is a local hash guess, so it is labelled and + // must not be shown with the calibrated probability and confidence of a real decision. + const offline = Boolean(data.offline); + const sentiment = offline ? 'neutral' : (data.sentiment || 'neutral'); + telemetryBadge.className = `telemetry-badge ${offline ? 'offline' : sentiment}`; + telemetryBadge.textContent = offline ? 'OFFLINE' : sentiment.toUpperCase(); tVerdict.textContent = data.answer; - tNoul.textContent = (data.noul !== undefined ? data.noul.toFixed(2) : '0.50'); + tNoul.textContent = offline ? '—' : (data.noul !== undefined ? data.noul.toFixed(2) : '0.50'); - noulFill.className = `meter-fill ${data.sentiment}`; - noulFill.style.width = `${Math.round((data.noul || 0.5) * 100)}%`; + noulFill.className = `meter-fill ${sentiment}`; + noulFill.style.width = offline ? '0%' : `${Math.round((data.noul || 0.5) * 100)}%`; - tConfidence.textContent = `${Math.round((data.confidence || 0.8) * 100)}%`; + tConfidence.textContent = offline ? '—' : `${Math.round((data.confidence || 0.8) * 100)}%`; tLatency.textContent = `${data.latency || 320}ms`; - tCost.textContent = data.cost ? `$${data.cost.toFixed(6)}` : '< $0.0001'; + tCost.textContent = offline ? '—' : data.cost ? `$${data.cost.toFixed(6)}` : '< $0.0001'; jsonDump.textContent = JSON.stringify(data.raw || data, null, 2); @@ -371,6 +416,8 @@ sentiment: data.sentiment, timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }); + + if (offline) showToast('Offline guess — the oracle could not be reached'); } function addHistoryItem(item) { @@ -408,8 +455,8 @@ }); } - function escapeHtml(str) { - return str + function escapeHtml(value) { + return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>')