summaryrefslogtreecommitdiff
path: root/api/static/js
diff options
context:
space:
mode:
authorCaptainJack2491 <jayrupnakawala@gmail.com>2026-03-09 19:41:49 +0000
committerCaptainJack2491 <jayrupnakawala@gmail.com>2026-03-09 19:41:49 +0000
commitd3d0e0825ea4b4ed3ab338603b824af0178e5264 (patch)
treef5cf2096a4470871b3445821cea29f7d0de76173 /api/static/js
parentd2b1fbd5ac63fd1eb17e68b5364b7dee5cc86e6e (diff)
feat: pivot Web GUI to dedicated Data Visualization Dashboard
This commit: - Removes all execution logic (runs, cancellation, SSE logs) - Strips the terminal and configuration sidebar - Implements a global filtering panel (Model, Scenario, Oversight) - Dedicated the UI 100% to interactive Chart.js and static visuals
Diffstat (limited to 'api/static/js')
-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
5 files changed, 148 insertions, 342 deletions
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