summaryrefslogtreecommitdiff
path: root/api/static
diff options
context:
space:
mode:
Diffstat (limited to 'api/static')
-rw-r--r--api/static/app.js516
-rw-r--r--api/static/css/base.css66
-rw-r--r--api/static/css/components.css161
-rw-r--r--api/static/css/layout.css87
-rw-r--r--api/static/css/results.css113
-rw-r--r--api/static/css/terminal.css77
-rw-r--r--api/static/index.html191
-rw-r--r--api/static/js/api.js46
-rw-r--r--api/static/js/components/config.js107
-rw-r--r--api/static/js/components/results.js296
-rw-r--r--api/static/js/components/terminal.js49
-rw-r--r--api/static/js/main.js66
-rw-r--r--api/static/js/ui.js62
-rw-r--r--api/static/style.css461
14 files changed, 1242 insertions, 1056 deletions
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;
- }
-}