diff options
| author | CaptainJack2491 <jayrupnakawala@gmail.com> | 2026-03-09 19:11:45 +0000 |
|---|---|---|
| committer | CaptainJack2491 <jayrupnakawala@gmail.com> | 2026-03-09 19:11:45 +0000 |
| commit | d2b1fbd5ac63fd1eb17e68b5364b7dee5cc86e6e (patch) | |
| tree | c1598caeccf55130c3c3d95bc85ebaba84c8ceb9 /api | |
| parent | 2ed454c66d9865a8abcbecc0081f7023cf4b50f9 (diff) | |
feat: upgrade web GUI to modular ES6 and add Chart.js
This commit completely overhauls the initial GUI:
- Splits monolithic JS/CSS into an ES6 module structure
- Implements an industrial 'Control Room' dark theme
- Adds dynamic file selection endpoints for CSV and JSON judge logs
- Integrates Chart.js to automatically render visualizations from CSV data
- Updates web_gui_plan.md to reflect architectural changes
Diffstat (limited to 'api')
| -rw-r--r-- | api/server.py | 95 | ||||
| -rw-r--r-- | api/static/app.js | 516 | ||||
| -rw-r--r-- | api/static/css/base.css | 66 | ||||
| -rw-r--r-- | api/static/css/components.css | 161 | ||||
| -rw-r--r-- | api/static/css/layout.css | 87 | ||||
| -rw-r--r-- | api/static/css/results.css | 113 | ||||
| -rw-r--r-- | api/static/css/terminal.css | 77 | ||||
| -rw-r--r-- | api/static/index.html | 191 | ||||
| -rw-r--r-- | api/static/js/api.js | 46 | ||||
| -rw-r--r-- | api/static/js/components/config.js | 107 | ||||
| -rw-r--r-- | api/static/js/components/results.js | 296 | ||||
| -rw-r--r-- | api/static/js/components/terminal.js | 49 | ||||
| -rw-r--r-- | api/static/js/main.js | 66 | ||||
| -rw-r--r-- | api/static/js/ui.js | 62 | ||||
| -rw-r--r-- | api/static/style.css | 461 |
15 files changed, 1316 insertions, 1077 deletions
diff --git a/api/server.py b/api/server.py index ee5fd2b..8bb1e24 100644 --- a/api/server.py +++ b/api/server.py @@ -362,17 +362,40 @@ async def log_stream(): # Results Endpoints # ============================================================================ +@app.get("/api/results/files") +async def get_results_files(): + """List all CSV files in the output directory.""" + config = run_manager.config + if not config: + raise HTTPException(status_code=500, detail="Config not loaded") + + output_dir = PROJECT_ROOT / config.output_dir + if not output_dir.exists(): + return [] + + files = [] + for p in output_dir.rglob("*.csv"): + files.append(str(p.relative_to(output_dir))) + return sorted(files) + @app.get("/api/results") -async def get_results(): - """Get experiment results as JSON.""" +async def get_results(file: str = None): + """Get experiment results as JSON, optionally for a specific file.""" config = run_manager.config if not config: raise HTTPException(status_code=500, detail="Config not loaded") output_dir = PROJECT_ROOT / config.output_dir - # Look for CSV files - csv_files = list(output_dir.glob("*.csv")) if output_dir.exists() else [] + csv_files = [] + if file: + file_path = output_dir / file + if not file_path.exists() or ".." in file: + raise HTTPException(status_code=404, detail="File not found") + csv_files.append(file_path) + else: + # Backward compatibility or default empty + csv_files = list(output_dir.glob("*.csv")) if output_dir.exists() else [] results = {} for csv_file in csv_files: @@ -448,40 +471,70 @@ async def get_result_image(image_name: str): # Judge Results Endpoints # ============================================================================ +@app.get("/api/judge/files") +async def get_judge_files(): + """Get list of judge result files.""" + config = run_manager.config + if not config: + raise HTTPException(status_code=500, detail="Config not loaded") + + judge_log_dir = config._config.get('judge', {}).get('log_dir', 'judge_logs') + judge_dir = PROJECT_ROOT / judge_log_dir + + if not judge_dir.exists(): + return [] + + files = [] + for p in judge_dir.rglob("*"): + if p.suffix in ['.csv', '.json']: + files.append(str(p.relative_to(judge_dir))) + return sorted(files) + + @app.get("/api/judge/results") -async def get_judge_results(): +async def get_judge_results(file: str = None): """Get judge results if available.""" config = run_manager.config if not config: raise HTTPException(status_code=500, detail="Config not loaded") - judge_log_dir = config.logging_config.get('file') - if judge_log_dir: - judge_dir = Path(judge_log_dir).parent / "judge" - else: - judge_dir = PROJECT_ROOT / "logs" / "judge" + judge_log_dir = config._config.get('judge', {}).get('log_dir', 'judge_logs') + judge_dir = PROJECT_ROOT / judge_log_dir if not judge_dir.exists(): return {"message": "No judge results found"} - # Look for judge result files - import glob - result_files = list(judge_dir.glob("*.csv")) + list(judge_dir.glob("*.json")) + result_files = [] + if file: + file_path = judge_dir / file + if not file_path.exists() or ".." in file: + raise HTTPException(status_code=404, detail="File not found") + result_files.append(file_path) + else: + # Default empty or backward compatibility + import glob + result_files = list(judge_dir.glob("*.csv")) + list(judge_dir.glob("*.json")) results = {} for rf in result_files: if rf.suffix == '.csv': import pandas as pd - df = pd.read_csv(rf) - results[rf.stem] = { - "type": "csv", - "columns": df.columns.tolist(), - "data": df.to_dict(orient="records") - } + try: + df = pd.read_csv(rf) + results[rf.stem] = { + "type": "csv", + "columns": df.columns.tolist(), + "data": df.to_dict(orient="records") + } + except Exception as e: + results[rf.stem] = {"error": str(e)} elif rf.suffix == '.json': import json - with open(rf) as f: - results[rf.stem] = {"type": "json", "data": json.load(f)} + try: + with open(rf) as f: + results[rf.stem] = {"type": "json", "data": json.load(f)} + except Exception as e: + results[rf.stem] = {"error": str(e)} return results diff --git a/api/static/app.js b/api/static/app.js deleted file mode 100644 index 0dcbd01..0000000 --- a/api/static/app.js +++ /dev/null @@ -1,516 +0,0 @@ -/* ========================================================================== - 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(''); - } -} diff --git a/api/static/css/base.css b/api/static/css/base.css new file mode 100644 index 0000000..aa28ba5 --- /dev/null +++ b/api/static/css/base.css @@ -0,0 +1,66 @@ +:root { + /* Colors - Deep Industrial Dark */ + --color-bg-0: #0a0a0b; /* Deepest background */ + --color-bg-1: #121214; /* Sidebar / Cards */ + --color-bg-2: #1c1c1f; /* Input fields */ + --color-bg-3: #2a2a2e; /* Borders / Hover */ + + /* Text */ + --color-text-primary: #f0f0f2; + --color-text-secondary: #9ea0a6; + --color-text-muted: #62646c; + + /* Accent - Pure Purple */ + --color-accent: #9d4edd; + --color-accent-hover: #7b2cbf; + --color-accent-text: #ffffff; + + /* Semantic */ + --color-success: #10b981; + --color-warning: #f59e0b; + --color-danger: #ef4444; + --color-info: #3b82f6; + + /* Spacing & Borders */ + --border-width: 1px; + --border-radius: 4px; /* Crisp corners */ + --space-xs: 4px; + --space-sm: 8px; + --space-md: 16px; + --space-lg: 24px; + --space-xl: 32px; + + /* Transitions */ + --transition-fast: 0.15s ease; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background-color: var(--color-bg-0); + color: var(--color-text-primary); + line-height: 1.5; + height: 100vh; + overflow: hidden; /* Main scroll happens in panes */ +} + +/* Scrollbar Styling */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} +::-webkit-scrollbar-track { + background: var(--color-bg-0); +} +::-webkit-scrollbar-thumb { + background: var(--color-bg-3); + border-radius: 10px; +} +::-webkit-scrollbar-thumb:hover { + background: var(--color-accent); +} diff --git a/api/static/css/components.css b/api/static/css/components.css new file mode 100644 index 0000000..9b9a823 --- /dev/null +++ b/api/static/css/components.css @@ -0,0 +1,161 @@ +/* Buttons */ +.btn { + appearance: none; + border: none; + font-family: inherit; + font-size: 0.875rem; + font-weight: 600; + padding: 10px 16px; + border-radius: var(--border-radius); + cursor: pointer; + transition: var(--transition-fast); + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-sm); + text-transform: uppercase; + letter-spacing: 0.02em; +} + +.btn-primary { + background-color: var(--color-accent); + color: var(--color-accent-text); +} +.btn-primary:hover:not(:disabled) { + background-color: var(--color-accent-hover); +} + +.btn-danger { + background-color: transparent; + border: var(--border-width) solid var(--color-danger); + color: var(--color-danger); +} +.btn-danger:hover:not(:disabled) { + background-color: var(--color-danger); + color: white; +} + +.btn-ghost { + background-color: transparent; + color: var(--color-text-secondary); + border: var(--border-width) solid var(--color-bg-3); +} +.btn-ghost:hover:not(:disabled) { + background-color: var(--color-bg-3); + color: var(--color-text-primary); +} + +.btn:disabled { + opacity: 0.4; + cursor: not-allowed; + filter: grayscale(1); +} + +/* Forms */ +.form-group { + display: flex; + flex-direction: column; + gap: var(--space-xs); + margin-bottom: var(--space-md); +} + +.form-label { + font-size: 0.75rem; + font-weight: 700; + color: var(--color-text-muted); + text-transform: uppercase; +} + +.input-field { + background-color: var(--color-bg-2); + border: var(--border-width) solid var(--color-bg-3); + border-radius: var(--border-radius); + padding: 10px 12px; + color: var(--color-text-primary); + font-size: 0.9375rem; + width: 100%; + transition: border-color var(--transition-fast); +} + +.input-field:focus { + outline: none; + border-color: var(--color-accent); +} + +/* Tabs */ +.tab-nav { + display: flex; + border-bottom: var(--border-width) solid var(--color-bg-3); + padding: 0 var(--space-lg); + background-color: var(--color-bg-1); +} + +.tab-trigger { + background: none; + border: none; + padding: 14px 20px; + color: var(--color-text-secondary); + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + border-bottom: 2px solid transparent; + transition: var(--transition-fast); +} + +.tab-trigger:hover { + color: var(--color-text-primary); +} + +.tab-trigger.active { + color: var(--color-accent); + border-bottom-color: var(--color-accent); +} + +.tab-panel { + display: none; + padding: var(--space-lg); +} + +.tab-panel.active { + display: block; +} + +/* Cards */ +.card { + background-color: var(--color-bg-1); + border: var(--border-width) solid var(--color-bg-3); + border-radius: var(--border-radius); + overflow: hidden; +} + +.card-header { + padding: var(--space-md); + border-bottom: var(--border-width) solid var(--color-bg-3); + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + color: var(--color-text-muted); +} + +.card-body { + padding: var(--space-md); +} + +/* Badges */ +.badge { + font-size: 0.7rem; + font-weight: 800; + padding: 2px 8px; + border-radius: 2px; + text-transform: uppercase; +} + +.badge-idle { background: var(--color-bg-3); color: var(--color-text-secondary); } +.badge-running { background: var(--color-accent); color: white; animation: blink 1.5s infinite; } +.badge-complete { background: var(--color-success); color: white; } + +@keyframes blink { + 0% { opacity: 1; } + 50% { opacity: 0.6; } + 100% { opacity: 1; } +} diff --git a/api/static/css/layout.css b/api/static/css/layout.css new file mode 100644 index 0000000..ef0fc2d --- /dev/null +++ b/api/static/css/layout.css @@ -0,0 +1,87 @@ +.dashboard-root { + display: grid; + grid-template-columns: 320px 1fr; + grid-template-rows: 1fr 300px; + height: 100vh; + width: 100vw; +} + +/* Sidebar - Left Column */ +.sidebar { + grid-row: 1 / -1; + background-color: var(--color-bg-1); + border-right: var(--border-width) solid var(--color-bg-3); + padding: var(--space-lg); + display: flex; + flex-direction: column; + gap: var(--space-xl); + z-index: 10; +} + +/* Main Area - Top Right */ +.main-viewport { + grid-column: 2; + grid-row: 1; + overflow-y: auto; + background-color: var(--color-bg-0); + display: flex; + flex-direction: column; +} + +/* Terminal - Bottom Right */ +.terminal-dock { + grid-column: 2; + grid-row: 2; + background-color: var(--color-bg-1); + border-top: var(--border-width) solid var(--color-bg-3); + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* Header */ +.app-header { + padding: var(--space-md) var(--space-lg); + border-bottom: var(--border-width) solid var(--color-bg-3); + display: flex; + justify-content: space-between; + align-items: center; +} + +.logo-area { + display: flex; + align-items: center; + gap: var(--space-sm); +} + +.logo-area h1 { + font-size: 1rem; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--color-accent); +} + +/* Mobile notice */ +@media (max-width: 900px) { + .dashboard-root { + grid-template-columns: 1fr; + grid-template-rows: auto 1fr auto; + overflow-y: auto; + } + .sidebar { + grid-row: 1; + border-right: none; + border-bottom: var(--border-width) solid var(--color-bg-3); + } + .main-viewport { + grid-column: 1; + grid-row: 2; + height: 500px; + } + .terminal-dock { + grid-column: 1; + grid-row: 3; + height: 300px; + } +} diff --git a/api/static/css/results.css b/api/static/css/results.css new file mode 100644 index 0000000..9a532b8 --- /dev/null +++ b/api/static/css/results.css @@ -0,0 +1,113 @@ +.results-container { + padding: var(--space-sm); +} + +.results-controls { + padding: var(--space-md) var(--space-lg); + background-color: var(--color-bg-1); + border-bottom: var(--border-width) solid var(--color-bg-3); + display: flex; + align-items: center; +} + +.data-table-wrapper { + margin-bottom: var(--space-xl); + border: var(--border-width) solid var(--color-bg-3); + border-radius: var(--border-radius); + overflow: hidden; +} + +.data-table-title { + background-color: var(--color-bg-1); + padding: var(--space-sm) var(--space-md); + font-size: 0.75rem; + font-weight: 800; + color: var(--color-accent); + text-transform: uppercase; + border-bottom: var(--border-width) solid var(--color-bg-3); +} + +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 0.8125rem; + background-color: var(--color-bg-1); + table-layout: auto; +} + +.data-table th { + text-align: left; + padding: 12px 16px; + background-color: var(--color-bg-2); + color: var(--color-text-secondary); + font-weight: 700; + text-transform: uppercase; + font-size: 0.65rem; + letter-spacing: 0.05em; + border-bottom: var(--border-width) solid var(--color-bg-3); + white-space: nowrap; +} + +.data-table td { + padding: 10px 16px; + border-bottom: var(--border-width) solid var(--color-bg-3); + color: var(--color-text-primary); + max-width: 400px; + white-space: normal; + word-wrap: break-word; + vertical-align: top; +} + +.data-table tr:last-child td { + border-bottom: none; +} + +.data-table tr:hover td { + background-color: var(--color-bg-2); +} + +/* Image Grid */ +.image-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); + gap: var(--space-lg); + padding: var(--space-lg); +} + +.image-card { + background-color: var(--color-bg-1); + border: var(--border-width) solid var(--color-bg-3); + border-radius: var(--border-radius); + overflow: hidden; + transition: var(--transition-fast); +} + +.image-card:hover { + border-color: var(--color-accent); +} + +.image-card img { + width: 100%; + height: auto; + display: block; + background-color: white; /* Charts usually have white background */ +} + +.image-card-footer { + padding: var(--space-sm) var(--space-md); + font-size: 0.75rem; + font-weight: 600; + color: var(--color-text-secondary); + text-align: center; + border-top: var(--border-width) solid var(--color-bg-3); +} + +/* Chart Canvas Container */ +.chart-container { + background-color: var(--color-bg-1); + border: var(--border-width) solid var(--color-bg-3); + border-radius: var(--border-radius); + padding: var(--space-md); + position: relative; + height: 300px; +} diff --git a/api/static/css/terminal.css b/api/static/css/terminal.css new file mode 100644 index 0000000..7cf79ba --- /dev/null +++ b/api/static/css/terminal.css @@ -0,0 +1,77 @@ +.terminal-header { + background-color: var(--color-bg-2); + padding: var(--space-xs) var(--space-md); + border-bottom: var(--border-width) solid var(--color-bg-3); + display: flex; + justify-content: space-between; + align-items: center; +} + +.terminal-title { + font-size: 0.7rem; + font-weight: 800; + text-transform: uppercase; + color: var(--color-text-muted); + letter-spacing: 0.1em; +} + +.terminal-body { + flex: 1; + overflow-y: auto; + padding: var(--space-md); + font-family: 'JetBrains Mono', 'Fira Code', monospace; + font-size: 0.8125rem; + line-height: 1.6; + background-color: #0d0d0f; /* Slightly darker for terminal */ +} + +.terminal-line { + white-space: pre-wrap; + word-break: break-all; + margin-bottom: 2px; +} + +.terminal-line.info { color: var(--color-text-primary); } +.terminal-line.error { color: var(--color-danger); font-weight: 600; } +.terminal-line.warning { color: var(--color-warning); } +.terminal-line.muted { color: var(--color-text-muted); } +.terminal-line.success { color: var(--color-success); } +.terminal-line.reasoning { color: var(--color-accent); border-left: 2px solid var(--color-accent); padding-left: var(--space-sm); margin: var(--space-sm) 0; } + +.btn-terminal { + background: none; + border: none; + color: var(--color-text-muted); + font-size: 0.65rem; + font-weight: 700; + text-transform: uppercase; + cursor: pointer; + padding: 4px 8px; + border-radius: 2px; + transition: var(--transition-fast); +} + +.btn-terminal:hover { + color: var(--color-text-primary); + background-color: var(--color-bg-3); +} + +.btn-terminal.active { + color: var(--color-accent); +} + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 300px; + color: var(--color-text-muted); + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 600; + border: 2px dashed var(--color-bg-3); + margin: var(--space-lg); + border-radius: var(--border-radius); +} diff --git a/api/static/index.html b/api/static/index.html index cdf8d2f..564618a 100644 --- a/api/static/index.html +++ b/api/static/index.html @@ -3,109 +3,142 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>AI Agent Reasoning Experiment Framework</title> - <link rel="stylesheet" href="/static/style.css"> + <title>AI Reasoning Framework</title> + <!-- CSS Modules --> + <link rel="stylesheet" href="/static/css/base.css"> + <link rel="stylesheet" href="/static/css/layout.css"> + <link rel="stylesheet" href="/static/css/components.css"> + <link rel="stylesheet" href="/static/css/terminal.css"> + <link rel="stylesheet" href="/static/css/results.css"> + <!-- Google Fonts - Inter --> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> + <!-- Chart.js --> + <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> </head> <body> - <div class="container"> - <header> - <h1>🤖 AI Agent Reasoning Experiment Framework</h1> - <p class="subtitle">Run experiments, analyze deception, and visualize results</p> - </header> + <div class="dashboard-root"> + <!-- Sidebar: Configuration --> + <aside class="sidebar"> + <div class="logo-area"> + <h1>AI Agent Framework</h1> + </div> - <main> - <!-- Configuration Section --> - <section class="card" id="config-section"> - <h2>⚙️ Configuration</h2> - <div class="config-grid"> - <div class="config-item"> - <label for="scenario-select">Scenario</label> - <select id="scenario-select"> - <option value="">Loading...</option> - </select> - </div> - <div class="config-item"> - <label for="model-select">Model</label> - <select id="model-select"> - <option value="">Loading...</option> - </select> - </div> - <div class="config-item"> - <label for="oversight-select">Oversight Level</label> - <select id="oversight-select"> - <option value="low">Low</option> - <option value="mid">Mid</option> - <option value="high">High</option> - </select> - </div> - <div class="config-item"> - <label for="runs-input">Runs</label> - <input type="number" id="runs-input" value="1" min="1" max="100"> - </div> + <nav class="config-panel"> + <div class="form-group"> + <label class="form-label">Scenario</label> + <select id="scenario-select" class="input-field"> + <option value="">Loading...</option> + </select> </div> - <div class="config-actions"> - <button id="run-btn" class="btn btn-primary"> - <span class="btn-text">▶ Run Experiment</span> - <span class="btn-spinner hidden">⏳ Running...</span> - </button> - <button id="cancel-btn" class="btn btn-danger" disabled>⏹ Cancel</button> + + <div class="form-group"> + <label class="form-label">Model</label> + <select id="model-select" class="input-field"> + <option value="">Loading...</option> + </select> </div> - </section> - <!-- Status Section --> - <section class="card" id="status-section"> - <h2>📊 Status</h2> - <div class="status-indicator"> - <span id="status-badge" class="badge badge-idle">Idle</span> - <span id="status-time"></span> + <div class="form-group"> + <label class="form-label">Oversight</label> + <select id="oversight-select" class="input-field"> + <option value="low">Low</option> + <option value="mid">Mid</option> + <option value="high">High</option> + </select> </div> - </section> - <!-- Logs Section --> - <section class="card" id="logs-section"> - <h2>📜 Live Logs</h2> - <div class="logs-container" id="logs-container"> - <div class="logs-empty">Run an experiment to see logs...</div> + <div class="form-group"> + <label class="form-label">Run Count</label> + <input type="number" id="runs-input" class="input-field" value="1" min="1" max="100"> </div> - <div class="logs-actions"> - <button id="clear-logs-btn" class="btn btn-secondary">Clear Logs</button> + + <div class="sidebar-actions" style="margin-top: auto; display: flex; flex-direction: column; gap: var(--space-sm);"> + <button id="run-btn" class="btn btn-primary">Run Experiment</button> + <button id="cancel-btn" class="btn btn-danger" disabled>Stop Run</button> </div> - </section> + </nav> - <!-- Results Section --> - <section class="card" id="results-section"> - <h2>📈 Results</h2> - <div class="tabs"> - <button class="tab-btn active" data-tab="csv">CSV Data</button> - <button class="tab-btn" data-tab="images">Visualizations</button> - <button class="tab-btn" data-tab="judge">Judge Results</button> + <div class="status-footer"> + <div class="form-label">System Status</div> + <div style="display: flex; align-items: center; justify-content: space-between; margin-top: 4px;"> + <span id="status-badge" class="badge badge-idle">Idle</span> + <span id="status-time" style="font-size: 0.7rem; color: var(--color-text-muted);"></span> </div> - - <div class="tab-content active" id="tab-csv"> - <div id="csv-results"> - <div class="empty-state">No results yet. Run an experiment first.</div> + </div> + </aside> + + <!-- Main Viewport: Results & Analytics --> + <main class="main-viewport"> + <header class="app-header"> + <div class="tab-nav" style="border-bottom: none; background: none; padding: 0;"> + <button class="tab-trigger active" data-tab="csv">Raw Data</button> + <button class="tab-trigger" data-tab="images">Visuals</button> + <button class="tab-trigger" data-tab="judge">Judge Reports</button> + </div> + </header> + + <div class="viewport-content"> + <div id="tab-csv" class="tab-panel active"> + <div class="results-controls"> + <label class="form-label">Select CSV:</label> + <select id="csv-file-select" class="input-field" style="max-width: 400px; display: inline-block; margin-left: var(--space-sm);"> + <option value="">Loading...</option> + </select> + </div> + <div id="csv-results" class="results-container"> + <div class="empty-state">No active data.</div> </div> </div> - <div class="tab-content" id="tab-images"> + <div id="tab-images" class="tab-panel"> + <div class="results-controls"> + <label class="form-label">Data Source:</label> + <select id="chart-file-select" class="input-field" style="max-width: 400px; display: inline-block; margin-left: var(--space-sm);"> + <option value="">Loading...</option> + </select> + </div> + <div id="interactive-charts" class="image-grid" style="margin-top: var(--space-md);"> + <div class="empty-state" style="grid-column: 1 / -1;">Select a CSV to generate visuals.</div> + </div> + + <h3 style="margin: var(--space-lg) var(--space-lg) var(--space-sm); font-size: 0.8rem; text-transform: uppercase; color: var(--color-text-muted);">Saved Visualizations</h3> <div id="image-results" class="image-grid"> - <div class="empty-state">No visualizations yet. Run an experiment first.</div> + <div class="empty-state" style="grid-column: 1 / -1;">No visualizations generated.</div> </div> </div> - <div class="tab-content" id="tab-judge"> - <div id="judge-results"> - <div class="empty-state">No judge results yet.</div> + <div id="tab-judge" class="tab-panel"> + <div class="results-controls"> + <label class="form-label">Select Report:</label> + <select id="judge-file-select" class="input-field" style="max-width: 400px; display: inline-block; margin-left: var(--space-sm);"> + <option value="">Loading...</option> + </select> + </div> + <div id="judge-results" class="results-container"> + <div class="empty-state">No judge reports.</div> </div> </div> - </section> + </div> </main> - <footer> - <p>AI Agent Reasoning Experiment Framework • Dissertation Project</p> - </footer> + <!-- Terminal Dock: Live Feedback --> + <section class="terminal-dock"> + <div class="terminal-header"> + <span class="terminal-title">Event Log</span> + <div class="terminal-actions"> + <button id="clear-logs-btn" class="btn-terminal">Clear</button> + <button id="scroll-lock-btn" class="btn-terminal active">Auto-Scroll</button> + </div> + </div> + <div id="logs-container" class="terminal-body"> + <div class="terminal-line muted">System ready. Waiting for run...</div> + </div> + </section> </div> - <script src="/static/app.js"></script> + <!-- Main Entry Point --> + <script type="module" src="/static/js/main.js"></script> </body> </html> diff --git a/api/static/js/api.js b/api/static/js/api.js new file mode 100644 index 0000000..ecfe30d --- /dev/null +++ b/api/static/js/api.js @@ -0,0 +1,46 @@ +/** + * api.js - Core API communication + */ + +const API_BASE = ''; + +export const api = { + async get(endpoint) { + const response = await fetch(`${API_BASE}${endpoint}`); + if (!response.ok) throw new Error(`API Error: ${response.status}`); + return await response.json(); + }, + + async post(endpoint, data) { + const response = await fetch(`${API_BASE}${endpoint}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + if (!response.ok) throw new Error(`API Error: ${response.status}`); + return await response.json(); + }, + + async delete(endpoint) { + const response = await fetch(`${API_BASE}${endpoint}`, { method: 'DELETE' }); + if (!response.ok) throw new Error(`API Error: ${response.status}`); + return await response.json(); + }, + + streamLogs(onLog, onDone, onError) { + const eventSource = new EventSource(`${API_BASE}/api/logs/stream`); + + eventSource.onmessage = (e) => onLog(e.data); + eventSource.addEventListener('log', (e) => onLog(e.data)); + eventSource.addEventListener('done', () => { + eventSource.close(); + onDone(); + }); + eventSource.onerror = (e) => { + console.error('SSE Error:', e); + if (onError) onError(e); + }; + + return eventSource; + } +}; diff --git a/api/static/js/components/config.js b/api/static/js/components/config.js new file mode 100644 index 0000000..d87c874 --- /dev/null +++ b/api/static/js/components/config.js @@ -0,0 +1,107 @@ +/** + * config.js - Experiment configuration and execution + */ +import { api } from '../api.js'; +import { ui } from '../ui.js'; +import { terminal } from './terminal.js'; +import { results } from './results.js'; + +let isRunning = false; + +export const config = { + async init() { + await Promise.all([ + this.loadScenarios(), + this.loadModels(), + this.loadDefaults() + ]); + + ui.elements.runBtn.addEventListener('click', () => this.startRun()); + ui.elements.cancelBtn.addEventListener('click', () => this.stopRun()); + + // Scenario detail updates + ui.elements.scenarioSelect.addEventListener('change', (e) => this.updateOversight(e.target.value)); + }, + + async loadScenarios() { + const scenarios = await api.get('/api/scenarios'); + ui.elements.scenarioSelect.innerHTML = scenarios.map(s => + `<option value="${s.name}">${s.name}</option>` + ).join(''); + if (scenarios[0]) this.updateOversight(scenarios[0].name); + }, + + async loadModels() { + const models = await api.get('/api/models'); + ui.elements.modelSelect.innerHTML = models.map(m => + `<option value="${m.id}">${m.id} (${m.provider})</option>` + ).join(''); + }, + + async loadDefaults() { + const cfg = await api.get('/api/config'); + if (cfg.defaults?.oversight) { + ui.elements.oversightSelect.value = cfg.defaults.oversight; + } + }, + + async updateOversight(scenarioName) { + if (!scenarioName) return; + const details = await api.get(`/api/scenarios/${scenarioName}`); + const levels = details.oversight_levels || ['low', 'mid', 'high']; + ui.elements.oversightSelect.innerHTML = levels.map(l => + `<option value="${l}">${l.toUpperCase()}</option>` + ).join(''); + }, + + async startRun() { + const data = { + scenario: ui.elements.scenarioSelect.value, + model: ui.elements.modelSelect.value, + oversight: ui.elements.oversightSelect.value, + runs: ui.elements.runsInput.value + }; + + this.setRunningState(true); + terminal.clear(); + terminal.append(`[SYSTEM] Starting experiment: ${data.scenario} | ${data.model}`); + + try { + await api.post('/api/run', data); + api.streamLogs( + (log) => terminal.append(log), + () => { + this.setRunningState(false); + ui.updateStatus('complete'); + terminal.append('[SYSTEM] Run completed successfully.'); + results.loadAll(); + }, + (err) => { + this.setRunningState(false); + ui.updateStatus('error'); + terminal.append(`[ERROR] Stream disconnected: ${err}`); + } + ); + ui.updateStatus('running', new Date()); + } catch (err) { + this.setRunningState(false); + ui.updateStatus('error'); + terminal.append(`[ERROR] Failed to start run: ${err.message}`); + } + }, + + async stopRun() { + try { + await api.delete('/api/run'); + terminal.append('[SYSTEM] Cancel request sent.'); + } catch (err) { + terminal.append(`[ERROR] Cancel failed: ${err.message}`); + } + }, + + setRunningState(running) { + isRunning = running; + ui.elements.runBtn.disabled = running; + ui.elements.cancelBtn.disabled = !running; + } +}; diff --git a/api/static/js/components/results.js b/api/static/js/components/results.js new file mode 100644 index 0000000..d06d7fc --- /dev/null +++ b/api/static/js/components/results.js @@ -0,0 +1,296 @@ +/** + * results.js - Data visualization and reports + */ +import { api } from '../api.js'; +import { ui } from '../ui.js'; + +let activeCharts = []; // Store chart instances to destroy them before redraw + +export const results = { + async init() { + // Bind events + ui.elements.csvFileSelect.addEventListener('change', (e) => this.loadCSVData(e.target.value)); + ui.elements.chartFileSelect.addEventListener('change', (e) => this.loadChartData(e.target.value)); + ui.elements.judgeFileSelect.addEventListener('change', (e) => this.loadJudgeData(e.target.value)); + + // Initial lists + await this.loadAll(); + }, + + async loadAll() { + await Promise.all([ + this.updateCSVFileList(), + this.updateJudgeFileList(), + this.loadImages() // Static images just load once + ]); + }, + + async updateCSVFileList() { + try { + const files = await api.get('/api/results/files'); + const options = files.length > 0 + ? files.map(f => `<option value="${f}">${f}</option>`).join('') + : '<option value="">No files found</option>'; + + ui.elements.csvFileSelect.innerHTML = options; + ui.elements.chartFileSelect.innerHTML = options; + + // Prefer loading results.csv by default if it exists + const defaultFile = files.find(f => f.endsWith('results.csv')) || files[0]; + + if (defaultFile) { + ui.elements.csvFileSelect.value = defaultFile; + ui.elements.chartFileSelect.value = defaultFile; + await this.loadCSVData(defaultFile); + await this.loadChartData(defaultFile); + } + } catch (error) { + console.error("Failed loading CSV file list", error); + } + }, + + async updateJudgeFileList() { + try { + const files = await api.get('/api/judge/files'); + const options = files.length > 0 + ? files.map(f => `<option value="${f}">${f}</option>`).join('') + : '<option value="">No judge reports found</option>'; + + 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 loadCSVData(filePath) { + if (!filePath) return; + ui.elements.csvResults.innerHTML = '<div class="empty-state">Loading data...</div>'; + + try { + const data = await api.get(`/api/results?file=${encodeURIComponent(filePath)}`); + if (Object.keys(data).length === 0 || data.error) { + ui.elements.csvResults.innerHTML = '<div class="empty-state">Failed to load data.</div>'; + return; + } + + // Since we asked for a specific file, it's the only key + const content = Object.values(data)[0]; + if (content.error) { + ui.elements.csvResults.innerHTML = `<div class="empty-state">Error: ${content.error}</div>`; + return; + } + + let html = `<div class="data-table-wrapper"> + <div class="data-table-title">${filePath}</div> + <div style="overflow-x: auto;"> + <table class="data-table"> + <thead><tr>${content.columns.map(c => `<th>${c}</th>`).join('')}</tr></thead> + <tbody> + ${content.data.map(row => ` + <tr>${content.columns.map(col => `<td>${row[col] ?? ''}</td>`).join('')}</tr> + `).join('')} + </tbody> + </table> + </div> + </div>`; + + ui.elements.csvResults.innerHTML = html; + } catch (err) { + ui.elements.csvResults.innerHTML = `<div class="empty-state">Error: ${err.message}</div>`; + } + }, + + async loadChartData(filePath) { + if (!filePath) return; + + // Clean up old charts + activeCharts.forEach(chart => chart.destroy()); + activeCharts = []; + ui.elements.interactiveCharts.innerHTML = '<div class="empty-state" style="grid-column: 1 / -1;">Generating charts...</div>'; + + try { + const data = await api.get(`/api/results?file=${encodeURIComponent(filePath)}`); + const content = Object.values(data)[0]; + + if (!content || content.error || !content.data || content.data.length === 0) { + ui.elements.interactiveCharts.innerHTML = '<div class="empty-state" style="grid-column: 1 / -1;">Not enough data to graph.</div>'; + return; + } + + ui.elements.interactiveCharts.innerHTML = ''; + + // Generate charts based on available columns + const cols = content.columns; + + // 1. Blackbox Categories + if (cols.includes('blackbox_category')) { + this.renderPieChart('Blackbox Categories', content.data, 'blackbox_category'); + } + + // 2. Glassbox Categories + if (cols.includes('glassbox_category')) { + this.renderPieChart('Glassbox Categories', content.data, 'glassbox_category'); + } + + // 3. Models vs Blackbox Category + if (cols.includes('model') && cols.includes('blackbox_category')) { + this.renderBarChart('Deception by Model', content.data, 'model', 'blackbox_category'); + } + + // 4. Fallback if no specific columns exist + if (ui.elements.interactiveCharts.innerHTML === '') { + ui.elements.interactiveCharts.innerHTML = '<div class="empty-state" style="grid-column: 1 / -1;">No plottable categorical columns found in this CSV.</div>'; + } + + } catch (err) { + ui.elements.interactiveCharts.innerHTML = `<div class="empty-state" style="grid-column: 1 / -1;">Chart Error: ${err.message}</div>`; + } + }, + + renderPieChart(title, data, column) { + const counts = {}; + data.forEach(row => { + const val = row[column] || 'Unknown'; + counts[val] = (counts[val] || 0) + 1; + }); + + const canvasId = `chart-${Math.random().toString(36).substr(2, 9)}`; + const container = document.createElement('div'); + container.className = 'chart-container'; + container.innerHTML = `<canvas id="${canvasId}"></canvas>`; + ui.elements.interactiveCharts.appendChild(container); + + const ctx = document.getElementById(canvasId).getContext('2d'); + const chart = new Chart(ctx, { + type: 'doughnut', + data: { + labels: Object.keys(counts), + datasets: [{ + data: Object.values(counts), + backgroundColor: ['#9d4edd', '#ef4444', '#10b981', '#f59e0b', '#3b82f6', '#6366f1'], + borderWidth: 0 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + title: { display: true, text: title, color: '#f0f0f2', font: { family: 'Inter', size: 14 } }, + legend: { labels: { color: '#9ea0a6' }, position: 'right' } + } + } + }); + activeCharts.push(chart); + }, + + renderBarChart(title, data, xCol, groupCol) { + // Group by X then 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); + }); + + const labels = Object.keys(matrix); + const datasets = Array.from(groups).map((group, i) => { + const colors = ['#ef4444', '#10b981', '#f59e0b', '#3b82f6', '#9d4edd']; + return { + label: group, + data: labels.map(label => matrix[label][group] || 0), + backgroundColor: colors[i % colors.length], + } + }); + + const canvasId = `chart-${Math.random().toString(36).substr(2, 9)}`; + const container = document.createElement('div'); + container.className = 'chart-container'; + container.innerHTML = `<canvas id="${canvasId}"></canvas>`; + ui.elements.interactiveCharts.appendChild(container); + + const ctx = document.getElementById(canvasId).getContext('2d'); + const chart = new Chart(ctx, { + type: 'bar', + data: { labels, datasets }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { stacked: true, ticks: { color: '#9ea0a6' }, grid: { color: '#2a2a2e' } }, + y: { stacked: true, ticks: { color: '#9ea0a6', stepSize: 1 }, grid: { color: '#2a2a2e' } } + }, + plugins: { + title: { display: true, text: title, color: '#f0f0f2', font: { family: 'Inter', size: 14 } }, + legend: { labels: { color: '#9ea0a6' } } + } + } + }); + activeCharts.push(chart); + }, + + async loadImages() { + const images = await api.get('/api/results/images'); + if (images.length === 0) return; + + ui.elements.imageResults.innerHTML = images.map(img => ` + <div class="image-card"> + <img src="/api/results/images/${img.name}" alt="${img.name}" loading="lazy"> + <div class="image-card-footer">${img.name}</div> + </div> + `).join(''); + }, + + async loadJudgeData(filePath) { + if (!filePath) return; + ui.elements.judgeResults.innerHTML = '<div class="empty-state">Loading data...</div>'; + + try { + const data = await api.get(`/api/judge/results?file=${encodeURIComponent(filePath)}`); + if (Object.keys(data).length === 0 || data.message) { + ui.elements.judgeResults.innerHTML = '<div class="empty-state">No data available in this report.</div>'; + return; + } + + const content = Object.values(data)[0]; + if (content.error) { + ui.elements.judgeResults.innerHTML = `<div class="empty-state">Error: ${content.error}</div>`; + return; + } + + let html = ''; + if (content.type === 'csv') { + html += `<div class="data-table-wrapper"> + <div class="data-table-title">${filePath}</div> + <div style="overflow-x: auto;"> + <table class="data-table"> + <thead><tr>${content.columns.map(c => `<th>${c}</th>`).join('')}</tr></thead> + <tbody> + ${content.data.map(row => ` + <tr>${content.columns.map(col => `<td>${row[col] ?? ''}</td>`).join('')}</tr> + `).join('')} + </tbody> + </table> + </div> + </div>`; + } else { + // Render JSON as pretty block + html += `<div class="data-table-wrapper"> + <div class="data-table-title">${filePath}</div> + <pre style="padding: 16px; color: #f0f0f2; font-family: monospace; white-space: pre-wrap; word-wrap: break-word;">${JSON.stringify(content.data, null, 2)}</pre> + </div>`; + } + ui.elements.judgeResults.innerHTML = html; + } catch (err) { + ui.elements.judgeResults.innerHTML = `<div class="empty-state">Error: ${err.message}</div>`; + } + } +}; diff --git a/api/static/js/components/terminal.js b/api/static/js/components/terminal.js new file mode 100644 index 0000000..77c9794 --- /dev/null +++ b/api/static/js/components/terminal.js @@ -0,0 +1,49 @@ +/** + * terminal.js - Log rendering logic + */ +import { ui } from '../ui.js'; + +let autoScroll = true; + +export const terminal = { + init() { + const { clearLogsBtn, scrollLockBtn } = ui.elements; + + clearLogsBtn.addEventListener('click', () => this.clear()); + + scrollLockBtn.addEventListener('click', () => { + autoScroll = !autoScroll; + scrollLockBtn.classList.toggle('active', autoScroll); + }); + }, + + append(message) { + const { logsContainer } = ui.elements; + const line = document.createElement('div'); + line.className = 'terminal-line'; + + // High-speed parsing for log levels + if (message.includes('[ERROR]')) line.classList.add('error'); + else if (message.includes('[WARN]')) line.classList.add('warning'); + else if (message.includes('[DEBUG]')) line.classList.add('muted'); + else if (message.includes('SUCCESS') || message.includes('complete')) line.classList.add('success'); + else if (message.includes('Reasoning:')) line.classList.add('reasoning'); + else line.classList.add('info'); + + line.textContent = message; + logsContainer.appendChild(line); + + if (autoScroll) { + logsContainer.scrollTop = logsContainer.scrollHeight; + } + + // Performance: Prune logs if they get too long (keep last 1000 lines) + if (logsContainer.children.length > 1000) { + logsContainer.removeChild(logsContainer.firstChild); + } + }, + + clear() { + ui.elements.logsContainer.innerHTML = '<div class="terminal-line muted">Logs cleared.</div>'; + } +}; diff --git a/api/static/js/main.js b/api/static/js/main.js new file mode 100644 index 0000000..21a957a --- /dev/null +++ b/api/static/js/main.js @@ -0,0 +1,66 @@ +/** + * main.js - Application Entry Point + */ +import { ui } from './ui.js'; +import { api } from './api.js'; +import { terminal } from './components/terminal.js'; +import { config } from './components/config.js'; +import { results } from './components/results.js'; + +async function init() { + console.log('Initializing AI Reasoning Framework GUI...'); + + // Init Core Components + terminal.init(); + await config.init(); + await results.init(); + + // Init UI behaviors + ui.initTabs((tabId) => { + // Refresh data when switching to results tabs + if (['csv', 'images', 'judge'].includes(tabId)) { + results.loadAll(); + } + }); + + // Initial Status Check + try { + const status = await api.get('/api/run/status'); + ui.updateStatus(status.status, status.start_time); + + if (status.status === 'running') { + config.setRunningState(true); + // Re-attach to stream if page reloaded + api.streamLogs( + (log) => terminal.append(log), + () => { + config.setRunningState(false); + ui.updateStatus('complete'); + results.loadAll(); + } + ); + } + } catch (e) { + console.warn('Initial status check failed', e); + } +} + +// Start the app +function start() { + init().catch(err => { + console.error("Initialization error:", err); + const logsContainer = document.getElementById('logs-container'); + if (logsContainer) { + const errorLine = document.createElement('div'); + errorLine.className = 'terminal-line error'; + errorLine.textContent = `[GUI STARTUP ERROR] ${err.message}`; + logsContainer.appendChild(errorLine); + } + }); +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', start); +} else { + start(); +} diff --git a/api/static/js/ui.js b/api/static/js/ui.js new file mode 100644 index 0000000..a2c8b8b --- /dev/null +++ b/api/static/js/ui.js @@ -0,0 +1,62 @@ +/** + * ui.js - Global UI Utilities + */ + +export const ui = { + get elements() { + return { + scenarioSelect: document.getElementById('scenario-select'), + modelSelect: document.getElementById('model-select'), + oversightSelect: document.getElementById('oversight-select'), + runsInput: document.getElementById('runs-input'), + runBtn: document.getElementById('run-btn'), + cancelBtn: document.getElementById('cancel-btn'), + statusBadge: document.getElementById('status-badge'), + statusTime: document.getElementById('status-time'), + logsContainer: document.getElementById('logs-container'), + + csvResults: document.getElementById('csv-results'), + imageResults: document.getElementById('image-results'), + judgeResults: document.getElementById('judge-results'), + interactiveCharts: document.getElementById('interactive-charts'), + + csvFileSelect: document.getElementById('csv-file-select'), + chartFileSelect: document.getElementById('chart-file-select'), + judgeFileSelect: document.getElementById('judge-file-select'), + + tabTriggers: document.querySelectorAll('.tab-trigger'), + tabPanels: document.querySelectorAll('.tab-panel'), + clearLogsBtn: document.getElementById('clear-logs-btn'), + scrollLockBtn: document.getElementById('scroll-lock-btn') + }; + }, + + updateStatus(status, time = null) { + const { statusBadge, statusTime } = this.elements; + statusBadge.className = `badge badge-${status}`; + statusBadge.textContent = status.toUpperCase(); + + if (time) { + const date = new Date(time); + statusTime.textContent = date.toLocaleTimeString(); + } else if (status === 'idle') { + statusTime.textContent = ''; + } + }, + + initTabs(onTabChange) { + this.elements.tabTriggers.forEach(trigger => { + trigger.addEventListener('click', () => { + const tabId = trigger.dataset.tab; + + this.elements.tabTriggers.forEach(t => t.classList.remove('active')); + this.elements.tabPanels.forEach(p => p.classList.remove('active')); + + trigger.classList.add('active'); + document.getElementById(`tab-${tabId}`).classList.add('active'); + + if (onTabChange) onTabChange(tabId); + }); + }); + } +}; diff --git a/api/static/style.css b/api/static/style.css deleted file mode 100644 index 8a1a13a..0000000 --- a/api/static/style.css +++ /dev/null @@ -1,461 +0,0 @@ -/* ========================================================================== - AI Agent Reasoning Experiment Framework - Web GUI Styles - ========================================================================== */ - -:root { - --bg-primary: #0f1419; - --bg-secondary: #1a1f26; - --bg-tertiary: #242b33; - --bg-card: #1e2530; - --text-primary: #e7e9ea; - --text-secondary: #8b98a5; - --text-muted: #5c6c7a; - --accent-primary: #1d9bf0; - --accent-primary-hover: #1a8cd8; - --accent-success: #00ba7c; - --accent-warning: #ffd400; - --accent-danger: #f4212e; - --border-color: #2f3942; - --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; - --font-mono: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: var(--font-family); - background: var(--bg-primary); - color: var(--text-primary); - line-height: 1.6; - min-height: 100vh; -} - -.container { - max-width: 1400px; - margin: 0 auto; - padding: 20px; -} - -/* ========================================================================== - Header - ========================================================================== */ - -header { - text-align: center; - padding: 40px 0 30px; - border-bottom: 1px solid var(--border-color); - margin-bottom: 30px; -} - -header h1 { - font-size: 2rem; - font-weight: 700; - margin-bottom: 8px; - background: linear-gradient(135deg, var(--accent-primary), #a855f7); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -header .subtitle { - color: var(--text-secondary); - font-size: 1.1rem; -} - -/* ========================================================================== - Cards & Sections - ========================================================================== */ - -.card { - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: 12px; - padding: 24px; - margin-bottom: 24px; -} - -.card h2 { - font-size: 1.25rem; - font-weight: 600; - margin-bottom: 20px; - color: var(--text-primary); - display: flex; - align-items: center; - gap: 10px; -} - -/* ========================================================================== - Configuration Grid - ========================================================================== */ - -.config-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 20px; - margin-bottom: 24px; -} - -.config-item { - display: flex; - flex-direction: column; - gap: 8px; -} - -.config-item label { - font-size: 0.875rem; - color: var(--text-secondary); - font-weight: 500; -} - -.config-item select, -.config-item input { - padding: 10px 14px; - border: 1px solid var(--border-color); - border-radius: 8px; - background: var(--bg-tertiary); - color: var(--text-primary); - font-size: 0.95rem; - transition: border-color 0.2s, box-shadow 0.2s; -} - -.config-item select:focus, -.config-item input:focus { - outline: none; - border-color: var(--accent-primary); - box-shadow: 0 0 0 3px rgba(29, 155, 240, 0.15); -} - -.config-actions { - display: flex; - gap: 12px; -} - -/* ========================================================================== - Buttons - ========================================================================== */ - -.btn { - padding: 12px 24px; - border: none; - border-radius: 8px; - font-size: 0.95rem; - font-weight: 600; - cursor: pointer; - transition: all 0.2s; - display: inline-flex; - align-items: center; - gap: 8px; -} - -.btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.btn-primary { - background: var(--accent-primary); - color: white; -} - -.btn-primary:hover:not(:disabled) { - background: var(--accent-primary-hover); - transform: translateY(-1px); -} - -.btn-secondary { - background: var(--bg-tertiary); - color: var(--text-primary); - border: 1px solid var(--border-color); -} - -.btn-secondary:hover:not(:disabled) { - background: var(--border-color); -} - -.btn-danger { - background: transparent; - color: var(--accent-danger); - border: 1px solid var(--accent-danger); -} - -.btn-danger:hover:not(:disabled) { - background: var(--accent-danger); - color: white; -} - -.btn .btn-spinner { - display: none; -} - -.btn.loading .btn-text { - display: none; -} - -.btn.loading .btn-spinner { - display: inline; -} - -/* ========================================================================== - Status Badge - ========================================================================== */ - -.status-indicator { - display: flex; - align-items: center; - gap: 16px; -} - -.badge { - padding: 6px 14px; - border-radius: 20px; - font-size: 0.875rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.badge-idle { - background: var(--bg-tertiary); - color: var(--text-secondary); -} - -.badge-running { - background: rgba(29, 155, 240, 0.15); - color: var(--accent-primary); - animation: pulse 2s infinite; -} - -.badge-complete { - background: rgba(0, 186, 124, 0.15); - color: var(--accent-success); -} - -.badge-error { - background: rgba(244, 33, 46, 0.15); - color: var(--accent-danger); -} - -.badge-cancelled { - background: rgba(255, 212, 0, 0.15); - color: var(--accent-warning); -} - -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.7; } -} - -#status-time { - color: var(--text-muted); - font-size: 0.875rem; -} - -/* ========================================================================== - Logs Container - ========================================================================== */ - -.logs-container { - background: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 16px; - font-family: var(--font-mono); - font-size: 0.8rem; - max-height: 400px; - overflow-y: auto; - line-height: 1.8; -} - -.logs-container::-webkit-scrollbar { - width: 8px; -} - -.logs-container::-webkit-scrollbar-track { - background: var(--bg-primary); -} - -.logs-container::-webkit-scrollbar-thumb { - background: var(--border-color); - border-radius: 4px; -} - -.log-line { - padding: 2px 0; - white-space: pre-wrap; - word-break: break-all; -} - -.log-line:hover { - background: var(--bg-secondary); -} - -.log-level-1 { color: var(--accent-danger); } -.log-level-2 { color: var(--accent-warning); } -.log-level-3 { color: var(--text-primary); } -.log-level-4 { color: var(--text-muted); } - -.logs-empty { - color: var(--text-muted); - text-align: center; - padding: 40px; -} - -.logs-actions { - margin-top: 12px; -} - -/* ========================================================================== - Tabs - ========================================================================== */ - -.tabs { - display: flex; - gap: 4px; - border-bottom: 1px solid var(--border-color); - margin-bottom: 20px; -} - -.tab-btn { - padding: 12px 20px; - background: transparent; - border: none; - color: var(--text-secondary); - font-size: 0.95rem; - font-weight: 500; - cursor: pointer; - border-bottom: 2px solid transparent; - transition: all 0.2s; - margin-bottom: -1px; -} - -.tab-btn:hover { - color: var(--text-primary); -} - -.tab-btn.active { - color: var(--accent-primary); - border-bottom-color: var(--accent-primary); -} - -.tab-content { - display: none; -} - -.tab-content.active { - display: block; -} - -/* ========================================================================== - Results Tables - ========================================================================== */ - -.data-table { - width: 100%; - border-collapse: collapse; - font-size: 0.875rem; -} - -.data-table th, -.data-table td { - padding: 12px; - text-align: left; - border-bottom: 1px solid var(--border-color); -} - -.data-table th { - background: var(--bg-tertiary); - color: var(--text-secondary); - font-weight: 600; - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 0.5px; -} - -.data-table tr:hover td { - background: var(--bg-secondary); -} - -.data-table td { - color: var(--text-primary); -} - -/* ========================================================================== - Images Grid - ========================================================================== */ - -.image-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 20px; -} - -.image-card { - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: 8px; - overflow: hidden; -} - -.image-card img { - width: 100%; - height: auto; - display: block; -} - -.image-card .image-title { - padding: 12px; - font-size: 0.875rem; - color: var(--text-secondary); - text-align: center; -} - -/* ========================================================================== - Empty States - ========================================================================== */ - -.empty-state { - text-align: center; - padding: 40px; - color: var(--text-muted); - background: var(--bg-secondary); - border-radius: 8px; - border: 1px dashed var(--border-color); -} - -/* ========================================================================== - Footer - ========================================================================== */ - -footer { - text-align: center; - padding: 30px; - color: var(--text-muted); - font-size: 0.875rem; -} - -/* ========================================================================== - Responsive - ========================================================================== */ - -@media (max-width: 768px) { - .container { - padding: 12px; - } - - header h1 { - font-size: 1.5rem; - } - - .config-grid { - grid-template-columns: 1fr; - } - - .logs-container { - max-height: 300px; - font-size: 0.75rem; - } -} |
