| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993 |
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>OpenCode Agent Test Dashboard</title>
- <style>
- /* CSS Variables for theming */
- :root {
- --bg-primary: #ffffff;
- --bg-secondary: #f8f9fa;
- --bg-card: #ffffff;
- --text-primary: #212529;
- --text-secondary: #6c757d;
- --border-color: #dee2e6;
- --success: #28a745;
- --danger: #dc3545;
- --warning: #ffc107;
- --info: #17a2b8;
- --primary: #007bff;
- --shadow: rgba(0, 0, 0, 0.1);
- }
- [data-theme="dark"] {
- --bg-primary: #1a1a1a;
- --bg-secondary: #2d2d2d;
- --bg-card: #242424;
- --text-primary: #e9ecef;
- --text-secondary: #adb5bd;
- --border-color: #495057;
- --shadow: rgba(0, 0, 0, 0.3);
- }
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
- background: var(--bg-primary);
- color: var(--text-primary);
- line-height: 1.6;
- transition: background-color 0.3s, color 0.3s;
- }
- .container {
- max-width: 1400px;
- margin: 0 auto;
- padding: 20px;
- }
- /* Header */
- header {
- background: var(--bg-card);
- border-bottom: 2px solid var(--border-color);
- padding: 20px 0;
- margin-bottom: 30px;
- box-shadow: 0 2px 4px var(--shadow);
- }
- .header-content {
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex-wrap: wrap;
- gap: 20px;
- }
- h1 {
- font-size: 28px;
- font-weight: 600;
- color: var(--text-primary);
- }
- .header-actions {
- display: flex;
- gap: 10px;
- align-items: center;
- }
- /* Buttons */
- button {
- padding: 8px 16px;
- border: 1px solid var(--border-color);
- background: var(--bg-card);
- color: var(--text-primary);
- border-radius: 6px;
- cursor: pointer;
- font-size: 14px;
- transition: all 0.2s;
- }
- button:hover {
- background: var(--bg-secondary);
- transform: translateY(-1px);
- }
- button.primary {
- background: var(--primary);
- color: white;
- border-color: var(--primary);
- }
- button.primary:hover {
- background: #0056b3;
- }
- /* Filters */
- .filters {
- background: var(--bg-card);
- padding: 20px;
- border-radius: 8px;
- margin-bottom: 30px;
- box-shadow: 0 2px 4px var(--shadow);
- }
- .filter-row {
- display: flex;
- gap: 15px;
- flex-wrap: wrap;
- align-items: center;
- }
- .filter-group {
- display: flex;
- flex-direction: column;
- gap: 5px;
- }
- .filter-group label {
- font-size: 12px;
- font-weight: 600;
- color: var(--text-secondary);
- text-transform: uppercase;
- }
- select, input {
- padding: 8px 12px;
- border: 1px solid var(--border-color);
- background: var(--bg-primary);
- color: var(--text-primary);
- border-radius: 6px;
- font-size: 14px;
- }
- /* Stats Cards */
- .stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: 20px;
- margin-bottom: 30px;
- }
- .stat-card {
- background: var(--bg-card);
- padding: 20px;
- border-radius: 8px;
- box-shadow: 0 2px 4px var(--shadow);
- border-left: 4px solid var(--primary);
- }
- .stat-card.success {
- border-left-color: var(--success);
- }
- .stat-card.danger {
- border-left-color: var(--danger);
- }
- .stat-card.warning {
- border-left-color: var(--warning);
- }
- .stat-label {
- font-size: 12px;
- font-weight: 600;
- color: var(--text-secondary);
- text-transform: uppercase;
- margin-bottom: 8px;
- }
- .stat-value {
- font-size: 32px;
- font-weight: 700;
- color: var(--text-primary);
- }
- .stat-subtitle {
- font-size: 14px;
- color: var(--text-secondary);
- margin-top: 5px;
- }
- /* Chart Container */
- .chart-container {
- background: var(--bg-card);
- padding: 20px;
- border-radius: 8px;
- margin-bottom: 30px;
- box-shadow: 0 2px 4px var(--shadow);
- }
- .chart-container h2 {
- font-size: 18px;
- margin-bottom: 15px;
- color: var(--text-primary);
- }
- #trendChart {
- max-height: 300px;
- }
- /* Test Results Table */
- .results-container {
- background: var(--bg-card);
- border-radius: 8px;
- box-shadow: 0 2px 4px var(--shadow);
- overflow: hidden;
- }
- .results-header {
- padding: 20px;
- border-bottom: 1px solid var(--border-color);
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .results-header h2 {
- font-size: 18px;
- color: var(--text-primary);
- }
- .search-box {
- position: relative;
- }
- .search-box input {
- padding-left: 35px;
- width: 300px;
- }
- .search-box::before {
- content: "🔍";
- position: absolute;
- left: 12px;
- top: 50%;
- transform: translateY(-50%);
- }
- table {
- width: 100%;
- border-collapse: collapse;
- }
- thead {
- background: var(--bg-secondary);
- }
- th {
- padding: 12px 16px;
- text-align: left;
- font-size: 12px;
- font-weight: 600;
- color: var(--text-secondary);
- text-transform: uppercase;
- cursor: pointer;
- user-select: none;
- }
- th:hover {
- background: var(--border-color);
- }
- th.sortable::after {
- content: " ↕";
- opacity: 0.3;
- }
- th.sort-asc::after {
- content: " ↑";
- opacity: 1;
- }
- th.sort-desc::after {
- content: " ↓";
- opacity: 1;
- }
- td {
- padding: 12px 16px;
- border-bottom: 1px solid var(--border-color);
- }
- tr:hover {
- background: var(--bg-secondary);
- }
- .status-badge {
- display: inline-block;
- padding: 4px 8px;
- border-radius: 4px;
- font-size: 12px;
- font-weight: 600;
- }
- .status-badge.passed {
- background: #d4edda;
- color: #155724;
- }
- .status-badge.failed {
- background: #f8d7da;
- color: #721c24;
- }
- [data-theme="dark"] .status-badge.passed {
- background: #1e4620;
- color: #7dce82;
- }
- [data-theme="dark"] .status-badge.failed {
- background: #5a1f1f;
- color: #f5a3a3;
- }
- .category-badge {
- display: inline-block;
- padding: 4px 8px;
- border-radius: 4px;
- font-size: 11px;
- font-weight: 600;
- background: var(--bg-secondary);
- color: var(--text-secondary);
- }
- .expandable-row {
- cursor: pointer;
- }
- .details-row {
- display: none;
- background: var(--bg-secondary);
- }
- .details-row.show {
- display: table-row;
- }
- .details-content {
- padding: 20px;
- }
- .violation-item {
- padding: 10px;
- margin: 5px 0;
- border-left: 3px solid var(--danger);
- background: var(--bg-card);
- border-radius: 4px;
- }
- .violation-item.warning {
- border-left-color: var(--warning);
- }
- /* Loading State */
- .loading {
- text-align: center;
- padding: 40px;
- color: var(--text-secondary);
- }
- .spinner {
- border: 3px solid var(--border-color);
- border-top: 3px solid var(--primary);
- border-radius: 50%;
- width: 40px;
- height: 40px;
- animation: spin 1s linear infinite;
- margin: 0 auto 20px;
- }
- @keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
- /* Empty State */
- .empty-state {
- text-align: center;
- padding: 60px 20px;
- color: var(--text-secondary);
- }
- .empty-state-icon {
- font-size: 64px;
- margin-bottom: 20px;
- opacity: 0.3;
- }
- /* Responsive */
- @media (max-width: 768px) {
- .header-content {
- flex-direction: column;
- align-items: flex-start;
- }
- .filter-row {
- flex-direction: column;
- align-items: stretch;
- }
- .search-box input {
- width: 100%;
- }
- table {
- font-size: 14px;
- }
- th, td {
- padding: 8px;
- }
- }
- /* Theme Toggle */
- .theme-toggle {
- background: none;
- border: none;
- font-size: 24px;
- cursor: pointer;
- padding: 8px;
- }
- </style>
- </head>
- <body>
- <header>
- <div class="container">
- <div class="header-content">
- <h1>🎯 OpenCode Agent Test Dashboard</h1>
- <div class="header-actions">
- <button id="refreshBtn" class="primary">🔄 Refresh</button>
- <button id="exportBtn">📊 Export CSV</button>
- <button class="theme-toggle" id="themeToggle" title="Toggle dark mode">🌙</button>
- </div>
- </div>
- </div>
- </header>
- <div class="container">
- <!-- Filters -->
- <div class="filters">
- <div class="filter-row">
- <div class="filter-group">
- <label>Agent</label>
- <select id="agentFilter">
- <option value="all">All Agents</option>
- </select>
- </div>
- <div class="filter-group">
- <label>Category</label>
- <select id="categoryFilter">
- <option value="all">All Categories</option>
- <option value="developer">Developer</option>
- <option value="business">Business</option>
- <option value="creative">Creative</option>
- <option value="edge-case">Edge Case</option>
- </select>
- </div>
- <div class="filter-group">
- <label>Status</label>
- <select id="statusFilter">
- <option value="all">All Tests</option>
- <option value="passed">Passed Only</option>
- <option value="failed">Failed Only</option>
- </select>
- </div>
- <div class="filter-group">
- <label>Time Range</label>
- <select id="timeFilter">
- <option value="latest">Latest Run</option>
- <option value="today">Today</option>
- <option value="week">Last 7 Days</option>
- <option value="month">Last 30 Days</option>
- </select>
- </div>
- </div>
- </div>
- <!-- Stats Cards -->
- <div class="stats-grid" id="statsGrid">
- <div class="stat-card">
- <div class="stat-label">Total Tests</div>
- <div class="stat-value" id="totalTests">-</div>
- <div class="stat-subtitle">Across all agents</div>
- </div>
- <div class="stat-card success">
- <div class="stat-label">Pass Rate</div>
- <div class="stat-value" id="passRate">-</div>
- <div class="stat-subtitle" id="passedCount">- passed</div>
- </div>
- <div class="stat-card danger">
- <div class="stat-label">Failed Tests</div>
- <div class="stat-value" id="failedTests">-</div>
- <div class="stat-subtitle" id="failedSubtitle">-</div>
- </div>
- <div class="stat-card warning">
- <div class="stat-label">Avg Duration</div>
- <div class="stat-value" id="avgDuration">-</div>
- <div class="stat-subtitle">Per test</div>
- </div>
- </div>
- <!-- Trend Chart -->
- <div class="chart-container">
- <h2>📈 Pass Rate Trend (Last 30 Days)</h2>
- <canvas id="trendChart"></canvas>
- </div>
- <!-- Test Results Table -->
- <div class="results-container">
- <div class="results-header">
- <h2>Test Results</h2>
- <div class="search-box">
- <input type="text" id="searchInput" placeholder="Search tests...">
- </div>
- </div>
- <div id="tableContainer">
- <div class="loading">
- <div class="spinner"></div>
- <p>Loading test results...</p>
- </div>
- </div>
- </div>
- </div>
- <!-- Chart.js from CDN -->
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
-
- <script>
- // Dashboard State
- let allResults = [];
- let filteredResults = [];
- let currentSort = { column: null, direction: 'asc' };
- let trendChart = null;
- // Initialize Dashboard
- document.addEventListener('DOMContentLoaded', () => {
- initializeTheme();
- setupEventListeners();
- loadResults();
- });
- // Theme Management
- function initializeTheme() {
- const savedTheme = localStorage.getItem('theme') || 'light';
- document.documentElement.setAttribute('data-theme', savedTheme);
- updateThemeIcon(savedTheme);
- }
- function toggleTheme() {
- const current = document.documentElement.getAttribute('data-theme');
- const newTheme = current === 'dark' ? 'light' : 'dark';
- document.documentElement.setAttribute('data-theme', newTheme);
- localStorage.setItem('theme', newTheme);
- updateThemeIcon(newTheme);
- }
- function updateThemeIcon(theme) {
- document.getElementById('themeToggle').textContent = theme === 'dark' ? '☀️' : '🌙';
- }
- // Event Listeners
- function setupEventListeners() {
- document.getElementById('themeToggle').addEventListener('click', toggleTheme);
- document.getElementById('refreshBtn').addEventListener('click', loadResults);
- document.getElementById('exportBtn').addEventListener('click', exportToCSV);
- document.getElementById('searchInput').addEventListener('input', applyFilters);
- document.getElementById('agentFilter').addEventListener('change', applyFilters);
- document.getElementById('categoryFilter').addEventListener('change', applyFilters);
- document.getElementById('statusFilter').addEventListener('change', applyFilters);
- document.getElementById('timeFilter').addEventListener('change', loadResults);
- }
- // Load Results
- async function loadResults() {
- showLoading();
-
- try {
- const timeFilter = document.getElementById('timeFilter').value;
- const results = await fetchResults(timeFilter);
-
- allResults = results;
- populateAgentFilter(results);
- applyFilters();
- updateStats(results);
- updateTrendChart(results);
- } catch (error) {
- showError('Failed to load results: ' + error.message);
- }
- }
- // Fetch Results
- async function fetchResults(timeFilter) {
- try {
- if (timeFilter === 'latest') {
- // Load latest.json
- const response = await fetch('latest.json');
- if (!response.ok) {
- throw new Error('Cannot load latest.json. See instructions below.');
- }
- const data = await response.json();
- return [data];
- } else {
- // Load from history
- const files = await fetchHistoryFiles(timeFilter);
- const results = await Promise.all(
- files.map(file => fetch(file).then(r => r.json()))
- );
- return results;
- }
- } catch (error) {
- // If fetch fails (CORS/local file), show helpful message
- throw new Error('Cannot load results from local file. Please use one of these methods:\n\n' +
- '1. Serve via HTTP:\n' +
- ' cd evals/results && python3 -m http.server 8000\n' +
- ' Then open: http://localhost:8000\n\n' +
- '2. Use browser flag:\n' +
- ' Chrome: --allow-file-access-from-files\n\n' +
- 'Original error: ' + error.message);
- }
- }
- // Fetch History Files
- async function fetchHistoryFiles(timeFilter) {
- // For now, we'll just load latest.json
- // In a real implementation, you'd need a file listing endpoint
- // or generate an index.json with all available files
- return ['latest.json'];
- }
- // Populate Agent Filter
- function populateAgentFilter(results) {
- const agents = [...new Set(results.map(r => r.meta.agent))];
- const select = document.getElementById('agentFilter');
-
- // Keep "All Agents" option
- select.innerHTML = '<option value="all">All Agents</option>';
-
- agents.forEach(agent => {
- const option = document.createElement('option');
- option.value = agent;
- option.textContent = agent.charAt(0).toUpperCase() + agent.slice(1);
- select.appendChild(option);
- });
- }
- // Apply Filters
- function applyFilters() {
- const searchTerm = document.getElementById('searchInput').value.toLowerCase();
- const agentFilter = document.getElementById('agentFilter').value;
- const categoryFilter = document.getElementById('categoryFilter').value;
- const statusFilter = document.getElementById('statusFilter').value;
- // Flatten all tests from all results
- const allTests = allResults.flatMap(result =>
- result.tests.map(test => ({
- ...test,
- agent: result.meta.agent,
- timestamp: result.meta.timestamp,
- model: result.meta.model
- }))
- );
- filteredResults = allTests.filter(test => {
- // Search filter
- if (searchTerm && !test.id.toLowerCase().includes(searchTerm)) {
- return false;
- }
- // Agent filter
- if (agentFilter !== 'all' && test.agent !== agentFilter) {
- return false;
- }
- // Category filter
- if (categoryFilter !== 'all' && test.category !== categoryFilter) {
- return false;
- }
- // Status filter
- if (statusFilter === 'passed' && !test.passed) {
- return false;
- }
- if (statusFilter === 'failed' && test.passed) {
- return false;
- }
- return true;
- });
- renderTable(filteredResults);
- }
- // Render Table
- function renderTable(tests) {
- const container = document.getElementById('tableContainer');
- if (tests.length === 0) {
- container.innerHTML = `
- <div class="empty-state">
- <div class="empty-state-icon">📭</div>
- <h3>No results found</h3>
- <p>Try adjusting your filters or run some tests</p>
- </div>
- `;
- return;
- }
- const html = `
- <table>
- <thead>
- <tr>
- <th class="sortable" data-column="id">Test ID</th>
- <th class="sortable" data-column="agent">Agent</th>
- <th class="sortable" data-column="category">Category</th>
- <th class="sortable" data-column="passed">Status</th>
- <th class="sortable" data-column="duration_ms">Duration</th>
- <th class="sortable" data-column="events">Events</th>
- <th class="sortable" data-column="violations.total">Violations</th>
- </tr>
- </thead>
- <tbody>
- ${tests.map((test, idx) => renderTestRow(test, idx)).join('')}
- </tbody>
- </table>
- `;
- container.innerHTML = html;
- // Add sort listeners
- container.querySelectorAll('th.sortable').forEach(th => {
- th.addEventListener('click', () => sortTable(th.dataset.column));
- });
- // Add expand listeners
- container.querySelectorAll('.expandable-row').forEach(row => {
- row.addEventListener('click', () => toggleDetails(row.dataset.index));
- });
- }
- // Render Test Row
- function renderTestRow(test, index) {
- const statusClass = test.passed ? 'passed' : 'failed';
- const statusText = test.passed ? '✅ Passed' : '❌ Failed';
- const duration = (test.duration_ms / 1000).toFixed(2) + 's';
-
- return `
- <tr class="expandable-row" data-index="${index}">
- <td><strong>${test.id}</strong></td>
- <td>${test.agent}</td>
- <td><span class="category-badge">${test.category}</span></td>
- <td><span class="status-badge ${statusClass}">${statusText}</span></td>
- <td>${duration}</td>
- <td>${test.events}</td>
- <td>${test.violations.total} ${test.violations.errors > 0 ? '⚠️' : ''}</td>
- </tr>
- <tr class="details-row" id="details-${index}">
- <td colspan="7">
- ${renderTestDetails(test)}
- </td>
- </tr>
- `;
- }
- // Render Test Details
- function renderTestDetails(test) {
- let html = '<div class="details-content">';
-
- html += `<p><strong>Approvals:</strong> ${test.approvals}</p>`;
- html += `<p><strong>Events:</strong> ${test.events}</p>`;
-
- if (test.violations.total > 0) {
- html += '<h4>Violations:</h4>';
- test.violations.details?.forEach(v => {
- html += `
- <div class="violation-item ${v.severity}">
- <strong>[${v.severity.toUpperCase()}] ${v.type}</strong><br>
- ${v.message}
- </div>
- `;
- });
- } else {
- html += '<p>✅ No violations</p>';
- }
-
- html += '</div>';
- return html;
- }
- // Toggle Details
- function toggleDetails(index) {
- const detailsRow = document.getElementById(`details-${index}`);
- detailsRow.classList.toggle('show');
- }
- // Sort Table
- function sortTable(column) {
- if (currentSort.column === column) {
- currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
- } else {
- currentSort.column = column;
- currentSort.direction = 'asc';
- }
- filteredResults.sort((a, b) => {
- let aVal = getNestedValue(a, column);
- let bVal = getNestedValue(b, column);
- if (typeof aVal === 'string') {
- aVal = aVal.toLowerCase();
- bVal = bVal.toLowerCase();
- }
- if (aVal < bVal) return currentSort.direction === 'asc' ? -1 : 1;
- if (aVal > bVal) return currentSort.direction === 'asc' ? 1 : -1;
- return 0;
- });
- renderTable(filteredResults);
- updateSortIndicators();
- }
- // Get Nested Value
- function getNestedValue(obj, path) {
- return path.split('.').reduce((curr, prop) => curr?.[prop], obj);
- }
- // Update Sort Indicators
- function updateSortIndicators() {
- document.querySelectorAll('th.sortable').forEach(th => {
- th.classList.remove('sort-asc', 'sort-desc');
- if (th.dataset.column === currentSort.column) {
- th.classList.add(`sort-${currentSort.direction}`);
- }
- });
- }
- // Update Stats
- function updateStats(results) {
- const allTests = results.flatMap(r => r.tests);
- const total = allTests.length;
- const passed = allTests.filter(t => t.passed).length;
- const failed = total - passed;
- const passRate = total > 0 ? ((passed / total) * 100).toFixed(1) : 0;
- const avgDuration = total > 0
- ? (allTests.reduce((sum, t) => sum + t.duration_ms, 0) / total / 1000).toFixed(2)
- : 0;
- document.getElementById('totalTests').textContent = total;
- document.getElementById('passRate').textContent = passRate + '%';
- document.getElementById('passedCount').textContent = `${passed} passed`;
- document.getElementById('failedTests').textContent = failed;
- document.getElementById('failedSubtitle').textContent = failed === 0 ? 'All tests passing! 🎉' : `${failed} tests need attention`;
- document.getElementById('avgDuration').textContent = avgDuration + 's';
- }
- // Update Trend Chart
- function updateTrendChart(results) {
- const ctx = document.getElementById('trendChart');
-
- // Sort by timestamp
- const sorted = [...results].sort((a, b) =>
- new Date(a.meta.timestamp) - new Date(b.meta.timestamp)
- );
- const labels = sorted.map(r => {
- const date = new Date(r.meta.timestamp);
- return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
- });
- const passRates = sorted.map(r => (r.summary.pass_rate * 100).toFixed(1));
- if (trendChart) {
- trendChart.destroy();
- }
- trendChart = new Chart(ctx, {
- type: 'line',
- data: {
- labels: labels,
- datasets: [{
- label: 'Pass Rate (%)',
- data: passRates,
- borderColor: '#28a745',
- backgroundColor: 'rgba(40, 167, 69, 0.1)',
- tension: 0.4,
- fill: true
- }]
- },
- options: {
- responsive: true,
- maintainAspectRatio: true,
- plugins: {
- legend: {
- display: false
- }
- },
- scales: {
- y: {
- beginAtZero: true,
- max: 100,
- ticks: {
- callback: function(value) {
- return value + '%';
- }
- }
- }
- }
- }
- });
- }
- // Export to CSV
- function exportToCSV() {
- const tests = filteredResults.length > 0 ? filteredResults : allResults.flatMap(r => r.tests);
-
- const headers = ['Test ID', 'Agent', 'Category', 'Status', 'Duration (ms)', 'Events', 'Approvals', 'Violations'];
- const rows = tests.map(test => [
- test.id,
- test.agent || 'unknown',
- test.category,
- test.passed ? 'Passed' : 'Failed',
- test.duration_ms,
- test.events,
- test.approvals,
- test.violations.total
- ]);
- const csv = [headers, ...rows]
- .map(row => row.map(cell => `"${cell}"`).join(','))
- .join('\n');
- const blob = new Blob([csv], { type: 'text/csv' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `test-results-${new Date().toISOString().split('T')[0]}.csv`;
- a.click();
- URL.revokeObjectURL(url);
- }
- // Show Loading
- function showLoading() {
- document.getElementById('tableContainer').innerHTML = `
- <div class="loading">
- <div class="spinner"></div>
- <p>Loading test results...</p>
- </div>
- `;
- }
- // Show Error
- function showError(message) {
- // Format multi-line messages
- const formattedMessage = message.split('\n').map(line =>
- line.trim() ? `<p style="margin: 5px 0; text-align: left;">${line}</p>` : '<br>'
- ).join('');
-
- document.getElementById('tableContainer').innerHTML = `
- <div class="empty-state">
- <div class="empty-state-icon">⚠️</div>
- <h3>Cannot Load Results</h3>
- <div style="max-width: 600px; margin: 20px auto; background: var(--bg-secondary); padding: 20px; border-radius: 8px; text-align: left;">
- <h4 style="margin-top: 0;">Solution: Serve via HTTP</h4>
- <pre style="background: var(--bg-card); padding: 10px; border-radius: 4px; overflow-x: auto;">cd evals/results
- python3 -m http.server 8000</pre>
- <p>Then open: <a href="http://localhost:8000" target="_blank">http://localhost:8000</a></p>
- <hr style="margin: 15px 0; border: none; border-top: 1px solid var(--border-color);">
- <p style="font-size: 12px; color: var(--text-secondary);">
- <strong>Why?</strong> Browsers block loading local JSON files for security.
- Serving via HTTP solves this.
- </p>
- </div>
- <button onclick="loadResults()" class="primary">Try Again</button>
- </div>
- `;
- }
- </script>
- </body>
- </html>
|