summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/server.py370
-rw-r--r--api/static/css/layout.css28
-rw-r--r--api/static/css/terminal.css77
-rw-r--r--api/static/index.html108
-rw-r--r--api/static/js/components/config.js107
-rw-r--r--api/static/js/components/results.js239
-rw-r--r--api/static/js/components/terminal.js49
-rw-r--r--api/static/js/main.js50
-rw-r--r--api/static/js/ui.js45
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