add whale/public semi-win scoring, expand to last 50, and full history modal

- Whale/public picks now track 2nd pick and score SEMI (0.5 pts) like model
- Prediction table expanded from 20 to 50 rows
- "View All History" modal with pagination (50/page), fetches up to 500
- Accuracy rows use semi-win scoring for all three columns
This commit is contained in:
2026-02-26 09:51:08 +05:00
parent d1dc8f62fa
commit 5fd4894599
2 changed files with 183 additions and 43 deletions

View File

@@ -257,10 +257,13 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
<div id="crowd-stats" class="crowd-stats"></div>
</div>
<!-- Last 20 Predictions vs Actual -->
<!-- Last 50 Predictions vs Actual -->
<div class="section">
<div class="section-title">Last 20 Predictions vs Actual</div>
<div class="panel" style="max-height:500px;overflow-y:auto">
<div class="section-title" style="display:flex;align-items:center;gap:12px">
Last 50 Predictions vs Actual
<button id="view-all-btn" onclick="openHistoryModal()" style="font-size:11px;padding:3px 12px;border-radius:12px;border:1px solid var(--accent);background:transparent;color:var(--accent);cursor:pointer;font-weight:600;text-transform:uppercase;letter-spacing:0.3px">View All History</button>
</div>
<div class="panel" style="max-height:600px;overflow-y:auto">
<table class="pred-history" id="pred-history">
<thead><tr><th>Game</th><th>Predicted</th><th>2nd Pick</th><th>P(A)</th><th>P(B)</th><th>P(C)</th><th>Whale</th><th>Public</th><th>Actual</th><th>Result</th></tr></thead>
<tbody></tbody>
@@ -268,6 +271,25 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
</div>
</div>
<!-- Full History Modal -->
<div id="history-modal" style="display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,0.7);backdrop-filter:blur(4px)">
<div style="position:absolute;inset:20px;background:var(--bg);border:1px solid var(--border);border-radius:12px;display:flex;flex-direction:column;overflow:hidden">
<div style="display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--border);background:var(--surface)">
<div style="font-size:15px;font-weight:700">Full Prediction History</div>
<button onclick="closeHistoryModal()" style="background:none;border:none;color:var(--text2);font-size:22px;cursor:pointer;padding:0 6px;line-height:1">&times;</button>
</div>
<div id="modal-accuracy" style="padding:12px 20px;border-bottom:1px solid var(--border);background:var(--surface2);font-size:12px;display:flex;gap:24px;flex-wrap:wrap"></div>
<div style="flex:1;overflow-y:auto;padding:0 20px">
<table class="pred-history" id="modal-table">
<thead><tr><th>Game</th><th>Predicted</th><th>2nd Pick</th><th>P(A)</th><th>P(B)</th><th>P(C)</th><th>Whale</th><th>Public</th><th>Actual</th><th>Result</th></tr></thead>
<tbody></tbody>
</table>
</div>
<div id="modal-pagination" style="display:flex;align-items:center;justify-content:center;gap:16px;padding:12px 20px;border-top:1px solid var(--border);background:var(--surface)">
</div>
</div>
</div>
<!-- Markov Matrices -->
<div class="section">
<div class="section-title">Markov Transition Matrices</div>
@@ -663,44 +685,31 @@ function renderCrowdStats(data) {
`;
}
function renderLast20(predictions) {
const tbody = $('pred-history').querySelector('tbody');
function renderPredictionRows(predictions, tbody) {
if (!predictions || predictions.length === 0) {
tbody.innerHTML = '<tr><td colspan="10" style="color:var(--text3)">Not enough data</td></tr>';
return;
}
const fullHits = predictions.filter(p => p.correct).length;
const semiHits = predictions.filter(p => p.semi_correct).length;
const score = fullHits + semiHits * 0.5;
// Whale/public accuracy
const whaleEntries = predictions.filter(p => p.whale_pick != null);
const whaleHits = whaleEntries.filter(p => p.whale_hit).length;
const publicEntries = predictions.filter(p => p.public_pick != null);
const publicHits = publicEntries.filter(p => p.public_hit).length;
const whalePct = whaleEntries.length > 0 ? (whaleHits / whaleEntries.length * 100).toFixed(1) : '--';
const publicPct = publicEntries.length > 0 ? (publicHits / publicEntries.length * 100).toFixed(1) : '--';
tbody.innerHTML = predictions.slice().reverse().map(p => {
const maxProb = Math.max(p.probs.A, p.probs.B, p.probs.C);
const resultClass = p.correct ? 'correct' : (p.semi_correct ? 'semi' : 'wrong');
const resultLabel = p.correct ? 'HIT' : (p.semi_correct ? 'SEMI' : 'MISS');
// Whale cell
// Whale cell with semi-win
let whaleCell;
if (p.whale_pick) {
const whCls = p.whale_hit ? 'correct' : 'wrong';
const whLabel = p.whale_hit ? 'HIT' : 'MISS';
const whCls = p.whale_hit ? 'correct' : (p.whale_semi ? 'semi' : 'wrong');
const whLabel = p.whale_hit ? 'HIT' : (p.whale_semi ? 'SEMI' : 'MISS');
whaleCell = `<td><span style="color:${CHAIR_COLORS[p.whale_pick]};font-weight:700">${p.whale_pick}</span> <span class="${whCls}" style="font-size:10px">${whLabel}</span></td>`;
} else {
whaleCell = '<td style="color:var(--text3)">--</td>';
}
// Public cell
// Public cell with semi-win
let pubCell;
if (p.public_pick) {
const puCls = p.public_hit ? 'correct' : 'wrong';
const puLabel = p.public_hit ? 'HIT' : 'MISS';
const puCls = p.public_hit ? 'correct' : (p.public_semi ? 'semi' : 'wrong');
const puLabel = p.public_hit ? 'HIT' : (p.public_semi ? 'SEMI' : 'MISS');
pubCell = `<td><span style="color:${CHAIR_COLORS[p.public_pick]};font-weight:700">${p.public_pick}</span> <span class="${puCls}" style="font-size:10px">${puLabel}</span></td>`;
} else {
pubCell = '<td style="color:var(--text3)">--</td>';
@@ -719,17 +728,114 @@ function renderLast20(predictions) {
<td class="winner-cell" style="color:${CHAIR_COLORS[p.actual]}">${p.actual}</td>
<td class="${resultClass}" style="font-weight:700">${resultLabel}</td>
</tr>`;
}).join('') +
`<tr style="border-top:2px solid var(--border);font-weight:700">
}).join('');
}
function computeAccuracy(predictions) {
const fullHits = predictions.filter(p => p.correct).length;
const semiHits = predictions.filter(p => p.semi_correct).length;
const score = fullHits + semiHits * 0.5;
const whaleEntries = predictions.filter(p => p.whale_pick != null);
const whaleHits = whaleEntries.filter(p => p.whale_hit).length;
const whaleSemi = whaleEntries.filter(p => p.whale_semi).length;
const whaleScore = whaleHits + whaleSemi * 0.5;
const whalePct = whaleEntries.length > 0 ? (whaleScore / whaleEntries.length * 100).toFixed(1) : '--';
const publicEntries = predictions.filter(p => p.public_pick != null);
const publicHits = publicEntries.filter(p => p.public_hit).length;
const publicSemi = publicEntries.filter(p => p.public_semi).length;
const publicScore = publicHits + publicSemi * 0.5;
const publicPct = publicEntries.length > 0 ? (publicScore / publicEntries.length * 100).toFixed(1) : '--';
const modelPct = predictions.length > 0 ? (score / predictions.length * 100).toFixed(1) : '--';
return { score, fullHits, semiHits, whaleHits, whaleSemi, whaleScore, whalePct, whaleTotal: whaleEntries.length,
publicHits, publicSemi, publicScore, publicPct, publicTotal: publicEntries.length, modelPct, total: predictions.length };
}
function renderLast20(predictions) {
const tbody = $('pred-history').querySelector('tbody');
if (!predictions || predictions.length === 0) {
tbody.innerHTML = '<tr><td colspan="10" style="color:var(--text3)">Not enough data</td></tr>';
return;
}
const acc = computeAccuracy(predictions);
renderPredictionRows(predictions, tbody);
// Append accuracy row
tbody.innerHTML += `<tr style="border-top:2px solid var(--border);font-weight:700">
<td colspan="6" style="text-align:right;color:var(--text2)">Accuracy (last ${predictions.length})</td>
<td style="color:${whalePct !== '--' && parseFloat(whalePct) > 33.3 ? 'var(--green)' : 'var(--text2)'}">${whalePct !== '--' ? whalePct + '%' : '--'}</td>
<td style="color:${publicPct !== '--' && parseFloat(publicPct) > 33.3 ? 'var(--green)' : 'var(--text2)'}">${publicPct !== '--' ? publicPct + '%' : '--'}</td>
<td colspan="2" style="color:${score/predictions.length > 1/3 ? 'var(--green)' : 'var(--red)'}">
Model: ${(score/predictions.length*100).toFixed(1)}%
<td style="color:${acc.whalePct !== '--' && parseFloat(acc.whalePct) > 33.3 ? 'var(--green)' : 'var(--text2)'}">${acc.whalePct !== '--' ? acc.whalePct + '%' : '--'}</td>
<td style="color:${acc.publicPct !== '--' && parseFloat(acc.publicPct) > 33.3 ? 'var(--green)' : 'var(--text2)'}">${acc.publicPct !== '--' ? acc.publicPct + '%' : '--'}</td>
<td colspan="2" style="color:${parseFloat(acc.modelPct) > 33.3 ? 'var(--green)' : 'var(--red)'}">
Model: ${acc.modelPct}%
</td>
</tr>`;
}
// ── Full History Modal ──
let historyCache = null;
let historyPage = 1;
const ROWS_PER_PAGE = 50;
function openHistoryModal() {
$('history-modal').style.display = 'block';
if (historyCache) { renderHistoryPage(1); return; }
$('modal-table').querySelector('tbody').innerHTML = '<tr><td colspan="10" style="color:var(--text3)">Loading...</td></tr>';
$('modal-pagination').innerHTML = '';
$('modal-accuracy').innerHTML = '';
fetch('/api/prediction-history?limit=500').then(r => r.json()).then(data => {
historyCache = data;
renderHistoryPage(1);
}).catch(err => {
$('modal-table').querySelector('tbody').innerHTML = `<tr><td colspan="10" style="color:var(--red)">Failed: ${err.message}</td></tr>`;
});
}
function closeHistoryModal() {
$('history-modal').style.display = 'none';
}
// Close on overlay click
$('history-modal')?.addEventListener('click', function(e) {
if (e.target === this) closeHistoryModal();
});
function renderHistoryPage(page) {
if (!historyCache) return;
const preds = historyCache.predictions || [];
const totalPages = Math.max(1, Math.ceil(preds.length / ROWS_PER_PAGE));
page = Math.max(1, Math.min(page, totalPages));
historyPage = page;
// Accuracy summary bar
const accData = historyCache.accuracy;
$('modal-accuracy').innerHTML = `
<span><b>Model:</b> <span style="color:var(--green)">${accData.model.pct}%</span> <span style="color:var(--text3)">(${accData.model.hits} hits + ${accData.model.semi} semi / ${accData.model.total})</span></span>
<span><b>Whale:</b> <span style="color:${accData.whale.pct > 33.3 ? 'var(--green)' : 'var(--text2)'}">${accData.whale.pct}%</span> <span style="color:var(--text3)">(${accData.whale.hits} hits${accData.whale.semi ? ' + ' + accData.whale.semi + ' semi' : ''} / ${accData.whale.total})</span></span>
<span><b>Public:</b> <span style="color:${accData.public.pct > 33.3 ? 'var(--green)' : 'var(--text2)'}">${accData.public.pct}%</span> <span style="color:var(--text3)">(${accData.public.hits} hits${accData.public.semi ? ' + ' + accData.public.semi + ' semi' : ''} / ${accData.public.total})</span></span>
<span style="color:var(--text3)">${preds.length} predictions total</span>
`;
// Slice for current page (preds are oldest-first, show newest-first)
const reversed = preds.slice().reverse();
const start = (page - 1) * ROWS_PER_PAGE;
const pageData = reversed.slice(start, start + ROWS_PER_PAGE);
const tbody = $('modal-table').querySelector('tbody');
// Reverse pageData back so renderPredictionRows (which reverses internally) shows correct order
renderPredictionRows(pageData.slice().reverse(), tbody);
// Pagination controls
$('modal-pagination').innerHTML = `
<button onclick="renderHistoryPage(${page - 1})" style="padding:6px 14px;border-radius:6px;border:1px solid var(--border);background:var(--surface2);color:var(--text);cursor:pointer;font-size:12px" ${page <= 1 ? 'disabled style="opacity:0.4;padding:6px 14px;border-radius:6px;border:1px solid var(--border);background:var(--surface2);color:var(--text);cursor:default;font-size:12px"' : ''}>&#8592; Prev</button>
<span style="font-size:12px;color:var(--text2)">Page ${page} of ${totalPages}</span>
<button onclick="renderHistoryPage(${page + 1})" style="padding:6px 14px;border-radius:6px;border:1px solid var(--border);background:var(--surface2);color:var(--text);cursor:pointer;font-size:12px" ${page >= totalPages ? 'disabled style="opacity:0.4;padding:6px 14px;border-radius:6px;border:1px solid var(--border);background:var(--surface2);color:var(--text);cursor:default;font-size:12px"' : ''}>Next &#8594;</button>
`;
}
function renderMarkov1(m) {
const t = $('markov1-table');
let html = '<tr><th>From \\ To</th>' + CHAIRS.map(c => `<th style="color:${CHAIR_COLORS[c]}">\u2192${c}</th>`).join('') + '</tr>';
@@ -1020,6 +1126,7 @@ function onRoundResult(data) {
(extra.length ? ` &nbsp;\u00b7&nbsp; ${extra.join(' &nbsp;\u00b7&nbsp; ')}` : '');
}
historyCache = null; // invalidate so modal fetches fresh data
setTimeout(refreshPredictions, 2000);
}