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:
@@ -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">×</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"' : ''}>← 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 →</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 ? ` \u00b7 ${extra.join(' \u00b7 ')}` : '');
|
||||
}
|
||||
|
||||
historyCache = null; // invalidate so modal fetches fresh data
|
||||
setTimeout(refreshPredictions, 2000);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user