/** * 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 => ``).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; // 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 => ` `).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 => ` `).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 => ``).join('')} ${data.map((row, idx) => ` ${visibleCols.map(col => { const val = row[col] ?? ''; const isLong = col.toLowerCase().includes('justification') || col.toLowerCase().includes('id'); return ``; }).join('')} `).join('')}
${c}
${val}
`; 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 = `
${jsonStr}
`; }, 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 `
${key}
${val ?? 'null'}
`; }).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 = `
${role}
`; 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 += `
`; msg.tool_calls.forEach(tc => { const func = tc.function || tc; html += `
${func.name}(${func.arguments || ''})
`; }); 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.

`; 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 => `
${img.name}
`).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 => ``).join('')} ${content.data.map(row => ` ${content.columns.map(col => ``).join('')} `).join('')}
${c}
${row[col] ?? ''}
`; } else { html += `
${JSON.stringify(content.data, null, 2)}
`; } ui.elements.judgeResults.innerHTML = html; } catch (err) { ui.elements.judgeResults.innerHTML = `
Error: ${err.message}
`; } } };