summaryrefslogtreecommitdiff
path: root/api
diff options
context:
space:
mode:
Diffstat (limited to 'api')
-rw-r--r--api/static/css/results.css150
-rw-r--r--api/static/index.html23
-rw-r--r--api/static/js/components/results.js213
-rw-r--r--api/static/js/ui.js13
4 files changed, 384 insertions, 15 deletions
diff --git a/api/static/css/results.css b/api/static/css/results.css
index 9a532b8..d9cdddd 100644
--- a/api/static/css/results.css
+++ b/api/static/css/results.css
@@ -36,6 +36,9 @@
}
.data-table th {
+ position: sticky;
+ top: 0;
+ z-index: 10;
text-align: left;
padding: 12px 16px;
background-color: var(--color-bg-2);
@@ -46,16 +49,29 @@
letter-spacing: 0.05em;
border-bottom: var(--border-width) solid var(--color-bg-3);
white-space: nowrap;
+ box-shadow: 0 2px 2px -1px rgba(0,0,0,0.4);
}
.data-table td {
padding: 10px 16px;
border-bottom: var(--border-width) solid var(--color-bg-3);
color: var(--color-text-primary);
+ max-width: 300px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: middle;
+}
+
+.data-table td.cell-long {
max-width: 400px;
- white-space: normal;
- word-wrap: break-word;
- vertical-align: top;
+}
+
+.data-table td.cell-action {
+ width: 60px;
+ padding: 8px;
+ text-align: center;
+ overflow: visible;
}
.data-table tr:last-child td {
@@ -64,6 +80,25 @@
.data-table tr:hover td {
background-color: var(--color-bg-2);
+ cursor: pointer;
+}
+
+/* Row Action Button */
+.btn-row-action {
+ background: var(--color-bg-3);
+ border: 1px solid transparent;
+ color: var(--color-text-primary);
+ padding: 4px 8px;
+ border-radius: var(--border-radius);
+ font-size: 0.7rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: var(--transition-fast);
+}
+
+.btn-row-action:hover {
+ background: var(--color-accent);
+ color: white;
}
/* Image Grid */
@@ -111,3 +146,112 @@
position: relative;
height: 300px;
}
+
+/* Column Selector */
+.column-selector {
+ position: relative;
+ display: inline-block;
+ margin-left: var(--space-md);
+}
+
+.column-dropdown {
+ position: absolute;
+ top: calc(100% + 8px);
+ right: 0;
+ background: var(--color-bg-1);
+ border: 1px solid var(--color-bg-3);
+ border-radius: var(--border-radius);
+ padding: var(--space-md);
+ z-index: 100;
+ min-width: 280px;
+ box-shadow: 0 15px 35px rgba(0, 0, 0, 0.6);
+ max-height: 500px;
+ overflow-y: auto;
+ transition: opacity var(--transition-fast), transform var(--transition-fast);
+}
+
+.column-grid {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.column-grid .checkbox-item {
+ padding: 6px 8px;
+ border-radius: var(--border-radius);
+}
+
+.column-grid .checkbox-item:hover {
+ background-color: var(--color-bg-2);
+}
+
+.column-grid .checkbox-item span {
+ font-size: 0.75rem;
+ color: var(--color-text-secondary);
+}
+
+/* Modal Enhancements for Inspector */
+.modal-content.wide {
+ width: 95%;
+ max-width: 1200px;
+ height: 90vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.inspector-layout {
+ display: grid;
+ grid-template-columns: 350px 1fr;
+ gap: var(--space-lg);
+ height: 100%;
+ overflow: hidden;
+}
+
+.inspector-sidebar {
+ border-right: 1px solid var(--color-bg-3);
+ padding-right: var(--space-md);
+ overflow-y: auto;
+}
+
+.inspector-main {
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.field-entry {
+ margin-bottom: var(--space-md);
+}
+
+.field-label {
+ font-size: 0.65rem;
+ font-weight: 800;
+ text-transform: uppercase;
+ color: var(--color-text-secondary);
+ margin-bottom: 2px;
+}
+
+.field-value {
+ font-size: 0.85rem;
+ word-break: break-all;
+}
+
+.json-viewer-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: var(--space-sm);
+}
+
+.json-viewer-container {
+ flex-grow: 1;
+ background: var(--color-bg-0);
+ border: 1px solid var(--color-bg-3);
+ border-radius: var(--border-radius);
+ padding: var(--space-md);
+ font-family: 'JetBrains Mono', monospace;
+ font-size: 0.8rem;
+ overflow-y: auto;
+ color: var(--color-text-primary);
+ white-space: pre-wrap;
+}
diff --git a/api/static/index.html b/api/static/index.html
index 5338fca..d365254 100644
--- a/api/static/index.html
+++ b/api/static/index.html
@@ -104,6 +104,18 @@
</div>
<div id="tab-csv" class="tab-panel">
+ <div class="results-controls">
+ <div id="csv-table-controls" style="display: flex; align-items: center; width: 100%;">
+ <span id="csv-table-stats" style="font-size: 0.8rem; font-weight: 600; color: var(--color-accent);"></span>
+ <div class="column-selector">
+ <button id="btn-toggle-columns" class="btn btn-secondary" style="font-size: 0.75rem; padding: 4px 12px;">Columns</button>
+ <div id="column-dropdown" class="column-dropdown" style="display: none;">
+ <div style="margin-bottom: 12px; font-weight: 700; font-size: 0.7rem; text-transform: uppercase;">Visible Columns</div>
+ <div id="column-list" class="column-grid"></div>
+ </div>
+ </div>
+ </div>
+ </div>
<div id="csv-results" class="results-container">
<div class="empty-state">No active data.</div>
</div>
@@ -134,5 +146,16 @@
<div id="modal-chart-container" class="chart-container" style="height: 80vh; border: none; background: transparent;"></div>
</div>
</div>
+
+ <!-- Inspector Modal -->
+ <div id="inspector-modal" class="modal-overlay" style="display: none;">
+ <div class="modal-content wide">
+ <button id="close-inspector-btn" class="btn-close">&times;</button>
+ <div id="inspector-content" style="height: 100%; overflow: hidden;">
+ <!-- Populated via JS -->
+ <div class="empty-state">Loading details...</div>
+ </div>
+ </div>
+ </div>
</body>
</html> \ No newline at end of file
diff --git a/api/static/js/components/results.js b/api/static/js/components/results.js
index b143fa3..2ce177c 100644
--- a/api/static/js/components/results.js
+++ b/api/static/js/components/results.js
@@ -7,6 +7,8 @@ import { ui } from '../ui.js';
let activeCharts = [];
let currentCsvData = null; // Store fetched data locally for quick filtering
let currentCsvColumns = [];
+let currentCsvDir = ""; // Directory of the active CSV
+let currentVisibleColumns = []; // Track which columns are shown in Table
let modalChart = null;
export const results = {
@@ -21,6 +23,25 @@ export const results = {
ui.elements.chartModal.addEventListener('click', (e) => {
if (e.target === ui.elements.chartModal) this.closeChartModal();
});
+
+ // Inspector Modal
+ ui.elements.closeInspectorBtn.addEventListener('click', () => this.closeInspector());
+ ui.elements.inspectorModal.addEventListener('click', (e) => {
+ if (e.target === ui.elements.inspectorModal) this.closeInspector();
+ });
+
+ // Column Toggle
+ ui.elements.btnToggleColumns.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const isVisible = ui.elements.columnDropdown.style.display === 'block';
+ ui.elements.columnDropdown.style.display = isVisible ? 'none' : 'block';
+ });
+
+ document.addEventListener('click', (e) => {
+ if (!ui.elements.columnDropdown.contains(e.target) && e.target !== ui.elements.btnToggleColumns) {
+ ui.elements.columnDropdown.style.display = 'none';
+ }
+ });
await Promise.all([
this.updateCSVFileList(),
@@ -112,6 +133,18 @@ export const results = {
currentCsvData = content.data;
currentCsvColumns = content.columns;
+ // Store directory of CSV for relative log path resolution
+ if (filePath.includes('/')) {
+ currentCsvDir = filePath.substring(0, filePath.lastIndexOf('/')) + '/';
+ } else {
+ currentCsvDir = "";
+ }
+
+ // Default visibility: Hide very long or redundant columns initially
+ const hideByDefault = ['BLACKBOX_JUSTIFICATION', 'GLASSBOX_JUSTIFICATION', 'log_path', 'full_log', 'messages'];
+ currentVisibleColumns = currentCsvColumns.filter(c => !hideByDefault.includes(c));
+
+ this.renderColumnSelector();
this.populateFilters();
this.applyFilters(); // This draws charts and tables
@@ -121,6 +154,48 @@ export const results = {
}
},
+ renderColumnSelector() {
+ ui.elements.columnList.innerHTML = `
+ <div style="display: flex; gap: 8px; margin-bottom: 8px; border-bottom: 1px solid var(--color-bg-3); padding-bottom: 8px;">
+ <span class="filter-link" id="cols-all">All</span>
+ <span class="filter-link" id="cols-none">None</span>
+ </div>
+ <div class="column-grid">
+ ${currentCsvColumns.map(col => `
+ <label class="checkbox-item">
+ <input type="checkbox" value="${col}" ${currentVisibleColumns.includes(col) ? 'checked' : ''}>
+ <span title="${col}">${col}</span>
+ </label>
+ `).join('')}
+ </div>
+ `;
+
+ // Bind events
+ ui.elements.columnList.querySelectorAll('input').forEach(input => {
+ input.addEventListener('change', () => {
+ const col = input.value;
+ if (input.checked) {
+ if (!currentVisibleColumns.includes(col)) currentVisibleColumns.push(col);
+ } else {
+ currentVisibleColumns = currentVisibleColumns.filter(c => c !== col);
+ }
+ this.applyFilters();
+ });
+ });
+
+ document.getElementById('cols-all').addEventListener('click', () => {
+ currentVisibleColumns = [...currentCsvColumns];
+ this.renderColumnSelector();
+ this.applyFilters();
+ });
+
+ document.getElementById('cols-none').addEventListener('click', () => {
+ currentVisibleColumns = [];
+ this.renderColumnSelector();
+ this.applyFilters();
+ });
+ },
+
populateFilters() {
const models = new Set();
const scenarios = new Set();
@@ -175,6 +250,9 @@ export const results = {
});
ui.elements.dataStats.textContent = `Showing ${filteredData.length} of ${currentCsvData.length} records`;
+ if (ui.elements.csvTableStats) {
+ ui.elements.csvTableStats.textContent = `Found ${filteredData.length} records`;
+ }
this.renderTable(filteredData);
this.renderCharts(filteredData);
@@ -186,20 +264,133 @@ export const results = {
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>
+ const visibleCols = currentVisibleColumns;
+
+ let html = `<div class="data-table-wrapper" style="height: calc(100vh - 280px); overflow-y: auto;">
+ <table class="data-table">
+ <thead>
+ <tr>
+ <th class="cell-action"></th>
+ ${visibleCols.map(c => `<th>${c}</th>`).join('')}
+ </tr>
+ </thead>
+ <tbody>
+ ${data.map((row, idx) => `
+ <tr data-idx="${idx}">
+ <td class="cell-action"><button class="btn-row-action" data-idx="${idx}">VIEW</button></td>
+ ${visibleCols.map(col => {
+ const val = row[col] ?? '';
+ const isLong = col.toLowerCase().includes('justification') || col.toLowerCase().includes('id');
+ return `<td class="${isLong ? 'cell-long' : ''}" title="${val}">${val}</td>`;
+ }).join('')}
+ </tr>
+ `).join('')}
+ </tbody>
+ </table>
</div>`;
ui.elements.csvResults.innerHTML = html;
+
+ // Bind Row Click
+ ui.elements.csvResults.querySelectorAll('tr[data-idx]').forEach(tr => {
+ tr.addEventListener('click', (e) => {
+ const idx = tr.dataset.idx;
+ this.showRowInspector(data[idx]);
+ });
+ });
+
+ // Bind View Button (redundant but explicit)
+ ui.elements.csvResults.querySelectorAll('.btn-row-action').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const idx = btn.dataset.idx;
+ this.showRowInspector(data[idx]);
+ });
+ });
+ },
+
+ closeInspector() {
+ ui.elements.inspectorModal.style.display = 'none';
+ ui.elements.inspectorContent.innerHTML = '<div class="empty-state">Loading details...</div>';
+ },
+
+ async showRowInspector(rowData) {
+ ui.elements.inspectorModal.style.display = 'flex';
+
+ // Initial layout
+ ui.elements.inspectorContent.innerHTML = `
+ <div class="inspector-layout">
+ <aside class="inspector-sidebar">
+ <h2 style="margin-bottom: var(--space-md); font-size: 1rem;">Record Details</h2>
+ <div id="inspector-fields">
+ ${Object.entries(rowData).map(([key, val]) => `
+ <div class="field-entry">
+ <div class="field-label">${key}</div>
+ <div class="field-value">${val ?? '<span style="color: var(--color-text-muted)">null</span>'}</div>
+ </div>
+ `).join('')}
+ </div>
+ </aside>
+ <main class="inspector-main">
+ <div class="json-viewer-header">
+ <h2 style="font-size: 1rem;">Raw Experiment Log</h2>
+ <span id="log-status" style="font-size: 0.7rem; color: var(--color-text-secondary);">Checking for log file...</span>
+ </div>
+ <div id="log-viewer" class="json-viewer-container">
+ <div class="empty-state">Searching for log file associated with this run...</div>
+ </div>
+ </main>
+ </div>
+ `;
+
+ // Strategy to find the log:
+ // 1. rowData.log_path
+ // 2. rowData.RUN_ID
+
+ let rawPath = rowData.log_path || rowData.RUN_ID || rowData.run_id;
+ if (!rawPath) {
+ document.getElementById('log-viewer').innerHTML = '<div class="empty-state">No log path metadata found in record.</div>';
+ document.getElementById('log-status').textContent = `Missing log metadata.`;
+ return;
+ }
+
+ // Normalize path: add .json if missing
+ let jsonPath = rawPath.endsWith('.json') ? rawPath : (rawPath + '.json');
+
+ // Trial paths:
+ const trials = [
+ jsonPath, // Direct
+ currentCsvDir + jsonPath, // Relative to CSV (e.g. logs/full_experiment/ + path)
+ 'logs/' + jsonPath // Absolute under logs/
+ ];
+
+ let loaded = false;
+ for (const trial of trials) {
+ if (loaded) break;
+ try {
+ document.getElementById('log-status').textContent = `Trying: ${trial}`;
+ const logData = await api.get(`/api/logs/browse?path=${encodeURIComponent(trial)}`);
+ document.getElementById('log-viewer').textContent = JSON.stringify(logData, null, 2);
+ document.getElementById('log-status').textContent = `Log loaded: ${trial}`;
+ document.getElementById('log-status').style.color = 'var(--color-success)';
+ loaded = true;
+ } catch (err) {
+ console.log(`Failed trial: ${trial}`);
+ }
+ }
+
+ if (!loaded) {
+ document.getElementById('log-viewer').innerHTML = `
+ <div class="empty-state" style="color: var(--color-danger)">
+ Failed to load log file: API Error: 404<br>
+ <div style="margin-top: 8px; color: var(--color-text-secondary); font-size: 0.7rem;">
+ Paths tried:<br>
+ ${trials.map(t => `- ${t}`).join('<br>')}
+ </div>
+ </div>`;
+ document.getElementById('log-status').textContent = `Error loading log.`;
+ document.getElementById('log-status').style.color = 'var(--color-danger)';
+ }
},
renderCharts(data) {
diff --git a/api/static/js/ui.js b/api/static/js/ui.js
index 2c402ab..27b187e 100644
--- a/api/static/js/ui.js
+++ b/api/static/js/ui.js
@@ -21,7 +21,18 @@ export const ui = {
modalChartContainer: document.getElementById('modal-chart-container'),
tabTriggers: document.querySelectorAll('.tab-trigger'),
- tabPanels: document.querySelectorAll('.tab-panel')
+ tabPanels: document.querySelectorAll('.tab-panel'),
+
+ // New CSV Table Elements
+ csvTableStats: document.getElementById('csv-table-stats'),
+ btnToggleColumns: document.getElementById('btn-toggle-columns'),
+ columnDropdown: document.getElementById('column-dropdown'),
+ columnList: document.getElementById('column-list'),
+
+ // Inspector Modal
+ inspectorModal: document.getElementById('inspector-modal'),
+ closeInspectorBtn: document.getElementById('close-inspector-btn'),
+ inspectorContent: document.getElementById('inspector-content')
};
},