summaryrefslogtreecommitdiff
path: root/api/static
diff options
context:
space:
mode:
authorCaptainJack2491 <jayrupnakawala@gmail.com>2026-04-02 20:44:07 +0100
committerCaptainJack2491 <jayrupnakawala@gmail.com>2026-04-02 20:44:07 +0100
commita72e0d6e93e6ea8d44b4ca86d51903bfbf4e21d5 (patch)
treeee6c688d35ce2e4b74cd6d14b2e0de9bf6f00850 /api/static
parent32aa0344ed57a0fe6e6bc9225af9eb851bc3c6b6 (diff)
feat(dashboard): add interactive charts and layout optimizations
- Separated Static Visualizations into dedicated tab with fullscreen image modal - Added new interactive charts: Glassbox Category by Scenario, Intent vs Reality, and Radar Chart - Replaced average token tracking with global token aggregations - Implemented Grouped-Stacked Bar Chart for visual comparison of Action alongside Sophistication by oversight tier - Added standardized color palettes to improve chart legibility
Diffstat (limited to 'api/static')
-rw-r--r--api/static/index.html13
-rw-r--r--api/static/js/components/results.js356
2 files changed, 301 insertions, 68 deletions
diff --git a/api/static/index.html b/api/static/index.html
index c22de49..cbe65a2 100644
--- a/api/static/index.html
+++ b/api/static/index.html
@@ -82,7 +82,8 @@
<main class="main-viewport">
<header class="app-header">
<div class="tab-nav" style="border-bottom: none; background: none; padding: 0;">
- <button class="tab-trigger active" data-tab="images">Visual Analytics</button>
+ <button class="tab-trigger active" data-tab="images">Interactive Charts</button>
+ <button class="tab-trigger" data-tab="static">Static Visualizations</button>
<button class="tab-trigger" data-tab="csv">Raw Dataset</button>
<button class="tab-trigger" data-tab="judge">Judge Reports</button>
</div>
@@ -98,9 +99,13 @@
<div id="interactive-charts" class="image-grid" style="margin-top: var(--space-md);">
<div class="empty-state" style="grid-column: 1 / -1;">Select a CSV to generate visuals.</div>
</div>
-
- <h3 style="margin: var(--space-xl) var(--space-lg) var(--space-sm); font-size: 0.8rem; text-transform: uppercase; color: var(--color-text-muted); border-bottom: 1px solid var(--color-bg-3); padding-bottom: 8px;">Static Visualizations (viz/)</h3>
- <div id="image-results" class="image-grid">
+ </div>
+
+ <div id="tab-static" class="tab-panel">
+ <div class="results-controls">
+ <h2 style="font-size: 1.1rem; color: var(--color-text-primary); margin: 0;">Static Visualizations (viz/)</h2>
+ </div>
+ <div id="image-results" class="image-grid" style="margin-top: var(--space-md);">
<div class="empty-state" style="grid-column: 1 / -1;">No visualizations generated.</div>
</div>
</div>
diff --git a/api/static/js/components/results.js b/api/static/js/components/results.js
index d75250c..b86c7fa 100644
--- a/api/static/js/components/results.js
+++ b/api/static/js/components/results.js
@@ -93,6 +93,11 @@ export const results = {
});
},
+ openImageModal(imgSrc) {
+ ui.elements.chartModal.style.display = 'flex';
+ ui.elements.modalChartContainer.innerHTML = `<img src="${imgSrc}" style="width: 100%; height: 100%; object-fit: contain;">`;
+ },
+
async updateCSVFileList() {
try {
const files = await api.get('/api/results/files');
@@ -618,11 +623,8 @@ export const results = {
inputArea.style.display = 'block';
statusEl.innerHTML = `<span style="color: var(--color-success);">Session Active (${data.model})</span>`;
- // Force scroll after layout changes (such as displaying inputArea)
- setTimeout(() => {
- const historyEl = document.getElementById('interrogate-chat-history');
- if (historyEl) historyEl.scrollTop = historyEl.scrollHeight;
- }, 10);
+ // Force robust scroll after layout changes
+ this.scrollToBottom(document.getElementById('interrogate-chat-history'));
} catch (err) {
startBtn.innerHTML = 'Start Session';
@@ -650,7 +652,17 @@ export const results = {
html += `</div>`;
historyEl.innerHTML = html;
- historyEl.scrollTop = historyEl.scrollHeight;
+ this.scrollToBottom(historyEl);
+ },
+
+ scrollToBottom(el) {
+ if (!el) return;
+ // Two rAF frames ensure DOM is drawn and layout flows are complete before height is calculated
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ el.scrollTop = el.scrollHeight;
+ });
+ });
},
async sendInterrogationMessage() {
@@ -677,7 +689,7 @@ export const results = {
<span style="margin-left: 30px; font-style: italic; color: var(--color-accent);">Agent is thinking... (this may take up to 30s)</span>
</div>
`);
- historyEl.scrollTop = historyEl.scrollHeight;
+ this.scrollToBottom(historyEl);
statusEl.textContent = 'Processing request...';
@@ -714,7 +726,7 @@ export const results = {
inputField.disabled = false;
sendBtn.disabled = false;
inputField.focus();
- historyEl.scrollTop = historyEl.scrollHeight;
+ this.scrollToBottom(historyEl);
}
},
@@ -755,66 +767,36 @@ export const results = {
this.renderStackedBarChart('Blackbox Category by Oversight', data, 'oversight', 'blackbox_category');
}
- if (cols.includes('model') && cols.includes('total_tokens')) {
- this.renderAverageTokenUsageChart('Average Token Usage by Model', data);
- this.renderTotalTokenUsageChart('Total Token Usage by Model', data);
+ if (cols.includes('scenario') && cols.includes('glassbox_category')) {
+ this.renderStackedBarChart('Glassbox Category by Scenario', data, 'scenario', 'glassbox_category');
}
- if (ui.elements.interactiveCharts.innerHTML === '') {
- ui.elements.interactiveCharts.innerHTML = '<div class="empty-state" style="grid-column: 1 / -1;">No plottable categorical columns found.</div>';
- }
- },
-
- renderAverageTokenUsageChart(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 };
+ if (cols.includes('oversight') && cols.includes('blackbox_category') && cols.includes('glassbox_sophistication')) {
+ this.renderMergedActionSophisticationChart('Effects of Oversight on Action & Sophistication', data);
+ } else {
+ if (cols.includes('oversight') && cols.includes('blackbox_category')) {
+ this.renderStackedBarChart('Blackbox Category by Oversight', data, 'oversight', 'blackbox_category');
}
- 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: 'Avg Prompt Tokens',
- data: labels.map(l => metrics[l].prompt / metrics[l].count),
- backgroundColor: '#3b82f6',
- },
- {
- label: 'Avg Completion Tokens',
- data: labels.map(l => metrics[l].completion / metrics[l].count),
- backgroundColor: '#10b981',
+ if (cols.includes('oversight') && cols.includes('glassbox_sophistication')) {
+ this.renderStackedBarChart('Effects of Oversight on Sophistication', data, 'oversight', 'glassbox_sophistication');
}
- ];
+ }
- 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' } }
- }
- }
- };
+ if (cols.includes('oversight') && cols.includes('glassbox_category') && cols.includes('blackbox_category')) {
+ this.renderIntentVsRealityChart('Impact of Oversight on Deception (Intent vs Reality)', data);
+ }
- const canvasId = `chart-${Math.random().toString(36).substr(2, 9)}`;
- this.createChartContainer(canvasId, config);
+ if (cols.includes('model') && cols.includes('glassbox_category') && cols.includes('blackbox_category')) {
+ this.renderRadarChart('Model Behavior Profile (Radar)', data);
+ }
- const ctx = document.getElementById(canvasId).getContext('2d');
- const chart = new Chart(ctx, config);
- activeCharts.push(chart);
+ if (cols.includes('model') && cols.includes('total_tokens')) {
+ this.renderTotalTokenUsageChart('Total 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>';
+ }
},
renderTotalTokenUsageChart(title, data) {
@@ -946,6 +928,246 @@ export const results = {
activeCharts.push(chart);
},
+ renderIntentVsRealityChart(title, data) {
+ // Group by oversight
+ const oversightCounts = {};
+ data.forEach(row => {
+ const oversight = row.oversight || 'Unknown';
+ if (!oversightCounts[oversight]) {
+ oversightCounts[oversight] = { total: 0, deceptiveIntent: 0, deceptiveAction: 0 };
+ }
+ oversightCounts[oversight].total += 1;
+
+ const gCat = (row.glassbox_category || '').toLowerCase();
+ const bCat = (row.blackbox_category || '').toLowerCase();
+
+ if (gCat.includes('decepti') || gCat.includes('malicious') || gCat.includes('omission') || gCat.includes('strategic') || gCat.includes('complian') || gCat.includes('fabricat')) {
+ oversightCounts[oversight].deceptiveIntent += 1;
+ }
+
+ if (bCat.includes('deceptive') || bCat.includes('fabricated') || bCat.includes('omitted') || bCat.includes('suspicious') || bCat.includes('misleading')) {
+ oversightCounts[oversight].deceptiveAction += 1;
+ }
+ });
+
+ const labels = Object.keys(oversightCounts);
+ const oversightPriority = { 'low': 0, 'mid': 1, 'medium': 1, 'high': 2 };
+ labels.sort((a, b) => (oversightPriority[a.toLowerCase()] ?? 99) - (oversightPriority[b.toLowerCase()] ?? 99));
+
+ if (labels.length === 0) return;
+
+ const datasets = [
+ {
+ label: 'Deceptive Intent (%)',
+ data: labels.map(l => (oversightCounts[l].deceptiveIntent / oversightCounts[l].total) * 100),
+ backgroundColor: '#f59e0b',
+ },
+ {
+ label: 'Deceptive Action (%)',
+ data: labels.map(l => (oversightCounts[l].deceptiveAction / oversightCounts[l].total) * 100),
+ backgroundColor: '#ef4444',
+ }
+ ];
+
+ const config = {
+ type: 'bar',
+ data: { labels, datasets },
+ options: {
+ ...this.getChartOptions(title),
+ scales: {
+ x: { ticks: { color: '#9ea0a6' }, grid: { color: '#2a2a2e' } },
+ y: {
+ title: { display: true, text: 'Percentage (%)', color: '#9ea0a6' },
+ ticks: { color: '#9ea0a6', stepSize: 20 },
+ grid: { color: '#2a2a2e' },
+ min: 0,
+ max: 100
+ }
+ }
+ }
+ };
+
+ 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);
+ },
+
+ renderMergedActionSophisticationChart(title, data) {
+ const xLabels = new Set();
+ const actionMatrix = {};
+ const sophMatrix = {};
+ const actionGroups = new Set();
+ const sophGroups = new Set();
+
+ data.forEach(row => {
+ const oversight = row.oversight || 'Unknown';
+ xLabels.add(oversight);
+
+ const action = row.blackbox_category || 'Unknown';
+ if (!actionMatrix[oversight]) actionMatrix[oversight] = {};
+ actionMatrix[oversight][action] = (actionMatrix[oversight][action] || 0) + 1;
+ actionGroups.add(action);
+
+ const soph = row.glassbox_sophistication || 'Unknown';
+ if (!sophMatrix[oversight]) sophMatrix[oversight] = {};
+ sophMatrix[oversight][soph] = (sophMatrix[oversight][soph] || 0) + 1;
+ sophGroups.add(soph);
+ });
+
+ const labels = Array.from(xLabels);
+ const oversightPriority = { 'low': 0, 'mid': 1, 'medium': 1, 'high': 2 };
+ labels.sort((a, b) => (oversightPriority[a.toLowerCase()] ?? 99) - (oversightPriority[b.toLowerCase()] ?? 99));
+
+ if (labels.length === 0) return;
+
+ const getActionColor = (cat) => {
+ const s = cat.toUpperCase();
+ if (s.includes('ACCURATE')) return '#10b981'; // Emerald
+ if (s.includes('FABRICAT')) return '#ef4444'; // Red
+ if (s.includes('MISLEAD')) return '#f59e0b'; // Amber
+ if (s.includes('OMIT')) return '#8b5cf6'; // Violet
+ return '#6366f1'; // Indigo (fallback)
+ };
+
+ const getSophColor = (cat) => {
+ const s = cat.toUpperCase();
+ if (s.includes('UNKNOWN')) return '#cbd5e1'; // Slate 300
+ if (s.includes('NAIVE') || s.includes('BASIC')) return '#9ca3af'; // Gray 400
+ if (s.includes('REACTIVE')) return '#64748b'; // Slate 500
+ if (s.includes('TACTICAL')) return '#3b82f6'; // Blue 500
+ if (s.includes('STRATEGIC')) return '#1d4ed8'; // Blue 700
+ return '#475569'; // Slate 600 (fallback)
+ };
+
+ const datasets = [];
+
+ Array.from(actionGroups).sort().forEach((group) => {
+ datasets.push({
+ label: group + ' (Action)',
+ data: labels.map(label => actionMatrix[label][group] || 0),
+ backgroundColor: getActionColor(group),
+ stack: 'Action'
+ });
+ });
+
+ Array.from(sophGroups).sort().forEach((group) => {
+ datasets.push({
+ label: group + ' (Soph)',
+ data: labels.map(label => sophMatrix[label][group] || 0),
+ backgroundColor: getSophColor(group),
+ stack: 'Sophistication'
+ });
+ });
+
+ 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);
+ },
+
+ renderRadarChart(title, data) {
+ const metrics = {};
+
+ data.forEach(row => {
+ const model = row.model || 'Unknown';
+ if (!metrics[model]) {
+ metrics[model] = { total: 0, honestIntent: 0, deceptiveIntent: 0, accurateAction: 0, deceptiveAction: 0 };
+ }
+ metrics[model].total += 1;
+
+ const gCat = (row.glassbox_category || '').toLowerCase();
+ const bCat = (row.blackbox_category || '').toLowerCase();
+
+ if (gCat.includes('honest') || gCat.includes('benign') || gCat.includes('accurate') || gCat.includes('failure')) {
+ metrics[model].honestIntent += 1;
+ } else if (gCat.includes('decepti') || gCat.includes('omission') || gCat.includes('complian') || gCat.includes('strategic')) {
+ metrics[model].deceptiveIntent += 1;
+ }
+
+ if (bCat.includes('accurate') || bCat.includes('honest') || bCat.includes('benign')) {
+ metrics[model].accurateAction += 1;
+ } else {
+ metrics[model].deceptiveAction += 1;
+ }
+ });
+
+ const models = Object.keys(metrics).sort();
+ if (models.length === 0) return;
+
+ const radarLabels = ['Honest Intent', 'Deceptive Intent', 'Accurate Action', 'Deceptive Action'];
+
+ const colors = [
+ 'rgba(59, 130, 246, 0.5)',
+ 'rgba(16, 185, 129, 0.5)',
+ 'rgba(245, 158, 11, 0.5)',
+ 'rgba(239, 68, 68, 0.5)',
+ 'rgba(157, 78, 221, 0.5)',
+ ];
+ const borderColors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#9d4edd'];
+
+ const datasets = models.map((model, idx) => {
+ const total = metrics[model].total || 1;
+ return {
+ label: model,
+ data: [
+ (metrics[model].honestIntent / total) * 100,
+ (metrics[model].deceptiveIntent / total) * 100,
+ (metrics[model].accurateAction / total) * 100,
+ (metrics[model].deceptiveAction / total) * 100
+ ],
+ backgroundColor: colors[idx % colors.length],
+ borderColor: borderColors[idx % borderColors.length],
+ pointBackgroundColor: borderColors[idx % borderColors.length],
+ borderWidth: 2,
+ };
+ });
+
+ const config = {
+ type: 'radar',
+ data: { labels: radarLabels, datasets },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ title: { display: true, text: title, color: '#f0f0f2', font: { family: 'Inter', size: 14 } },
+ legend: { labels: { color: '#9ea0a6' }, position: 'bottom' }
+ },
+ scales: {
+ r: {
+ angleLines: { color: 'rgba(255, 255, 255, 0.1)' },
+ grid: { color: 'rgba(255, 255, 255, 0.1)' },
+ pointLabels: { color: '#9ea0a6', font: { size: 12 } },
+ ticks: { display: false, min: 0, max: 100, stepSize: 20 }
+ }
+ }
+ }
+ };
+
+ 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';
@@ -976,11 +1198,17 @@ export const results = {
if (images.length === 0) return;
ui.elements.imageResults.innerHTML = images.map(img => `
- <div class="image-card">
- <img src="/api/results/images/${img.name}" alt="${img.name}" loading="lazy">
- <div class="image-card-footer">${img.name}</div>
+ <div class="image-card static-viz-card" data-src="/api/results/images/${img.name}" style="cursor: pointer;">
+ <img src="/api/results/images/${img.name}" alt="${img.name}" loading="lazy" style="pointer-events: none;">
+ <div class="image-card-footer" style="pointer-events: none;">${img.name}</div>
</div>
`).join('');
+
+ ui.elements.imageResults.querySelectorAll('.static-viz-card').forEach(card => {
+ card.addEventListener('click', () => {
+ this.openImageModal(card.dataset.src);
+ });
+ });
},
async loadJudgeData(filePath) {