diff options
| author | CaptainJack2491 <jayrupnakawala@gmail.com> | 2026-03-09 20:16:05 +0000 |
|---|---|---|
| committer | CaptainJack2491 <jayrupnakawala@gmail.com> | 2026-03-09 20:16:05 +0000 |
| commit | 17acb7a802d6d6f844c51dca5f2f171e360eca8d (patch) | |
| tree | fd3ad7642e06289a43bc0f059307bff07aed4cd9 /api/static | |
| parent | 1bbb4938d9f714eb29c8981e0d834ce85ccafacc (diff) | |
feat: add token usage metrics and click-to-zoom charts
This commit:
- Bakes total/prompt/completion tokens directly into results.csv
- Reverts dynamic JSON scraping backend logic for speed
- Adds modal overlay functionality to zoom Chart.js graphs on click
Diffstat (limited to 'api/static')
| -rw-r--r-- | api/static/css/components.css | 45 | ||||
| -rw-r--r-- | api/static/index.html | 8 | ||||
| -rw-r--r-- | api/static/js/components/results.js | 130 | ||||
| -rw-r--r-- | api/static/js/ui.js | 4 |
4 files changed, 173 insertions, 14 deletions
diff --git a/api/static/css/components.css b/api/static/css/components.css index de237c9..e576b3a 100644 --- a/api/static/css/components.css +++ b/api/static/css/components.css @@ -217,6 +217,51 @@ text-decoration: underline; } +/* Modal */ +.modal-overlay { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background-color: rgba(0, 0, 0, 0.85); + backdrop-filter: blur(4px); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-xl); +} + +.modal-content { + background-color: var(--color-bg-1); + border: var(--border-width) solid var(--color-bg-3); + border-radius: var(--border-radius); + width: 90vw; + max-width: 1200px; + position: relative; + padding: var(--space-lg); + box-shadow: 0 10px 30px rgba(0,0,0,0.5); +} + +.btn-close { + position: absolute; + top: 10px; + right: 15px; + background: none; + border: none; + color: var(--color-text-muted); + font-size: 1.5rem; + cursor: pointer; + transition: var(--transition-fast); +} + +.btn-close:hover { + color: var(--color-danger); +} + +/* Make chart containers clickable */ +.chart-container canvas { + cursor: pointer; +} + @keyframes blink { 0% { opacity: 1; } 50% { opacity: 0.6; } diff --git a/api/static/index.html b/api/static/index.html index 6b4c513..5338fca 100644 --- a/api/static/index.html +++ b/api/static/index.html @@ -126,5 +126,13 @@ <!-- Main Entry Point --> <script type="module" src="/static/js/main.js"></script> + + <!-- Chart Zoom Modal --> + <div id="chart-modal" class="modal-overlay" style="display: none;"> + <div class="modal-content"> + <button id="close-modal-btn" class="btn-close">×</button> + <div id="modal-chart-container" class="chart-container" style="height: 80vh; border: none; background: transparent;"></div> + </div> + </div> </body> </html>
\ No newline at end of file diff --git a/api/static/js/components/results.js b/api/static/js/components/results.js index 82f34f1..9b530f4 100644 --- a/api/static/js/components/results.js +++ b/api/static/js/components/results.js @@ -7,6 +7,7 @@ import { ui } from '../ui.js'; let activeCharts = []; let currentCsvData = null; // Store fetched data locally for quick filtering let currentCsvColumns = []; +let modalChart = null; export const results = { async init() { @@ -15,6 +16,12 @@ export const results = { ui.elements.judgeFileSelect.addEventListener('change', (e) => this.loadJudgeData(e.target.value)); ui.elements.refreshBtn.addEventListener('click', () => this.applyFilters()); + // Modal Setup + ui.elements.closeModalBtn.addEventListener('click', () => this.closeChartModal()); + ui.elements.chartModal.addEventListener('click', (e) => { + if (e.target === ui.elements.chartModal) this.closeChartModal(); + }); + await Promise.all([ this.updateCSVFileList(), this.updateJudgeFileList(), @@ -22,6 +29,34 @@ export const results = { ]); }, + closeChartModal() { + ui.elements.chartModal.style.display = 'none'; + if (modalChart) { + modalChart.destroy(); + modalChart = null; + } + }, + + openChartModal(config) { + ui.elements.chartModal.style.display = 'flex'; + ui.elements.modalChartContainer.innerHTML = '<canvas id="modal-canvas"></canvas>'; + const ctx = document.getElementById('modal-canvas').getContext('2d'); + + const modalOptions = { ...config.options, maintainAspectRatio: false }; + if (modalOptions.plugins && modalOptions.plugins.legend) { + modalOptions.plugins.legend.labels = { color: '#e7e9ea', font: { size: 14 } }; + } + if (modalOptions.plugins && modalOptions.plugins.title) { + modalOptions.plugins.title.font = { size: 20, family: 'Inter' }; + } + + modalChart = new Chart(ctx, { + type: config.type, + data: config.data, + options: modalOptions + }); + }, + async updateCSVFileList() { try { const files = await api.get('/api/results/files'); @@ -195,11 +230,67 @@ export const results = { this.renderStackedBarChart('Blackbox Category by Oversight', data, 'oversight', 'blackbox_category'); } + if (cols.includes('model') && cols.includes('total_tokens')) { + this.renderTokenUsageChart('Average Token Usage by Model', data); + } + if (ui.elements.interactiveCharts.innerHTML === '') { ui.elements.interactiveCharts.innerHTML = '<div class="empty-state" style="grid-column: 1 / -1;">No plottable categorical columns found.</div>'; } }, + renderTokenUsageChart(title, data) { + const metrics = {}; // { model: { total: 0, count: 0, prompt: 0, completion: 0 } } + + data.forEach(row => { + const model = row.model || 'Unknown'; + if (!row.total_tokens) return; + + if (!metrics[model]) { + metrics[model] = { total: 0, prompt: 0, completion: 0, count: 0 }; + } + metrics[model].total += row.total_tokens || 0; + metrics[model].prompt += row.prompt_tokens || 0; + metrics[model].completion += row.completion_tokens || 0; + metrics[model].count += 1; + }); + + const labels = Object.keys(metrics).sort(); + if (labels.length === 0) return; + + const datasets = [ + { + label: 'Prompt Tokens', + data: labels.map(l => metrics[l].prompt / metrics[l].count), + backgroundColor: '#3b82f6', + }, + { + label: 'Completion Tokens', + data: labels.map(l => metrics[l].completion / metrics[l].count), + backgroundColor: '#10b981', + } + ]; + + const config = { + type: 'bar', + data: { labels, datasets }, + options: { + ...this.getChartOptions(title), + scales: { + x: { stacked: true, ticks: { color: '#9ea0a6' }, grid: { color: '#2a2a2e' } }, + y: { stacked: true, ticks: { color: '#9ea0a6' }, grid: { color: '#2a2a2e' } } + } + } + }; + + const canvasId = `chart-${Math.random().toString(36).substr(2, 9)}`; + this.createChartContainer(canvasId, config); + + const ctx = document.getElementById(canvasId).getContext('2d'); + const chart = new Chart(ctx, config); + activeCharts.push(chart); + }, + renderDoughnutChart(title, data, column) { const counts = {}; data.forEach(row => { @@ -207,11 +298,7 @@ export const results = { counts[val] = (counts[val] || 0) + 1; }); - const canvasId = `chart-${Math.random().toString(36).substr(2, 9)}`; - this.createChartContainer(canvasId); - - const ctx = document.getElementById(canvasId).getContext('2d'); - const chart = new Chart(ctx, { + const config = { type: 'doughnut', data: { labels: Object.keys(counts), @@ -222,7 +309,13 @@ export const results = { }] }, options: this.getChartOptions(title) - }); + }; + + const canvasId = `chart-${Math.random().toString(36).substr(2, 9)}`; + this.createChartContainer(canvasId, config); + + const ctx = document.getElementById(canvasId).getContext('2d'); + const chart = new Chart(ctx, config); activeCharts.push(chart); }, @@ -257,11 +350,7 @@ export const results = { } }); - const canvasId = `chart-${Math.random().toString(36).substr(2, 9)}`; - this.createChartContainer(canvasId); - - const ctx = document.getElementById(canvasId).getContext('2d'); - const chart = new Chart(ctx, { + const config = { type: 'bar', data: { labels, datasets }, options: { @@ -271,15 +360,28 @@ export const results = { y: { stacked: true, ticks: { color: '#9ea0a6', stepSize: 1 }, grid: { color: '#2a2a2e' } } } } - }); + }; + + const canvasId = `chart-${Math.random().toString(36).substr(2, 9)}`; + this.createChartContainer(canvasId, config); + + const ctx = document.getElementById(canvasId).getContext('2d'); + const chart = new Chart(ctx, config); activeCharts.push(chart); }, - createChartContainer(canvasId) { + createChartContainer(canvasId, config) { const container = document.createElement('div'); container.className = 'chart-container'; - container.innerHTML = `<canvas id="${canvasId}"></canvas>`; + container.innerHTML = `<canvas id="${canvasId}" title="Click to expand"></canvas>`; ui.elements.interactiveCharts.appendChild(container); + + setTimeout(() => { + const canvas = document.getElementById(canvasId); + if (canvas && config) { + canvas.addEventListener('click', () => this.openChartModal(config)); + } + }, 0); }, getChartOptions(title) { diff --git a/api/static/js/ui.js b/api/static/js/ui.js index 8dd9586..2c402ab 100644 --- a/api/static/js/ui.js +++ b/api/static/js/ui.js @@ -16,6 +16,10 @@ export const ui = { interactiveCharts: document.getElementById('interactive-charts'), dataStats: document.getElementById('data-stats'), + chartModal: document.getElementById('chart-modal'), + closeModalBtn: document.getElementById('close-modal-btn'), + modalChartContainer: document.getElementById('modal-chart-container'), + tabTriggers: document.querySelectorAll('.tab-trigger'), tabPanels: document.querySelectorAll('.tab-panel') }; |
