diff options
Diffstat (limited to 'api')
| -rw-r--r-- | api/static/css/results.css | 150 | ||||
| -rw-r--r-- | api/static/index.html | 23 | ||||
| -rw-r--r-- | api/static/js/components/results.js | 213 | ||||
| -rw-r--r-- | api/static/js/ui.js | 13 |
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">×</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') }; }, |
