diff options
Diffstat (limited to 'api')
| -rw-r--r-- | api/server.py | 370 | ||||
| -rw-r--r-- | api/static/css/layout.css | 28 | ||||
| -rw-r--r-- | api/static/css/terminal.css | 77 | ||||
| -rw-r--r-- | api/static/index.html | 108 | ||||
| -rw-r--r-- | api/static/js/components/config.js | 107 | ||||
| -rw-r--r-- | api/static/js/components/results.js | 239 | ||||
| -rw-r--r-- | api/static/js/components/terminal.js | 49 | ||||
| -rw-r--r-- | api/static/js/main.js | 50 | ||||
| -rw-r--r-- | api/static/js/ui.js | 45 |
9 files changed, 218 insertions, 855 deletions
diff --git a/api/server.py b/api/server.py index 8bb1e24..c6c788c 100644 --- a/api/server.py +++ b/api/server.py @@ -1,124 +1,28 @@ """ -FastAPI server for the Web GUI. -Wraps core project functionality without modifying it. +FastAPI server for the AI Evaluation Visualization Dashboard. +Serves read-only results, JSON logs, and static visuals. """ import os import sys -import asyncio -import subprocess from pathlib import Path -from typing import Optional, Dict, Any, List -from datetime import datetime -from contextlib import asynccontextmanager +from typing import Dict, Any, List +import pandas as pd +import json +import math -from fastapi import FastAPI, HTTPException, BackgroundTasks, Request -from fastapi.responses import HTMLResponse, FileResponse, JSONResponse +from fastapi import FastAPI, HTTPException +from fastapi.responses import HTMLResponse, FileResponse from fastapi.staticfiles import StaticFiles -from sse_starlette.sse import EventSourceResponse -import yaml # Add project root to path to import core modules PROJECT_ROOT = Path(__file__).parent.parent sys.path.insert(0, str(PROJECT_ROOT)) - from src.config_loader import ConfigLoader - -# Global state for run management -class RunManager: - """Manages experiment runs.""" - - def __init__(self): - self.current_process: Optional[subprocess.Popen] = None - self.status: str = "idle" # idle, running, complete, error - self.start_time: Optional[datetime] = None - self.log_file_path: Optional[str] = None - self.config: Optional[ConfigLoader] = None - - def load_config(self, config_path: str = "config.yaml"): - """Load configuration and update log file path.""" - self.config = ConfigLoader(config_path) - self.config.load() - self.log_file_path = self.config.logging_config.get('file') - - # If relative path, make it absolute from project root - if self.log_file_path and not os.path.isabs(self.log_file_path): - self.log_file_path = os.path.join(PROJECT_ROOT, self.log_file_path) - - return self.config - - async def start_run(self, config_path: str = "config.yaml"): - """Start an experiment run in background.""" - if self.status == "running": - raise HTTPException(status_code=409, detail="A run is already in progress") - - # Load config to get log file path - self.load_config(config_path) - - self.status = "running" - self.start_time = datetime.now() - - # Start the run in background - cmd = [sys.executable, "-m", "uv", "run", "src/main.py"] - self.current_process = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - bufsize=1, - cwd=PROJECT_ROOT - ) - - return {"status": "started", "message": "Experiment run started"} - - def cancel_run(self): - """Cancel the current run.""" - if self.current_process: - self.current_process.terminate() - self.current_process = None - self.status = "cancelled" - return {"status": "cancelled"} - return {"status": "idle", "message": "No run to cancel"} - - def get_status(self): - """Get current run status.""" - if self.current_process and self.current_process.poll() is None: - self.status = "running" - elif self.status == "running": - self.status = "complete" - - return { - "status": self.status, - "start_time": self.start_time.isoformat() if self.start_time else None, - } - - -# Global instance -run_manager = RunManager() - - -@asynccontextmanager -async def lifespan(app: FastAPI): - """Application lifespan handler.""" - # Startup: Load config - try: - run_manager.load_config() - except Exception as e: - print(f"Warning: Could not load config: {e}") - - yield - - # Shutdown: Cancel any running process - if run_manager.current_process: - run_manager.cancel_run() - - -# Create FastAPI app app = FastAPI( - title="AI Agent Reasoning Experiment Framework", - description="Web GUI for running experiments and viewing results", - version="0.1.0", - lifespan=lifespan + title="AI Agent Evaluation Dashboard", + description="Web GUI for analyzing experiment results", + version="0.2.0" ) # Mount static files @@ -126,6 +30,15 @@ static_dir = Path(__file__).parent / "static" if static_dir.exists(): app.mount("/static", StaticFiles(directory=str(static_dir)), name="static") +def get_config(): + """Helper to load config safely.""" + try: + config = ConfigLoader(str(PROJECT_ROOT / "config.yaml")) + config.load() + return config + except Exception as e: + print(f"Warning: Could not load config: {e}") + return None # ============================================================================ # Root Endpoint - Serve HTML @@ -139,225 +52,6 @@ async def root(): return FileResponse(index_path) return HTMLResponse(content="<h1>index.html not found</h1>", status_code=404) - -# ============================================================================ -# Config Endpoints -# ============================================================================ - -@app.get("/api/config") -async def get_config(): - """Read current config.yaml.""" - config_path = PROJECT_ROOT / "config.yaml" - if not config_path.exists(): - raise HTTPException(status_code=404, detail="config.yaml not found") - - with open(config_path) as f: - config_data = yaml.safe_load(f) - - return config_data - - -@app.put("/api/config") -async def update_config(config_data: Dict[str, Any]): - """Update config.yaml.""" - config_path = PROJECT_ROOT / "config.yaml" - - with open(config_path, 'w') as f: - yaml.dump(config_data, f, default_flow_style=False) - - # Reload config in run manager - run_manager.load_config() - - return {"status": "saved", "message": "Configuration updated"} - - -@app.get("/api/logging") -async def get_logging_config(): - """Get logging configuration including file path.""" - config = run_manager.config - if not config: - raise HTTPException(status_code=500, detail="Config not loaded") - - return { - "level": config.logging_config.get('level'), - "format": config.logging_config.get('format'), - "output": config.logging_config.get('output'), - "file": config.logging_config.get('file'), - "file_absolute": run_manager.log_file_path - } - - -# ============================================================================ -# Discovery Endpoints -# ============================================================================ - -@app.get("/api/scenarios") -async def list_scenarios(): - """List all available scenarios from scenarios/ directory.""" - scenarios_dir = PROJECT_ROOT / "scenarios" - if not scenarios_dir.exists(): - return [] - - scenarios = [] - for item in scenarios_dir.iterdir(): - if item.is_dir() and not item.name.startswith('.'): - # Check for oversight levels - oversight_dir = item / "oversight" - oversight_levels = [] - if oversight_dir.exists(): - oversight_levels = [f.stem for f in oversight_dir.glob("*.md")] - - scenarios.append({ - "name": item.name, - "path": str(item.relative_to(PROJECT_ROOT)), - "oversight_levels": oversight_levels - }) - - return scenarios - - -@app.get("/api/scenarios/{scenario_name}") -async def get_scenario(scenario_name: str): - """Get details for a specific scenario.""" - scenario_path = PROJECT_ROOT / "scenarios" / scenario_name - if not scenario_path.exists(): - raise HTTPException(status_code=404, detail="Scenario not found") - - # Read scenario files - files = {} - for md_file in scenario_path.glob("*.md"): - if md_file.name != "regex_rules.yaml": - with open(md_file) as f: - files[md_file.stem] = f.read() - - # Check oversight levels - oversight_dir = scenario_path / "oversight" - oversight_levels = {} - if oversight_dir.exists(): - for md_file in oversight_dir.glob("*.md"): - with open(md_file) as f: - oversight_levels[md_file.stem] = f.read() - - return { - "name": scenario_name, - "files": files, - "oversight_levels": oversight_levels - } - - -@app.get("/api/models") -async def list_models(): - """List models from config.""" - config = run_manager.config - if not config: - raise HTTPException(status_code=500, detail="Config not loaded") - - return [ - { - "id": model.id, - "provider": model.provider, - "temperature": model.temperature, - "max_tokens": model.max_tokens - } - for model in config.models - ] - - -@app.get("/api/providers") -async def list_providers(): - """List providers from config.""" - config = run_manager.config - if not config: - raise HTTPException(status_code=500, detail="Config not loaded") - - return { - name: { - "base_url": provider.base_url, - "api_key_env": provider.api_key_env - } - for name, provider in config.providers.items() - } - - -# ============================================================================ -# Execution Endpoints -# ============================================================================ - -@app.post("/api/run") -async def start_run(background_tasks: BackgroundTasks): - """Start an experiment run.""" - try: - result = await run_manager.start_run() - return result - except HTTPException: - raise - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - - -@app.get("/api/run/status") -async def get_run_status(): - """Get current run status.""" - return run_manager.get_status() - - -@app.delete("/api/run") -async def cancel_run(): - """Cancel the current run.""" - return run_manager.cancel_run() - - -@app.get("/api/logs/stream") -async def log_stream(): - """Stream logs in real-time using SSE.""" - async def event_generator(): - log_file = run_manager.log_file_path - - if not log_file or not os.path.exists(log_file): - yield {"event": "error", "data": "Log file not found"} - return - - # Track file position for tailing - file_pos = 0 - - while True: - # Check if process is still running - status = run_manager.get_status() - if status["status"] == "idle" and not run_manager.current_process: - break - - try: - if os.path.exists(log_file): - with open(log_file, 'r') as f: - f.seek(file_pos) - new_lines = f.readlines() - file_pos = f.tell() - - for line in new_lines: - yield {"event": "log", "data": line.rstrip()} - - # Check if process ended - if run_manager.current_process and run_manager.current_process.poll() is not None: - # Process finished, yield remaining logs - if os.path.exists(log_file): - with open(log_file, 'r') as f: - f.seek(file_pos) - remaining = f.read() - if remaining: - yield {"event": "log", "data": remaining} - break - - except Exception as e: - yield {"event": "error", "data": str(e)} - break - - await asyncio.sleep(0.5) - - yield {"event": "done", "data": "Run completed"} - - return EventSourceResponse(event_generator()) - - # ============================================================================ # Results Endpoints # ============================================================================ @@ -365,7 +59,7 @@ async def log_stream(): @app.get("/api/results/files") async def get_results_files(): """List all CSV files in the output directory.""" - config = run_manager.config + config = get_config() if not config: raise HTTPException(status_code=500, detail="Config not loaded") @@ -381,7 +75,7 @@ async def get_results_files(): @app.get("/api/results") async def get_results(file: str = None): """Get experiment results as JSON, optionally for a specific file.""" - config = run_manager.config + config = get_config() if not config: raise HTTPException(status_code=500, detail="Config not loaded") @@ -394,13 +88,10 @@ async def get_results(file: str = None): 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: - import pandas as pd - import math try: df = pd.read_csv(csv_file) @@ -425,11 +116,10 @@ async def get_results(file: str = None): return results - @app.get("/api/results/images") async def list_result_images(): """List generated visualization images.""" - config = run_manager.config + config = get_config() if not config: return [] @@ -449,11 +139,10 @@ async def list_result_images(): return images - @app.get("/api/results/images/{image_name}") async def get_result_image(image_name: str): """Serve a specific image.""" - config = run_manager.config + config = get_config() if not config: raise HTTPException(status_code=500, detail="Config not loaded") @@ -466,7 +155,6 @@ async def get_result_image(image_name: str): return FileResponse(image_path) - # ============================================================================ # Judge Results Endpoints # ============================================================================ @@ -474,7 +162,7 @@ async def get_result_image(image_name: str): @app.get("/api/judge/files") async def get_judge_files(): """Get list of judge result files.""" - config = run_manager.config + config = get_config() if not config: raise HTTPException(status_code=500, detail="Config not loaded") @@ -490,11 +178,10 @@ async def get_judge_files(): files.append(str(p.relative_to(judge_dir))) return sorted(files) - @app.get("/api/judge/results") async def get_judge_results(file: str = None): """Get judge results if available.""" - config = run_manager.config + config = get_config() if not config: raise HTTPException(status_code=500, detail="Config not loaded") @@ -511,14 +198,11 @@ async def get_judge_results(file: str = None): 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 try: df = pd.read_csv(rf) results[rf.stem] = { @@ -529,7 +213,6 @@ async def get_judge_results(file: str = None): except Exception as e: results[rf.stem] = {"error": str(e)} elif rf.suffix == '.json': - import json try: with open(rf) as f: results[rf.stem] = {"type": "json", "data": json.load(f)} @@ -538,7 +221,6 @@ async def get_judge_results(file: str = None): return results - if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/api/static/css/layout.css b/api/static/css/layout.css index ef0fc2d..2946461 100644 --- a/api/static/css/layout.css +++ b/api/static/css/layout.css @@ -1,14 +1,14 @@ .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; + grid-column: 1; + grid-row: 1; background-color: var(--color-bg-1); border-right: var(--border-width) solid var(--color-bg-3); padding: var(--space-lg); @@ -18,7 +18,7 @@ z-index: 10; } -/* Main Area - Top Right */ +/* Main Area - Right */ .main-viewport { grid-column: 2; grid-row: 1; @@ -28,17 +28,6 @@ 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); @@ -66,7 +55,7 @@ @media (max-width: 900px) { .dashboard-root { grid-template-columns: 1fr; - grid-template-rows: auto 1fr auto; + grid-template-rows: auto 1fr; overflow-y: auto; } .sidebar { @@ -77,11 +66,6 @@ .main-viewport { grid-column: 1; grid-row: 2; - height: 500px; + height: auto; } - .terminal-dock { - grid-column: 1; - grid-row: 3; - height: 300px; - } -} +}
\ No newline at end of file diff --git a/api/static/css/terminal.css b/api/static/css/terminal.css deleted file mode 100644 index 7cf79ba..0000000 --- a/api/static/css/terminal.css +++ /dev/null @@ -1,77 +0,0 @@ -.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 564618a..4903c06 100644 --- a/api/static/index.html +++ b/api/static/index.html @@ -3,12 +3,11 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>AI Reasoning Framework</title> + <title>AI Agent Evaluation Dashboard</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"> @@ -19,95 +18,78 @@ </head> <body> <div class="dashboard-root"> - <!-- Sidebar: Configuration --> + <!-- Sidebar: Data Filters --> <aside class="sidebar"> <div class="logo-area"> - <h1>AI Agent Framework</h1> + <h1>AI Dashboard</h1> </div> <nav class="config-panel"> <div class="form-group"> - <label class="form-label">Scenario</label> - <select id="scenario-select" class="input-field"> + <label class="form-label">Data Source (CSV)</label> + <select id="csv-file-select" class="input-field"> <option value="">Loading...</option> </select> </div> - - <div class="form-group"> - <label class="form-label">Model</label> - <select id="model-select" class="input-field"> - <option value="">Loading...</option> + + <div class="form-group" style="margin-top: var(--space-lg);"> + <label class="form-label">Global Filters</label> + <div style="font-size: 0.8rem; color: var(--color-text-muted); margin-bottom: var(--space-sm);">Filter active charts by attributes:</div> + + <label class="form-label" style="font-size: 0.65rem;">Model</label> + <select id="filter-model" class="input-field"> + <option value="all">All Models</option> </select> - </div> - - <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> + + <label class="form-label" style="font-size: 0.65rem; margin-top: 8px;">Scenario</label> + <select id="filter-scenario" class="input-field"> + <option value="all">All Scenarios</option> + </select> + + <label class="form-label" style="font-size: 0.65rem; margin-top: 8px;">Oversight</label> + <select id="filter-oversight" class="input-field"> + <option value="all">All Levels</option> </select> </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="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 class="sidebar-actions" style="margin-top: auto;"> + <button id="refresh-btn" class="btn btn-primary" style="width: 100%;">Apply Filters</button> </div> </nav> - - <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> </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 active" data-tab="images">Visual Analytics</button> + <button class="tab-trigger" data-tab="csv">Raw Dataset</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 id="tab-images" class="tab-panel"> + <div id="tab-images" class="tab-panel active"> <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> + <h2 style="font-size: 1.1rem; color: var(--color-text-primary); margin: 0;">Interactive Dashboard</h2> + <span id="data-stats" style="margin-left: auto; font-size: 0.8rem; font-weight: 600; color: var(--color-accent);"></span> </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> + <h3 style="margin: var(--space-xl) var(--space-lg) var(--space-sm); font-size: 0.8rem; text-transform: uppercase; color: var(--color-text-muted); border-bottom: 1px solid var(--color-bg-3); padding-bottom: 8px;">Static Visualizations (viz/)</h3> <div id="image-results" class="image-grid"> <div class="empty-state" style="grid-column: 1 / -1;">No visualizations generated.</div> </div> </div> + + <div id="tab-csv" class="tab-panel"> + <div id="csv-results" class="results-container"> + <div class="empty-state">No active data.</div> + </div> + </div> <div id="tab-judge" class="tab-panel"> <div class="results-controls"> @@ -122,23 +104,9 @@ </div> </div> </main> - - <!-- 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> <!-- Main Entry Point --> <script type="module" src="/static/js/main.js"></script> </body> -</html> +</html>
\ No newline at end of file diff --git a/api/static/js/components/config.js b/api/static/js/components/config.js deleted file mode 100644 index d87c874..0000000 --- a/api/static/js/components/config.js +++ /dev/null @@ -1,107 +0,0 @@ -/** - * 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 index d06d7fc..92514c4 100644 --- a/api/static/js/components/results.js +++ b/api/static/js/components/results.js @@ -4,24 +4,21 @@ import { api } from '../api.js'; import { ui } from '../ui.js'; -let activeCharts = []; // Store chart instances to destroy them before redraw +let activeCharts = []; +let currentCsvData = null; // Store fetched data locally for quick filtering +let currentCsvColumns = []; 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.csvFileSelect.addEventListener('change', (e) => this.handleCsvChange(e.target.value)); ui.elements.judgeFileSelect.addEventListener('change', (e) => this.loadJudgeData(e.target.value)); + ui.elements.refreshBtn.addEventListener('click', () => this.applyFilters()); - // Initial lists - await this.loadAll(); - }, - - async loadAll() { await Promise.all([ this.updateCSVFileList(), this.updateJudgeFileList(), - this.loadImages() // Static images just load once + this.loadImages() ]); }, @@ -33,19 +30,14 @@ export const results = { : '<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); + await this.handleCsvChange(defaultFile); } } catch (error) { - console.error("Failed loading CSV file list", error); + console.error("Failed loading CSV list", error); } }, @@ -57,7 +49,6 @@ export const results = { : '<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]); @@ -67,92 +58,130 @@ export const results = { } }, - async loadCSVData(filePath) { + async handleCsvChange(filePath) { if (!filePath) return; ui.elements.csvResults.innerHTML = '<div class="empty-state">Loading data...</div>'; + ui.elements.interactiveCharts.innerHTML = '<div class="empty-state" style="grid-column: 1 / -1;">Loading charts...</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>`; + + if (!content || content.error) { + const err = content?.error || "Unknown error"; + ui.elements.csvResults.innerHTML = `<div class="empty-state">Error: ${err}</div>`; + ui.elements.interactiveCharts.innerHTML = `<div class="empty-state" style="grid-column: 1 / -1;">Error: ${err}</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>`; + currentCsvData = content.data; + currentCsvColumns = content.columns; - ui.elements.csvResults.innerHTML = html; + this.populateFilters(); + this.applyFilters(); // This draws charts and tables + } catch (err) { ui.elements.csvResults.innerHTML = `<div class="empty-state">Error: ${err.message}</div>`; + ui.elements.interactiveCharts.innerHTML = `<div class="empty-state" style="grid-column: 1 / -1;">Error: ${err.message}</div>`; } }, - async loadChartData(filePath) { - if (!filePath) return; + populateFilters() { + const models = new Set(); + const scenarios = new Set(); + const oversights = new Set(); + + currentCsvData.forEach(row => { + if (row.model) models.add(row.model); + if (row.scenario) scenarios.add(row.scenario); + if (row.oversight) oversights.add(row.oversight); + }); + + ui.elements.filterModel.innerHTML = '<option value="all">All Models</option>' + + Array.from(models).sort().map(m => `<option value="${m}">${m}</option>`).join(''); + + ui.elements.filterScenario.innerHTML = '<option value="all">All Scenarios</option>' + + Array.from(scenarios).sort().map(s => `<option value="${s}">${s}</option>`).join(''); + + ui.elements.filterOversight.innerHTML = '<option value="all">All Levels</option>' + + Array.from(oversights).sort().map(o => `<option value="${o}">${o}</option>`).join(''); + }, + + applyFilters() { + if (!currentCsvData) return; + + const modelFilter = ui.elements.filterModel.value; + const scenarioFilter = ui.elements.filterScenario.value; + const oversightFilter = ui.elements.filterOversight.value; + + const filteredData = currentCsvData.filter(row => { + return (modelFilter === 'all' || row.model === modelFilter) && + (scenarioFilter === 'all' || row.scenario === scenarioFilter) && + (oversightFilter === 'all' || row.oversight === oversightFilter); + }); + + ui.elements.dataStats.textContent = `Showing ${filteredData.length} of ${currentCsvData.length} records`; + + this.renderTable(filteredData); + this.renderCharts(filteredData); + }, + + renderTable(data) { + if (data.length === 0) { + ui.elements.csvResults.innerHTML = '<div class="empty-state">No data matches filters.</div>'; + return; + } + + let html = `<div class="data-table-wrapper"> + <div style="overflow-x: auto;"> + <table class="data-table"> + <thead><tr>${currentCsvColumns.map(c => `<th>${c}</th>`).join('')}</tr></thead> + <tbody> + ${data.map(row => ` + <tr>${currentCsvColumns.map(col => `<td>${row[col] ?? ''}</td>`).join('')}</tr> + `).join('')} + </tbody> + </table> + </div> + </div>`; - // Clean up old charts + ui.elements.csvResults.innerHTML = html; + }, + + renderCharts(data) { activeCharts.forEach(chart => chart.destroy()); activeCharts = []; - ui.elements.interactiveCharts.innerHTML = '<div class="empty-state" style="grid-column: 1 / -1;">Generating charts...</div>'; + ui.elements.interactiveCharts.innerHTML = ''; - 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; - } + if (data.length === 0) { + ui.elements.interactiveCharts.innerHTML = '<div class="empty-state" style="grid-column: 1 / -1;">No data to chart.</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'); - } + const cols = currentCsvColumns; + + if (cols.includes('blackbox_category')) { + this.renderDoughnutChart('Blackbox Categories', data, 'blackbox_category'); + } + + if (cols.includes('glassbox_category')) { + this.renderDoughnutChart('Glassbox Categories', data, 'glassbox_category'); + } - // 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>'; - } + if (cols.includes('model') && cols.includes('blackbox_category')) { + this.renderStackedBarChart('Blackbox Category by Model', data, 'model', 'blackbox_category'); + } - } catch (err) { - ui.elements.interactiveCharts.innerHTML = `<div class="empty-state" style="grid-column: 1 / -1;">Chart Error: ${err.message}</div>`; + if (cols.includes('oversight') && cols.includes('blackbox_category')) { + this.renderStackedBarChart('Blackbox Category by Oversight', data, 'oversight', 'blackbox_category'); + } + + if (ui.elements.interactiveCharts.innerHTML === '') { + ui.elements.interactiveCharts.innerHTML = '<div class="empty-state" style="grid-column: 1 / -1;">No plottable categorical columns found.</div>'; } }, - renderPieChart(title, data, column) { + renderDoughnutChart(title, data, column) { const counts = {}; data.forEach(row => { const val = row[column] || 'Unknown'; @@ -160,10 +189,7 @@ export const results = { }); 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); + this.createChartContainer(canvasId); const ctx = document.getElementById(canvasId).getContext('2d'); const chart = new Chart(ctx, { @@ -176,20 +202,12 @@ export const results = { 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' } - } - } + options: this.getChartOptions(title) }); activeCharts.push(chart); }, - renderBarChart(title, data, xCol, groupCol) { - // Group by X then GroupCol + renderStackedBarChart(title, data, xCol, groupCol) { const matrix = {}; const groups = new Set(); @@ -203,7 +221,7 @@ export const results = { const labels = Object.keys(matrix); const datasets = Array.from(groups).map((group, i) => { - const colors = ['#ef4444', '#10b981', '#f59e0b', '#3b82f6', '#9d4edd']; + const colors = ['#ef4444', '#10b981', '#f59e0b', '#3b82f6', '#9d4edd', '#6366f1']; return { label: group, data: labels.map(label => matrix[label][group] || 0), @@ -212,31 +230,41 @@ export const results = { }); 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); + this.createChartContainer(canvasId); const ctx = document.getElementById(canvasId).getContext('2d'); const chart = new Chart(ctx, { type: 'bar', data: { labels, datasets }, options: { - responsive: true, - maintainAspectRatio: false, + ...this.getChartOptions(title), 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); }, + createChartContainer(canvasId) { + const container = document.createElement('div'); + container.className = 'chart-container'; + container.innerHTML = `<canvas id="${canvasId}"></canvas>`; + ui.elements.interactiveCharts.appendChild(container); + }, + + getChartOptions(title) { + return { + responsive: true, + maintainAspectRatio: false, + plugins: { + title: { display: true, text: title, color: '#f0f0f2', font: { family: 'Inter', size: 14 } }, + legend: { labels: { color: '#9ea0a6' }, position: 'bottom' } + } + }; + }, + async loadImages() { const images = await api.get('/api/results/images'); if (images.length === 0) return; @@ -269,7 +297,6 @@ export const results = { 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> @@ -282,9 +309,7 @@ export const results = { </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>`; } @@ -293,4 +318,4 @@ export const results = { ui.elements.judgeResults.innerHTML = `<div class="empty-state">Error: ${err.message}</div>`; } } -}; +};
\ No newline at end of file diff --git a/api/static/js/components/terminal.js b/api/static/js/components/terminal.js deleted file mode 100644 index 77c9794..0000000 --- a/api/static/js/components/terminal.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * 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 index 21a957a..8c540c3 100644 --- a/api/static/js/main.js +++ b/api/static/js/main.js @@ -1,61 +1,21 @@ -/** - * 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(); + console.log('Initializing AI Reasoning Dashboard...'); + await results.init(); // Init UI behaviors ui.initTabs((tabId) => { - // Refresh data when switching to results tabs - if (['csv', 'images', 'judge'].includes(tabId)) { - results.loadAll(); - } + // We could lazy load data here, but for now everything triggers on file/filter change }); - - // 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); - } + alert(`Dashboard Failed to Load: ${err.message}`); }); } @@ -63,4 +23,4 @@ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', start); } else { start(); -} +}
\ No newline at end of file diff --git a/api/static/js/ui.js b/api/static/js/ui.js index a2c8b8b..359909b 100644 --- a/api/static/js/ui.js +++ b/api/static/js/ui.js @@ -1,49 +1,26 @@ -/** - * 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'), + csvFileSelect: document.getElementById('csv-file-select'), + judgeFileSelect: document.getElementById('judge-file-select'), + + filterModel: document.getElementById('filter-model'), + filterScenario: document.getElementById('filter-scenario'), + filterOversight: document.getElementById('filter-oversight'), + + refreshBtn: document.getElementById('refresh-btn'), 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'), + dataStats: document.getElementById('data-stats'), tabTriggers: document.querySelectorAll('.tab-trigger'), - tabPanels: document.querySelectorAll('.tab-panel'), - clearLogsBtn: document.getElementById('clear-logs-btn'), - scrollLockBtn: document.getElementById('scroll-lock-btn') + tabPanels: document.querySelectorAll('.tab-panel') }; }, - 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', () => { @@ -59,4 +36,4 @@ export const ui = { }); }); } -}; +};
\ No newline at end of file |
