/**
* results.js - Data visualization and reports
*/
import { api } from '../api.js';
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() {
// Bind events
ui.elements.csvFileSelect.addEventListener('change', (e) => this.handleCsvChange(e.target.value));
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(),
this.loadImages()
]);
},
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 = '';
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');
const options = files.length > 0
? files.map(f => ``).join('')
: '';
ui.elements.csvFileSelect.innerHTML = options;
const defaultFile = files.find(f => f.endsWith('results.csv')) || files[0];
if (defaultFile) {
ui.elements.csvFileSelect.value = defaultFile;
await this.handleCsvChange(defaultFile);
}
} catch (error) {
console.error("Failed loading CSV list", error);
}
},
async updateJudgeFileList() {
try {
const files = await api.get('/api/judge/files');
const options = files.length > 0
? files.map(f => ``).join('')
: '';
ui.elements.judgeFileSelect.innerHTML = options;
if (files.length > 0) {
ui.elements.judgeFileSelect.value = files[0];
await this.loadJudgeData(files[0]);
}
} catch (error) {
console.error("Failed loading Judge file list", error);
}
},
async handleCsvChange(filePath) {
if (!filePath) return;
ui.elements.csvResults.innerHTML = '
Loading data...
';
ui.elements.interactiveCharts.innerHTML = 'Loading charts...
';
try {
const data = await api.get(`/api/results?file=${encodeURIComponent(filePath)}`);
const content = Object.values(data)[0];
if (!content || content.error) {
const err = content?.error || "Unknown error";
ui.elements.csvResults.innerHTML = `Error: ${err}
`;
ui.elements.interactiveCharts.innerHTML = `Error: ${err}
`;
return;
}
currentCsvData = content.data;
currentCsvColumns = content.columns;
this.populateFilters();
this.applyFilters(); // This draws charts and tables
} catch (err) {
ui.elements.csvResults.innerHTML = `Error: ${err.message}
`;
ui.elements.interactiveCharts.innerHTML = `Error: ${err.message}
`;
}
},
populateFilters() {
const models = new Set();
const scenarios = new Set();
const oversights = new Set();
currentCsvData.forEach(row => {
if (row.model) models.add(row.model);
if (row.scenario) scenarios.add(row.scenario);
if (row.oversight) oversights.add(row.oversight);
});
this.renderCheckboxList(ui.elements.filterModelList, Array.from(models).sort(), 'model');
this.renderCheckboxList(ui.elements.filterScenarioList, Array.from(scenarios).sort(), 'scenario');
// Custom Oversight Sorting
const oversightPriority = { 'low': 0, 'mid': 1, 'medium': 1, 'high': 2 };
const sortedOversights = Array.from(oversights).sort((a, b) => {
const pA = oversightPriority[a.toLowerCase()] ?? 99;
const pB = oversightPriority[b.toLowerCase()] ?? 99;
return pA - pB;
});
this.renderCheckboxList(ui.elements.filterOversightList, sortedOversights, 'oversight');
},
renderCheckboxList(container, items, type) {
if (items.length === 0) {
container.innerHTML = 'No data
';
return;
}
container.innerHTML = items.map(item => `
`).join('');
},
applyFilters() {
if (!currentCsvData) return;
const getChecked = (name) => Array.from(document.querySelectorAll(`input[name="filter-${name}"]:checked`)).map(cb => cb.value);
const modelFilters = getChecked('model');
const scenarioFilters = getChecked('scenario');
const oversightFilters = getChecked('oversight');
const filteredData = currentCsvData.filter(row => {
return (modelFilters.length === 0 || modelFilters.includes(row.model)) &&
(scenarioFilters.length === 0 || scenarioFilters.includes(row.scenario)) &&
(oversightFilters.length === 0 || oversightFilters.includes(row.oversight));
});
ui.elements.dataStats.textContent = `Showing ${filteredData.length} of ${currentCsvData.length} records`;
this.renderTable(filteredData);
this.renderCharts(filteredData);
},
renderTable(data) {
if (data.length === 0) {
ui.elements.csvResults.innerHTML = 'No data matches filters.
';
return;
}
let html = `
${currentCsvColumns.map(c => `| ${c} | `).join('')}
${data.map(row => `
${currentCsvColumns.map(col => `| ${row[col] ?? ''} | `).join('')}
`).join('')}
`;
ui.elements.csvResults.innerHTML = html;
},
renderCharts(data) {
activeCharts.forEach(chart => chart.destroy());
activeCharts = [];
ui.elements.interactiveCharts.innerHTML = '';
if (data.length === 0) {
ui.elements.interactiveCharts.innerHTML = 'No data to chart.
';
return;
}
const cols = currentCsvColumns;
if (cols.includes('blackbox_category')) {
this.renderDoughnutChart('Blackbox Categories', data, 'blackbox_category');
}
if (cols.includes('glassbox_category')) {
this.renderDoughnutChart('Glassbox Categories', data, 'glassbox_category');
}
if (cols.includes('model') && cols.includes('blackbox_category')) {
this.renderStackedBarChart('Blackbox Category by Model', data, 'model', 'blackbox_category');
}
if (cols.includes('oversight') && cols.includes('blackbox_category')) {
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 = 'No plottable categorical columns found.
';
}
},
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 => {
const val = row[column] || 'Unknown';
counts[val] = (counts[val] || 0) + 1;
});
const config = {
type: 'doughnut',
data: {
labels: Object.keys(counts),
datasets: [{
data: Object.values(counts),
backgroundColor: ['#9d4edd', '#ef4444', '#10b981', '#f59e0b', '#3b82f6', '#6366f1'],
borderWidth: 0
}]
},
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);
},
renderStackedBarChart(title, data, xCol, groupCol) {
const matrix = {};
const groups = new Set();
data.forEach(row => {
const xVal = row[xCol] || 'Unknown';
const gVal = row[groupCol] || 'Unknown';
if (!matrix[xVal]) matrix[xVal] = {};
matrix[xVal][gVal] = (matrix[xVal][gVal] || 0) + 1;
groups.add(gVal);
});
let labels = Object.keys(matrix);
// Custom sorting for X-axis labels (like Oversight)
if (xCol === 'oversight') {
const oversightPriority = { 'low': 0, 'mid': 1, 'medium': 1, 'high': 2 };
labels.sort((a, b) => (oversightPriority[a.toLowerCase()] ?? 99) - (oversightPriority[b.toLowerCase()] ?? 99));
} else {
labels.sort();
}
const datasets = Array.from(groups).map((group, i) => {
const colors = ['#ef4444', '#10b981', '#f59e0b', '#3b82f6', '#9d4edd', '#6366f1'];
return {
label: group,
data: labels.map(label => matrix[label][group] || 0),
backgroundColor: colors[i % colors.length],
}
});
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', 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, config) {
const container = document.createElement('div');
container.className = 'chart-container';
container.innerHTML = ``;
ui.elements.interactiveCharts.appendChild(container);
setTimeout(() => {
const canvas = document.getElementById(canvasId);
if (canvas && config) {
canvas.addEventListener('click', () => this.openChartModal(config));
}
}, 0);
},
getChartOptions(title) {
return {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: { display: true, text: title, color: '#f0f0f2', font: { family: 'Inter', size: 14 } },
legend: { labels: { color: '#9ea0a6' }, position: 'bottom' }
}
};
},
async loadImages() {
const images = await api.get('/api/results/images');
if (images.length === 0) return;
ui.elements.imageResults.innerHTML = images.map(img => `
`).join('');
},
async loadJudgeData(filePath) {
if (!filePath) return;
ui.elements.judgeResults.innerHTML = 'Loading data...
';
try {
const data = await api.get(`/api/judge/results?file=${encodeURIComponent(filePath)}`);
if (Object.keys(data).length === 0 || data.message) {
ui.elements.judgeResults.innerHTML = 'No data available in this report.
';
return;
}
const content = Object.values(data)[0];
if (content.error) {
ui.elements.judgeResults.innerHTML = `Error: ${content.error}
`;
return;
}
let html = '';
if (content.type === 'csv') {
html += `
${content.columns.map(c => `| ${c} | `).join('')}
${content.data.map(row => `
${content.columns.map(col => `| ${row[col] ?? ''} | `).join('')}
`).join('')}
`;
} else {
html += `
${JSON.stringify(content.data, null, 2)}
`;
}
ui.elements.judgeResults.innerHTML = html;
} catch (err) {
ui.elements.judgeResults.innerHTML = `Error: ${err.message}
`;
}
}
};