-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Add local student dashboard + Code Lab runner #156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| # Frontend (Local Student Dashboard) | ||
|
|
||
| This repo ships a local-first student UI (Scaler-style): | ||
|
|
||
| - `dashboard.html` → phase cards + global progress | ||
| - `phase.html` → per-phase lesson tiles + sidebar + progress | ||
| - `lesson.html` → lesson reader + sticky progress bar + Code Lab (run/test locally) | ||
|
|
||
| All course content is read directly from this repo via a local server (no GitHub/raw content fetch). | ||
|
|
||
| ## Quick start | ||
|
|
||
| From the repo root: | ||
|
|
||
| ```bash | ||
| cd ai-engineering-from-scratch | ||
| node site/local-server.mjs | ||
| ``` | ||
|
|
||
| Open: | ||
|
|
||
| - Dashboard: `http://127.0.0.1:5174/dashboard.html` | ||
| - About/home: `http://127.0.0.1:5174/index.html` | ||
|
|
||
| ## How it works | ||
|
|
||
| The server lives at: | ||
|
|
||
| - `site/local-server.mjs` | ||
|
|
||
| It provides: | ||
|
|
||
| - Static files: `GET /` serves files from `site/` | ||
| - Local content: `GET /content/<repo-path>` serves files from the course repo (e.g. lesson markdown at `/content/phases/.../docs/en.md`) | ||
| - Directory listing: `GET /api/list?path=<repo-path>` returns local directory entries (used by lesson panels) | ||
| - Lesson meta: `GET /api/lesson-meta?path=phases/.../...` returns metadata and resolved runnable files | ||
| - Runner APIs: | ||
| - `POST /api/run` executes `python3` for a selected file | ||
| - `POST /api/test` runs `pytest` if installed, else `unittest` discovery | ||
| - Rubrics/AI hooks: | ||
| - `GET /api/rubric?path=...` returns `rubric.json` if present, else a generated rubric | ||
| - `POST /api/review` currently returns `501` (stub) until an LLM provider is wired up | ||
|
|
||
| ## Code Lab | ||
|
|
||
| In `lesson.html`, the **Code Lab** panel: | ||
|
|
||
| - auto-detects runnable artifacts via `/api/lesson-meta` (uses `catalog.json` + filesystem fallback) | ||
| - fetches the runnable file content from `/content/...` | ||
| - runs code via `/api/run` | ||
| - runs tests via `/api/test` | ||
|
|
||
| Notes: | ||
|
|
||
| - `Run` currently runs Python (`python3 -I ...`). Other languages are listed (Julia/TS/etc.) but not executed yet. | ||
| - Tests only run if the lesson directory includes a `tests/` folder or `test_*.py` files. | ||
|
|
||
| ## Progress tracking (local only) | ||
|
|
||
| Progress is stored in browser `localStorage` by: | ||
|
|
||
| - `site/progress.js` | ||
|
|
||
| Resetting progress clears local completion + quiz answers for this browser only. | ||
|
|
||
| ## Next steps (planned) | ||
|
|
||
| - Add non-Python runners (Node/TS, Julia, Rust) behind the same API. | ||
| - Implement `/api/review` with a configurable LLM provider and per-lesson rubrics. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en" data-theme="light"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>Dashboard - AI Engineering from Scratch</title> | ||
| <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' fill='%23fafaf5'/><rect x='2' y='2' width='28' height='28' fill='none' stroke='%233553ff' stroke-width='1.2'/><text x='6' y='22' font-size='14' font-family='monospace' fill='%233553ff'>AI</text></svg>"> | ||
| <meta name="description" content="Local-first student dashboard for AI Engineering from Scratch. Browse phases, track progress, and continue where you left off."> | ||
| <link rel="preconnect" href="https://fonts.googleapis.com"> | ||
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | ||
| <link href="https://fonts.googleapis.com/css2?family=VT323&family=Source+Serif+4:ital,opsz,wght@0,8..60,400..700;1,8..60,400..700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"> | ||
| <link rel="stylesheet" href="style.css?v=20260508a"> | ||
| </head> | ||
| <body> | ||
| <header class="site-header"> | ||
| <div class="header-inner"> | ||
| <a href="dashboard.html" class="logo"> | ||
| <span class="logo-icon" aria-hidden="true"></span> AI / FROM SCRATCH | ||
| </a> | ||
| <nav class="header-nav"> | ||
| <a href="dashboard.html" class="active">Dashboard</a> | ||
| <a href="catalog.html">Catalog</a> | ||
| <a href="glossary.html">Glossary</a> | ||
| <a href="index.html">About</a> | ||
| </nav> | ||
| <div class="header-controls"> | ||
| <button class="theme-toggle" id="themeToggle" aria-label="Toggle theme" type="button"> | ||
| <span id="themeIcon" aria-hidden="true"></span> | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </header> | ||
|
|
||
| <main class="dash"> | ||
| <div class="container"> | ||
| <div class="dash-top"> | ||
| <div> | ||
| <div class="dash-eyebrow">Student dashboard</div> | ||
| <h1 class="dash-title">Your phases</h1> | ||
| <p class="dash-sub">Pick a phase, track progress, and keep shipping.</p> | ||
| </div> | ||
| <div class="dash-cta"> | ||
| <a class="btn btn-primary" id="continueBtn" href="phase.html?phase=1">Continue</a> | ||
| <button class="btn" id="resetBtn" type="button" title="Clear local progress">Reset progress</button> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div class="dash-kpis" id="dashKpis"></div> | ||
|
|
||
| <div class="phase-grid" id="phaseGrid"></div> | ||
| </div> | ||
| </main> | ||
|
|
||
| <script src="data.js?v=20260508a"></script> | ||
| <script src="progress.js?v=20260508a"></script> | ||
| <script src="header.js?v=20260508a" defer></script> | ||
| <script src="cmdpalette.js?v=20260508a" defer></script> | ||
| <script src="dashboard.js?v=20260508a"></script> | ||
| </body> | ||
| </html> | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| (function () { | ||
| var root = document.documentElement; | ||
| var stored = localStorage.getItem('theme'); | ||
| if (stored) { | ||
| root.setAttribute('data-theme', stored); | ||
| } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { | ||
| root.setAttribute('data-theme', 'dark'); | ||
| } else { | ||
| root.setAttribute('data-theme', 'light'); | ||
| } | ||
|
|
||
| document.addEventListener('DOMContentLoaded', function () { | ||
| initThemeToggle(); | ||
| render(); | ||
| }); | ||
|
|
||
| function initThemeToggle() { | ||
| var btn = document.getElementById('themeToggle'); | ||
| var icon = document.getElementById('themeIcon'); | ||
| if (!btn || !icon) return; | ||
|
|
||
| function paint() { | ||
| var theme = root.getAttribute('data-theme'); | ||
| icon.textContent = theme === 'light' ? 'N' : 'D'; | ||
| } | ||
|
|
||
| btn.addEventListener('click', function () { | ||
| var current = root.getAttribute('data-theme'); | ||
| var next = current === 'light' ? 'dark' : 'light'; | ||
| root.setAttribute('data-theme', next); | ||
| localStorage.setItem('theme', next); | ||
| paint(); | ||
| }); | ||
| paint(); | ||
| } | ||
|
|
||
| function extractLessonPath(url) { | ||
| var m = url ? url.match(/(phases\/[^/]+\/[^/]+)\/?$/) : null; | ||
| return m ? m[1] : ''; | ||
| } | ||
|
|
||
| function countPhaseDone(phase) { | ||
| var total = phase.lessons.length; | ||
| var done = 0; | ||
| var hasProgress = !!window.AIFSProgress; | ||
| for (var i = 0; i < phase.lessons.length; i++) { | ||
| var l = phase.lessons[i]; | ||
| var userDone = false; | ||
| if (hasProgress && l.url) { | ||
| var lp = extractLessonPath(l.url); | ||
| if (lp) userDone = window.AIFSProgress.isLessonComplete(lp); | ||
| } | ||
| if (userDone) done++; | ||
| } | ||
| return { done: done, total: total }; | ||
| } | ||
|
|
||
| function findContinueTarget(phases) { | ||
| var hasProgress = !!window.AIFSProgress; | ||
| for (var i = 0; i < phases.length; i++) { | ||
| var p = phases[i]; | ||
| for (var j = 0; j < p.lessons.length; j++) { | ||
| var l = p.lessons[j]; | ||
| var lp = extractLessonPath(l.url); | ||
| if (!lp) continue; | ||
| if (!hasProgress) return { phaseId: p.id, lessonPath: lp }; | ||
| if (!window.AIFSProgress.isLessonComplete(lp)) return { phaseId: p.id, lessonPath: lp }; | ||
| } | ||
| } | ||
| return { phaseId: 1, lessonPath: 'phases/01-math-foundations/01-linear-algebra-intuition' }; | ||
| } | ||
|
|
||
| function renderKpis(phases) { | ||
| var el = document.getElementById('dashKpis'); | ||
| if (!el) return; | ||
|
|
||
| var totalLessons = 0; | ||
| var doneLessons = 0; | ||
| for (var i = 0; i < phases.length; i++) { | ||
| totalLessons += phases[i].lessons.length; | ||
| var c = countPhaseDone(phases[i]); | ||
| doneLessons += c.done; | ||
| } | ||
|
|
||
| var pct = totalLessons ? Math.round((doneLessons / totalLessons) * 100) : 0; | ||
| el.innerHTML = | ||
| '<div class="kpi-card"><div class="kpi-label">Lessons<\/div><div class="kpi-value">' + doneLessons + ' / ' + totalLessons + '<\/div><div class="kpi-sub">' + pct + '% complete<\/div><\/div>' + | ||
| '<div class="kpi-card"><div class="kpi-label">Phases<\/div><div class="kpi-value">' + phases.length + '<\/div><div class="kpi-sub">Browse any time<\/div><\/div>' + | ||
| '<div class="kpi-card"><div class="kpi-label">Storage<\/div><div class="kpi-value">Local<\/div><div class="kpi-sub">No account needed<\/div><\/div>'; | ||
| } | ||
|
|
||
| function renderPhaseCards(phases) { | ||
| var grid = document.getElementById('phaseGrid'); | ||
| if (!grid) return; | ||
| var html = ''; | ||
| for (var i = 0; i < phases.length; i++) { | ||
| var p = phases[i]; | ||
| var c = countPhaseDone(p); | ||
| var pct = c.total ? Math.round((c.done / c.total) * 100) : 0; | ||
| html += '<a class="phase-card" href="phase.html?phase=' + encodeURIComponent(p.id) + '">'; | ||
| html += '<div class="phase-card-top">'; | ||
| html += '<div class="phase-card-num">PHASE ' + String(p.id).padStart(2, '0') + '<\/div>'; | ||
| html += '<div class="phase-card-name">' + escapeHtml(p.name) + '<\/div>'; | ||
| html += '<div class="phase-card-meta">' + c.done + ' / ' + c.total + ' lessons<\/div>'; | ||
| html += '<\/div>'; | ||
| html += '<div class="phase-card-bar"><span style="width:' + pct + '%"><\/span><\/div>'; | ||
| html += '<div class="phase-card-desc">' + escapeHtml(p.desc || '') + '<\/div>'; | ||
| html += '<\/a>'; | ||
| } | ||
| grid.innerHTML = html; | ||
| } | ||
|
|
||
| function render() { | ||
| if (typeof PHASES === 'undefined' || !Array.isArray(PHASES)) return; | ||
|
|
||
| renderKpis(PHASES); | ||
| renderPhaseCards(PHASES); | ||
|
|
||
| var target = findContinueTarget(PHASES); | ||
| var btn = document.getElementById('continueBtn'); | ||
| if (btn && target.lessonPath) btn.href = 'lesson.html?path=' + encodeURIComponent(target.lessonPath); | ||
|
|
||
| var resetBtn = document.getElementById('resetBtn'); | ||
| if (resetBtn) { | ||
| resetBtn.addEventListener('click', function () { | ||
| if (!window.AIFSProgress) return; | ||
| var ok = window.confirm('Clear all your local progress (quiz answers and completed lessons)? This cannot be undone.'); | ||
| if (!ok) return; | ||
| window.AIFSProgress.reset(); | ||
| renderKpis(PHASES); | ||
| renderPhaseCards(PHASES); | ||
| }); | ||
|
Comment on lines
+123
to
+132
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reset flow leaves the Continue button target stale. After Suggested fix- window.AIFSProgress.reset();
- renderKpis(PHASES);
- renderPhaseCards(PHASES);
+ window.AIFSProgress.reset();
+ render();🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| })(); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -42,6 +42,18 @@ | |||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function load() { | ||||||||||||||||||||||||||||||
| // Local-first mode: avoid any network calls. | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| var host = String(window.location && window.location.hostname || ''); | ||||||||||||||||||||||||||||||
| var proto = String(window.location && window.location.protocol || ''); | ||||||||||||||||||||||||||||||
| if (proto === 'file:' || host === '127.0.0.1' || host === 'localhost') { | ||||||||||||||||||||||||||||||
| paint(0); | ||||||||||||||||||||||||||||||
|
Comment on lines
+47
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Local-host detection should include Current guard misses common local dev hosts, so “local-first” can still make network calls in those setups. Suggested fix- if (proto === 'file:' || host === '127.0.0.1' || host === 'localhost') {
+ if (
+ proto === 'file:' ||
+ host === '127.0.0.1' ||
+ host === 'localhost' ||
+ host === '0.0.0.0' ||
+ host === '::1'
+ ) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| var els = document.querySelectorAll('.header-github .star-count, #starCount'); | ||||||||||||||||||||||||||||||
| for (var i = 0; i < els.length; i++) els[i].textContent = '—'; | ||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } catch (e) {} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| var cached = readCache(); | ||||||||||||||||||||||||||||||
| if (cached != null) { | ||||||||||||||||||||||||||||||
| paint(cached); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quick-start command conflicts with stated working directory.
Line 13 says “From the repo root,” but Line 16 then
cds into the repo again, which fails for users already at root.Suggested doc fix
## Quick start From the repo root: ```bash -cd ai-engineering-from-scratch node site/local-server.mjs🤖 Prompt for AI Agents