From d3d0e0825ea4b4ed3ab338603b824af0178e5264 Mon Sep 17 00:00:00 2001 From: CaptainJack2491 Date: Mon, 9 Mar 2026 19:41:49 +0000 Subject: feat: pivot Web GUI to dedicated Data Visualization Dashboard This commit: - Removes all execution logic (runs, cancellation, SSE logs) - Strips the terminal and configuration sidebar - Implements a global filtering panel (Model, Scenario, Oversight) - Dedicated the UI 100% to interactive Chart.js and static visuals --- api/static/js/components/config.js | 107 ---------------- api/static/js/components/results.js | 239 +++++++++++++++++++---------------- api/static/js/components/terminal.js | 49 ------- 3 files changed, 132 insertions(+), 263 deletions(-) delete mode 100644 api/static/js/components/config.js delete mode 100644 api/static/js/components/terminal.js (limited to 'api/static/js/components') diff --git a/api/static/js/components/config.js b/api/static/js/components/config.js deleted file mode 100644 index d87c874..0000000 --- a/api/static/js/components/config.js +++ /dev/null @@ -1,107 +0,0 @@ -/** - * config.js - Experiment configuration and execution - */ -import { api } from '../api.js'; -import { ui } from '../ui.js'; -import { terminal } from './terminal.js'; -import { results } from './results.js'; - -let isRunning = false; - -export const config = { - async init() { - await Promise.all([ - this.loadScenarios(), - this.loadModels(), - this.loadDefaults() - ]); - - ui.elements.runBtn.addEventListener('click', () => this.startRun()); - ui.elements.cancelBtn.addEventListener('click', () => this.stopRun()); - - // Scenario detail updates - ui.elements.scenarioSelect.addEventListener('change', (e) => this.updateOversight(e.target.value)); - }, - - async loadScenarios() { - const scenarios = await api.get('/api/scenarios'); - ui.elements.scenarioSelect.innerHTML = scenarios.map(s => - `` - ).join(''); - if (scenarios[0]) this.updateOversight(scenarios[0].name); - }, - - async loadModels() { - const models = await api.get('/api/models'); - ui.elements.modelSelect.innerHTML = models.map(m => - `` - ).join(''); - }, - - async loadDefaults() { - const cfg = await api.get('/api/config'); - if (cfg.defaults?.oversight) { - ui.elements.oversightSelect.value = cfg.defaults.oversight; - } - }, - - async updateOversight(scenarioName) { - if (!scenarioName) return; - const details = await api.get(`/api/scenarios/${scenarioName}`); - const levels = details.oversight_levels || ['low', 'mid', 'high']; - ui.elements.oversightSelect.innerHTML = levels.map(l => - `` - ).join(''); - }, - - async startRun() { - const data = { - scenario: ui.elements.scenarioSelect.value, - model: ui.elements.modelSelect.value, - oversight: ui.elements.oversightSelect.value, - runs: ui.elements.runsInput.value - }; - - this.setRunningState(true); - terminal.clear(); - terminal.append(`[SYSTEM] Starting experiment: ${data.scenario} | ${data.model}`); - - try { - await api.post('/api/run', data); - api.streamLogs( - (log) => terminal.append(log), - () => { - this.setRunningState(false); - ui.updateStatus('complete'); - terminal.append('[SYSTEM] Run completed successfully.'); - results.loadAll(); - }, - (err) => { - this.setRunningState(false); - ui.updateStatus('error'); - terminal.append(`[ERROR] Stream disconnected: ${err}`); - } - ); - ui.updateStatus('running', new Date()); - } catch (err) { - this.setRunningState(false); - ui.updateStatus('error'); - terminal.append(`[ERROR] Failed to start run: ${err.message}`); - } - }, - - async stopRun() { - try { - await api.delete('/api/run'); - terminal.append('[SYSTEM] Cancel request sent.'); - } catch (err) { - terminal.append(`[ERROR] Cancel failed: ${err.message}`); - } - }, - - setRunningState(running) { - isRunning = running; - ui.elements.runBtn.disabled = running; - ui.elements.cancelBtn.disabled = !running; - } -}; diff --git a/api/static/js/components/results.js b/api/static/js/components/results.js index d06d7fc..92514c4 100644 --- a/api/static/js/components/results.js +++ b/api/static/js/components/results.js @@ -4,24 +4,21 @@ import { api } from '../api.js'; import { ui } from '../ui.js'; -let activeCharts = []; // Store chart instances to destroy them before redraw +let activeCharts = []; +let currentCsvData = null; // Store fetched data locally for quick filtering +let currentCsvColumns = []; export const results = { async init() { // Bind events - ui.elements.csvFileSelect.addEventListener('change', (e) => this.loadCSVData(e.target.value)); - ui.elements.chartFileSelect.addEventListener('change', (e) => this.loadChartData(e.target.value)); + 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()); - // Initial lists - await this.loadAll(); - }, - - async loadAll() { await Promise.all([ this.updateCSVFileList(), this.updateJudgeFileList(), - this.loadImages() // Static images just load once + this.loadImages() ]); }, @@ -33,19 +30,14 @@ export const results = { : ''; ui.elements.csvFileSelect.innerHTML = options; - ui.elements.chartFileSelect.innerHTML = options; - // Prefer loading results.csv by default if it exists const defaultFile = files.find(f => f.endsWith('results.csv')) || files[0]; - if (defaultFile) { ui.elements.csvFileSelect.value = defaultFile; - ui.elements.chartFileSelect.value = defaultFile; - await this.loadCSVData(defaultFile); - await this.loadChartData(defaultFile); + await this.handleCsvChange(defaultFile); } } catch (error) { - console.error("Failed loading CSV file list", error); + console.error("Failed loading CSV list", error); } }, @@ -57,7 +49,6 @@ export const results = { : ''; ui.elements.judgeFileSelect.innerHTML = options; - if (files.length > 0) { ui.elements.judgeFileSelect.value = files[0]; await this.loadJudgeData(files[0]); @@ -67,92 +58,130 @@ export const results = { } }, - async loadCSVData(filePath) { + 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)}`); - if (Object.keys(data).length === 0 || data.error) { - ui.elements.csvResults.innerHTML = '
Failed to load data.
'; - return; - } - - // Since we asked for a specific file, it's the only key const content = Object.values(data)[0]; - if (content.error) { - ui.elements.csvResults.innerHTML = `
Error: ${content.error}
`; + + if (!content || content.error) { + const err = content?.error || "Unknown error"; + ui.elements.csvResults.innerHTML = `
Error: ${err}
`; + ui.elements.interactiveCharts.innerHTML = `
Error: ${err}
`; return; } - let html = `
-
${filePath}
-
- - ${content.columns.map(c => ``).join('')} - - ${content.data.map(row => ` - ${content.columns.map(col => ``).join('')} - `).join('')} - -
${c}
${row[col] ?? ''}
-
-
`; + currentCsvData = content.data; + currentCsvColumns = content.columns; - ui.elements.csvResults.innerHTML = html; + 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}
`; } }, - async loadChartData(filePath) { - if (!filePath) return; + 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); + }); + + ui.elements.filterModel.innerHTML = '' + + Array.from(models).sort().map(m => ``).join(''); + + ui.elements.filterScenario.innerHTML = '' + + Array.from(scenarios).sort().map(s => ``).join(''); + + ui.elements.filterOversight.innerHTML = '' + + Array.from(oversights).sort().map(o => ``).join(''); + }, + + applyFilters() { + if (!currentCsvData) return; + + const modelFilter = ui.elements.filterModel.value; + const scenarioFilter = ui.elements.filterScenario.value; + const oversightFilter = ui.elements.filterOversight.value; + + const filteredData = currentCsvData.filter(row => { + return (modelFilter === 'all' || row.model === modelFilter) && + (scenarioFilter === 'all' || row.scenario === scenarioFilter) && + (oversightFilter === 'all' || row.oversight === oversightFilter); + }); + + 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 => ``).join('')} + + ${data.map(row => ` + ${currentCsvColumns.map(col => ``).join('')} + `).join('')} + +
${c}
${row[col] ?? ''}
+
+
`; - // Clean up old charts + ui.elements.csvResults.innerHTML = html; + }, + + renderCharts(data) { activeCharts.forEach(chart => chart.destroy()); activeCharts = []; - ui.elements.interactiveCharts.innerHTML = '
Generating charts...
'; + ui.elements.interactiveCharts.innerHTML = ''; - try { - const data = await api.get(`/api/results?file=${encodeURIComponent(filePath)}`); - const content = Object.values(data)[0]; - - if (!content || content.error || !content.data || content.data.length === 0) { - ui.elements.interactiveCharts.innerHTML = '
Not enough data to graph.
'; - return; - } + if (data.length === 0) { + ui.elements.interactiveCharts.innerHTML = '
No data to chart.
'; + return; + } - ui.elements.interactiveCharts.innerHTML = ''; - - // Generate charts based on available columns - const cols = content.columns; - - // 1. Blackbox Categories - if (cols.includes('blackbox_category')) { - this.renderPieChart('Blackbox Categories', content.data, 'blackbox_category'); - } - - // 2. Glassbox Categories - if (cols.includes('glassbox_category')) { - this.renderPieChart('Glassbox Categories', content.data, 'glassbox_category'); - } + 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'); + } - // 3. Models vs Blackbox Category - if (cols.includes('model') && cols.includes('blackbox_category')) { - this.renderBarChart('Deception by Model', content.data, 'model', 'blackbox_category'); - } - - // 4. Fallback if no specific columns exist - if (ui.elements.interactiveCharts.innerHTML === '') { - ui.elements.interactiveCharts.innerHTML = '
No plottable categorical columns found in this CSV.
'; - } + if (cols.includes('model') && cols.includes('blackbox_category')) { + this.renderStackedBarChart('Blackbox Category by Model', data, 'model', 'blackbox_category'); + } - } catch (err) { - ui.elements.interactiveCharts.innerHTML = `
Chart Error: ${err.message}
`; + if (cols.includes('oversight') && cols.includes('blackbox_category')) { + this.renderStackedBarChart('Blackbox Category by Oversight', data, 'oversight', 'blackbox_category'); + } + + if (ui.elements.interactiveCharts.innerHTML === '') { + ui.elements.interactiveCharts.innerHTML = '
No plottable categorical columns found.
'; } }, - renderPieChart(title, data, column) { + renderDoughnutChart(title, data, column) { const counts = {}; data.forEach(row => { const val = row[column] || 'Unknown'; @@ -160,10 +189,7 @@ export const results = { }); const canvasId = `chart-${Math.random().toString(36).substr(2, 9)}`; - const container = document.createElement('div'); - container.className = 'chart-container'; - container.innerHTML = ``; - ui.elements.interactiveCharts.appendChild(container); + this.createChartContainer(canvasId); const ctx = document.getElementById(canvasId).getContext('2d'); const chart = new Chart(ctx, { @@ -176,20 +202,12 @@ export const results = { borderWidth: 0 }] }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - title: { display: true, text: title, color: '#f0f0f2', font: { family: 'Inter', size: 14 } }, - legend: { labels: { color: '#9ea0a6' }, position: 'right' } - } - } + options: this.getChartOptions(title) }); activeCharts.push(chart); }, - renderBarChart(title, data, xCol, groupCol) { - // Group by X then GroupCol + renderStackedBarChart(title, data, xCol, groupCol) { const matrix = {}; const groups = new Set(); @@ -203,7 +221,7 @@ export const results = { const labels = Object.keys(matrix); const datasets = Array.from(groups).map((group, i) => { - const colors = ['#ef4444', '#10b981', '#f59e0b', '#3b82f6', '#9d4edd']; + const colors = ['#ef4444', '#10b981', '#f59e0b', '#3b82f6', '#9d4edd', '#6366f1']; return { label: group, data: labels.map(label => matrix[label][group] || 0), @@ -212,31 +230,41 @@ export const results = { }); const canvasId = `chart-${Math.random().toString(36).substr(2, 9)}`; - const container = document.createElement('div'); - container.className = 'chart-container'; - container.innerHTML = ``; - ui.elements.interactiveCharts.appendChild(container); + this.createChartContainer(canvasId); const ctx = document.getElementById(canvasId).getContext('2d'); const chart = new Chart(ctx, { type: 'bar', data: { labels, datasets }, options: { - responsive: true, - maintainAspectRatio: false, + ...this.getChartOptions(title), scales: { x: { stacked: true, ticks: { color: '#9ea0a6' }, grid: { color: '#2a2a2e' } }, y: { stacked: true, ticks: { color: '#9ea0a6', stepSize: 1 }, grid: { color: '#2a2a2e' } } - }, - plugins: { - title: { display: true, text: title, color: '#f0f0f2', font: { family: 'Inter', size: 14 } }, - legend: { labels: { color: '#9ea0a6' } } } } }); activeCharts.push(chart); }, + createChartContainer(canvasId) { + const container = document.createElement('div'); + container.className = 'chart-container'; + container.innerHTML = ``; + ui.elements.interactiveCharts.appendChild(container); + }, + + 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; @@ -269,7 +297,6 @@ export const results = { let html = ''; if (content.type === 'csv') { html += `
-
${filePath}
${content.columns.map(c => ``).join('')} @@ -282,9 +309,7 @@ export const results = { `; } else { - // Render JSON as pretty block html += `
-
${filePath}
${JSON.stringify(content.data, null, 2)}
`; } @@ -293,4 +318,4 @@ export const results = { ui.elements.judgeResults.innerHTML = `
Error: ${err.message}
`; } } -}; +}; \ No newline at end of file diff --git a/api/static/js/components/terminal.js b/api/static/js/components/terminal.js deleted file mode 100644 index 77c9794..0000000 --- a/api/static/js/components/terminal.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * terminal.js - Log rendering logic - */ -import { ui } from '../ui.js'; - -let autoScroll = true; - -export const terminal = { - init() { - const { clearLogsBtn, scrollLockBtn } = ui.elements; - - clearLogsBtn.addEventListener('click', () => this.clear()); - - scrollLockBtn.addEventListener('click', () => { - autoScroll = !autoScroll; - scrollLockBtn.classList.toggle('active', autoScroll); - }); - }, - - append(message) { - const { logsContainer } = ui.elements; - const line = document.createElement('div'); - line.className = 'terminal-line'; - - // High-speed parsing for log levels - if (message.includes('[ERROR]')) line.classList.add('error'); - else if (message.includes('[WARN]')) line.classList.add('warning'); - else if (message.includes('[DEBUG]')) line.classList.add('muted'); - else if (message.includes('SUCCESS') || message.includes('complete')) line.classList.add('success'); - else if (message.includes('Reasoning:')) line.classList.add('reasoning'); - else line.classList.add('info'); - - line.textContent = message; - logsContainer.appendChild(line); - - if (autoScroll) { - logsContainer.scrollTop = logsContainer.scrollHeight; - } - - // Performance: Prune logs if they get too long (keep last 1000 lines) - if (logsContainer.children.length > 1000) { - logsContainer.removeChild(logsContainer.firstChild); - } - }, - - clear() { - ui.elements.logsContainer.innerHTML = '
Logs cleared.
'; - } -}; -- cgit v1.2.3
${c}