Algoline / static /js /app.js
Al1Abdullah's picture
Algoline - Automated Machine Learning Platform
9fb2e52
Raw
History Blame Contribute Delete
39.8 kB
/* ──────────────────────────────────────────────
APPLICATION STATE
────────────────────────────────────────────── */
window.APP = {
columns: [],
numericColumns: [],
target: '',
task: '',
leaderboard: null,
metrics: [],
bestModel: '',
nPredictions: 0
};
/* ──────────────────────────────────────────────
NAVIGATION
────────────────────────────────────────────── */
function showSection(name) {
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
const sec = document.getElementById('sec-' + name);
if (sec) sec.classList.add('active');
const nav = document.querySelector('.nav-item[data-section="' + name + '"]');
if (nav) nav.classList.add('active');
}
/* ──────────────────────────────────────────────
TABLE RENDERER
────────────────────────────────────────────── */
function renderTable(containerId, rows, columns) {
const el = document.getElementById(containerId);
if (!el || !rows || !columns) { if (el) el.innerHTML = ''; return; }
let html = '<table class="data-table"><thead><tr>';
columns.forEach(c => { html += '<th>' + escHtml(String(c)) + '</th>'; });
html += '</tr></thead><tbody>';
rows.forEach(r => {
html += '<tr>';
columns.forEach(c => {
const val = r[c] !== undefined && r[c] !== null ? String(r[c]) : '';
html += '<td>' + escHtml(val) + '</td>';
});
html += '</tr>';
});
html += '</tbody></table>';
el.innerHTML = html;
}
function escHtml(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
/* ──────────────────────────────────────────────
PLOTLY HELPER
────────────────────────────────────────────── */
function renderPlotly(divId, figure) {
const el = document.getElementById(divId);
if (!el) return;
// Always clear any existing content (spinner, old chart) first
el.innerHTML = '';
if (!figure || !figure.data) return;
const style = getComputedStyle(document.documentElement);
const textColor = style.getPropertyValue('--chart-text').trim() || '#71717a';
const gridColor = style.getPropertyValue('--chart-grid').trim() || 'rgba(30,27,75,0.15)';
const layout = Object.assign({}, figure.layout || {}, {
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
font: { family: 'Inter, sans-serif', size: 12, color: textColor },
margin: figure.layout && figure.layout.margin ? figure.layout.margin : { l: 40, r: 20, t: 40, b: 40 }
});
layout.xaxis = Object.assign({}, layout.xaxis || {}, { gridcolor: gridColor, zeroline: false });
layout.yaxis = Object.assign({}, layout.yaxis || {}, { gridcolor: gridColor, zeroline: false });
try {
Plotly.newPlot(divId, figure.data, layout, { responsive: true, displayModeBar: false });
} catch (e) {
el.innerHTML = '<p style="color:#ef4444;padding:20px;text-align:center">Chart rendering failed</p>';
}
}
/* ──────────────────────────────────────────────
FETCH WRAPPER
────────────────────────────────────────────── */
async function apiFetch(url, options) {
try {
const res = await fetch(url, options);
if (!res.ok) {
let msg = 'Request failed';
try { const e = await res.json(); msg = e.detail || e.error || e.message || msg; } catch (_) {}
throw new Error(msg);
}
return res;
} catch (err) {
console.error('API Error:', url, err);
throw err;
}
}
async function apiJson(url, options) {
const res = await apiFetch(url, options);
return res.json();
}
/* ──────────────────────────────────────────────
FILE UPLOAD & DRAG-DROP
────────────────────────────────────────────── */
(function initUpload() {
const area = document.getElementById('upload-area');
const input = document.getElementById('file-input');
area.addEventListener('dragover', e => {
e.preventDefault(); e.stopPropagation();
area.classList.add('dragover');
});
area.addEventListener('dragleave', e => {
e.preventDefault(); e.stopPropagation();
area.classList.remove('dragover');
});
area.addEventListener('drop', e => {
e.preventDefault(); e.stopPropagation();
area.classList.remove('dragover');
if (e.dataTransfer.files.length) {
input.files = e.dataTransfer.files;
uploadFile(e.dataTransfer.files[0]);
}
});
input.addEventListener('change', () => {
if (input.files.length) uploadFile(input.files[0]);
});
})();
async function uploadFile(file) {
const area = document.getElementById('upload-area');
area.innerHTML = `
<div class="spinner" style="margin:0 auto 12px"></div>
<div class="upload-area-title">Uploading ${escHtml(file.name)}...</div>
<div class="upload-area-sub">Please wait</div>
`;
// Step 1: Upload the file
let data;
try {
const fd = new FormData();
fd.append('file', file);
const res = await fetch('/api/upload', { method: 'POST', body: fd });
if (!res.ok) {
let msg = 'Upload failed';
try { const e = await res.json(); msg = e.detail || e.error || e.message || msg; } catch (_) {}
throw new Error(msg);
}
data = await res.json();
} catch (err) {
console.error('Upload error:', err);
area.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:36px;height:36px;margin-bottom:10px">
<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>
</svg>
<div class="upload-area-title" style="color:#ef4444">Upload failed</div>
<div class="upload-area-sub">${escHtml(err.message)}</div>
`;
area.onclick = () => document.getElementById('file-input').click();
return;
}
// Step 2: Process the response (errors here should NOT show "Upload failed")
try {
APP.columns = data.columns || [];
APP.numericColumns = data.numeric_columns || [];
area.innerHTML = `
<div style="display:flex;align-items:center;justify-content:space-between;width:100%">
<div style="display:flex;align-items:center;gap:12px">
<svg viewBox="0 0 24 24" fill="none" stroke="#6366f1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:28px;height:28px;flex-shrink:0">
<path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>
</svg>
<div>
<div style="font-size:14px;font-weight:600;color:var(--accent)">${escHtml(file.name)}</div>
<div style="font-size:12px;color:var(--text-muted);margin-top:2px">${data.rows || 0} rows, ${(data.columns || []).length} columns</div>
</div>
</div>
<button onclick="event.stopPropagation();document.getElementById('file-input').click()" class="btn btn-secondary" style="padding:6px 14px;font-size:12px;white-space:nowrap">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:4px"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
Replace
</button>
</div>
`;
area.onclick = null;
// Reset Build section (clear previous training results)
const resultsEl = document.getElementById('build-results');
if (resultsEl) resultsEl.style.display = 'none';
const evalPlots = document.getElementById('eval-plots');
if (evalPlots) evalPlots.innerHTML = '';
const lbTable = document.getElementById('leaderboard-table');
if (lbTable) lbTable.innerHTML = '';
const metricChart = document.getElementById('metric-chart');
if (metricChart) metricChart.innerHTML = '';
// Reset Explore chart
const exploreChart = document.getElementById('explore-chart');
if (exploreChart) exploreChart.innerHTML = '';
// Reset Export
const exportModel = document.getElementById('export-model');
if (exportModel) exportModel.textContent = '--';
const exportTask = document.getElementById('export-task');
if (exportTask) exportTask.textContent = '--';
renderMetrics(data);
populateTargetSelect(data.columns || []);
if (data.target) {
document.getElementById('target-select').value = data.target;
APP.target = data.target;
APP.task = data.task || 'classification';
document.getElementById('task-badge-wrap').innerHTML =
'<span class="task-badge">' + escHtml(APP.task) + '</span>';
if (APP.task === 'regression') { selectTaskByValue('regression'); }
else { selectTaskByValue('classification'); }
}
if (data.preview) renderTable('preview-table', data.preview.rows, data.preview.columns);
if (data.types) renderTable('types-table', data.types.rows, data.types.columns);
if (data.stats) renderTable('stats-table', data.stats.rows, data.stats.columns);
document.getElementById('data-metrics').style.display = '';
document.getElementById('target-section').style.display = '';
document.getElementById('data-tabs-section').style.display = '';
// Render auto-insights
if (data.insights && data.insights.length) {
renderInsights(data.insights);
document.getElementById('data-insights').style.display = '';
}
populateExploreDropdowns();
} catch (err2) {
console.error('Post-upload processing error:', err2);
}
}
function renderMetrics(data) {
const grid = document.getElementById('data-metrics');
const items = [
{ label: 'Rows', value: (data.rows || 0).toLocaleString() },
{ label: 'Columns', value: data.columns ? data.columns.length : 0 },
{ label: 'Missing', value: (data.missing || 0).toLocaleString() },
{ label: 'Duplicates', value: (data.duplicates || 0).toLocaleString() }
];
grid.innerHTML = items.map(m => `
<div class="card">
<div class="card-label">${m.label}</div>
<div class="card-value">${m.value}</div>
</div>
`).join('');
}
function populateTargetSelect(cols) {
const sel = document.getElementById('target-select');
sel.innerHTML = '<option value="">Select target...</option>';
cols.forEach(c => {
sel.innerHTML += '<option value="' + escHtml(c) + '">' + escHtml(c) + '</option>';
});
}
async function setTarget(col) {
if (!col) { document.getElementById('task-badge-wrap').innerHTML = ''; return; }
try {
const fd = new FormData();
fd.append('target', col);
const data = await apiJson('/api/target', { method: 'POST', body: fd });
APP.target = col;
APP.task = data.task || 'Classification';
document.getElementById('task-badge-wrap').innerHTML =
'<span class="task-badge">' + escHtml(APP.task) + '</span>';
// Sync build section radio
if (APP.task.toLowerCase() === 'regression') {
selectTaskByValue('regression');
} else {
selectTaskByValue('classification');
}
} catch (err) {
document.getElementById('task-badge-wrap').innerHTML =
'<span class="task-badge" style="color:#ef4444;border-color:rgba(239,68,68,0.2);background:rgba(239,68,68,0.1)">Error</span>';
}
}
/* ──────────────────────────────────────────────
DATA TABS
────────────────────────────────────────────── */
function switchDataTab(tab) {
document.querySelectorAll('#data-tab-bar .tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('#data-tabs-section .tab-panel').forEach(p => p.classList.remove('active'));
event.target.classList.add('active');
document.getElementById('data-tab-' + tab).classList.add('active');
}
/* ──────────────────────────────────────────────
EXPLORE SECTION
────────────────────────────────────────────── */
// Plot type configuration: which features each plot needs
const PLOT_CONF = {
histogram: { feat1: true, feat2: false, endpoint: '/api/explore/distribution' },
kde: { feat1: true, feat2: false, endpoint: '/api/explore/kde' },
boxplot: { feat1: true, feat2: false, endpoint: '/api/explore/boxplot' },
violin: { feat1: true, feat2: false, endpoint: '/api/explore/violin' },
missing_bar: { feat1: false, feat2: false, endpoint: '/api/explore/missing' },
missing_heatmap: { feat1: false, feat2: false, endpoint: '/api/explore/missing_heatmap' },
correlation: { feat1: false, feat2: false, endpoint: '/api/explore/correlation' },
pairplot: { feat1: false, feat2: false, endpoint: '/api/explore/pairplot' },
scatter: { feat1: true, feat2: true, endpoint: '/api/explore/scatter_xy' },
jointplot: { feat1: true, feat2: true, endpoint: '/api/explore/jointplot' },
countplot: { feat1: true, feat2: false, endpoint: '/api/explore/countplot' },
pie: { feat1: false, feat2: false, endpoint: '/api/explore/pie' },
class_dist: { feat1: false, feat2: false, endpoint: '/api/explore/target' },
target_hist: { feat1: false, feat2: false, endpoint: '/api/explore/target' },
mean_target: { feat1: true, feat2: false, endpoint: '/api/explore/mean_target' },
scatter_index: { feat1: true, feat2: false, endpoint: '/api/explore/scatter_index' },
grouped_box: { feat1: true, feat2: false, endpoint: '/api/explore/grouped_box' },
facetgrid: { feat1: true, feat2: false, endpoint: '/api/explore/facetgrid' }
};
function populateExploreDropdowns() {
const cols = APP.columns;
['plot-feat1', 'plot-feat2'].forEach(id => {
const sel = document.getElementById(id);
if (!sel) return;
sel.innerHTML = '<option value="">Select...</option>';
cols.forEach(c => {
sel.innerHTML += '<option value="' + escHtml(c) + '">' + escHtml(c) + '</option>';
});
});
}
function switchExploreMain(tab) {
document.querySelectorAll('#explore-main-tabs .tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('#sec-explore .tab-panel').forEach(p => p.classList.remove('active'));
event.target.classList.add('active');
document.getElementById('explore-panel-' + tab).classList.add('active');
if (tab === 'quality') loadQuality();
}
function onPlotTypeChange() {
const type = document.getElementById('plot-type').value;
const conf = PLOT_CONF[type] || {};
document.getElementById('feat1-group').style.display = conf.feat1 ? '' : 'none';
document.getElementById('feat2-group').style.display = conf.feat2 ? '' : 'none';
// Reset chart visibility state
const chart = document.getElementById('explore-chart');
const msg = document.getElementById('explore-msg');
chart.style.display = '';
// Auto-generate if no feature selection needed
if (!conf.feat1 && !conf.feat2) {
generatePlot();
} else {
// Check if a feature is already selected — auto-generate
const f1 = document.getElementById('plot-feat1').value;
const f2 = document.getElementById('plot-feat2').value;
if (conf.feat1 && f1 && (!conf.feat2 || f2)) {
generatePlot();
} else {
chart.innerHTML = '';
msg.style.display = '';
msg.textContent = 'Select a feature to generate the plot';
}
}
}
async function generatePlot() {
const type = document.getElementById('plot-type').value;
const conf = PLOT_CONF[type];
if (!conf) return;
const f1 = document.getElementById('plot-feat1').value;
const f2 = document.getElementById('plot-feat2').value;
// Validate required features
if (conf.feat1 && !f1) return;
if (conf.feat2 && !f2) return;
const chart = document.getElementById('explore-chart');
const msg = document.getElementById('explore-msg');
// Always reset visibility before loading
chart.style.display = '';
msg.style.display = 'none';
chart.innerHTML = '<div style="display:flex;justify-content:center;padding:60px 0"><div class="spinner"></div></div>';
try {
const fd = new FormData();
if (conf.feat1 && f1) fd.append('feature', f1);
if (conf.feat2 && f2) fd.append('feature2', f2);
const data = await apiJson(conf.endpoint, { method: 'POST', body: fd });
if (data.figure) {
chart.style.display = '';
msg.style.display = 'none';
renderPlotly('explore-chart', data.figure);
} else {
chart.innerHTML = '';
chart.style.display = 'none';
msg.style.display = '';
msg.textContent = data.message || 'No data available for this plot';
}
} catch (err) {
chart.style.display = '';
chart.innerHTML = '<p style="color:#ef4444;padding:20px;text-align:center">' + escHtml(err.message) + '</p>';
msg.style.display = 'none';
}
}
async function loadQuality() {
try {
const data = await apiJson('/api/explore/quality', { method: 'POST' });
if (data.rows && data.rows.length) {
const cols = Object.keys(data.rows[0]);
renderTable('quality-table', data.rows, cols);
} else {
document.getElementById('quality-table').innerHTML = '<p style="color:var(--text-muted);padding:20px;text-align:center">No quality data available</p>';
}
} catch (err) {
document.getElementById('quality-table').innerHTML = '<p style="color:#ef4444;padding:20px">' + escHtml(err.message) + '</p>';
}
}
/* ──────────────────────────────────────────────
BUILD SECTION
────────────────────────────────────────────── */
function selectTask(el, value) {
document.querySelectorAll('#task-radio-group .radio-item').forEach(r => r.classList.remove('active'));
el.classList.add('active');
el.querySelector('input').checked = true;
APP.task = value.charAt(0).toUpperCase() + value.slice(1);
}
function selectTaskByValue(value) {
const items = document.querySelectorAll('#task-radio-group .radio-item');
items.forEach(item => {
const input = item.querySelector('input');
if (input.value === value) {
item.classList.add('active');
input.checked = true;
} else {
item.classList.remove('active');
}
});
}
function toggleCollapsible(header) {
header.classList.toggle('open');
header.nextElementSibling.classList.toggle('open');
}
async function trainModels() {
const btn = document.getElementById('train-btn');
const loading = document.getElementById('train-loading');
const statusEl = document.getElementById('train-status');
btn.disabled = true;
loading.classList.remove('hidden');
statusEl.textContent = 'Initializing pipeline...';
document.getElementById('build-results').style.display = 'none';
const fd = new FormData();
const task = document.querySelector('input[name="task"]:checked').value;
fd.append('task', task);
fd.append('test_size', document.getElementById('test-size').value);
fd.append('cv_folds', document.getElementById('cv-folds').value);
// Preprocessing options
document.querySelectorAll('input[name="prep"]').forEach(cb => {
fd.append(cb.value, cb.checked ? 'true' : 'false');
});
// Progress simulation
const phases = [
'Preprocessing data...',
'Setting up experiment...',
'Comparing models...',
'Evaluating best model...',
'Generating plots...'
];
let pi = 0;
const progressTimer = setInterval(() => {
if (pi < phases.length) { statusEl.textContent = phases[pi++]; }
}, 4000);
try {
const data = await apiJson('/api/train', { method: 'POST', body: fd });
clearInterval(progressTimer);
loading.classList.add('hidden');
btn.disabled = false;
// Store state
APP.bestModel = data.model_name || '--';
APP.leaderboard = data.leaderboard || null;
APP.metrics = data.metrics || [];
APP.nPredictions = data.n_predictions || 0;
// Show results
document.getElementById('build-results').style.display = '';
document.getElementById('best-model-name').textContent = APP.bestModel;
// Training Summary Banner
if (data.summary) renderTrainingSummary(data.summary);
// Leaderboard
if (data.leaderboard) {
renderTable('leaderboard-table', data.leaderboard.rows, data.leaderboard.columns);
}
// Metric dropdown
const metricSel = document.getElementById('metric-select');
metricSel.innerHTML = '';
(data.metrics || []).forEach(m => {
metricSel.innerHTML += '<option value="' + escHtml(m) + '">' + escHtml(m) + '</option>';
});
renderMetricChart();
// Eval plots
renderEvalPlots(data.plots || []);
// Update export
document.getElementById('export-model').textContent = APP.bestModel;
document.getElementById('export-task').textContent = APP.task || task;
document.getElementById('export-preds').textContent = APP.nPredictions.toLocaleString();
} catch (err) {
clearInterval(progressTimer);
loading.classList.add('hidden');
btn.disabled = false;
statusEl.textContent = 'Training failed: ' + err.message;
setTimeout(() => loading.classList.add('hidden'), 2000);
alert('Training failed: ' + err.message);
}
}
function renderMetricChart() {
if (!APP.leaderboard) return;
const metric = document.getElementById('metric-select').value;
if (!metric) return;
const lb = APP.leaderboard;
const modelCol = lb.columns.find(c => c.toLowerCase() === 'model') || lb.columns[0];
const metricIdx = lb.columns.indexOf(metric);
if (metricIdx === -1) return;
const models = lb.rows.map(r => r[modelCol] || '');
const values = lb.rows.map(r => parseFloat(r[metric]) || 0);
const cs = getComputedStyle(document.documentElement);
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
// Color palette: top 3 get strong indigo, rest get medium indigo (NEVER faded/transparent)
const barPalette = ['#4f46e5', '#6366f1', '#818cf8', '#a5b4fc', '#93a0f5', '#7c8cf0', '#6b7de8', '#8b96f2', '#7988ed', '#6a7ae6'];
const colors = values.map((_, i) => barPalette[i % barPalette.length]);
const barOutline = isLight ? '#4338ca' : 'rgba(255,255,255,0.15)';
const barOutlineWidth = isLight ? 1.5 : 1;
const valTextColor = cs.getPropertyValue('--chart-text').trim() || '#a1a1aa';
const figure = {
data: [{
type: 'bar',
x: values,
y: models,
orientation: 'h',
marker: { color: colors, line: { width: barOutlineWidth, color: barOutline } },
text: values.map(v => v.toFixed(4)),
textposition: 'outside',
textfont: { family: 'Inter', size: 11, color: valTextColor }
}],
layout: {
yaxis: { autorange: 'reversed', tickfont: { size: 11 } },
xaxis: { title: metric },
margin: { l: 160, r: 60, t: 20, b: 40 },
height: Math.max(300, models.length * 36 + 80)
}
};
renderPlotly('metric-chart', figure);
}
function renderEvalPlots(plots) {
const container = document.getElementById('eval-plots');
if (!plots || !plots.length) {
container.innerHTML = '<p style="color:var(--text-muted);font-size:13px">No evaluation plots available</p>';
return;
}
container.innerHTML = plots.map((p, i) => `
<div class="plot-card">
<div class="plot-card-title">${escHtml(p.label || 'Plot')}</div>
<div id="eval-plot-${i}" style="width:100%;min-height:350px"></div>
</div>
`).join('');
// Render each Plotly figure
plots.forEach((p, i) => {
if (p.figure && p.figure.data) {
renderPlotly('eval-plot-' + i, p.figure);
}
});
}
/* ──────────────────────────────────────────────
TUNING
────────────────────────────────────────────── */
async function tuneModel() {
const btn = document.getElementById('tune-btn');
const loading = document.getElementById('tune-loading');
btn.disabled = true;
loading.classList.remove('hidden');
document.getElementById('tune-results').style.display = 'none';
const fd = new FormData();
fd.append('method', document.getElementById('tune-method').value);
fd.append('iterations', document.getElementById('tune-iter').value);
try {
const data = await apiJson('/api/tune', { method: 'POST', body: fd });
loading.classList.add('hidden');
btn.disabled = false;
document.getElementById('tune-results').style.display = '';
const improvedEl = document.getElementById('tune-improved');
if (data.improved) {
improvedEl.innerHTML = '<span style="color:var(--accent)">Model improved after tuning: ' + escHtml(data.model_name || '') + '</span>';
APP.bestModel = data.model_name || APP.bestModel;
document.getElementById('best-model-name').textContent = APP.bestModel;
document.getElementById('export-model').textContent = APP.bestModel;
} else {
improvedEl.innerHTML = '<span style="color:#f59e0b">No improvement found. Keeping original model.</span>';
}
if (data.tune_results) {
renderTable('tune-table', data.tune_results.rows, data.tune_results.columns);
}
// Update eval plots if provided
if (data.plots && data.plots.length) {
renderEvalPlots(data.plots);
}
} catch (err) {
loading.classList.add('hidden');
btn.disabled = false;
alert('Tuning failed: ' + err.message);
}
}
/* ──────────────────────────────────────────────
EXPORT
────────────────────────────────────────────── */
function downloadExport(type) {
const url = '/api/export/' + type;
const a = document.createElement('a');
a.href = url;
a.download = '';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
/* ──────────────────────────────────────────────
AUTO INSIGHTS
────────────────────────────────────────────── */
const INSIGHT_ICONS = {
success: '<svg class="insight-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
warning: '<svg class="insight-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
info: '<svg class="insight-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>'
};
function renderInsights(insights) {
const el = document.getElementById('data-insights');
if (!el || !insights || !insights.length) return;
el.innerHTML = '<div class="insights-grid">' + insights.map((ins, i) => `
<div class="insight-card ${ins.type || 'info'}" style="animation-delay:${i * 0.06}s">
${INSIGHT_ICONS[ins.type] || INSIGHT_ICONS.info}
<div>
<div class="insight-title">${escHtml(ins.title || '')}</div>
<div class="insight-detail">${escHtml(ins.detail || '')}</div>
</div>
</div>
`).join('') + '</div>';
}
/* ──────────────────────────────────────────────
TRAINING SUMMARY
────────────────────────────────────────────── */
function renderTrainingSummary(summary) {
if (!summary) return;
const container = document.getElementById('training-summary');
if (!container) return;
const featurePills = (summary.top_features || []).map(f =>
`<span class="feature-pill">${escHtml(f)}</span>`
).join('');
container.innerHTML = `
<div class="summary-banner">
<div class="card-label">Training Summary</div>
<div class="summary-stats">
<div class="summary-stat">
<div class="summary-stat-label">${escHtml(summary.key_metric || '')}</div>
<div class="summary-stat-value" style="color:var(--accent)">${summary.key_metric_value || '--'}</div>
</div>
<div class="summary-stat">
<div class="summary-stat-label">Models Compared</div>
<div class="summary-stat-value">${summary.n_models || '--'}</div>
</div>
<div class="summary-stat">
<div class="summary-stat-label">Best Model</div>
<div class="summary-stat-value">${escHtml(summary.model || '--')}</div>
</div>
</div>
${featurePills ? '<div style="margin-top:14px"><div class="summary-stat-label" style="margin-bottom:6px">Top Predictive Features</div>' + featurePills + '</div>' : ''}
<div class="summary-text">${escHtml(summary.text || '')}</div>
</div>
`;
container.style.display = '';
}
/* ──────────────────────────────────────────────
THEME TOGGLE
────────────────────────────────────────────── */
function toggleTheme() {
const html = document.documentElement;
const isLight = html.getAttribute('data-theme') === 'light';
const newTheme = isLight ? 'dark' : 'light';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('automl-theme', newTheme);
updateThemeIcon(newTheme);
reThemePlotly(newTheme);
}
function updateThemeIcon(theme) {
const moon = document.getElementById('icon-moon');
const sun = document.getElementById('icon-sun');
if (theme === 'light') {
moon.style.display = 'none';
sun.style.display = '';
} else {
moon.style.display = '';
sun.style.display = 'none';
}
}
function reThemePlotly(theme) {
// Small delay to let CSS variables update
requestAnimationFrame(() => {
const style = getComputedStyle(document.documentElement);
const textColor = style.getPropertyValue('--chart-text').trim() || '#71717a';
const gridColor = style.getPropertyValue('--chart-grid').trim() || 'rgba(30,27,75,0.15)';
document.querySelectorAll('.js-plotly-plot').forEach(el => {
try {
Plotly.relayout(el, {
'font.color': textColor,
'xaxis.gridcolor': gridColor,
'yaxis.gridcolor': gridColor,
});
} catch (_) {}
});
});
}
// Restore saved theme
(function() {
const saved = localStorage.getItem('automl-theme');
if (saved === 'light') {
document.documentElement.setAttribute('data-theme', 'light');
updateThemeIcon('light');
}
})();
/* ──────────────────────────────────────────────
INIT
────────────────────────────────────────────── */
showSection('data');