Spaces:
Sleeping
Sleeping
| {% extends "base.html" %} | |
| {% set active_page = "dashboard" %} | |
| {% block title %}Dashboard{% endblock %} | |
| {% block page_title %}<i class="fa-solid fa-gauge-high" style="color:var(--accent)"></i> Dashboard{% endblock %} | |
| {% block topnav_actions %} | |
| <button class="btn btn-primary btn-sm" onclick="openTrainModal()"> | |
| <i class="fa-solid fa-play"></i> New Run | |
| </button> | |
| {% endblock %} | |
| {% block content %} | |
| <div class="page-title">Platform Overview</div> | |
| <div class="page-sub">AutoML experiment sweeps + Airflow-style pipeline orchestration, tracked end-to-end with MLflow</div> | |
| <!-- ββ Tech stack explanation βββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="grid-2 mb-20"> | |
| <div class="card tech-card" style="border-top:3px solid var(--accent)"> | |
| <div class="card-header" style="margin-bottom:10px"> | |
| <div class="card-title"> | |
| <i class="fa-solid fa-wand-magic-sparkles" style="color:var(--accent)"></i> | |
| AutoML Engine | |
| </div> | |
| <span class="badge badge-purple">scikit-learn Β· XGBoost Β· LightGBM</span> | |
| </div> | |
| <p style="font-size:.84rem;color:var(--text-secondary);line-height:1.6;margin-bottom:12px"> | |
| Sweeps <strong>50+ algorithms</strong> across Linear, Tree-Based, Ensemble, SVM, KNN and Neural Network families. | |
| Every trial is logged to MLflow β metrics, params, and serialised model artefacts. | |
| Use the <a href="/automl">AutoML</a> page to rank algorithms on any dataset in one click. | |
| </p> | |
| <div style="display:flex;gap:8px;flex-wrap:wrap"> | |
| <span class="badge badge-muted"><i class="fa-solid fa-robot" style="color:var(--accent)"></i> 50+ Algorithms</span> | |
| <span class="badge badge-muted"><i class="fa-solid fa-chart-line" style="color:var(--accent-blue)"></i> MLflow Tracking</span> | |
| <span class="badge badge-muted"><i class="fa-solid fa-trophy" style="color:var(--warning)"></i> Auto-Ranking</span> | |
| <span class="badge badge-muted"><i class="fa-solid fa-code-branch" style="color:var(--success)"></i> Model Registry</span> | |
| </div> | |
| </div> | |
| <div class="card tech-card" style="border-top:3px solid var(--accent-blue)"> | |
| <div class="card-header" style="margin-bottom:10px"> | |
| <div class="card-title"> | |
| <i class="fa-solid fa-diagram-project" style="color:var(--accent-blue)"></i> | |
| Pipeline Orchestration | |
| </div> | |
| <span class="badge badge-info">Airflow-inspired DAG engine</span> | |
| </div> | |
| <p style="font-size:.84rem;color:var(--text-secondary);line-height:1.6;margin-bottom:12px"> | |
| Runs ML workflows as <strong>Directed Acyclic Graphs</strong> β the same paradigm used by Apache Airflow. | |
| Three built-in pipelines: <em>Training</em> (ingest β train β register β deploy), | |
| <em>Retraining</em> (drift detection β retrain β A/B test β promote), and | |
| <em>Data Processing</em> (clean β encode β scale β feature store). | |
| </p> | |
| <div style="display:flex;gap:8px;flex-wrap:wrap"> | |
| <span class="badge badge-muted"><i class="fa-solid fa-network-wired" style="color:var(--accent-blue)"></i> DAG Execution</span> | |
| <span class="badge badge-muted"><i class="fa-solid fa-wave-square" style="color:var(--danger)"></i> Drift Detection</span> | |
| <span class="badge badge-muted"><i class="fa-solid fa-rotate" style="color:var(--success)"></i> Auto-Retraining</span> | |
| <span class="badge badge-muted"><i class="fa-solid fa-rocket" style="color:var(--warning)"></i> Staging Deploy</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ββ Workflow banner ββββββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="card mb-20" style="background:linear-gradient(135deg,#8b5cf608,#3b82f608);border-color:var(--border-color);padding:16px 22px"> | |
| <div style="display:flex;align-items:center;gap:0;flex-wrap:wrap;justify-content:center"> | |
| <div class="workflow-step"><i class="fa-solid fa-database" style="color:var(--accent-blue)"></i><span>Load Dataset</span></div> | |
| <div class="workflow-arrow"><i class="fa-solid fa-arrow-right"></i></div> | |
| <div class="workflow-step"><i class="fa-solid fa-wand-magic-sparkles" style="color:var(--accent)"></i><span>AutoML Sweep</span></div> | |
| <div class="workflow-arrow"><i class="fa-solid fa-arrow-right"></i></div> | |
| <div class="workflow-step"><i class="fa-solid fa-flask" style="color:var(--success)"></i><span>MLflow Tracking</span></div> | |
| <div class="workflow-arrow"><i class="fa-solid fa-arrow-right"></i></div> | |
| <div class="workflow-step"><i class="fa-solid fa-diagram-project" style="color:var(--accent-blue)"></i><span>Pipeline DAG</span></div> | |
| <div class="workflow-arrow"><i class="fa-solid fa-arrow-right"></i></div> | |
| <div class="workflow-step"><i class="fa-solid fa-box-archive" style="color:var(--warning)"></i><span>Model Registry</span></div> | |
| <div class="workflow-arrow"><i class="fa-solid fa-arrow-right"></i></div> | |
| <div class="workflow-step"><i class="fa-solid fa-rocket" style="color:var(--danger)"></i><span>Deploy</span></div> | |
| </div> | |
| </div> | |
| <!-- ββ Live metrics βββββββββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="page-sub" style="margin-top:4px;margin-bottom:12px"> | |
| <i class="fa-solid fa-chart-bar" style="color:var(--accent)"></i> Live Experiment Metrics | |
| </div> | |
| <!-- Stat cards --> | |
| <div class="stat-grid"> | |
| <div class="stat-card purple"> | |
| <div class="stat-label">Total Runs</div> | |
| <div class="stat-value" id="stat-total">{{ total_runs }}</div> | |
| <div class="stat-sub"><i class="fa-solid fa-arrow-trend-up"></i> all time</div> | |
| </div> | |
| <div class="stat-card blue"> | |
| <div class="stat-label">Completed</div> | |
| <div class="stat-value" id="stat-completed">{{ completed_runs }}</div> | |
| <div class="stat-sub">finished successfully</div> | |
| </div> | |
| <div class="stat-card green"> | |
| <div class="stat-label">Best Score</div> | |
| <div class="stat-value" id="stat-best">{{ best_metric }}</div> | |
| <div class="stat-sub">accuracy / RΒ²</div> | |
| </div> | |
| <div class="stat-card yellow"> | |
| <div class="stat-label">Experiments</div> | |
| <div class="stat-value" id="stat-exps">{{ n_experiments }}</div> | |
| <div class="stat-sub">active datasets</div> | |
| </div> | |
| </div> | |
| <!-- Charts row --> | |
| <div class="grid-2 mb-20"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <div class="card-title"><i class="fa-solid fa-chart-pie" style="color:var(--accent)"></i> Algorithm Categories</div> | |
| </div> | |
| <div id="chart-algo" style="height:220px"></div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <div class="card-title"><i class="fa-solid fa-database" style="color:var(--accent-blue)"></i> Runs by Dataset</div> | |
| </div> | |
| <div id="chart-ds" style="height:220px"></div> | |
| </div> | |
| </div> | |
| <!-- Recent runs --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <div class="card-title"><i class="fa-solid fa-clock-rotate-left" style="color:var(--warning)"></i> Recent Training Runs <span style="font-size:.75rem;font-weight:400;color:var(--text-muted);margin-left:6px">β each row is one MLflow-tracked experiment</span></div> | |
| <a href="/experiments" class="btn btn-ghost btn-sm">View all <i class="fa-solid fa-arrow-right"></i></a> | |
| </div> | |
| <div class="table-wrap"> | |
| <table id="recent-table"> | |
| <thead> | |
| <tr> | |
| <th>Run ID</th><th>Algorithm</th><th>Category</th><th>Dataset</th> | |
| <th>Primary Metric</th><th>Duration</th><th>Status</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {% for r in recent_runs %} | |
| <tr> | |
| <td><code style="font-size:.8rem;color:var(--accent-light)">{{ r.run_id }}</code></td> | |
| <td><strong>{{ r.algorithm }}</strong></td> | |
| <td><span class="badge badge-purple">{{ r.category }}</span></td> | |
| <td>{{ r.dataset }}</td> | |
| <td> | |
| <span class="metric-val {% if r.primary_metric >= 0.9 %}metric-good{% elif r.primary_metric >= 0.7 %}metric-medium{% else %}metric-bad{% endif %}"> | |
| {{ r.primary_metric }} | |
| </span> | |
| </td> | |
| <td>{{ r.duration }}s</td> | |
| <td> | |
| {% if r.status == 'FINISHED' %} | |
| <span class="badge badge-success"><i class="fa-solid fa-check"></i> Done</span> | |
| {% elif r.status == 'RUNNING' %} | |
| <span class="badge badge-info"><span class="spinner" style="width:10px;height:10px;border-width:1.5px"></span> Running</span> | |
| {% else %} | |
| <span class="badge badge-muted">{{ r.status }}</span> | |
| {% endif %} | |
| </td> | |
| </tr> | |
| {% else %} | |
| <tr><td colspan="7"> | |
| <div class="empty-state"> | |
| <div class="empty-state-icon">π¬</div> | |
| <div class="empty-state-title">No runs yet</div> | |
| <div>Click <strong>New Run</strong> to train your first model</div> | |
| </div> | |
| </td></tr> | |
| {% endfor %} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- ββ Quick Train Modal ββββββββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="modal-overlay" id="train-modal"> | |
| <div class="modal"> | |
| <div class="modal-header"> | |
| <div class="modal-title"><i class="fa-solid fa-play" style="color:var(--accent)"></i> New Training Run</div> | |
| <button class="modal-close" onclick="closeTrainModal()"><i class="fa-solid fa-xmark"></i></button> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label">Dataset</label> | |
| <select class="form-select" id="sel-dataset" onchange="updateTaskType()"> | |
| {% for name, cfg in datasets.items() %} | |
| <option value="{{ name }}" data-task="{{ cfg.task }}">{{ cfg.icon }} {{ name }} β {{ cfg.description[:55] }}β¦</option> | |
| {% endfor %} | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label">Task Type</label> | |
| <select class="form-select" id="sel-task" onchange="populateCategories()"> | |
| <option value="classification">Classification</option> | |
| <option value="regression">Regression</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label">Algorithm Category</label> | |
| <select class="form-select" id="sel-category" onchange="populateAlgorithms()"></select> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label">Algorithm</label> | |
| <select class="form-select" id="sel-algorithm"></select> | |
| </div> | |
| <!-- Progress (hidden until running) --> | |
| <div id="train-progress-wrap" style="display:none;margin-top:12px"> | |
| <div class="flex-between mb-20" style="margin-bottom:6px"> | |
| <span style="font-size:.85rem;color:var(--text-secondary)" id="train-status-text">Initialisingβ¦</span> | |
| <span style="font-size:.85rem;font-weight:600" id="train-pct">0%</span> | |
| </div> | |
| <div class="progress-bar-wrap"><div class="progress-bar" id="train-bar" style="width:0%"></div></div> | |
| </div> | |
| <!-- Result (hidden until done) --> | |
| <div id="train-result" style="display:none;margin-top:14px"></div> | |
| <div class="modal-footer"> | |
| <button class="btn btn-ghost" onclick="closeTrainModal()">Cancel</button> | |
| <button class="btn btn-primary" id="btn-start-train" onclick="startTraining()"> | |
| <i class="fa-solid fa-play"></i> Train | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| {% endblock %} | |
| {% block scripts %} | |
| <script> | |
| const ALGO_DATA = {{ algorithms | tojson }}; | |
| let _algoCounts = {{ algo_counts }}; | |
| let _dsCounts = {{ ds_counts }}; | |
| const CHART_COLORS = ['#8b5cf6','#3b82f6','#22c55e','#f59e0b','#ef4444','#06b6d4','#ec4899','#a855f7']; | |
| // Returns Plotly-compatible colors matching the current theme | |
| function getChartTheme() { | |
| const light = document.documentElement.getAttribute('data-theme') === 'light'; | |
| return { | |
| bg: light ? '#ffffff' : '#161b22', | |
| border: light ? '#d0d7de' : '#30363d', | |
| txt: light ? '#57606a' : '#8b949e', | |
| annotation: light ? '#24292f' : '#e6edf3', | |
| }; | |
| } | |
| // ββ Chart rendering (called on load, after refresh, and on theme change) ββ | |
| function renderCharts(algoCounts, dsCounts) { | |
| const c = getChartTheme(); | |
| const cats = Object.keys(algoCounts); | |
| const vals = Object.values(algoCounts); | |
| Plotly.react('chart-algo', [{ | |
| type: 'pie', hole: .55, | |
| labels: cats, values: vals, | |
| marker: { colors: CHART_COLORS.slice(0, cats.length) }, | |
| textinfo: 'none', | |
| hovertemplate: '<b>%{label}</b><br>%{value} runs<extra></extra>', | |
| }], { | |
| paper_bgcolor: c.bg, plot_bgcolor: c.bg, | |
| margin: { t:8, b:8, l:8, r:8 }, | |
| legend: { font: { color: c.txt, size: 11 }, bgcolor: 'transparent', x: 1.05 }, | |
| showlegend: cats.length > 0, | |
| annotations: [{ text: `<b>${vals.reduce((a,b)=>a+b,0)}</b><br><span style="font-size:10px">runs</span>`, | |
| x:.5, y:.5, font:{size:14, color:c.annotation}, showarrow:false }], | |
| }, { responsive: true, displayModeBar: false }); | |
| const dsKeys = Object.keys(dsCounts); | |
| const dsVals = Object.values(dsCounts); | |
| Plotly.react('chart-ds', [{ | |
| type: 'bar', orientation: 'h', | |
| y: dsKeys, x: dsVals, | |
| marker: { color: '#3b82f6', opacity: .85 }, | |
| hovertemplate: '<b>%{y}</b>: %{x} runs<extra></extra>', | |
| }], { | |
| paper_bgcolor: c.bg, plot_bgcolor: c.bg, | |
| margin: { t:8, b:24, l:8, r:16 }, | |
| xaxis: { gridcolor: c.border, color: c.txt, tickfont:{size:10} }, | |
| yaxis: { color: c.txt, tickfont:{size:10}, automargin:true }, | |
| bargap: .35, | |
| }, { responsive: true, displayModeBar: false }); | |
| } | |
| // ββ Live-update the recent-runs table ββββββββββββββββββββββββββββββββββββββ | |
| function updateRecentTable(rows) { | |
| const tbody = document.querySelector('#recent-table tbody'); | |
| if (!tbody) return; | |
| if (!rows || rows.length === 0) { | |
| tbody.innerHTML = `<tr><td colspan="7"> | |
| <div class="empty-state"> | |
| <div class="empty-state-icon">π¬</div> | |
| <div class="empty-state-title">No runs yet</div> | |
| <div>Click <strong>New Run</strong> to train your first model</div> | |
| </div></td></tr>`; | |
| return; | |
| } | |
| tbody.innerHTML = rows.map(r => { | |
| const mc = r.primary_metric >= 0.9 ? 'metric-good' : r.primary_metric >= 0.7 ? 'metric-medium' : 'metric-bad'; | |
| const sb = r.status === 'FINISHED' | |
| ? `<span class="badge badge-success"><i class="fa-solid fa-check"></i> Done</span>` | |
| : r.status === 'RUNNING' | |
| ? `<span class="badge badge-info"><span class="spinner" style="width:10px;height:10px;border-width:1.5px"></span> Running</span>` | |
| : `<span class="badge badge-muted">${r.status}</span>`; | |
| return `<tr> | |
| <td><code style="font-size:.8rem;color:var(--accent-light)">${r.run_id}</code></td> | |
| <td><strong>${r.algorithm}</strong></td> | |
| <td><span class="badge badge-purple">${r.category}</span></td> | |
| <td>${r.dataset}</td> | |
| <td><span class="metric-val ${mc}">${r.primary_metric}</span></td> | |
| <td>${r.duration != null ? r.duration + 's' : 'β'}</td> | |
| <td>${sb}</td> | |
| </tr>`; | |
| }).join(''); | |
| } | |
| document.addEventListener('DOMContentLoaded', () => { | |
| renderCharts(_algoCounts, _dsCounts); | |
| populateCategories(); | |
| }); | |
| // Re-render charts when theme changes so colors match | |
| document.addEventListener('themechange', () => renderCharts(_algoCounts, _dsCounts)); | |
| // ββ Train modal helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function updateTaskType() { | |
| const ds = document.getElementById('sel-dataset'); | |
| const opt = ds.options[ds.selectedIndex]; | |
| document.getElementById('sel-task').value = opt.dataset.task || 'classification'; | |
| populateCategories(); | |
| } | |
| function populateCategories() { | |
| const task = document.getElementById('sel-task').value; | |
| const sel = document.getElementById('sel-category'); | |
| sel.innerHTML = ''; | |
| const cats = Object.keys(ALGO_DATA[task] || {}); | |
| cats.forEach(c => { const o = document.createElement('option'); o.value = c; o.text = c; sel.add(o); }); | |
| populateAlgorithms(); | |
| } | |
| function populateAlgorithms() { | |
| const task = document.getElementById('sel-task').value; | |
| const cat = document.getElementById('sel-category').value; | |
| const sel = document.getElementById('sel-algorithm'); | |
| sel.innerHTML = ''; | |
| const algos = Object.keys((ALGO_DATA[task] || {})[cat] || {}); | |
| algos.forEach(a => { const o = document.createElement('option'); o.value = a; o.text = a; sel.add(o); }); | |
| } | |
| function openTrainModal() { document.getElementById('train-modal').classList.add('open'); resetTrainModal(); } | |
| function closeTrainModal() { document.getElementById('train-modal').classList.remove('open'); } | |
| function resetTrainModal() { | |
| document.getElementById('train-progress-wrap').style.display = 'none'; | |
| document.getElementById('train-result').style.display = 'none'; | |
| document.getElementById('btn-start-train').disabled = false; | |
| document.getElementById('btn-start-train').innerHTML = '<i class="fa-solid fa-play"></i> Train'; | |
| } | |
| async function startTraining() { | |
| const dataset = document.getElementById('sel-dataset').value; | |
| const task_type = document.getElementById('sel-task').value; | |
| const category = document.getElementById('sel-category').value; | |
| const algorithm = document.getElementById('sel-algorithm').value; | |
| document.getElementById('btn-start-train').disabled = true; | |
| document.getElementById('btn-start-train').innerHTML = '<span class="spinner"></span> Runningβ¦'; | |
| document.getElementById('train-progress-wrap').style.display = 'block'; | |
| document.getElementById('train-result').style.display = 'none'; | |
| try { | |
| const res = await fetch('/api/train', { | |
| method: 'POST', headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({ dataset, task_type, category, algorithm }), | |
| }); | |
| const data = await res.json(); | |
| if (data.error) { showToast(data.error, 'error'); resetTrainModal(); return; } | |
| pollTraining(data.job_id); | |
| } catch(e) { showToast('Request failed', 'error'); resetTrainModal(); } | |
| } | |
| function pollTraining(jobId) { | |
| const bar = document.getElementById('train-bar'); | |
| const pct = document.getElementById('train-pct'); | |
| const statusT = document.getElementById('train-status-text'); | |
| const resultD = document.getElementById('train-result'); | |
| const iv = setInterval(async () => { | |
| const res = await fetch(`/api/run/${jobId}/status`); | |
| const job = await res.json(); | |
| bar.style.width = job.progress + '%'; | |
| pct.textContent = job.progress + '%'; | |
| statusT.textContent = job.status.charAt(0).toUpperCase() + job.status.slice(1) + 'β¦'; | |
| if (job.status === 'completed') { | |
| clearInterval(iv); | |
| statusT.textContent = 'Completed'; | |
| const m = job.metrics || {}; | |
| const keys = Object.keys(m); | |
| const primary = keys[0]; | |
| resultD.style.display = 'block'; | |
| resultD.innerHTML = ` | |
| <div style="background:var(--bg-tertiary);border-radius:8px;padding:14px"> | |
| <div style="font-weight:600;margin-bottom:10px;color:var(--success)"> | |
| <i class="fa-solid fa-circle-check"></i> Training complete | |
| </div> | |
| <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px"> | |
| ${keys.map(k=>`<div style="background:var(--bg-secondary);padding:8px 10px;border-radius:6px"> | |
| <div style="font-size:.72rem;color:var(--text-muted);text-transform:uppercase">${k}</div> | |
| <div style="font-size:1.1rem;font-weight:700;color:var(--success)">${m[k]}</div> | |
| </div>`).join('')} | |
| </div> | |
| </div>`; | |
| document.getElementById('btn-start-train').innerHTML = '<i class="fa-solid fa-rotate-right"></i> Run Again'; | |
| document.getElementById('btn-start-train').disabled = false; | |
| showToast(`${job.algorithm} β ${primary}: ${m[primary]}`, 'success'); | |
| refreshStats(); | |
| } else if (job.status === 'failed') { | |
| clearInterval(iv); | |
| showToast('Training failed: ' + (job.error || 'unknown'), 'error'); | |
| resetTrainModal(); | |
| } | |
| }, 1000); | |
| } | |
| // ββ Full dashboard refresh (stats + table + charts) ββββββββββββββββββββββββ | |
| async function refreshStats() { | |
| try { | |
| const r = await fetch('/api/stats'); | |
| const s = await r.json(); | |
| document.getElementById('stat-total').textContent = s.total_runs; | |
| document.getElementById('stat-completed').textContent = s.completed_runs; | |
| document.getElementById('stat-best').textContent = s.best_metric; | |
| document.getElementById('stat-exps').textContent = s.n_experiments; | |
| if (s.recent_runs) updateRecentTable(s.recent_runs); | |
| if (s.algo_counts && s.ds_counts) { | |
| _algoCounts = s.algo_counts; | |
| _dsCounts = s.ds_counts; | |
| renderCharts(_algoCounts, _dsCounts); | |
| } | |
| } catch(_) {} | |
| } | |
| </script> | |
| {% endblock %} | |