/**
* 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 currentCsvDir = ""; // Directory of the active CSV
let currentVisibleColumns = []; // Track which columns are shown in Table
let modalChart = null;
// Inspector state
let currentInspectorLog = null;
let currentInspectorRow = null;
let currentViewMode = 'visual'; // 'visual' or 'raw'
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();
});
// Inspector Modal
ui.elements.closeInspectorBtn.addEventListener('click', () => this.closeInspector());
ui.elements.inspectorModal.addEventListener('click', (e) => {
if (e.target === ui.elements.inspectorModal) this.closeInspector();
});
// Column Toggle
ui.elements.btnToggleColumns.addEventListener('click', (e) => {
e.stopPropagation();
const isVisible = ui.elements.columnDropdown.style.display === 'block';
ui.elements.columnDropdown.style.display = isVisible ? 'none' : 'block';
});
document.addEventListener('click', (e) => {
if (ui.elements.columnDropdown && !ui.elements.columnDropdown.contains(e.target) && e.target !== ui.elements.btnToggleColumns) {
ui.elements.columnDropdown.style.display = 'none';
}
});
// Inspector View Toggle
if (ui.elements.inspectorToggleBtns) {
ui.elements.inspectorToggleBtns.forEach(btn => {
btn.addEventListener('click', () => {
const mode = btn.dataset.view;
this.setViewMode(mode);
});
});
}
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
});
},
openImageModal(imgSrc) {
ui.elements.chartModal.style.display = 'flex';
ui.elements.modalChartContainer.innerHTML = ` `;
},
async updateCSVFileList() {
try {
const files = await api.get('/api/results/files');
const options = files.length > 0
? files.map(f => `${f} `).join('')
: 'No files found ';
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 => `${f} `).join('')
: 'No judge reports found ';
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;
// Store directory of CSV for relative log path resolution
if (filePath.includes('/')) {
currentCsvDir = filePath.substring(0, filePath.lastIndexOf('/')) + '/';
} else {
currentCsvDir = "";
}
// Default visibility: Hide very long or redundant columns initially
const hideByDefault = ['BLACKBOX_JUSTIFICATION', 'GLASSBOX_JUSTIFICATION', 'log_path', 'full_log', 'messages'];
currentVisibleColumns = currentCsvColumns.filter(c => !hideByDefault.includes(c));
this.renderColumnSelector();
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}
`;
}
},
renderColumnSelector() {
ui.elements.columnList.innerHTML = `
All
None
${currentCsvColumns.map(col => `
${col}
`).join('')}
`;
// Bind events
ui.elements.columnList.querySelectorAll('input').forEach(input => {
input.addEventListener('change', () => {
const col = input.value;
if (input.checked) {
if (!currentVisibleColumns.includes(col)) currentVisibleColumns.push(col);
} else {
currentVisibleColumns = currentVisibleColumns.filter(c => c !== col);
}
this.applyFilters();
});
});
document.getElementById('cols-all').addEventListener('click', () => {
currentVisibleColumns = [...currentCsvColumns];
this.renderColumnSelector();
this.applyFilters();
});
document.getElementById('cols-none').addEventListener('click', () => {
currentVisibleColumns = [];
this.renderColumnSelector();
this.applyFilters();
});
},
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 => `
${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`;
if (ui.elements.csvTableStats) {
ui.elements.csvTableStats.textContent = `Found ${filteredData.length} records`;
}
this.renderTable(filteredData);
this.renderCharts(filteredData);
},
renderTable(data) {
if (data.length === 0) {
ui.elements.csvResults.innerHTML = 'No data matches filters.
';
return;
}
const visibleCols = currentVisibleColumns;
let html = `
${visibleCols.map(c => `${c} `).join('')}
${data.map((row, idx) => `
VIEW
${visibleCols.map(col => {
const val = row[col] ?? '';
const isLong = col.toLowerCase().includes('justification') || col.toLowerCase().includes('id');
return `${val} `;
}).join('')}
`).join('')}
`;
ui.elements.csvResults.innerHTML = html;
// Bind Row Click
ui.elements.csvResults.querySelectorAll('tr[data-idx]').forEach(tr => {
tr.addEventListener('click', (e) => {
const idx = tr.dataset.idx;
this.showRowInspector(data[idx]);
});
});
// Bind View Button (redundant but explicit)
ui.elements.csvResults.querySelectorAll('.btn-row-action').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const idx = btn.dataset.idx;
this.showRowInspector(data[idx]);
});
});
},
closeInspector() {
ui.elements.inspectorModal.style.display = 'none';
currentInspectorLog = null;
currentInspectorRow = null;
},
setViewMode(mode) {
currentViewMode = mode;
if (ui.elements.inspectorToggleBtns) {
ui.elements.inspectorToggleBtns.forEach(btn => {
btn.classList.toggle('active', btn.dataset.view === mode);
});
}
this.renderInspectorContent();
},
async showRowInspector(rowData) {
currentInspectorRow = rowData;
currentInspectorLog = null;
ui.elements.inspectorModal.style.display = 'flex';
ui.elements.inspectorTitle.textContent = `Run: ${rowData.run_id || rowData.RUN_ID || 'Details'}`;
this.setViewMode('visual'); // Default to visual
// Strategy to find the log:
let rawPath = rowData.log_path || rowData.RUN_ID || rowData.run_id;
if (!rawPath) {
ui.elements.inspectorContent.innerHTML = 'No log path metadata found in record.
';
return;
}
let jsonPath = rawPath.endsWith('.json') ? rawPath : (rawPath + '.json');
const trials = [jsonPath, currentCsvDir + jsonPath, 'logs/' + jsonPath];
let loaded = false;
for (const trial of trials) {
if (loaded) break;
try {
const logData = await api.get(`/api/logs/browse?path=${encodeURIComponent(trial)}`);
currentInspectorLog = logData;
currentInspectorLog._successfulPath = trial; // Track successful path
loaded = true;
this.renderInspectorContent();
} catch (err) {
console.log(`Failed trial: ${trial}`);
}
}
if (!loaded) {
ui.elements.inspectorContent.innerHTML = `
Failed to load log file.
Paths tried:
${trials.map(t => `- ${t}`).join(' ')}
`;
}
},
renderInspectorContent() {
if (!currentInspectorLog && currentViewMode === 'visual') {
this.renderVisualLoading();
return;
}
if (currentViewMode === 'visual') {
this.renderVisualInspector();
} else if (currentViewMode === 'raw') {
this.renderRawInspector();
} else if (currentViewMode === 'interrogate') {
this.renderInterrogateInspector();
}
},
renderVisualLoading() {
ui.elements.inspectorContent.innerHTML = `
Loading log file for full conversation view...
`;
},
renderRawInspector() {
const jsonStr = currentInspectorLog ? JSON.stringify(currentInspectorLog, null, 2) : "Log not loaded yet.";
ui.elements.inspectorContent.innerHTML = `
`;
},
renderRecordSidebar(rowData) {
return `
${Object.entries(rowData).map(([key, val]) => {
const hide = ['full_log', 'messages', 'log_path', 'RUN_ID', 'run_id'];
if (hide.includes(key)) return '';
const isJustification = key.includes('JUSTIFICATION');
return `
`;
}).join('')}
${currentInspectorLog?.final_vfs_state ? `
Final VFS State
${Object.keys(currentInspectorLog.final_vfs_state['/']?.data || {}).map(f => `
${f}
`).join('')}
` : ''}
`;
},
renderVisualInspector() {
const row = currentInspectorRow;
const log = currentInspectorLog;
let html = `
`;
// Conversation
if (log && log.conversation) {
html += `
`;
log.conversation.forEach(msg => {
html += this.renderChatMessage(msg);
});
html += `
`;
} else {
html += `
Conversation data not found in log.
`;
}
html += `
`;
ui.elements.inspectorContent.innerHTML = html;
},
renderChatMessage(msg) {
const role = msg.role;
const content = typeof msg.content === 'object' ? JSON.stringify(msg.content, null, 2) : (msg.content || "");
const reasoning = msg.reasoning || this.extractReasoningFromContent(content);
let cleanContent = typeof content === 'string' ? content.replace(/[\s\S]*?<\/thought>/g, '').trim() : content;
let html = `
`;
if (reasoning) {
const parsedReasoning = window.marked ? marked.parse(reasoning) : reasoning.replace(/\n/g, '
');
html += `
${parsedReasoning}
`;
}
// Only show main content for non-tool roles to avoid double rendering
if (cleanContent && role !== 'tool') {
const parsedContent = window.marked ? marked.parse(String(cleanContent)) : String(cleanContent).replace(/\n/g, '
');
html += `
${parsedContent}
`;
}
if (msg.tool_calls && msg.tool_calls.length > 0) {
html += `
`;
}
if (role === 'tool') {
const maxLength = 300;
const isLong = cleanContent.length > maxLength;
const displayContent = isLong ? cleanContent.substring(0, maxLength) + '...' : cleanContent;
const parsedToolOutput = window.marked ? marked.parse(displayContent) : displayContent.replace(/\n/g, '
');
html += `
Output ${isLong ? '(Truncated)' : ''}
${parsedToolOutput}
`;
}
html += `
`;
return html;
},
extractReasoningFromContent(content) {
if (typeof content !== 'string') return null;
const match = content.match(/([\s\S]*?)<\/thought>/);
return match ? match[1].trim() : null;
},
renderInterrogateInspector() {
const row = currentInspectorRow;
let html = `
Interrogation Sandbox
Start a dynamic session to continue the conversation with the agent from this point.
Start Session
`;
ui.elements.inspectorContent.innerHTML = html;
setTimeout(() => {
const startBtn = document.getElementById('btn-start-interrogation');
if (startBtn) startBtn.addEventListener('click', () => this.startInterrogationSession());
const sendBtn = document.getElementById('btn-send-interrogate');
if (sendBtn) sendBtn.addEventListener('click', () => this.sendInterrogationMessage());
const inputField = document.getElementById('interrogate-input');
if (inputField) {
inputField.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
this.sendInterrogationMessage();
}
});
}
}, 0);
},
async startInterrogationSession() {
const rowData = currentInspectorRow;
const statusEl = document.getElementById('interrogate-status');
const inputArea = document.getElementById('interrogate-input-area');
const startBtn = document.getElementById('btn-start-interrogation');
// Use the successful path established during GET
let targetLogPath = currentInspectorLog && currentInspectorLog._successfulPath
? currentInspectorLog._successfulPath : null;
if (!targetLogPath) {
// Fallback heuristics just in case
let rawPath = rowData.log_path || rowData.RUN_ID || rowData.run_id;
if (!rawPath) return;
let jsonPath = rawPath.endsWith('.json') ? rawPath : (rawPath + '.json');
targetLogPath = currentCsvDir ? currentCsvDir + jsonPath : jsonPath;
}
startBtn.innerHTML = 'Initializing...';
startBtn.disabled = true;
try {
let response = await fetch('/api/interrogate/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ log_path: targetLogPath })
});
if (!response.ok) {
// Secondary fallback attempt
let rawPath = rowData.log_path || rowData.RUN_ID || rowData.run_id;
let jsonPath = rawPath.endsWith('.json') ? rawPath : (rawPath + '.json');
response = await fetch('/api/interrogate/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ log_path: jsonPath })
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || "Failed to initialize session. Could not load log.");
}
}
const data = await response.json();
this.currentInterrogationSession = data.session_id;
this.interrogationNewMessages = []; // Reset local new messages
this.renderInterrogateHistory();
inputArea.style.display = 'block';
statusEl.innerHTML = `Session Active (${data.model}) `;
// Force robust scroll after layout changes
this.scrollToBottom(document.getElementById('interrogate-chat-history'));
} catch (err) {
startBtn.innerHTML = 'Start Session';
startBtn.disabled = false;
alert(err.message);
}
},
renderInterrogateHistory() {
const historyEl = document.getElementById('interrogate-chat-history');
if (!historyEl || !currentInspectorLog) return;
let html = ``;
if (currentInspectorLog.conversation) {
currentInspectorLog.conversation.forEach(msg => {
html += this.renderChatMessage(msg);
});
}
if (this.interrogationNewMessages) {
this.interrogationNewMessages.forEach(msg => {
html += this.renderChatMessage(msg);
});
}
html += `
`;
historyEl.innerHTML = html;
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() {
const inputField = document.getElementById('interrogate-input');
const sendBtn = document.getElementById('btn-send-interrogate');
const statusEl = document.getElementById('interrogate-status');
const historyEl = document.getElementById('interrogate-chat-history');
const message = inputField.value.trim();
if (!message || !this.currentInterrogationSession) return;
inputField.value = '';
inputField.disabled = true;
sendBtn.disabled = true;
const userMsg = { role: 'user', content: message };
this.interrogationNewMessages.push(userMsg);
this.renderInterrogateHistory();
const thinkingId = 'thinking-' + Date.now();
historyEl.insertAdjacentHTML('beforeend', `
Agent is thinking... (this may take up to 30s)
`);
this.scrollToBottom(historyEl);
statusEl.textContent = 'Processing request...';
try {
const response = await fetch('/api/interrogate/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: this.currentInterrogationSession,
message: message
})
});
const data = await response.json();
if (!response.ok) throw new Error(data.detail || "Error processing chat");
document.getElementById(thinkingId)?.remove();
const assistantMsg = {
role: 'assistant',
content: data.content,
reasoning: data.reasoning,
tool_calls: data.tool_calls
};
this.interrogationNewMessages.push(assistantMsg);
this.renderInterrogateHistory();
statusEl.textContent = `Log saved incrementally (${new Date().toLocaleTimeString()})`;
} catch (err) {
document.getElementById(thinkingId)?.remove();
statusEl.innerHTML = `Error: ${err.message} `;
} finally {
inputField.disabled = false;
sendBtn.disabled = false;
inputField.focus();
this.scrollToBottom(historyEl);
}
},
getCategoryColor(cat) {
if (!cat) return 'var(--color-accent)';
const c = cat.toLowerCase();
if (c.includes('deceptive') || c.includes('fabricated') || c.includes('harmful')) return '#ef4444';
if (c.includes('omitted') || c.includes('suspicious')) return '#f59e0b';
if (c.includes('accurate') || c.includes('honest')) return '#10b981';
return 'var(--color-accent)';
},
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('scenario') && cols.includes('glassbox_category')) {
this.renderStackedBarChart('Glassbox Category by Scenario', data, 'scenario', 'glassbox_category');
}
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');
}
if (cols.includes('oversight') && cols.includes('glassbox_sophistication')) {
this.renderStackedBarChart('Effects of Oversight on Sophistication', data, 'oversight', 'glassbox_sophistication');
}
}
if (cols.includes('oversight') && cols.includes('glassbox_category') && cols.includes('blackbox_category')) {
this.renderIntentVsRealityChart('Impact of Oversight on Deception (Intent vs Reality)', data);
}
if (cols.includes('model') && cols.includes('glassbox_category') && cols.includes('blackbox_category')) {
this.renderRadarChart('Model Behavior Profile (Radar)', data);
}
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 = 'No plottable categorical columns found.
';
}
},
renderTotalTokenUsageChart(title, data) {
const metrics = {}; // { model: { prompt: 0, completion: 0 } }
data.forEach(row => {
const model = row.model || 'Unknown';
if (!row.total_tokens) return;
if (!metrics[model]) {
metrics[model] = { prompt: 0, completion: 0 };
}
metrics[model].prompt += row.prompt_tokens || 0;
metrics[model].completion += row.completion_tokens || 0;
});
const labels = Object.keys(metrics).sort();
if (labels.length === 0) return;
const datasets = [
{
label: 'Total Prompt Tokens',
data: labels.map(l => metrics[l].prompt),
backgroundColor: '#6366f1',
},
{
label: 'Total Completion Tokens',
data: labels.map(l => metrics[l].completion),
backgroundColor: '#f59e0b',
}
];
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);
},
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';
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('');
ui.elements.imageResults.querySelectorAll('.static-viz-card').forEach(card => {
card.addEventListener('click', () => {
this.openImageModal(card.dataset.src);
});
});
},
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}
`;
}
}
};