diff options
Diffstat (limited to 'api/static/app.js')
| -rw-r--r-- | api/static/app.js | 516 |
1 files changed, 516 insertions, 0 deletions
diff --git a/api/static/app.js b/api/static/app.js new file mode 100644 index 0000000..0dcbd01 --- /dev/null +++ b/api/static/app.js @@ -0,0 +1,516 @@ +/* ========================================================================== + AI Agent Reasoning Experiment Framework - Web GUI App + ========================================================================== */ + +const API_BASE = ''; + +// State +let eventSource = null; +let isRunning = false; + +// ========================================================================== +// DOM Elements +// ========================================================================== + +const elements = { + // Configuration + scenarioSelect: document.getElementById('scenario-select'), + modelSelect: document.getElementById('model-select'), + oversightSelect: document.getElementById('oversight-select'), + runsInput: document.getElementById('runs-input'), + + // Buttons + runBtn: document.getElementById('run-btn'), + cancelBtn: document.getElementById('cancel-btn'), + clearLogsBtn: document.getElementById('clear-logs-btn'), + + // Status + statusBadge: document.getElementById('status-badge'), + statusTime: document.getElementById('status-time'), + + // Logs + logsContainer: document.getElementById('logs-container'), + + // Results + csvResults: document.getElementById('csv-results'), + imageResults: document.getElementById('image-results'), + judgeResults: document.getElementById('judge-results'), + + // Tabs + tabBtns: document.querySelectorAll('.tab-btn'), + tabContents: document.querySelectorAll('.tab-content'), +}; + +// ========================================================================== +// Initialization +// ========================================================================== + +document.addEventListener('DOMContentLoaded', async () => { + await loadConfig(); + await loadScenarios(); + await loadModels(); + setupEventListeners(); + startStatusPolling(); +}); + +// ========================================================================== +// API Functions +// ========================================================================== + +async function loadConfig() { + try { + const response = await fetch(`${API_BASE}/api/config`); + const config = await response.json(); + + // Set default values from config + if (config.defaults?.oversight) { + elements.oversightSelect.value = config.defaults.oversight; + } + } catch (error) { + console.error('Failed to load config:', error); + } +} + +async function loadScenarios() { + try { + const response = await fetch(`${API_BASE}/api/scenarios`); + const scenarios = await response.json(); + + elements.scenarioSelect.innerHTML = scenarios.map(s => + `<option value="${s.name}">${s.name}</option>` + ).join(''); + + // If only one scenario, auto-select it and load oversight levels + if (scenarios.length === 1) { + elements.scenarioSelect.value = scenarios[0].name; + updateOversightLevels(scenarios[0]); + } + } catch (error) { + console.error('Failed to load scenarios:', error); + elements.scenarioSelect.innerHTML = '<option value="">Error loading scenarios</option>'; + } +} + +async function loadModels() { + try { + const response = await fetch(`${API_BASE}/api/models`); + const models = await response.json(); + + elements.modelSelect.innerHTML = models.map(m => + `<option value="${m.id}">${m.id} (${m.provider})</option>` + ).join(''); + } catch (error) { + console.error('Failed to load models:', error); + elements.modelSelect.innerHTML = '<option value="">Error loading models</option>'; + } +} + +async function startRun() { + const scenario = elements.scenarioSelect.value; + const model = elements.modelSelect.value; + const oversight = elements.oversightSelect.value; + const runs = elements.runsInput.value; + + if (!scenario || !model) { + alert('Please select a scenario and model'); + return; + } + + // Show loading state + elements.runBtn.classList.add('loading'); + elements.runBtn.disabled = true; + elements.cancelBtn.disabled = false; + isRunning = true; + + // Clear logs + elements.logsContainer.innerHTML = ''; + appendLog('info', `Starting experiment: ${scenario} with ${model} (oversight: ${oversight}, runs: ${runs})`); + + try { + // TODO: For now, we just trigger the run with current config + // In the future, we could modify config via API + const response = await fetch(`${API_BASE}/api/run`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ scenario, model, oversight, runs }) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + // Start log streaming + startLogStream(); + updateStatus('running'); + + } catch (error) { + console.error('Failed to start run:', error); + appendLog('error', `Failed to start: ${error.message}`); + resetRunState(); + } +} + +async function cancelRun() { + try { + const response = await fetch(`${API_BASE}/api/run`, { + method: 'DELETE' + }); + + const result = await response.json(); + appendLog('warn', 'Run cancelled by user'); + updateStatus('cancelled'); + + } catch (error) { + console.error('Failed to cancel run:', error); + } finally { + stopLogStream(); + resetRunState(); + } +} + +// ========================================================================== +// Log Streaming +// ========================================================================== + +function startLogStream() { + stopLogStream(); // Close any existing connection + + eventSource = new EventSource(`${API_BASE}/api/logs/stream`); + + eventSource.onmessage = (event) => { + const data = event.data; + if (data) { + appendLog('info', data); + } + }; + + eventSource.addEventListener('log', (event) => { + const data = event.data; + if (data) { + appendLog('info', data); + } + }); + + eventSource.addEventListener('done', (event) => { + appendLog('info', '=== Run Complete ==='); + stopLogStream(); + isRunning = false; + updateStatus('complete'); + resetRunState(); + loadResults(); + }); + + eventSource.addEventListener('error', (event) => { + console.error('SSE Error:', event); + }); +} + +function stopLogStream() { + if (eventSource) { + eventSource.close(); + eventSource = null; + } +} + +function appendLog(level, message) { + // Remove empty state if present + const emptyState = elements.logsContainer.querySelector('.logs-empty'); + if (emptyState) { + emptyState.remove(); + } + + const line = document.createElement('div'); + line.className = 'log-line'; + + // Color based on content + let logLevel = 'log-level-3'; + if (message.includes('[ERROR]') || message.includes('error') || message.includes('Error')) { + logLevel = 'log-level-1'; + } else if (message.includes('[WARN]') || message.includes('warning') || message.includes('Warning')) { + logLevel = 'log-level-2'; + } else if (message.includes('[DEBUG]') || message.includes('[DEBUG+]')) { + logLevel = 'log-level-4'; + } + + line.classList.add(logLevel); + line.textContent = message; + + elements.logsContainer.appendChild(line); + + // Auto-scroll to bottom + elements.logsContainer.scrollTop = elements.logsContainer.scrollHeight; +} + +function clearLogs() { + elements.logsContainer.innerHTML = '<div class="logs-empty">Run an experiment to see logs...</div>'; +} + +// ========================================================================== +// Status Polling +// ========================================================================== + +let statusPollingInterval = null; + +function startStatusPolling() { + statusPollingInterval = setInterval(async () => { + try { + const response = await fetch(`${API_BASE}/api/run/status`); + const status = await response.json(); + + if (status.status !== 'idle' && !isRunning) { + // Something is running but we don't know about it + isRunning = true; + elements.runBtn.classList.add('loading'); + elements.runBtn.disabled = true; + elements.cancelBtn.disabled = false; + startLogStream(); + } + + updateStatusDisplay(status.status, status.start_time); + + } catch (error) { + console.error('Status poll error:', error); + } + }, 2000); +} + +function updateStatus(status) { + updateStatusDisplay(status); + + if (status === 'running') { + elements.runBtn.classList.add('loading'); + elements.runBtn.disabled = true; + elements.cancelBtn.disabled = false; + } +} + +function updateStatusDisplay(status, startTime = null) { + elements.statusBadge.className = `badge badge-${status}`; + + const statusText = { + 'idle': 'Idle', + 'running': 'Running...', + 'complete': 'Complete', + 'error': 'Error', + 'cancelled': 'Cancelled' + }; + + elements.statusBadge.textContent = statusText[status] || status; + + if (startTime) { + const date = new Date(startTime); + elements.statusTime.textContent = `Started: ${date.toLocaleTimeString()}`; + } else if (status === 'idle') { + elements.statusTime.textContent = ''; + } +} + +function resetRunState() { + elements.runBtn.classList.remove('loading'); + elements.runBtn.disabled = false; + elements.cancelBtn.disabled = true; + isRunning = false; +} + +// ========================================================================== +// Results Loading +// ========================================================================== + +async function loadResults() { + await Promise.all([ + loadCSVResults(), + loadImageResults(), + loadJudgeResults() + ]); +} + +async function loadCSVResults() { + try { + const response = await fetch(`${API_BASE}/api/results`); + const results = await response.json(); + + if (Object.keys(results).length === 0) { + elements.csvResults.innerHTML = '<div class="empty-state">No results yet</div>'; + return; + } + + let html = ''; + + for (const [filename, data] of Object.entries(results)) { + if (data.error) { + html += `<h3>${filename}</h3><p>Error: ${data.error}</p>`; + continue; + } + + html += `<h3>${filename}.csv</h3>`; + html += '<div style="overflow-x: auto;"><table class="data-table">'; + + // Header + html += '<thead><tr>'; + for (const col of data.columns) { + html += `<th>${col}</th>`; + } + html += '</tr></thead>'; + + // Body + html += '<tbody>'; + for (const row of data.data) { + html += '<tr>'; + for (const col of data.columns) { + html += `<td>${row[col] ?? ''}</td>`; + } + html += '</tr>'; + } + html += '</tbody></table></div>'; + } + + elements.csvResults.innerHTML = html || '<div class="empty-state">No results yet</div>'; + + } catch (error) { + console.error('Failed to load CSV results:', error); + } +} + +async function loadImageResults() { + try { + const response = await fetch(`${API_BASE}/api/results/images`); + const images = await response.json(); + + if (images.length === 0) { + elements.imageResults.innerHTML = '<div class="empty-state">No visualizations yet</div>'; + return; + } + + elements.imageResults.innerHTML = images.map(img => ` + <div class="image-card"> + <img src="${API_BASE}/api/results/images/${img.name}" alt="${img.name}"> + <div class="image-title">${img.name}</div> + </div> + `).join(''); + + } catch (error) { + console.error('Failed to load images:', error); + } +} + +async function loadJudgeResults() { + try { + const response = await fetch(`${API_BASE}/api/judge/results`); + const results = await response.json(); + + if (results.message || Object.keys(results).length === 0) { + elements.judgeResults.innerHTML = '<div class="empty-state">No judge results yet</div>'; + return; + } + + let html = ''; + + for (const [filename, data] of Object.entries(results)) { + if (data.type === 'csv') { + html += `<h3>${filename}</h3>`; + html += '<div style="overflow-x: auto;"><table class="data-table">'; + + html += '<thead><tr>'; + for (const col of data.columns) { + html += `<th>${col}</th>`; + } + html += '</tr></thead><tbody>'; + + for (const row of data.data) { + html += '<tr>'; + for (const col of data.columns) { + html += `<td>${row[col] ?? ''}</td>`; + } + html += '</tr>'; + } + html += '</tbody></table></div>'; + } else { + html += `<h3>${filename}</h3><pre>${JSON.stringify(data.data, null, 2)}</pre>`; + } + } + + elements.judgeResults.innerHTML = html || '<div class="empty-state">No judge results</div>'; + + } catch (error) { + console.error('Failed to load judge results:', error); + } +} + +// ========================================================================== +// Event Listeners +// ========================================================================== + +function setupEventListeners() { + // Run button + elements.runBtn.addEventListener('click', startRun); + + // Cancel button + elements.cancelBtn.addEventListener('click', cancelRun); + + // Clear logs button + elements.clearLogsBtn.addEventListener('click', clearLogs); + + // Scenario selection - update oversight levels + elements.scenarioSelect.addEventListener('change', async (e) => { + const scenarioName = e.target.value; + if (!scenarioName) return; + + try { + const response = await fetch(`${API_BASE}/api/scenarios/${scenarioName}`); + const scenario = await response.json(); + updateOversightLevels(scenario); + } catch (error) { + console.error('Failed to load scenario details:', error); + } + }); + + // Tab switching + elements.tabBtns.forEach(btn => { + btn.addEventListener('click', () => { + const tabId = btn.dataset.tab; + + // Update active tab button + elements.tabBtns.forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + + // Update active tab content + elements.tabContents.forEach(content => { + content.classList.remove('active'); + if (content.id === `tab-${tabId}`) { + content.classList.add('active'); + } + }); + + // Load results if switching to results tab + if (tabId === 'csv' || tabId === 'images' || tabId === 'judge') { + loadResults(); + } + }); + }); + + // Keyboard shortcuts + document.addEventListener('keydown', (e) => { + // Ctrl/Cmd + Enter to run + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' && !isRunning) { + startRun(); + } + }); +} + +function updateOversightLevels(scenario) { + const oversightSelect = elements.oversightSelect; + const levels = scenario.oversight_levels || []; + + if (levels.length === 0) { + // No scenario-specific oversight, use global levels + oversightSelect.innerHTML = ` + <option value="low">Low</option> + <option value="mid">Mid</option> + <option value="high">High</option> + `; + } else { + oversightSelect.innerHTML = levels.map(level => + `<option value="${level}">${level.charAt(0).toUpperCase() + level.slice(1)}</option>` + ).join(''); + } +} |
