mnoorchenar commited on
Commit
9f28e8f
Β·
1 Parent(s): 84e5a7a

Update 2026-03-25 16:05:19

Browse files
.claude/settings.local.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(find /e/HuggingFace/AutoMLOps -not -path */__pycache__/* -not -path */.git/* -not -path */node_modules/*)"
5
+ ]
6
+ }
7
+ }
app.py CHANGED
@@ -444,7 +444,10 @@ def api_datasets():
444
  def api_stats():
445
  client = _mlflow_client()
446
  try:
447
- runs = client.search_runs(experiment_ids=[], max_results=500)
 
 
 
448
  except Exception:
449
  runs = []
450
  finished = [r for r in runs if r.info.status == "FINISHED"]
@@ -453,11 +456,38 @@ def api_stats():
453
  v = r.data.metrics.get("accuracy") or r.data.metrics.get("r2_score") or 0
454
  if v > best:
455
  best = v
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
  return jsonify({
457
  "total_runs": len(runs),
458
  "completed_runs": len(finished),
459
  "best_metric": round(best, 4),
460
  "n_experiments": len(set(r.info.experiment_id for r in runs)),
 
 
 
461
  })
462
 
463
 
 
444
  def api_stats():
445
  client = _mlflow_client()
446
  try:
447
+ runs = client.search_runs(
448
+ experiment_ids=[], max_results=500,
449
+ order_by=["start_time DESC"],
450
+ )
451
  except Exception:
452
  runs = []
453
  finished = [r for r in runs if r.info.status == "FINISHED"]
 
456
  v = r.data.metrics.get("accuracy") or r.data.metrics.get("r2_score") or 0
457
  if v > best:
458
  best = v
459
+
460
+ recent = []
461
+ for r in runs[:8]:
462
+ m = r.data.metrics
463
+ primary = m.get("accuracy") or m.get("r2_score") or 0
464
+ recent.append({
465
+ "run_id": r.info.run_id[:8],
466
+ "algorithm": r.data.tags.get("algorithm", "β€”"),
467
+ "category": r.data.tags.get("category", "β€”"),
468
+ "dataset": r.data.tags.get("dataset", "β€”"),
469
+ "primary_metric": round(primary, 4),
470
+ "status": r.info.status,
471
+ "duration": round((r.info.end_time - r.info.start_time) / 1000, 1)
472
+ if r.info.end_time else None,
473
+ })
474
+
475
+ algo_counts: dict = {}
476
+ ds_counts: dict = {}
477
+ for r in finished:
478
+ cat = r.data.tags.get("category", "Other")
479
+ algo_counts[cat] = algo_counts.get(cat, 0) + 1
480
+ ds = r.data.tags.get("dataset", "Other")
481
+ ds_counts[ds] = ds_counts.get(ds, 0) + 1
482
+
483
  return jsonify({
484
  "total_runs": len(runs),
485
  "completed_runs": len(finished),
486
  "best_metric": round(best, 4),
487
  "n_experiments": len(set(r.info.experiment_id for r in runs)),
488
+ "recent_runs": recent,
489
+ "algo_counts": algo_counts,
490
+ "ds_counts": ds_counts,
491
  })
492
 
493
 
static/css/style.css CHANGED
@@ -484,3 +484,33 @@ tr:hover td { background: var(--bg-hover); }
484
  }
485
  .empty-state-icon { font-size: 3rem; margin-bottom: 12px; }
486
  .empty-state-title { font-size: 1rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
  }
485
  .empty-state-icon { font-size: 3rem; margin-bottom: 12px; }
486
  .empty-state-title { font-size: 1rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; }
487
+
488
+ /* ── Light theme ──────────────────────────────────────────────────────────── */
489
+ [data-theme="light"] {
490
+ --bg-primary: #f6f8fa;
491
+ --bg-secondary: #ffffff;
492
+ --bg-tertiary: #eaeef2;
493
+ --bg-hover: #d0d7de30;
494
+ --border-color: #d0d7de;
495
+ --text-primary: #24292f;
496
+ --text-secondary:#57606a;
497
+ --text-muted: #8c959f;
498
+ --shadow: 0 4px 24px rgba(0,0,0,.10);
499
+ }
500
+ [data-theme="light"] .form-select {
501
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2357606a' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
502
+ }
503
+ [data-theme="light"] .topnav-badge {
504
+ color: #fff;
505
+ }
506
+ [data-theme="light"] .pipeline-log {
507
+ color: var(--text-secondary);
508
+ }
509
+
510
+ /* ── Smooth theme transition ──────────────────────────────────────────────── */
511
+ body.theme-transition,
512
+ body.theme-transition *,
513
+ body.theme-transition *::before,
514
+ body.theme-transition *::after {
515
+ transition: background-color .22s ease, color .22s ease, border-color .22s ease !important;
516
+ }
static/js/app.js CHANGED
@@ -47,3 +47,22 @@ function activateTab(panelId, btn, groupClass) {
47
  document.getElementById(panelId)?.classList.add('active');
48
  if (btn) btn.classList.add('active');
49
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  document.getElementById(panelId)?.classList.add('active');
48
  if (btn) btn.classList.add('active');
49
  }
50
+
51
+ // ── Light / dark theme ─────────────────────────────────────────────────────
52
+ function toggleTheme() {
53
+ document.body.classList.add('theme-transition');
54
+ const current = document.documentElement.getAttribute('data-theme') || 'dark';
55
+ const next = current === 'dark' ? 'light' : 'dark';
56
+ document.documentElement.setAttribute('data-theme', next);
57
+ localStorage.setItem('theme', next);
58
+ const icon = document.getElementById('theme-icon');
59
+ if (icon) icon.className = next === 'dark' ? 'fa-solid fa-moon' : 'fa-solid fa-sun';
60
+ document.dispatchEvent(new CustomEvent('themechange', { detail: next }));
61
+ setTimeout(() => document.body.classList.remove('theme-transition'), 300);
62
+ }
63
+
64
+ document.addEventListener('DOMContentLoaded', function () {
65
+ const current = document.documentElement.getAttribute('data-theme') || 'dark';
66
+ const icon = document.getElementById('theme-icon');
67
+ if (icon) icon.className = current === 'dark' ? 'fa-solid fa-moon' : 'fa-solid fa-sun';
68
+ });
templates/base.html CHANGED
@@ -18,6 +18,9 @@
18
 
19
  <link rel="stylesheet" href="/static/css/style.css">
20
 
 
 
 
21
  {% block head_extra %}{% endblock %}
22
  </head>
23
  <body>
@@ -73,6 +76,9 @@
73
  </button>
74
  <span class="topnav-title">{% block page_title %}AutoMLOps{% endblock %}</span>
75
  <span class="topnav-badge"><i class="fa-solid fa-circle" style="color:#22c55e;font-size:.55rem"></i> Live</span>
 
 
 
76
  {% block topnav_actions %}{% endblock %}
77
  </header>
78
 
 
18
 
19
  <link rel="stylesheet" href="/static/css/style.css">
20
 
21
+ <!-- Apply saved theme before render to prevent flash -->
22
+ <script>document.documentElement.setAttribute('data-theme',localStorage.getItem('theme')||'dark');</script>
23
+
24
  {% block head_extra %}{% endblock %}
25
  </head>
26
  <body>
 
76
  </button>
77
  <span class="topnav-title">{% block page_title %}AutoMLOps{% endblock %}</span>
78
  <span class="topnav-badge"><i class="fa-solid fa-circle" style="color:#22c55e;font-size:.55rem"></i> Live</span>
79
+ <button class="btn btn-ghost btn-sm" id="theme-toggle" onclick="toggleTheme()" title="Toggle light/dark theme" style="width:32px;padding:5px 0;justify-content:center">
80
+ <i class="fa-solid fa-moon" id="theme-icon"></i>
81
+ </button>
82
  {% block topnav_actions %}{% endblock %}
83
  </header>
84
 
templates/dashboard.html CHANGED
@@ -164,52 +164,100 @@
164
 
165
  {% block scripts %}
166
  <script>
167
- const ALGO_DATA = {{ algorithms | tojson }};
168
- const ALGO_COUNTS = {{ algo_counts }};
169
- const DS_COUNTS = {{ ds_counts }};
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
- // ── Charts ────────────────────────────────────────────────────────────────
172
- document.addEventListener('DOMContentLoaded', () => {
173
- const dark = '#0d1117', bg2 = '#161b22', border = '#30363d', txt = '#8b949e';
 
 
174
 
175
- // Donut β€” algorithm categories
176
- const cats = Object.keys(ALGO_COUNTS);
177
- const vals = Object.values(ALGO_COUNTS);
178
- const COLORS = ['#8b5cf6','#3b82f6','#22c55e','#f59e0b','#ef4444','#06b6d4','#ec4899','#a855f7'];
179
- Plotly.newPlot('chart-algo', [{
180
  type: 'pie', hole: .55,
181
  labels: cats, values: vals,
182
- marker: { colors: COLORS.slice(0, cats.length) },
183
  textinfo: 'none',
184
  hovertemplate: '<b>%{label}</b><br>%{value} runs<extra></extra>',
185
  }], {
186
- paper_bgcolor: bg2, plot_bgcolor: bg2,
187
  margin: { t:8, b:8, l:8, r:8 },
188
- legend: { font: { color: txt, size: 11 }, bgcolor: 'transparent', x: 1.05 },
189
  showlegend: cats.length > 0,
190
  annotations: [{ text: `<b>${vals.reduce((a,b)=>a+b,0)}</b><br><span style="font-size:10px">runs</span>`,
191
- x:.5, y:.5, font:{size:14,color:'#e6edf3'}, showarrow:false }],
192
  }, { responsive: true, displayModeBar: false });
193
 
194
- // Bar β€” datasets
195
- const dsKeys = Object.keys(DS_COUNTS);
196
- const dsVals = Object.values(DS_COUNTS);
197
- Plotly.newPlot('chart-ds', [{
198
  type: 'bar', orientation: 'h',
199
  y: dsKeys, x: dsVals,
200
  marker: { color: '#3b82f6', opacity: .85 },
201
  hovertemplate: '<b>%{y}</b>: %{x} runs<extra></extra>',
202
  }], {
203
- paper_bgcolor: bg2, plot_bgcolor: bg2,
204
  margin: { t:8, b:24, l:8, r:16 },
205
- xaxis: { gridcolor: border, color: txt, tickfont:{size:10} },
206
- yaxis: { color: txt, tickfont:{size:10}, automargin:true },
207
  bargap: .35,
208
  }, { responsive: true, displayModeBar: false });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
 
 
 
210
  populateCategories();
211
  });
212
 
 
 
 
213
  // ── Train modal helpers ────────────────────────────────────────────────────
214
  function updateTaskType() {
215
  const ds = document.getElementById('sel-dataset');
@@ -275,8 +323,8 @@ function pollTraining(jobId) {
275
  const resultD = document.getElementById('train-result');
276
 
277
  const iv = setInterval(async () => {
278
- const res = await fetch(`/api/run/${jobId}/status`);
279
- const job = await res.json();
280
 
281
  bar.style.width = job.progress + '%';
282
  pct.textContent = job.progress + '%';
@@ -285,7 +333,7 @@ function pollTraining(jobId) {
285
  if (job.status === 'completed') {
286
  clearInterval(iv);
287
  statusT.textContent = 'Completed';
288
- const m = job.metrics || {};
289
  const keys = Object.keys(m);
290
  const primary = keys[0];
291
  resultD.style.display = 'block';
@@ -313,6 +361,7 @@ function pollTraining(jobId) {
313
  }, 1000);
314
  }
315
 
 
316
  async function refreshStats() {
317
  try {
318
  const r = await fetch('/api/stats');
@@ -321,6 +370,12 @@ async function refreshStats() {
321
  document.getElementById('stat-completed').textContent = s.completed_runs;
322
  document.getElementById('stat-best').textContent = s.best_metric;
323
  document.getElementById('stat-exps').textContent = s.n_experiments;
 
 
 
 
 
 
324
  } catch(_) {}
325
  }
326
  </script>
 
164
 
165
  {% block scripts %}
166
  <script>
167
+ const ALGO_DATA = {{ algorithms | tojson }};
168
+ let _algoCounts = {{ algo_counts }};
169
+ let _dsCounts = {{ ds_counts }};
170
+
171
+ const CHART_COLORS = ['#8b5cf6','#3b82f6','#22c55e','#f59e0b','#ef4444','#06b6d4','#ec4899','#a855f7'];
172
+
173
+ // Returns Plotly-compatible colors matching the current theme
174
+ function getChartTheme() {
175
+ const light = document.documentElement.getAttribute('data-theme') === 'light';
176
+ return {
177
+ bg: light ? '#ffffff' : '#161b22',
178
+ border: light ? '#d0d7de' : '#30363d',
179
+ txt: light ? '#57606a' : '#8b949e',
180
+ annotation: light ? '#24292f' : '#e6edf3',
181
+ };
182
+ }
183
 
184
+ // ── Chart rendering (called on load, after refresh, and on theme change) ──
185
+ function renderCharts(algoCounts, dsCounts) {
186
+ const c = getChartTheme();
187
+ const cats = Object.keys(algoCounts);
188
+ const vals = Object.values(algoCounts);
189
 
190
+ Plotly.react('chart-algo', [{
 
 
 
 
191
  type: 'pie', hole: .55,
192
  labels: cats, values: vals,
193
+ marker: { colors: CHART_COLORS.slice(0, cats.length) },
194
  textinfo: 'none',
195
  hovertemplate: '<b>%{label}</b><br>%{value} runs<extra></extra>',
196
  }], {
197
+ paper_bgcolor: c.bg, plot_bgcolor: c.bg,
198
  margin: { t:8, b:8, l:8, r:8 },
199
+ legend: { font: { color: c.txt, size: 11 }, bgcolor: 'transparent', x: 1.05 },
200
  showlegend: cats.length > 0,
201
  annotations: [{ text: `<b>${vals.reduce((a,b)=>a+b,0)}</b><br><span style="font-size:10px">runs</span>`,
202
+ x:.5, y:.5, font:{size:14, color:c.annotation}, showarrow:false }],
203
  }, { responsive: true, displayModeBar: false });
204
 
205
+ const dsKeys = Object.keys(dsCounts);
206
+ const dsVals = Object.values(dsCounts);
207
+ Plotly.react('chart-ds', [{
 
208
  type: 'bar', orientation: 'h',
209
  y: dsKeys, x: dsVals,
210
  marker: { color: '#3b82f6', opacity: .85 },
211
  hovertemplate: '<b>%{y}</b>: %{x} runs<extra></extra>',
212
  }], {
213
+ paper_bgcolor: c.bg, plot_bgcolor: c.bg,
214
  margin: { t:8, b:24, l:8, r:16 },
215
+ xaxis: { gridcolor: c.border, color: c.txt, tickfont:{size:10} },
216
+ yaxis: { color: c.txt, tickfont:{size:10}, automargin:true },
217
  bargap: .35,
218
  }, { responsive: true, displayModeBar: false });
219
+ }
220
+
221
+ // ── Live-update the recent-runs table ──────────────────────────────────────
222
+ function updateRecentTable(rows) {
223
+ const tbody = document.querySelector('#recent-table tbody');
224
+ if (!tbody) return;
225
+ if (!rows || rows.length === 0) {
226
+ tbody.innerHTML = `<tr><td colspan="7">
227
+ <div class="empty-state">
228
+ <div class="empty-state-icon">πŸ”¬</div>
229
+ <div class="empty-state-title">No runs yet</div>
230
+ <div>Click <strong>New Run</strong> to train your first model</div>
231
+ </div></td></tr>`;
232
+ return;
233
+ }
234
+ tbody.innerHTML = rows.map(r => {
235
+ const mc = r.primary_metric >= 0.9 ? 'metric-good' : r.primary_metric >= 0.7 ? 'metric-medium' : 'metric-bad';
236
+ const sb = r.status === 'FINISHED'
237
+ ? `<span class="badge badge-success"><i class="fa-solid fa-check"></i> Done</span>`
238
+ : r.status === 'RUNNING'
239
+ ? `<span class="badge badge-info"><span class="spinner" style="width:10px;height:10px;border-width:1.5px"></span> Running</span>`
240
+ : `<span class="badge badge-muted">${r.status}</span>`;
241
+ return `<tr>
242
+ <td><code style="font-size:.8rem;color:var(--accent-light)">${r.run_id}</code></td>
243
+ <td><strong>${r.algorithm}</strong></td>
244
+ <td><span class="badge badge-purple">${r.category}</span></td>
245
+ <td>${r.dataset}</td>
246
+ <td><span class="metric-val ${mc}">${r.primary_metric}</span></td>
247
+ <td>${r.duration != null ? r.duration + 's' : 'β€”'}</td>
248
+ <td>${sb}</td>
249
+ </tr>`;
250
+ }).join('');
251
+ }
252
 
253
+ document.addEventListener('DOMContentLoaded', () => {
254
+ renderCharts(_algoCounts, _dsCounts);
255
  populateCategories();
256
  });
257
 
258
+ // Re-render charts when theme changes so colors match
259
+ document.addEventListener('themechange', () => renderCharts(_algoCounts, _dsCounts));
260
+
261
  // ── Train modal helpers ────────────────────────────────────────────────────
262
  function updateTaskType() {
263
  const ds = document.getElementById('sel-dataset');
 
323
  const resultD = document.getElementById('train-result');
324
 
325
  const iv = setInterval(async () => {
326
+ const res = await fetch(`/api/run/${jobId}/status`);
327
+ const job = await res.json();
328
 
329
  bar.style.width = job.progress + '%';
330
  pct.textContent = job.progress + '%';
 
333
  if (job.status === 'completed') {
334
  clearInterval(iv);
335
  statusT.textContent = 'Completed';
336
+ const m = job.metrics || {};
337
  const keys = Object.keys(m);
338
  const primary = keys[0];
339
  resultD.style.display = 'block';
 
361
  }, 1000);
362
  }
363
 
364
+ // ── Full dashboard refresh (stats + table + charts) ────────────────────────
365
  async function refreshStats() {
366
  try {
367
  const r = await fetch('/api/stats');
 
370
  document.getElementById('stat-completed').textContent = s.completed_runs;
371
  document.getElementById('stat-best').textContent = s.best_metric;
372
  document.getElementById('stat-exps').textContent = s.n_experiments;
373
+ if (s.recent_runs) updateRecentTable(s.recent_runs);
374
+ if (s.algo_counts && s.ds_counts) {
375
+ _algoCounts = s.algo_counts;
376
+ _dsCounts = s.ds_counts;
377
+ renderCharts(_algoCounts, _dsCounts);
378
+ }
379
  } catch(_) {}
380
  }
381
  </script>