From 32aa0344ed57a0fe6e6bc9225af9eb851bc3c6b6 Mon Sep 17 00:00:00 2001 From: CaptainJack2491 Date: Thu, 2 Apr 2026 18:46:43 +0100 Subject: feat(dashboard): integrate interrogation module into web dashboard - Added HTTP POST /api/interrogate/start and /api/interrogate/chat to api/server.py - Implemented ACTIVE_SESSIONS cache to persist synchronous agent state across requests - Updated sys.path in api/server.py to resolve inner core imports correctly - Added Inspector Modal 'Interrogate' view toggle in index.html - Implemented JS interrogation loop, fallback log path resolution, auto-scroll, and markdown rendering in results.js - Added custom CSS animations (.thinking-pulse, .pulse-ring) to mimic agent processing --- api/static/js/components/results.js | 430 ++++++++++++++++++++++++++++++++---- 1 file changed, 382 insertions(+), 48 deletions(-) (limited to 'api/static/js/components/results.js') diff --git a/api/static/js/components/results.js b/api/static/js/components/results.js index 2ce177c..d75250c 100644 --- a/api/static/js/components/results.js +++ b/api/static/js/components/results.js @@ -11,6 +11,11 @@ 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 @@ -38,10 +43,20 @@ export const results = { }); document.addEventListener('click', (e) => { - if (!ui.elements.columnDropdown.contains(e.target) && e.target !== ui.elements.btnToggleColumns) { + 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(), @@ -311,88 +326,407 @@ export const results = { closeInspector() { ui.elements.inspectorModal.style.display = 'none'; - ui.elements.inspectorContent.innerHTML = '
Loading details...
'; + 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'}`; - // Initial layout - ui.elements.inspectorContent.innerHTML = ` -
- -
-
-

Raw Experiment Log

- Checking for log file... -
-
-
Searching for log file associated with this run...
-
-
-
- `; + this.setViewMode('visual'); // Default to visual // Strategy to find the log: - // 1. rowData.log_path - // 2. rowData.RUN_ID - let rawPath = rowData.log_path || rowData.RUN_ID || rowData.run_id; if (!rawPath) { - document.getElementById('log-viewer').innerHTML = '
No log path metadata found in record.
'; - document.getElementById('log-status').textContent = `Missing log metadata.`; + ui.elements.inspectorContent.innerHTML = '
No log path metadata found in record.
'; return; } - // Normalize path: add .json if missing let jsonPath = rawPath.endsWith('.json') ? rawPath : (rawPath + '.json'); - - // Trial paths: - const trials = [ - jsonPath, // Direct - currentCsvDir + jsonPath, // Relative to CSV (e.g. logs/full_experiment/ + path) - 'logs/' + jsonPath // Absolute under logs/ - ]; + const trials = [jsonPath, currentCsvDir + jsonPath, 'logs/' + jsonPath]; let loaded = false; for (const trial of trials) { if (loaded) break; try { - document.getElementById('log-status').textContent = `Trying: ${trial}`; const logData = await api.get(`/api/logs/browse?path=${encodeURIComponent(trial)}`); - document.getElementById('log-viewer').textContent = JSON.stringify(logData, null, 2); - document.getElementById('log-status').textContent = `Log loaded: ${trial}`; - document.getElementById('log-status').style.color = 'var(--color-success)'; + currentInspectorLog = logData; + currentInspectorLog._successfulPath = trial; // Track successful path loaded = true; + this.renderInspectorContent(); } catch (err) { console.log(`Failed trial: ${trial}`); } } if (!loaded) { - document.getElementById('log-viewer').innerHTML = ` + ui.elements.inspectorContent.innerHTML = `
- Failed to load log file: API Error: 404
+ Failed to load log file.
Paths tried:
${trials.map(t => `- ${t}`).join('
')}
`; - document.getElementById('log-status').textContent = `Error loading log.`; - document.getElementById('log-status').style.color = 'var(--color-danger)'; } }, + 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 scroll after layout changes (such as displaying inputArea) + setTimeout(() => { + const historyEl = document.getElementById('interrogate-chat-history'); + if (historyEl) historyEl.scrollTop = historyEl.scrollHeight; + }, 10); + + } 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; + historyEl.scrollTop = historyEl.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) +
+ `); + historyEl.scrollTop = historyEl.scrollHeight; + + 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(); + historyEl.scrollTop = historyEl.scrollHeight; + } + }, + + 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 = []; -- cgit v1.2.3