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);
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -170,13 +170,32 @@
|
|||
const toast = document.getElementById('toast');
|
||||
|
||||
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
|
||||
// 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) historyData = JSON.parse(saved);
|
||||
} catch (e) {}
|
||||
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<br>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<br>TO YES';
|
||||
// Never show an affirmative prophecy for a request that failed.
|
||||
dieText.innerHTML = 'CANNOT<br>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, '<')
|
||||
.replace(/>/g, '>')
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue