summaryrefslogtreecommitdiff
path: root/api
diff options
context:
space:
mode:
authorCaptainJack2491 <jayrupnakawala@gmail.com>2026-03-09 20:16:05 +0000
committerCaptainJack2491 <jayrupnakawala@gmail.com>2026-03-09 20:16:05 +0000
commit17acb7a802d6d6f844c51dca5f2f171e360eca8d (patch)
treefd3ad7642e06289a43bc0f059307bff07aed4cd9 /api
parent1bbb4938d9f714eb29c8981e0d834ce85ccafacc (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')
-rw-r--r--api/static/css/components.css45
-rw-r--r--api/static/index.html8
-rw-r--r--api/static/js/components/results.js130
-rw-r--r--api/static/js/ui.js4
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">&times;</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')
};