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.
This commit is contained in:
parent
4826b9c2b5
commit
889b3f153f
3 changed files with 73 additions and 21 deletions
|
|
@ -545,6 +545,11 @@ h1 {
|
||||||
border-color: var(--red);
|
border-color: var(--red);
|
||||||
background: rgba(239, 68, 68, 0.08);
|
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 {
|
.telemetry-body {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@
|
||||||
id="questionInput"
|
id="questionInput"
|
||||||
class="prompt-input"
|
class="prompt-input"
|
||||||
placeholder="ask the oracle a yes/no question..."
|
placeholder="ask the oracle a yes/no question..."
|
||||||
maxlength="200"
|
maxlength="300"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -170,13 +170,32 @@
|
||||||
const toast = document.getElementById('toast');
|
const toast = document.getElementById('toast');
|
||||||
|
|
||||||
let isSubmitting = false;
|
let isSubmitting = false;
|
||||||
let historyData = [];
|
let historyData = loadHistory();
|
||||||
|
|
||||||
// Load history from localStorage
|
// Read the stored log defensively: a stale, hand-edited or truncated value must never
|
||||||
try {
|
// break the interface, so anything that is not a well formed entry is dropped.
|
||||||
const saved = localStorage.getItem('jev_history');
|
function loadHistory() {
|
||||||
if (saved) historyData = JSON.parse(saved);
|
const sentiments = ['affirmative', 'negative', 'neutral'];
|
||||||
} catch (e) {}
|
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 ───
|
// ─── SOUND TOGGLE ───
|
||||||
function updateSoundUI() {
|
function updateSoundUI() {
|
||||||
|
|
@ -296,7 +315,21 @@
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ question })
|
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]);
|
const [_, result] = await Promise.all([minRollTime, apiCall]);
|
||||||
|
|
||||||
|
|
@ -305,6 +338,10 @@
|
||||||
if (ballShadow) ballShadow.classList.remove('rolling-shadow');
|
if (ballShadow) ballShadow.classList.remove('rolling-shadow');
|
||||||
ballSphere.style.transform = 'rotateX(0deg) rotateY(0deg)';
|
ballSphere.style.transform = 'rotateX(0deg) rotateY(0deg)';
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('Oracle returned an empty response.');
|
||||||
|
}
|
||||||
|
|
||||||
if (result.error && !result.fallback) {
|
if (result.error && !result.fallback) {
|
||||||
showToast(result.error || 'Decision failed');
|
showToast(result.error || 'Decision failed');
|
||||||
dieText.innerHTML = 'CANNOT<br>PREDICT';
|
dieText.innerHTML = 'CANNOT<br>PREDICT';
|
||||||
|
|
@ -320,9 +357,12 @@
|
||||||
ballSphere.classList.remove('rolling');
|
ballSphere.classList.remove('rolling');
|
||||||
if (ballShadow) ballShadow.classList.remove('rolling-shadow');
|
if (ballShadow) ballShadow.classList.remove('rolling-shadow');
|
||||||
ballSphere.style.transform = 'rotateX(0deg) rotateY(0deg)';
|
ballSphere.style.transform = 'rotateX(0deg) rotateY(0deg)';
|
||||||
dieText.innerHTML = 'SIGNS POINT<br>TO YES';
|
// Never show an affirmative prophecy for a request that failed.
|
||||||
|
dieText.innerHTML = 'CANNOT<br>PREDICT';
|
||||||
floatingDie.className = 'floating-die surfacing';
|
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 {
|
} finally {
|
||||||
isSubmitting = false;
|
isSubmitting = false;
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
|
|
@ -334,7 +374,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayOracleAnswer(data) {
|
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(' ');
|
const words = formattedText.split(' ');
|
||||||
let htmlAnswer = formattedText;
|
let htmlAnswer = formattedText;
|
||||||
if (words.length > 2) {
|
if (words.length > 2) {
|
||||||
|
|
@ -348,19 +390,22 @@
|
||||||
// Audio chime
|
// Audio chime
|
||||||
audio.revealChime(data.sentiment);
|
audio.revealChime(data.sentiment);
|
||||||
|
|
||||||
// Update Telemetry
|
// Update Telemetry. An offline fallback is a local hash guess, so it is labelled and
|
||||||
telemetryBadge.className = `telemetry-badge ${data.sentiment}`;
|
// must not be shown with the calibrated probability and confidence of a real decision.
|
||||||
telemetryBadge.textContent = data.sentiment.toUpperCase();
|
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;
|
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.className = `meter-fill ${sentiment}`;
|
||||||
noulFill.style.width = `${Math.round((data.noul || 0.5) * 100)}%`;
|
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`;
|
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);
|
jsonDump.textContent = JSON.stringify(data.raw || data, null, 2);
|
||||||
|
|
||||||
|
|
@ -371,6 +416,8 @@
|
||||||
sentiment: data.sentiment,
|
sentiment: data.sentiment,
|
||||||
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (offline) showToast('Offline guess — the oracle could not be reached');
|
||||||
}
|
}
|
||||||
|
|
||||||
function addHistoryItem(item) {
|
function addHistoryItem(item) {
|
||||||
|
|
@ -408,8 +455,8 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(str) {
|
function escapeHtml(value) {
|
||||||
return str
|
return String(value ?? '')
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue