3442 lines
123 KiB
HTML
3442 lines
123 KiB
HTML
<!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>Lesson - 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="A lesson from the AI Engineering from Scratch curriculum. 503 lessons across 20 phases, four languages, every algorithm built from raw math.">
|
|
<link rel="canonical" href="https://aiengineeringfromscratch.com/lesson.html">
|
|
<meta property="og:title" content="AI Engineering from Scratch · Lesson">
|
|
<meta property="og:description" content="503 lessons. 20 phases. Write the backprop, the tokenizer, the attention mechanism, and the agent loop by hand.">
|
|
<meta property="og:image" content="https://aiengineeringfromscratch.com/og-image.png?v=3">
|
|
<!-- og:url omitted intentionally: lesson.html is a static template that loads the lesson body client-side via ?path=. Static hosting cannot template the URL, so we let crawlers fall back to the request URL (which is the lesson-specific share link). -->
|
|
<meta property="og:type" content="article">
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:title" content="AI Engineering from Scratch">
|
|
<meta name="twitter:description" content="503 lessons. 20 phases. Build it from raw math, by hand.">
|
|
<meta name="twitter:image" content="https://aiengineeringfromscratch.com/og-image.png?v=3">
|
|
<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=20260525a">
|
|
<style>
|
|
.scroll-progress {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
height: 2px;
|
|
background: var(--blueprint);
|
|
z-index: 9999;
|
|
transition: width 0.05s linear;
|
|
width: 0%;
|
|
}
|
|
|
|
.lesson-layout {
|
|
display: flex;
|
|
padding-top: 64px;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.lesson-sidebar {
|
|
position: fixed;
|
|
top: 64px;
|
|
left: 0;
|
|
bottom: 0;
|
|
width: 240px;
|
|
background: var(--bg);
|
|
border-right: 1px solid var(--rule-soft);
|
|
overflow-y: auto;
|
|
z-index: 50;
|
|
padding: 24px 0;
|
|
transition: transform 0.25s ease;
|
|
}
|
|
|
|
.lesson-sidebar-toggle {
|
|
display: none;
|
|
position: fixed;
|
|
top: 74px;
|
|
left: 10px;
|
|
z-index: 60;
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--rule-soft);
|
|
width: 36px;
|
|
height: 36px;
|
|
cursor: pointer;
|
|
font-family: var(--font-mono);
|
|
font-size: 1rem;
|
|
color: var(--ink);
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.lesson-sidebar-toggle:hover {
|
|
border-color: var(--blueprint);
|
|
color: var(--blueprint);
|
|
}
|
|
|
|
.lesson-sidebar::-webkit-scrollbar { width: 4px; }
|
|
.lesson-sidebar::-webkit-scrollbar-thumb { background: var(--rule-soft); }
|
|
|
|
.sidebar-phase-header {
|
|
padding: 18px 20px 8px;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.7rem;
|
|
font-weight: 500;
|
|
color: var(--ink-mute);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.14em;
|
|
}
|
|
|
|
.sidebar-lesson-link {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 7px 20px;
|
|
font-family: var(--font-body);
|
|
font-size: 0.92rem;
|
|
color: var(--ink-soft);
|
|
text-decoration: none;
|
|
border-left: 2px solid transparent;
|
|
transition: color 0.15s, background 0.15s, border-color 0.15s;
|
|
}
|
|
|
|
.sidebar-lesson-link:hover {
|
|
background: var(--blueprint-tint);
|
|
color: var(--ink);
|
|
}
|
|
|
|
.sidebar-lesson-link.active {
|
|
color: var(--blueprint);
|
|
background: var(--blueprint-tint);
|
|
border-left-color: var(--blueprint);
|
|
}
|
|
|
|
.sidebar-lesson-link.disabled {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.sidebar-lesson-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
flex-shrink: 0;
|
|
border: 1px solid var(--ink-mute);
|
|
background: transparent;
|
|
}
|
|
|
|
.sidebar-lesson-dot.complete {
|
|
background: var(--blueprint);
|
|
border-color: var(--blueprint);
|
|
}
|
|
|
|
.sidebar-lesson-dot.in-progress {
|
|
background: linear-gradient(135deg, var(--blueprint) 0%, var(--blueprint) 50%, transparent 50%, transparent 100%);
|
|
border-color: var(--blueprint);
|
|
}
|
|
|
|
.sidebar-lesson-dot.planned {
|
|
border-style: dashed;
|
|
}
|
|
|
|
.sidebar-phase-nav {
|
|
padding: 18px 20px;
|
|
border-top: 1px solid var(--rule-soft);
|
|
margin-top: 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.sidebar-phase-nav a {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.72rem;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--ink-soft);
|
|
text-decoration: none;
|
|
border-bottom: none;
|
|
}
|
|
|
|
.sidebar-phase-nav a:hover {
|
|
color: var(--blueprint);
|
|
}
|
|
|
|
.lesson-main {
|
|
flex: 1;
|
|
min-width: 0;
|
|
margin-left: 240px;
|
|
display: flex;
|
|
justify-content: center;
|
|
padding: 56px 32px 96px;
|
|
}
|
|
|
|
.lesson-content {
|
|
max-width: 720px;
|
|
width: 100%;
|
|
min-width: 0;
|
|
}
|
|
|
|
.lesson-article {
|
|
max-width: 100%;
|
|
overflow-wrap: break-word;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.lesson-loading {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 60vh;
|
|
gap: 16px;
|
|
}
|
|
|
|
.spinner {
|
|
width: 24px;
|
|
height: 24px;
|
|
border: 2px solid var(--rule-soft);
|
|
border-top-color: var(--blueprint);
|
|
border-radius: 50%;
|
|
animation: spin 0.9s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.lesson-loading-text {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.85rem;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
color: var(--ink-mute);
|
|
}
|
|
|
|
.lesson-error {
|
|
text-align: left;
|
|
padding: 64px 0;
|
|
}
|
|
|
|
.lesson-error h2 {
|
|
font-family: var(--font-display);
|
|
font-size: 1.8rem;
|
|
text-transform: uppercase;
|
|
color: var(--blueprint);
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.lesson-error p {
|
|
color: var(--ink-soft);
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.lesson-article h1 {
|
|
font-family: var(--font-display);
|
|
font-size: clamp(2rem, 5vw, 3rem);
|
|
font-weight: 400;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.02em;
|
|
color: var(--blueprint);
|
|
margin-bottom: 20px;
|
|
padding-bottom: 16px;
|
|
border-bottom: 2px solid var(--blueprint);
|
|
line-height: 1;
|
|
}
|
|
|
|
.lesson-article h2 {
|
|
font-family: var(--font-display);
|
|
font-size: clamp(1.5rem, 3vw, 1.9rem);
|
|
font-weight: 400;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.02em;
|
|
color: var(--ink);
|
|
margin-top: 56px;
|
|
margin-bottom: 18px;
|
|
padding-bottom: 0;
|
|
border-bottom: none;
|
|
line-height: 1.05;
|
|
}
|
|
|
|
.lesson-article h2.section-build,
|
|
.lesson-article h2.section-use,
|
|
.lesson-article h2.section-ship {
|
|
border-left: 3px solid var(--blueprint);
|
|
padding-left: 16px;
|
|
}
|
|
|
|
.lesson-article h3 {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.95rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
color: var(--ink);
|
|
margin-top: 36px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.lesson-article h1,
|
|
.lesson-article h2,
|
|
.lesson-article h3 {
|
|
scroll-margin-top: var(--header-offset, 92px);
|
|
}
|
|
|
|
.lesson-article p {
|
|
font-family: var(--font-body);
|
|
font-size: 1.06rem;
|
|
line-height: 1.7;
|
|
color: var(--ink);
|
|
margin-bottom: 18px;
|
|
text-align: justify;
|
|
hyphens: auto;
|
|
-webkit-hyphens: auto;
|
|
}
|
|
|
|
.lesson-article > p:first-of-type::first-letter,
|
|
.lesson-article .drop-cap::first-letter {
|
|
float: left;
|
|
font-family: var(--font-display);
|
|
font-size: 4rem;
|
|
line-height: 0.85;
|
|
padding: 0.06em 0.16em 0 0;
|
|
color: var(--blueprint);
|
|
}
|
|
|
|
.lesson-article .motto {
|
|
font-family: var(--font-body);
|
|
font-style: italic;
|
|
font-size: 1.15rem;
|
|
line-height: 1.55;
|
|
color: var(--ink);
|
|
border-left: 2px solid var(--blueprint);
|
|
background: var(--blueprint-tint);
|
|
padding: 18px 22px;
|
|
margin: 24px 0 32px;
|
|
text-align: left;
|
|
}
|
|
|
|
.lesson-article blockquote {
|
|
border-left: 2px solid var(--blueprint);
|
|
background: var(--blueprint-tint);
|
|
padding: 14px 22px;
|
|
margin: 20px 0;
|
|
color: var(--ink);
|
|
font-family: var(--font-body);
|
|
font-style: italic;
|
|
text-align: left;
|
|
}
|
|
|
|
.lesson-article strong {
|
|
color: var(--ink);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.lesson-article em {
|
|
font-style: italic;
|
|
}
|
|
|
|
.lesson-article a {
|
|
color: var(--blueprint);
|
|
text-decoration: none;
|
|
border-bottom: 1px solid var(--blueprint);
|
|
transition: background 0.15s;
|
|
}
|
|
|
|
.lesson-article a:hover {
|
|
background: var(--blueprint-tint);
|
|
}
|
|
|
|
.lesson-article hr {
|
|
border: none;
|
|
height: 6px;
|
|
margin: 40px 0;
|
|
background-image:
|
|
repeating-linear-gradient(to right, var(--blueprint) 0, var(--blueprint) 4px, transparent 4px, transparent 8px),
|
|
repeating-linear-gradient(to right, transparent 0, transparent 8px, var(--blueprint-tint-strong) 8px, var(--blueprint-tint-strong) 14px);
|
|
background-size: 100% 3px, 100% 3px;
|
|
background-position: 0 0, 0 3px;
|
|
background-repeat: no-repeat;
|
|
}
|
|
|
|
.lesson-article ul {
|
|
list-style: none;
|
|
padding-left: 22px;
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.lesson-article ul li {
|
|
font-family: var(--font-body);
|
|
font-size: 1.06rem;
|
|
line-height: 1.7;
|
|
color: var(--ink);
|
|
position: relative;
|
|
padding-left: 6px;
|
|
margin-bottom: 4px;
|
|
text-align: left;
|
|
}
|
|
|
|
.lesson-article ul li::before {
|
|
content: "";
|
|
position: absolute;
|
|
left: -16px;
|
|
top: 0.65em;
|
|
width: 6px;
|
|
height: 6px;
|
|
background: var(--blueprint);
|
|
}
|
|
|
|
.lesson-article ol {
|
|
list-style: none;
|
|
padding-left: 28px;
|
|
margin-bottom: 18px;
|
|
counter-reset: ol-counter;
|
|
}
|
|
|
|
.lesson-article ol li {
|
|
font-family: var(--font-body);
|
|
font-size: 1.06rem;
|
|
line-height: 1.7;
|
|
color: var(--ink);
|
|
position: relative;
|
|
padding-left: 6px;
|
|
margin-bottom: 4px;
|
|
counter-increment: ol-counter;
|
|
text-align: left;
|
|
}
|
|
|
|
.lesson-article ol li::before {
|
|
content: counter(ol-counter, decimal-leading-zero);
|
|
position: absolute;
|
|
left: -28px;
|
|
top: 0.1em;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.78rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.06em;
|
|
color: var(--blueprint);
|
|
}
|
|
|
|
.lesson-article code {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.88em;
|
|
background: var(--code-bg);
|
|
padding: 1px 6px;
|
|
border: 1px solid var(--rule-soft);
|
|
color: var(--blueprint);
|
|
}
|
|
|
|
.lesson-article pre {
|
|
position: relative;
|
|
margin: 20px 0;
|
|
overflow: hidden;
|
|
border: 1px solid var(--rule-soft);
|
|
background: var(--code-bg);
|
|
}
|
|
|
|
.lesson-article pre code {
|
|
display: block;
|
|
background: var(--code-bg);
|
|
color: var(--ink);
|
|
padding: 22px 24px;
|
|
font-size: 0.86rem;
|
|
line-height: 1.6;
|
|
overflow-x: auto;
|
|
border: none;
|
|
}
|
|
|
|
.code-lang {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.66rem;
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.16em;
|
|
padding: 4px 10px;
|
|
background: var(--blueprint);
|
|
color: var(--bg);
|
|
z-index: 2;
|
|
}
|
|
|
|
.code-copy {
|
|
position: absolute;
|
|
top: 6px;
|
|
right: 8px;
|
|
background: transparent;
|
|
border: 1px solid var(--rule-soft);
|
|
padding: 3px 10px;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.66rem;
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
color: var(--ink-soft);
|
|
cursor: pointer;
|
|
z-index: 2;
|
|
transition: color 0.15s, border-color 0.15s;
|
|
}
|
|
|
|
.code-copy:hover {
|
|
color: var(--blueprint);
|
|
border-color: var(--blueprint);
|
|
}
|
|
|
|
.syn-keyword { color: var(--blueprint); font-weight: 500; }
|
|
.syn-string { color: var(--ink); }
|
|
.syn-number { color: var(--blueprint); }
|
|
.syn-comment { color: var(--ink-mute); font-style: italic; }
|
|
.syn-function { color: var(--ink); font-weight: 500; }
|
|
.syn-operator { color: var(--ink-soft); }
|
|
|
|
[data-theme="dark"] .syn-keyword { color: var(--blueprint); }
|
|
[data-theme="dark"] .syn-string { color: var(--ink); }
|
|
[data-theme="dark"] .syn-number { color: var(--blueprint); }
|
|
[data-theme="dark"] .syn-comment { color: var(--ink-mute); }
|
|
[data-theme="dark"] .syn-function { color: var(--ink); }
|
|
[data-theme="dark"] .syn-operator { color: var(--ink-soft); }
|
|
|
|
.lesson-article .table-wrap {
|
|
overflow-x: auto;
|
|
margin: 24px 0;
|
|
border: 1px solid var(--rule-soft);
|
|
}
|
|
|
|
.lesson-article table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-family: var(--font-body);
|
|
font-size: 0.96rem;
|
|
}
|
|
|
|
.lesson-article thead th {
|
|
background: var(--bg-surface);
|
|
color: var(--ink);
|
|
font-family: var(--font-mono);
|
|
font-size: 0.74rem;
|
|
font-weight: 500;
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
padding: 12px 16px;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--ink);
|
|
}
|
|
|
|
.lesson-article tbody td {
|
|
padding: 11px 16px;
|
|
border-bottom: 1px solid var(--rule-soft);
|
|
color: var(--ink);
|
|
vertical-align: top;
|
|
}
|
|
|
|
.lesson-article tbody tr:last-child td { border-bottom: none; }
|
|
|
|
.lesson-article tbody tr:hover {
|
|
background: var(--blueprint-tint);
|
|
}
|
|
|
|
.lesson-article .key-terms-table .col-says {
|
|
font-family: var(--font-body);
|
|
font-style: italic;
|
|
color: var(--ink-soft);
|
|
}
|
|
|
|
.learning-objectives {
|
|
background: transparent;
|
|
border: 1px solid var(--blueprint);
|
|
padding: 22px 26px;
|
|
margin: 24px 0 32px;
|
|
}
|
|
|
|
.learning-objectives-title {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.74rem;
|
|
font-weight: 500;
|
|
letter-spacing: 0.16em;
|
|
text-transform: uppercase;
|
|
color: var(--blueprint);
|
|
margin: 0 0 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.learning-objectives ul { margin: 0; padding-left: 20px; }
|
|
.learning-objectives li {
|
|
margin-bottom: 6px;
|
|
color: var(--ink);
|
|
line-height: 1.55;
|
|
font-size: 0.98rem;
|
|
text-align: left;
|
|
}
|
|
|
|
.lesson-meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
margin: 0 0 28px;
|
|
padding: 14px 0;
|
|
border-top: 1px solid var(--rule-soft);
|
|
border-bottom: 1px solid var(--rule-soft);
|
|
}
|
|
|
|
.lesson-meta-tag {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.72rem;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
padding: 3px 10px;
|
|
border: 1px solid var(--rule-soft);
|
|
color: var(--ink-soft);
|
|
background: transparent;
|
|
}
|
|
|
|
.lesson-meta-tag.time-tag {
|
|
border-color: var(--blueprint);
|
|
color: var(--blueprint);
|
|
background: var(--blueprint-tint);
|
|
}
|
|
|
|
.quiz-section,
|
|
.quiz-container {
|
|
border: 1px solid var(--blueprint);
|
|
padding: 22px 26px;
|
|
margin: 32px 0;
|
|
background: transparent;
|
|
}
|
|
|
|
.quiz-title {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.74rem;
|
|
font-weight: 500;
|
|
letter-spacing: 0.16em;
|
|
text-transform: uppercase;
|
|
color: var(--blueprint);
|
|
margin: 0 0 18px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.quiz-question {
|
|
margin-bottom: 24px;
|
|
border: none;
|
|
padding: 0;
|
|
background: transparent;
|
|
}
|
|
|
|
.quiz-question:last-child { margin-bottom: 0; }
|
|
|
|
.quiz-question-num {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.7rem;
|
|
letter-spacing: 0.14em;
|
|
text-transform: uppercase;
|
|
color: var(--blueprint);
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.quiz-question-text {
|
|
font-family: var(--font-body);
|
|
font-size: 1rem;
|
|
font-weight: 500;
|
|
color: var(--ink);
|
|
margin-bottom: 12px;
|
|
line-height: 1.5;
|
|
text-align: left;
|
|
}
|
|
|
|
.quiz-options {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.quiz-option {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 10px 14px;
|
|
border: 1px solid var(--rule-soft);
|
|
font-family: var(--font-body);
|
|
font-size: 0.96rem;
|
|
color: var(--ink);
|
|
cursor: pointer;
|
|
transition: border-color 0.15s, background 0.15s;
|
|
background: transparent;
|
|
text-align: left;
|
|
width: 100%;
|
|
}
|
|
|
|
.quiz-option:hover:not(.selected):not(.disabled) {
|
|
border-color: var(--blueprint);
|
|
background: var(--blueprint-tint);
|
|
}
|
|
|
|
.quiz-option .opt-letter,
|
|
.quiz-option .quiz-marker {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.72rem;
|
|
font-weight: 600;
|
|
width: 22px;
|
|
height: 22px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
border: 1px solid var(--ink-soft);
|
|
color: var(--ink-soft);
|
|
}
|
|
|
|
.quiz-option.selected.correct,
|
|
.quiz-option.correct,
|
|
.quiz-option.disabled.is-answer {
|
|
border-color: var(--blueprint);
|
|
background: var(--blueprint-tint);
|
|
color: var(--blueprint);
|
|
}
|
|
|
|
.quiz-option.selected.correct .opt-letter,
|
|
.quiz-option.selected.correct .quiz-marker,
|
|
.quiz-option.correct .opt-letter,
|
|
.quiz-option.correct .quiz-marker {
|
|
border-color: var(--blueprint);
|
|
background: var(--blueprint);
|
|
color: var(--bg);
|
|
}
|
|
|
|
.quiz-option.selected.wrong,
|
|
.quiz-option.incorrect {
|
|
border-color: var(--ink);
|
|
color: var(--ink);
|
|
}
|
|
|
|
.quiz-option.selected.wrong .opt-letter,
|
|
.quiz-option.selected.wrong .quiz-marker,
|
|
.quiz-option.incorrect .opt-letter,
|
|
.quiz-option.incorrect .quiz-marker {
|
|
border-color: var(--ink);
|
|
background: var(--ink);
|
|
color: var(--bg);
|
|
}
|
|
|
|
.quiz-option.disabled { cursor: default; opacity: 0.7; }
|
|
|
|
.quiz-explanation {
|
|
font-family: var(--font-body);
|
|
font-size: 0.92rem;
|
|
color: var(--ink-soft);
|
|
line-height: 1.55;
|
|
padding: 12px 16px;
|
|
border-left: 2px solid var(--blueprint);
|
|
background: var(--blueprint-tint);
|
|
display: none;
|
|
}
|
|
|
|
.quiz-explanation.visible { display: block; }
|
|
|
|
.quiz-score {
|
|
text-align: center;
|
|
padding: 24px;
|
|
border: 1px solid var(--blueprint);
|
|
background: var(--blueprint-tint);
|
|
display: none;
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.quiz-score.visible { display: block; }
|
|
|
|
.quiz-score-number {
|
|
font-family: var(--font-display);
|
|
font-size: 2.6rem;
|
|
color: var(--blueprint);
|
|
line-height: 1;
|
|
}
|
|
|
|
.quiz-score-label {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.74rem;
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
color: var(--ink-soft);
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.quiz-deeper {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.78rem;
|
|
color: var(--ink-soft);
|
|
margin-top: 16px;
|
|
padding: 12px;
|
|
background: var(--code-bg);
|
|
border: 1px solid var(--rule-soft);
|
|
}
|
|
|
|
.quiz-deeper code { color: var(--blueprint); }
|
|
|
|
.lab-challenge {
|
|
background: transparent;
|
|
border: 1px dashed var(--blueprint);
|
|
padding: 20px 26px;
|
|
margin: 28px 0;
|
|
}
|
|
|
|
.lab-challenge h2 {
|
|
font-family: var(--font-display);
|
|
color: var(--blueprint) !important;
|
|
border: none !important;
|
|
padding: 0 !important;
|
|
margin: 0 0 12px !important;
|
|
font-size: 1.4rem;
|
|
text-transform: uppercase;
|
|
line-height: 1;
|
|
}
|
|
|
|
.lesson-article .mermaid-container {
|
|
margin: 24px 0;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.mermaid-block { width: 100%; max-width: 100%; }
|
|
|
|
.mermaid-toolbar {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.mermaid-btn {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.7rem;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
padding: 4px 10px;
|
|
border: 1px solid var(--rule-soft);
|
|
color: var(--ink-soft);
|
|
background: transparent;
|
|
cursor: pointer;
|
|
transition: color 0.15s, border-color 0.15s;
|
|
}
|
|
|
|
.mermaid-btn:hover {
|
|
border-color: var(--blueprint);
|
|
color: var(--blueprint);
|
|
}
|
|
|
|
.mermaid-render {
|
|
width: 100%;
|
|
overflow-x: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
border: 1px solid var(--rule-soft);
|
|
padding: 16px;
|
|
background: var(--bg-surface);
|
|
}
|
|
|
|
.mermaid-source { display: none; }
|
|
|
|
.mermaid-modal-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: var(--overlay-bg);
|
|
z-index: 99999;
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 24px;
|
|
}
|
|
|
|
.mermaid-modal-overlay.open { display: flex; }
|
|
|
|
.mermaid-modal {
|
|
width: min(1100px, 96vw);
|
|
height: min(760px, 88vh);
|
|
background: var(--modal-bg);
|
|
border: 1px solid var(--ink);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.mermaid-modal-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 12px 14px;
|
|
border-bottom: 1px solid var(--rule-soft);
|
|
background: var(--bg-surface);
|
|
}
|
|
|
|
.mermaid-modal-title {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.78rem;
|
|
font-weight: 500;
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
color: var(--ink);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.mermaid-modal-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.mermaid-modal-body {
|
|
flex: 1;
|
|
overflow: auto;
|
|
padding: 18px;
|
|
background: var(--bg);
|
|
}
|
|
|
|
.mermaid-modal-center {
|
|
min-height: 100%;
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.mermaid-modal-body svg { max-width: none; height: auto; }
|
|
|
|
.lesson-article .mermaid-container pre {
|
|
overflow: visible;
|
|
background: none;
|
|
border: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.lesson-article .mermaid-container svg {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
:where(
|
|
.lesson-article .mermaid-render foreignObject p,
|
|
.lesson-article .mermaid-render foreignObject span,
|
|
.mermaid-modal-body foreignObject p,
|
|
.mermaid-modal-body foreignObject span
|
|
) {
|
|
font-family: var(--font-mono);
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
letter-spacing: 0.02em;
|
|
text-align: center;
|
|
text-transform: none;
|
|
margin: 0;
|
|
hyphens: none;
|
|
-webkit-hyphens: none;
|
|
color: var(--ink);
|
|
}
|
|
|
|
.lesson-nav-bottom {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: stretch;
|
|
gap: 16px;
|
|
margin-top: 64px;
|
|
padding-top: 32px;
|
|
border-top: 1px solid var(--rule-soft);
|
|
}
|
|
|
|
.lesson-nav-btn {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
padding: 16px 20px;
|
|
background: transparent;
|
|
border: 1px solid var(--rule-soft);
|
|
text-decoration: none;
|
|
transition: border-color 0.15s, background 0.15s;
|
|
max-width: 48%;
|
|
}
|
|
|
|
.lesson-nav-btn:hover {
|
|
border-color: var(--blueprint);
|
|
background: var(--blueprint-tint);
|
|
}
|
|
|
|
.lesson-nav-btn .nav-label {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.68rem;
|
|
letter-spacing: 0.16em;
|
|
text-transform: uppercase;
|
|
color: var(--blueprint);
|
|
}
|
|
|
|
.lesson-nav-btn .nav-title {
|
|
font-family: var(--font-body);
|
|
font-size: 0.98rem;
|
|
color: var(--ink);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.lesson-nav-btn.next {
|
|
text-align: right;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.ai-panels {
|
|
margin-top: 64px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 40px;
|
|
}
|
|
|
|
.ai-panel {
|
|
border: 1px solid var(--ink);
|
|
padding: 28px 26px;
|
|
background: transparent;
|
|
}
|
|
|
|
.ai-panel-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-bottom: 20px;
|
|
padding-bottom: 14px;
|
|
border-bottom: 1px solid var(--rule-soft);
|
|
}
|
|
|
|
.ai-panel-icon {
|
|
width: 28px;
|
|
height: 28px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.85rem;
|
|
flex-shrink: 0;
|
|
border: 1px solid var(--blueprint);
|
|
color: var(--blueprint);
|
|
background: var(--blueprint-tint);
|
|
}
|
|
|
|
.ai-panel-icon.coral,
|
|
.ai-panel-icon.blue,
|
|
.ai-panel-icon.green {
|
|
background: var(--blueprint-tint);
|
|
color: var(--blueprint);
|
|
border-color: var(--blueprint);
|
|
}
|
|
|
|
.ai-panel-title {
|
|
font-family: var(--font-display);
|
|
font-size: 1.4rem;
|
|
font-weight: 400;
|
|
text-transform: uppercase;
|
|
color: var(--ink);
|
|
letter-spacing: 0.02em;
|
|
line-height: 1;
|
|
}
|
|
|
|
.ai-panel-subtitle {
|
|
font-family: var(--font-body);
|
|
font-size: 0.96rem;
|
|
color: var(--ink-soft);
|
|
margin-top: -8px;
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.output-cards,
|
|
.code-cards {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
gap: 14px;
|
|
}
|
|
|
|
.output-card {
|
|
border: 1px solid var(--rule-soft);
|
|
padding: 18px;
|
|
background: var(--bg);
|
|
transition: border-color 0.15s, background 0.15s;
|
|
}
|
|
|
|
.output-card:hover {
|
|
border-color: var(--blueprint);
|
|
background: var(--blueprint-tint);
|
|
}
|
|
|
|
.output-card-name {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.84rem;
|
|
color: var(--ink);
|
|
word-break: break-all;
|
|
margin-bottom: 10px;
|
|
text-transform: none;
|
|
}
|
|
|
|
.output-badge {
|
|
display: inline-block;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.62rem;
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.12em;
|
|
padding: 2px 8px;
|
|
margin-bottom: 10px;
|
|
border: 1px solid var(--blueprint);
|
|
color: var(--blueprint);
|
|
background: var(--blueprint-tint);
|
|
}
|
|
|
|
.output-badge.prompt,
|
|
.output-badge.skill,
|
|
.output-badge.other {
|
|
border-color: var(--blueprint);
|
|
color: var(--blueprint);
|
|
background: var(--blueprint-tint);
|
|
}
|
|
|
|
.output-desc {
|
|
font-family: var(--font-body);
|
|
font-size: 0.96rem;
|
|
color: var(--ink-soft);
|
|
line-height: 1.5;
|
|
margin-bottom: 12px;
|
|
text-transform: none;
|
|
letter-spacing: 0;
|
|
font-variant: normal;
|
|
text-align: left;
|
|
}
|
|
|
|
.output-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.output-btn,
|
|
.code-card-btn {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.68rem;
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
padding: 4px 10px;
|
|
text-decoration: none;
|
|
border: 1px solid var(--rule-soft);
|
|
color: var(--ink-soft);
|
|
background: transparent;
|
|
cursor: pointer;
|
|
transition: color 0.15s, border-color 0.15s;
|
|
}
|
|
|
|
.output-btn:hover,
|
|
.code-card-btn:hover {
|
|
border-color: var(--blueprint);
|
|
color: var(--blueprint);
|
|
}
|
|
|
|
.output-install-hint {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.74rem;
|
|
color: var(--ink-soft);
|
|
background: var(--code-bg);
|
|
padding: 8px 12px;
|
|
border: 1px solid var(--rule-soft);
|
|
margin-top: 10px;
|
|
word-break: break-all;
|
|
display: none;
|
|
}
|
|
|
|
.output-install-hint.visible { display: block; }
|
|
|
|
.code-card {
|
|
background: var(--code-bg);
|
|
border: 1px solid var(--rule-soft);
|
|
padding: 18px;
|
|
transition: border-color 0.15s;
|
|
}
|
|
|
|
.code-card:hover {
|
|
border-color: var(--blueprint);
|
|
}
|
|
|
|
.code-card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.code-lang-icon {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.72rem;
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
padding: 2px 8px;
|
|
border: 1px solid var(--rule-soft);
|
|
color: var(--ink-soft);
|
|
line-height: 1;
|
|
}
|
|
|
|
.code-card-name {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.84rem;
|
|
color: var(--ink);
|
|
word-break: break-all;
|
|
text-transform: none;
|
|
}
|
|
|
|
.code-card-size {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.7rem;
|
|
color: var(--ink-mute);
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.code-card-run {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.78rem;
|
|
color: var(--blueprint);
|
|
background: var(--blueprint-tint);
|
|
padding: 10px 12px;
|
|
margin-bottom: 12px;
|
|
border: 1px solid var(--blueprint);
|
|
text-transform: none;
|
|
line-height: 1.4;
|
|
white-space: nowrap;
|
|
overflow-x: auto;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--rule-soft) transparent;
|
|
}
|
|
|
|
.code-card-run::-webkit-scrollbar {
|
|
height: 6px;
|
|
}
|
|
|
|
.code-card-run::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.code-card-run::-webkit-scrollbar-thumb {
|
|
background: var(--rule-soft);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.code-card-run::-webkit-scrollbar-thumb:hover {
|
|
background: var(--ink-mute);
|
|
}
|
|
|
|
.code-card-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.learning-timeline {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0;
|
|
overflow-x: auto;
|
|
padding: 24px 0;
|
|
position: relative;
|
|
}
|
|
|
|
.timeline-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 8px;
|
|
min-width: 92px;
|
|
flex-shrink: 0;
|
|
position: relative;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.timeline-dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
background: transparent;
|
|
border: 1px solid var(--ink-mute);
|
|
z-index: 2;
|
|
transition: transform 0.15s, background 0.15s, border-color 0.15s;
|
|
}
|
|
|
|
.timeline-item.current .timeline-dot {
|
|
width: 18px;
|
|
height: 18px;
|
|
background: var(--blueprint);
|
|
border-color: var(--blueprint);
|
|
}
|
|
|
|
.timeline-item.prev .timeline-dot,
|
|
.timeline-item.done .timeline-dot {
|
|
background: var(--blueprint);
|
|
border-color: var(--blueprint);
|
|
}
|
|
|
|
.timeline-line {
|
|
width: 36px;
|
|
height: 1px;
|
|
background: var(--rule-soft);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.timeline-line.done,
|
|
.timeline-line.active {
|
|
background: var(--blueprint);
|
|
}
|
|
|
|
.timeline-label {
|
|
font-family: var(--font-body);
|
|
font-size: 0.84rem;
|
|
letter-spacing: 0;
|
|
text-transform: none;
|
|
color: var(--ink-mute);
|
|
text-align: center;
|
|
max-width: 120px;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.timeline-item.current .timeline-label {
|
|
color: var(--blueprint);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.timeline-ellipsis {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.78rem;
|
|
color: var(--ink-mute);
|
|
letter-spacing: 0.04em;
|
|
padding: 0 8px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
|
|
.phase-progress-bar {
|
|
width: 100%;
|
|
height: 4px;
|
|
background: var(--rule-soft);
|
|
overflow: hidden;
|
|
margin: 14px 0 8px;
|
|
}
|
|
|
|
.phase-progress-fill {
|
|
height: 100%;
|
|
background: var(--blueprint);
|
|
transition: width 0.5s ease;
|
|
}
|
|
|
|
.phase-progress-text {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.78rem;
|
|
letter-spacing: 0.06em;
|
|
color: var(--ink-soft);
|
|
text-align: left;
|
|
}
|
|
|
|
.phase-complete-callout {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.85rem;
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
color: var(--blueprint);
|
|
text-align: center;
|
|
margin-top: 14px;
|
|
padding: 14px;
|
|
border: 1px solid var(--blueprint);
|
|
background: var(--blueprint-tint);
|
|
display: none;
|
|
}
|
|
|
|
.phase-complete-callout.visible { display: block; }
|
|
|
|
.continue-panel {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 16px;
|
|
}
|
|
|
|
.phase-finished {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.78rem;
|
|
letter-spacing: 0.16em;
|
|
text-transform: uppercase;
|
|
color: var(--bg);
|
|
background: var(--blueprint);
|
|
padding: 8px 16px;
|
|
}
|
|
|
|
.continue-links {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.continue-link {
|
|
font-family: var(--font-body);
|
|
font-size: 0.96rem;
|
|
color: var(--ink);
|
|
text-decoration: none;
|
|
border-bottom: 1px solid transparent;
|
|
}
|
|
|
|
.continue-link:hover {
|
|
color: var(--blueprint);
|
|
border-bottom-color: var(--blueprint);
|
|
}
|
|
|
|
.continue-callout {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.78rem;
|
|
letter-spacing: 0.06em;
|
|
color: var(--ink-soft);
|
|
background: var(--code-bg);
|
|
padding: 12px 18px;
|
|
border: 1px solid var(--rule-soft);
|
|
max-width: 480px;
|
|
}
|
|
|
|
.continue-callout code { color: var(--blueprint); }
|
|
|
|
.panel-loading {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.78rem;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
color: var(--ink-mute);
|
|
text-align: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
.panel-fallback {
|
|
text-align: left;
|
|
padding: 16px 0;
|
|
}
|
|
|
|
.panel-fallback a {
|
|
color: var(--blueprint);
|
|
text-decoration: none;
|
|
border-bottom: 1px solid var(--blueprint);
|
|
font-family: var(--font-body);
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.toc-sidebar {
|
|
position: fixed;
|
|
top: 64px;
|
|
right: 0;
|
|
bottom: 0;
|
|
width: 240px;
|
|
background: transparent;
|
|
overflow-y: auto;
|
|
z-index: 40;
|
|
padding: 56px 24px 80px;
|
|
display: none;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--rule-soft) transparent;
|
|
}
|
|
|
|
.toc-sidebar[aria-hidden="true"] { display: none; }
|
|
|
|
.toc-sidebar::-webkit-scrollbar { width: 3px; }
|
|
.toc-sidebar::-webkit-scrollbar-thumb { background: var(--rule-soft); }
|
|
|
|
.toc-header {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.7rem;
|
|
font-weight: 500;
|
|
color: var(--blueprint);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.16em;
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.toc-nav {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0;
|
|
position: relative;
|
|
border-left: 1px solid var(--rule-soft);
|
|
}
|
|
|
|
.toc-item { line-height: 1.4; }
|
|
|
|
.toc-link {
|
|
font-family: var(--font-body);
|
|
font-size: 0.84rem;
|
|
color: var(--ink-soft);
|
|
text-decoration: none;
|
|
display: block;
|
|
border-left: 2px solid transparent;
|
|
padding: 5px 16px;
|
|
margin-left: -1px;
|
|
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.toc-h3 .toc-link {
|
|
padding-left: 28px;
|
|
font-size: 0.78rem;
|
|
}
|
|
|
|
.toc-link:hover {
|
|
color: var(--ink);
|
|
background: var(--blueprint-tint);
|
|
}
|
|
|
|
.toc-link:focus-visible {
|
|
outline: 2px solid var(--blueprint);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.toc-link.active {
|
|
color: var(--blueprint);
|
|
border-left-color: var(--blueprint);
|
|
font-weight: 500;
|
|
}
|
|
|
|
@media (min-width: 1280px) {
|
|
.lesson-main { padding-right: 240px; }
|
|
.toc-sidebar { display: block; }
|
|
}
|
|
|
|
@media (max-width: 1279px) {
|
|
.lesson-main { padding-right: 32px; }
|
|
}
|
|
|
|
@media (min-width: 1440px) {
|
|
.lesson-article { max-width: 760px; }
|
|
.lesson-sidebar { width: 280px; min-width: 280px; }
|
|
.lesson-main { margin-left: 280px; padding-right: 280px; }
|
|
.toc-sidebar { width: 280px; }
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.lesson-sidebar { transform: translateX(-100%); }
|
|
.lesson-sidebar.open { transform: translateX(0); }
|
|
.lesson-sidebar-toggle {
|
|
display: flex;
|
|
top: 70px;
|
|
right: 12px;
|
|
left: auto;
|
|
width: 32px;
|
|
height: 32px;
|
|
font-size: 0.95rem;
|
|
}
|
|
.lesson-main { margin-left: 0; padding: 56px 20px 80px; }
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.lesson-main { padding: 56px 16px 60px; }
|
|
.lesson-article { max-width: 100%; }
|
|
.lesson-article p,
|
|
.lesson-article ul li,
|
|
.lesson-article ol li { font-size: 1rem; line-height: 1.65; }
|
|
.lesson-article > p:first-of-type::first-letter { font-size: 3rem; }
|
|
.lesson-article pre code { font-size: 0.78rem; padding: 14px 14px; }
|
|
.lesson-article table { font-size: 0.84rem; display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
|
.lesson-article table th,
|
|
.lesson-article table td { padding: 8px 10px; white-space: nowrap; }
|
|
.lesson-article .motto { font-size: 1.05rem; padding: 14px 16px; }
|
|
.ai-panels { margin-top: 40px; gap: 28px; }
|
|
.ai-panel { padding: 22px 16px; }
|
|
.output-cards,
|
|
.code-cards { grid-template-columns: 1fr; }
|
|
.lesson-nav-bottom { flex-direction: column; gap: 12px; }
|
|
.lesson-nav-btn { max-width: 100%; }
|
|
.lesson-nav-btn.next { text-align: left; }
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.lesson-article p,
|
|
.lesson-article ul li,
|
|
.lesson-article ol li { font-size: 0.96rem; }
|
|
.lesson-article > p:first-of-type::first-letter { font-size: 2.6rem; }
|
|
.ai-panel { padding: 18px 12px; }
|
|
.timeline-item { min-width: 70px; }
|
|
.timeline-line { width: 20px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<a href="#main" class="skip-link">Skip to content</a>
|
|
|
|
<div class="scroll-progress" id="scrollProgress"></div>
|
|
|
|
<header class="site-header">
|
|
<div class="header-inner">
|
|
<a href="index.html" class="logo">
|
|
<span class="logo-icon" aria-hidden="true"></span> AI / FROM SCRATCH
|
|
</a>
|
|
<nav class="header-nav">
|
|
<a href="index.html#contents">Contents</a>
|
|
<a href="catalog.html">Catalog</a>
|
|
<a href="prereqs.html">Roadmap</a>
|
|
<a href="glossary.html">Glossary</a>
|
|
<a href="https://github.com/rohitg00/ai-engineering-from-scratch" target="_blank" rel="noopener" class="header-github">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.4 3-.405 1.02.005 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
|
<svg class="star-icon" width="12" height="12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .587l3.668 7.568 8.332 1.151-6.064 5.828 1.48 8.279L12 19.896l-7.416 3.517 1.48-8.279L0 9.306l8.332-1.151z"/></svg>
|
|
<span class="star-count" data-loading="true" aria-label="GitHub stars">…</span>
|
|
</a>
|
|
</nav>
|
|
<button class="search-toggle" type="button" data-cmd-palette
|
|
aria-label="Search (⌘K)" title="Search (⌘K)">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none"
|
|
stroke="currentColor" stroke-width="2.5"
|
|
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<circle cx="11" cy="11" r="8"/>
|
|
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
</svg>
|
|
</button>
|
|
<button class="theme-toggle" id="themeToggle" aria-label="Toggle theme" type="button">
|
|
<span class="theme-icon" id="themeIcon">N</span>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<button class="lesson-sidebar-toggle" id="sidebarToggle" aria-label="Toggle sidebar">☰</button>
|
|
|
|
<div class="mermaid-modal-overlay" id="mermaidModalOverlay" aria-hidden="true">
|
|
<div class="mermaid-modal" role="dialog" aria-modal="true" aria-label="Expanded diagram">
|
|
<div class="mermaid-modal-header">
|
|
<div class="mermaid-modal-title" id="mermaidModalTitle">Diagram</div>
|
|
<div class="mermaid-modal-actions">
|
|
<button class="mermaid-btn" type="button" id="mermaidModalClose">Close</button>
|
|
</div>
|
|
</div>
|
|
<div class="mermaid-modal-body" id="mermaidModalBody"><div class="mermaid-modal-center" id="mermaidModalCenter"></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="lesson-layout">
|
|
<aside class="lesson-sidebar" id="lessonSidebar"></aside>
|
|
<main class="lesson-main" id="main">
|
|
<div class="lesson-content" id="lessonContent">
|
|
<div class="lesson-loading" id="lessonLoading">
|
|
<div class="spinner"></div>
|
|
<div class="lesson-loading-text">Loading lesson...</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
<aside class="toc-sidebar" id="tocSidebar" aria-hidden="true"></aside>
|
|
</div>
|
|
|
|
<script src="build-meta.js"></script>
|
|
<script src="data.js?v=20260525a"></script>
|
|
<script src="progress.js?v=20260525a"></script>
|
|
<script src="header.js?v=20260525a" defer></script>
|
|
<script src="cmdpalette.js?v=20260525a" defer></script>
|
|
<script src="figures.js?v=20260610e"></script>
|
|
<script src="lesson-figures.js?v=20260610e"></script>
|
|
<script src="figures-math.js?v=20260610e"></script>
|
|
<script src="figures-ml.js?v=20260610e"></script>
|
|
<script src="figures-dl.js?v=20260610e"></script>
|
|
<script src="figures-vision-speech.js?v=20260610e"></script>
|
|
<script src="figures-transformers.js?v=20260610e"></script>
|
|
<script src="figures-genai-rl.js?v=20260610e"></script>
|
|
<script src="figures-llms-systems.js?v=20260610e"></script>
|
|
<script src="figures-agents-alignment.js?v=20260610e"></script>
|
|
<script src="figures-math2.js?v=20260610e"></script>
|
|
<script src="figures-nlp2.js?v=20260610e"></script>
|
|
<script src="figures-llms2.js?v=20260610e"></script>
|
|
<script src="figures-infra.js?v=20260610e"></script>
|
|
<script src="figures-frontier.js?v=20260610e"></script>
|
|
<script type="module">
|
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
mermaid.initialize({
|
|
startOnLoad: false,
|
|
theme: document.documentElement.getAttribute('data-theme') === 'light' ? 'default' : 'dark',
|
|
fontFamily: 'JetBrains Mono, ui-monospace, monospace',
|
|
themeVariables: {
|
|
fontSize: '13px'
|
|
},
|
|
flowchart: {
|
|
useMaxWidth: true,
|
|
htmlLabels: true,
|
|
nodeSpacing: 40,
|
|
rankSpacing: 50,
|
|
padding: 12
|
|
}
|
|
});
|
|
window._mermaidReady = mermaid;
|
|
</script>
|
|
<script>
|
|
(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');
|
|
}
|
|
updateThemeIcon();
|
|
|
|
function updateThemeIcon() {
|
|
var icon = document.getElementById('themeIcon');
|
|
if (!icon) return;
|
|
var theme = root.getAttribute('data-theme');
|
|
icon.textContent = theme === 'light' ? 'N' : 'D';
|
|
}
|
|
|
|
var btn = document.getElementById('themeToggle');
|
|
if (btn) {
|
|
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);
|
|
updateThemeIcon();
|
|
updateMermaidThemeAndRerender();
|
|
});
|
|
}
|
|
|
|
var params = new URLSearchParams(window.location.search);
|
|
var lessonPath = params.get('path');
|
|
|
|
if (!lessonPath) {
|
|
showError('No lesson path specified', 'Add ?path=phases/01-math-foundations/01-linear-algebra-intuition to the URL.');
|
|
return;
|
|
}
|
|
|
|
if (window.AIFSProgress) {
|
|
window.AIFSProgress.recordVisit(lessonPath);
|
|
}
|
|
|
|
var phaseMatch = lessonPath.match(/phases\/(\d+-[^/]+)/);
|
|
var lessonMatch = lessonPath.match(/phases\/\d+-[^/]+\/(\d+-[^/]+)/);
|
|
var currentPhaseSlug = phaseMatch ? phaseMatch[1] : null;
|
|
var currentLessonSlug = lessonMatch ? lessonMatch[1] : null;
|
|
|
|
var currentPhaseIndex = -1;
|
|
var currentLessonIndex = -1;
|
|
var flatLessons = [];
|
|
|
|
if (typeof PHASES !== 'undefined') {
|
|
for (var i = 0; i < PHASES.length; i++) {
|
|
var p = PHASES[i];
|
|
var pSlug = extractPhaseSlug(p, i);
|
|
for (var j = 0; j < p.lessons.length; j++) {
|
|
var l = p.lessons[j];
|
|
var lSlug = extractLessonSlug(l, j);
|
|
flatLessons.push({
|
|
phaseIndex: i,
|
|
lessonIndex: j,
|
|
phaseName: p.name,
|
|
phaseId: p.id,
|
|
phaseSlug: pSlug,
|
|
lessonName: l.name,
|
|
lessonSlug: lSlug,
|
|
status: l.status,
|
|
type: l.type,
|
|
lang: l.lang,
|
|
url: l.url,
|
|
path: 'phases/' + pSlug + '/' + lSlug,
|
|
isReadable: l.status === 'complete' || !!l.url
|
|
});
|
|
if (pSlug === currentPhaseSlug && lSlug === currentLessonSlug) {
|
|
currentPhaseIndex = i;
|
|
currentLessonIndex = flatLessons.length - 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
buildSidebar();
|
|
initSidebarToggle();
|
|
initScrollProgress();
|
|
fetchLesson(lessonPath);
|
|
|
|
function extractPhaseSlug(phase, index) {
|
|
if (phase.url) {
|
|
var m = phase.url.match(/phases\/([^/]+)/);
|
|
if (m) return m[1];
|
|
}
|
|
if (phase.lessons && phase.lessons.length > 0 && phase.lessons[0].url) {
|
|
var m2 = phase.lessons[0].url.match(/phases\/([^/]+)/);
|
|
if (m2) return m2[1];
|
|
}
|
|
return String(phase.id).padStart(2, '0') + '-' + phase.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
}
|
|
|
|
function extractLessonSlug(lesson, index) {
|
|
if (lesson.url) {
|
|
var m = lesson.url.match(/phases\/[^/]+\/([^/]+)/);
|
|
if (m) return m[1].replace(/\/$/, '');
|
|
}
|
|
return String(index + 1).padStart(2, '0') + '-' + lesson.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
}
|
|
|
|
function buildSidebar() {
|
|
var sidebar = document.getElementById('lessonSidebar');
|
|
if (!sidebar || typeof PHASES === 'undefined') return;
|
|
|
|
var html = '';
|
|
|
|
var prevPhaseIdx = currentPhaseIndex > 0 ? currentPhaseIndex - 1 : -1;
|
|
var nextPhaseIdx = (currentPhaseIndex >= 0 && currentPhaseIndex < PHASES.length - 1) ? currentPhaseIndex + 1 : -1;
|
|
|
|
if (prevPhaseIdx >= 0 || nextPhaseIdx >= 0) {
|
|
html += '<div class="sidebar-phase-nav">';
|
|
if (prevPhaseIdx >= 0) {
|
|
var pp = PHASES[prevPhaseIdx];
|
|
var ppSlug = extractPhaseSlug(pp, prevPhaseIdx);
|
|
var ppFirstLesson = pp.lessons[0] ? extractLessonSlug(pp.lessons[0], 0) : '';
|
|
html += '<a href="lesson.html?path=phases/' + ppSlug + '/' + ppFirstLesson + '">← Phase ' + String(pp.id).padStart(2, '0') + ': ' + escapeHtml(pp.name) + '</a>';
|
|
}
|
|
if (nextPhaseIdx >= 0) {
|
|
var np = PHASES[nextPhaseIdx];
|
|
var npSlug = extractPhaseSlug(np, nextPhaseIdx);
|
|
var npFirstLesson = np.lessons[0] ? extractLessonSlug(np.lessons[0], 0) : '';
|
|
html += '<a href="lesson.html?path=phases/' + npSlug + '/' + npFirstLesson + '">Phase ' + String(np.id).padStart(2, '0') + ': ' + escapeHtml(np.name) + ' →</a>';
|
|
}
|
|
html += '</div>';
|
|
}
|
|
|
|
if (currentPhaseIndex >= 0) {
|
|
var phase = PHASES[currentPhaseIndex];
|
|
html += '<div class="sidebar-phase-header">Phase ' + String(phase.id).padStart(2, '0') + ' · ' + escapeHtml(phase.name) + '</div>';
|
|
for (var k = 0; k < phase.lessons.length; k++) {
|
|
var les = phase.lessons[k];
|
|
var lesSlug = extractLessonSlug(les, k);
|
|
var pSlugCurrent = extractPhaseSlug(phase, currentPhaseIndex);
|
|
var isActive = lesSlug === currentLessonSlug;
|
|
var lesReadable = les.status === 'complete' || !!les.url;
|
|
if (lesReadable) {
|
|
html += '<a class="sidebar-lesson-link' + (isActive ? ' active' : '') + '" href="lesson.html?path=phases/' + pSlugCurrent + '/' + lesSlug + '">';
|
|
} else {
|
|
html += '<span class="sidebar-lesson-link disabled">';
|
|
}
|
|
html += '<span class="sidebar-lesson-dot ' + les.status + '"></span>';
|
|
html += escapeHtml(les.name);
|
|
html += lesReadable ? '</a>' : '</span>';
|
|
}
|
|
}
|
|
|
|
sidebar.innerHTML = html;
|
|
}
|
|
|
|
function initSidebarToggle() {
|
|
var toggle = document.getElementById('sidebarToggle');
|
|
var sidebar = document.getElementById('lessonSidebar');
|
|
if (!toggle || !sidebar) return;
|
|
|
|
toggle.addEventListener('click', function () {
|
|
sidebar.classList.toggle('open');
|
|
});
|
|
|
|
document.addEventListener('click', function (e) {
|
|
if (window.innerWidth <= 900 && sidebar.classList.contains('open')) {
|
|
if (!sidebar.contains(e.target) && e.target !== toggle) {
|
|
sidebar.classList.remove('open');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function initScrollProgress() {
|
|
var bar = document.getElementById('scrollProgress');
|
|
if (!bar) return;
|
|
window.addEventListener('scroll', function () {
|
|
var scrollTop = window.scrollY || document.documentElement.scrollTop;
|
|
var docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
|
var pct = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
|
|
bar.style.width = pct + '%';
|
|
}, { passive: true });
|
|
}
|
|
|
|
function fetchLesson(path) {
|
|
// Fetch docs from the branch this site was built from (set by build.js
|
|
// via build-meta.js), so PR preview deploys render their own lesson
|
|
// edits instead of always pulling main. Falls back to main.
|
|
var ref = (typeof window !== 'undefined' && window.__AIFS_REF) ? window.__AIFS_REF : 'main';
|
|
var base = 'https://raw.githubusercontent.com/rohitg00/ai-engineering-from-scratch/' + ref + '/';
|
|
var rawUrl = base + path + '/docs/en.md';
|
|
var quizUrl = base + path + '/quiz.json';
|
|
|
|
fetch(rawUrl)
|
|
.then(function (res) {
|
|
if (!res.ok) throw new Error('fetch-failed');
|
|
return res.text();
|
|
})
|
|
.then(function (md) {
|
|
try {
|
|
renderLesson(md);
|
|
} catch (err) {
|
|
console.error('renderLesson failed', err);
|
|
showError('Render error', 'Loaded the lesson markdown but failed to render it. Details in the browser console.');
|
|
return;
|
|
}
|
|
fetch(quizUrl)
|
|
.then(function (res) { return res.ok ? res.json() : null; })
|
|
.catch(function () { return null; })
|
|
.then(function (data) {
|
|
if (!data) return;
|
|
try { renderQuiz(data); }
|
|
catch (err) { console.error('renderQuiz failed', err); }
|
|
});
|
|
})
|
|
.catch(function () {
|
|
showError('Lesson not found', 'Could not fetch the lesson at <code>' + escapeHtml(path) + '</code>. It may not have been written yet.');
|
|
});
|
|
}
|
|
|
|
function showError(title, msg) {
|
|
var el = document.getElementById('lessonContent');
|
|
el.innerHTML = '<div class="lesson-error"><h2>' + title + '</h2><p>' + msg + '</p><a href="index.html" class="btn btn-primary">Back to Home</a></div>';
|
|
}
|
|
|
|
function lessonDescription(md) {
|
|
var lines = md.split(/\r?\n/);
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var t = lines[i].trim();
|
|
if (t.indexOf('>') === 0) return t.replace(/^>\s*/, '').replace(/[*_`]/g, '').slice(0, 200);
|
|
}
|
|
for (var j = 0; j < lines.length; j++) {
|
|
var p = lines[j].trim();
|
|
if (p && p[0] !== '#' && p[0] !== '>' && p[0] !== '|' && p[0] !== '`' && p.indexOf('**') !== 0) {
|
|
return p.replace(/[*_`]/g, '').slice(0, 200);
|
|
}
|
|
}
|
|
return '503 lessons. 20 phases. Build it from raw math, by hand.';
|
|
}
|
|
|
|
// Client-side per-lesson SEO. Robust indexing comes with build-time
|
|
// prerender (next PR); this covers JS-executing crawlers + correct tabs.
|
|
function updateLessonSeo(title, md) {
|
|
var ORIGIN = 'https://aiengineeringfromscratch.com';
|
|
var path = new URLSearchParams(location.search).get('path') || '';
|
|
var url = ORIGIN + '/lesson.html?path=' + path;
|
|
var desc = lessonDescription(md);
|
|
var fullTitle = title + ' · AI Engineering from Scratch';
|
|
var set = function (sel, val) { var m = document.querySelector(sel); if (m) m.setAttribute('content', val); };
|
|
|
|
var canon = document.querySelector('link[rel="canonical"]');
|
|
if (canon) canon.setAttribute('href', url);
|
|
set('meta[name="description"]', desc);
|
|
set('meta[property="og:title"]', fullTitle);
|
|
set('meta[property="og:description"]', desc);
|
|
set('meta[name="twitter:title"]', fullTitle);
|
|
set('meta[name="twitter:description"]', desc);
|
|
var ogu = document.querySelector('meta[property="og:url"]');
|
|
if (!ogu) { ogu = document.createElement('meta'); ogu.setAttribute('property', 'og:url'); document.head.appendChild(ogu); }
|
|
ogu.setAttribute('content', url);
|
|
|
|
var prev = document.getElementById('lessonJsonLd');
|
|
if (prev) prev.remove();
|
|
var ld = {
|
|
'@context': 'https://schema.org',
|
|
'@graph': [
|
|
{ '@type': 'LearningResource', name: title, description: desc, url: url,
|
|
inLanguage: 'en', isAccessibleForFree: true,
|
|
isPartOf: { '@type': 'Course', name: 'AI Engineering from Scratch', url: ORIGIN } },
|
|
{ '@type': 'BreadcrumbList', itemListElement: [
|
|
{ '@type': 'ListItem', position: 1, name: 'Home', item: ORIGIN },
|
|
{ '@type': 'ListItem', position: 2, name: 'Catalog', item: ORIGIN + '/catalog.html' },
|
|
{ '@type': 'ListItem', position: 3, name: title, item: url }
|
|
] }
|
|
]
|
|
};
|
|
var s = document.createElement('script');
|
|
s.type = 'application/ld+json';
|
|
s.id = 'lessonJsonLd';
|
|
s.textContent = JSON.stringify(ld);
|
|
document.head.appendChild(s);
|
|
}
|
|
|
|
function renderLesson(md) {
|
|
var el = document.getElementById('lessonContent');
|
|
var html = parseMd(md);
|
|
html += '<div class="ai-panels" id="aiPanels"></div>';
|
|
html = addBottomNav(html);
|
|
el.innerHTML = '<article class="lesson-article">' + html + '</article>';
|
|
|
|
document.title = extractTitle(md) + ' - AI Engineering from Scratch';
|
|
updateLessonSeo(extractTitle(md), md);
|
|
|
|
initCodeCopy();
|
|
renderMermaidBlocks();
|
|
if (window.mountLessonFigures) window.mountLessonFigures(el);
|
|
renderAIPanels();
|
|
buildTOC();
|
|
|
|
document.querySelectorAll('a[href^="#"]').forEach(function (link) {
|
|
link.addEventListener('click', function (e) {
|
|
if (link.classList && link.classList.contains('toc-link')) return;
|
|
var target = document.querySelector(link.getAttribute('href'));
|
|
if (target) {
|
|
e.preventDefault();
|
|
target.scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function buildTOC() {
|
|
var sidebar = document.getElementById('tocSidebar');
|
|
if (!sidebar) return;
|
|
|
|
var article = document.querySelector('.lesson-article');
|
|
if (!article) {
|
|
sidebar.innerHTML = '';
|
|
sidebar.removeAttribute('aria-hidden');
|
|
return;
|
|
}
|
|
|
|
var headings = article.querySelectorAll('h2, h3');
|
|
if (!headings.length) {
|
|
sidebar.innerHTML = '';
|
|
sidebar.removeAttribute('aria-hidden');
|
|
return;
|
|
}
|
|
|
|
var used = Object.create(null);
|
|
|
|
function ensureHeadingId(heading, idx) {
|
|
if (!heading) return '';
|
|
|
|
var base = '';
|
|
if (heading.id) {
|
|
base = heading.id;
|
|
} else if (typeof slugify === 'function') {
|
|
base = slugify(heading.textContent || '');
|
|
} else {
|
|
base = String(heading.textContent || '').toLowerCase().trim().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-');
|
|
}
|
|
if (!base) base = 'section-' + String(idx + 1);
|
|
|
|
var candidate = base;
|
|
var n = 2;
|
|
|
|
while (used[candidate] || (document.getElementById(candidate) && document.getElementById(candidate) !== heading)) {
|
|
candidate = base + '-' + n;
|
|
n++;
|
|
}
|
|
|
|
used[candidate] = true;
|
|
heading.id = candidate;
|
|
return candidate;
|
|
}
|
|
|
|
var html = '<div class="toc-header">On this page</div>';
|
|
html += '<nav aria-label="On this page"><ul class="toc-nav">';
|
|
for (var i = 0; i < headings.length; i++) {
|
|
var heading = headings[i];
|
|
var level = heading.tagName.toLowerCase();
|
|
var id = ensureHeadingId(heading, i);
|
|
if (!id) continue;
|
|
var fullText = (heading.textContent || '').trim();
|
|
var text = fullText.replace(/^[^\p{L}\p{N}]+/u, '').trim();
|
|
if (!text) continue;
|
|
html += '<li class="toc-item toc-' + level + '"><a href="#' + id + '" class="toc-link" data-toc-id="' + id + '" title="' + escapeHtml(fullText) + '">' + escapeHtml(text) + '</a></li>';
|
|
}
|
|
html += '</ul></nav>';
|
|
sidebar.innerHTML = html;
|
|
sidebar.removeAttribute('aria-hidden');
|
|
|
|
sidebar.querySelectorAll('.toc-link').forEach(function (a) {
|
|
a.addEventListener('click', function (e) {
|
|
var href = a.getAttribute('href');
|
|
if (!href || href.charAt(0) !== '#') return;
|
|
var id = href.slice(1);
|
|
var target = document.getElementById(id);
|
|
if (!target) return;
|
|
e.preventDefault();
|
|
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
history.replaceState(null, '', '#' + id);
|
|
if (!target.hasAttribute('tabindex')) {
|
|
target.setAttribute('tabindex', '-1');
|
|
target.addEventListener('blur', function blurHandler() {
|
|
target.removeAttribute('tabindex');
|
|
target.removeEventListener('blur', blurHandler);
|
|
});
|
|
}
|
|
target.focus({ preventScroll: true });
|
|
});
|
|
});
|
|
|
|
initScrollspy(headings, sidebar);
|
|
}
|
|
|
|
function initScrollspy(headings, sidebar) {
|
|
sidebar = sidebar || document.getElementById('tocSidebar');
|
|
if (!sidebar) return;
|
|
|
|
var links = sidebar.querySelectorAll('.toc-link');
|
|
if (!links.length || !headings || !headings.length) return;
|
|
|
|
var headerOffsetStr = getComputedStyle(document.documentElement).getPropertyValue('--header-offset').trim();
|
|
var headerOffset = parseInt(headerOffsetStr, 10) || 92;
|
|
|
|
if (sidebar._tocObserver && sidebar._tocObserver.disconnect) {
|
|
sidebar._tocObserver.disconnect();
|
|
sidebar._tocObserver = null;
|
|
}
|
|
if (sidebar._tocScrollHandler) {
|
|
window.removeEventListener('scroll', sidebar._tocScrollHandler);
|
|
sidebar._tocScrollHandler = null;
|
|
}
|
|
if (sidebar._tocResizeHandler) {
|
|
window.removeEventListener('resize', sidebar._tocResizeHandler);
|
|
sidebar._tocResizeHandler = null;
|
|
}
|
|
|
|
function setActive(id) {
|
|
if (!id || sidebar._tocActiveId === id) return;
|
|
sidebar._tocActiveId = id;
|
|
|
|
links.forEach(function (link) {
|
|
var href = link.getAttribute('href');
|
|
var match = href && href.charAt(0) === '#' ? href.slice(1) : '';
|
|
if (match === id) link.classList.add('active');
|
|
else link.classList.remove('active');
|
|
});
|
|
|
|
var activeLink = sidebar.querySelector('.toc-link.active');
|
|
if (activeLink) {
|
|
var linkTop = activeLink.offsetTop;
|
|
var sidebarHeight = sidebar.clientHeight;
|
|
var sidebarScroll = sidebar.scrollTop;
|
|
if (linkTop < sidebarScroll + 50 || linkTop > sidebarScroll + sidebarHeight - 100) {
|
|
sidebar.scrollTo({ top: Math.max(0, linkTop - sidebarHeight / 3), behavior: 'smooth' });
|
|
}
|
|
}
|
|
}
|
|
|
|
function fallbackByScroll() {
|
|
var y = (window.scrollY || document.documentElement.scrollTop || 0) + headerOffset + 18;
|
|
var active = '';
|
|
for (var i = 0; i < headings.length; i++) {
|
|
var h = headings[i];
|
|
if (!h || !h.id) continue;
|
|
var top = h.getBoundingClientRect().top + window.scrollY;
|
|
if (top <= y) active = h.id;
|
|
else break;
|
|
}
|
|
if (!active && headings[0] && headings[0].id) active = headings[0].id;
|
|
if (active) setActive(active);
|
|
}
|
|
|
|
if (typeof IntersectionObserver !== 'undefined') {
|
|
var marginOffset = headerOffset - 4;
|
|
var obs = new IntersectionObserver(function (entries) {
|
|
var best = null;
|
|
for (var i = 0; i < entries.length; i++) {
|
|
var e = entries[i];
|
|
if (!e.isIntersecting) continue;
|
|
if (!best) best = e;
|
|
else if (e.boundingClientRect.top < best.boundingClientRect.top) best = e;
|
|
}
|
|
if (best && best.target && best.target.id) setActive(best.target.id);
|
|
}, {
|
|
root: null,
|
|
threshold: [0.01, 0.1],
|
|
rootMargin: '-' + marginOffset + 'px 0px -70% 0px'
|
|
});
|
|
|
|
Array.from(headings).forEach(function (h) {
|
|
if (h && h.id) obs.observe(h);
|
|
});
|
|
sidebar._tocObserver = obs;
|
|
|
|
var initial = (location.hash && location.hash.charAt(0) === '#') ? location.hash.slice(1) : '';
|
|
if (initial && document.getElementById(initial)) setActive(initial);
|
|
else if (headings[0] && headings[0].id) setActive(headings[0].id);
|
|
|
|
return;
|
|
}
|
|
|
|
sidebar._tocScrollHandler = function () {
|
|
if (window._tocTicking) return;
|
|
window._tocTicking = true;
|
|
window.requestAnimationFrame(function () {
|
|
fallbackByScroll();
|
|
window._tocTicking = false;
|
|
});
|
|
};
|
|
window.addEventListener('scroll', sidebar._tocScrollHandler, { passive: true });
|
|
|
|
sidebar._tocResizeHandler = function () {
|
|
fallbackByScroll();
|
|
};
|
|
window.addEventListener('resize', sidebar._tocResizeHandler);
|
|
|
|
fallbackByScroll();
|
|
}
|
|
|
|
function extractTitle(md) {
|
|
var m = md.match(/^# (.+)/m);
|
|
return m ? m[1] : 'Lesson';
|
|
}
|
|
|
|
function parseMd(md) {
|
|
var lines = md.split(/\r?\n/);
|
|
var out = '';
|
|
var inCodeBlock = false;
|
|
var codeLang = '';
|
|
var codeLines = [];
|
|
var inTable = false;
|
|
var tableRows = [];
|
|
var inList = false;
|
|
var listType = '';
|
|
var listItems = [];
|
|
var firstBlockquoteDone = false;
|
|
var firstParaAfterMottoDone = false;
|
|
var inBlockquote = false;
|
|
var blockquoteLines = [];
|
|
var mermaidBlocks = 0;
|
|
var inLabChallenge = false;
|
|
|
|
function flushLabChallenge() {
|
|
if (!inLabChallenge) return '';
|
|
inLabChallenge = false;
|
|
return '</div>';
|
|
}
|
|
|
|
function flushList() {
|
|
if (!inList) return '';
|
|
inList = false;
|
|
var tag = listType === 'ol' ? 'ol' : 'ul';
|
|
var h = '<' + tag + '>';
|
|
for (var li = 0; li < listItems.length; li++) {
|
|
h += '<li>' + inlineFormat(listItems[li]) + '</li>';
|
|
}
|
|
h += '</' + tag + '>';
|
|
listItems = [];
|
|
return h;
|
|
}
|
|
|
|
function flushTable() {
|
|
if (!inTable) return '';
|
|
inTable = false;
|
|
if (tableRows.length < 2) return '';
|
|
var headers = tableRows[0];
|
|
var isKeyTerms = false;
|
|
for (var th = 0; th < headers.length; th++) {
|
|
if (headers[th].toLowerCase().indexOf('what people say') >= 0 || headers[th].toLowerCase().indexOf('people say') >= 0) {
|
|
isKeyTerms = true;
|
|
}
|
|
}
|
|
var h = '<div class="table-wrap' + (isKeyTerms ? ' key-terms-table' : '') + '"><table><thead><tr>';
|
|
for (var ti = 0; ti < headers.length; ti++) {
|
|
h += '<th>' + inlineFormat(headers[ti].trim()) + '</th>';
|
|
}
|
|
h += '</tr></thead><tbody>';
|
|
for (var ri = 2; ri < tableRows.length; ri++) {
|
|
var cells = tableRows[ri];
|
|
h += '<tr>';
|
|
for (var ci = 0; ci < headers.length; ci++) {
|
|
var cls = '';
|
|
if (isKeyTerms) {
|
|
var headerLower = (headers[ci] || '').toLowerCase();
|
|
if (headerLower.indexOf('say') >= 0) cls = ' class="col-says"';
|
|
}
|
|
h += '<td' + cls + '>' + inlineFormat((cells[ci] || '').trim()) + '</td>';
|
|
}
|
|
h += '</tr>';
|
|
}
|
|
h += '</tbody></table></div>';
|
|
tableRows = [];
|
|
return h;
|
|
}
|
|
|
|
function flushBlockquote() {
|
|
if (!inBlockquote) return '';
|
|
inBlockquote = false;
|
|
var text = blockquoteLines.join(' ');
|
|
blockquoteLines = [];
|
|
if (!firstBlockquoteDone) {
|
|
firstBlockquoteDone = true;
|
|
return '<div class="motto">' + inlineFormat(text) + '</div>';
|
|
}
|
|
return '<blockquote><p>' + inlineFormat(text) + '</p></blockquote>';
|
|
}
|
|
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var line = lines[i];
|
|
|
|
if (inCodeBlock) {
|
|
if (line.match(/^```\s*$/)) {
|
|
inCodeBlock = false;
|
|
var raw = codeLines.join('\n');
|
|
if (codeLang === 'figure') {
|
|
out += '<div class="lesson-figure" data-figure="' + escapeHtml(raw.trim()) + '"></div>';
|
|
} else if (codeLang === 'mermaid') {
|
|
mermaidBlocks++;
|
|
out += '<div class="mermaid-container">';
|
|
out += '<div class="mermaid-block" data-mermaid-index="' + mermaidBlocks + '">';
|
|
out += '<div class="mermaid-toolbar">';
|
|
out += '<button type="button" class="mermaid-btn mermaid-expand" data-mermaid-index="' + mermaidBlocks + '">Expand</button>';
|
|
out += '</div>';
|
|
out += '<pre class="mermaid mermaid-source" id="mermaid-' + mermaidBlocks + '">' + escapeHtml(raw) + '</pre>';
|
|
out += '<div class="mermaid-render" id="mermaid-render-' + mermaidBlocks + '"></div>';
|
|
out += '</div>';
|
|
out += '</div>';
|
|
} else {
|
|
out += renderCodeBlock(raw, codeLang);
|
|
}
|
|
codeLines = [];
|
|
codeLang = '';
|
|
} else {
|
|
codeLines.push(line);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
var codeStart = line.match(/^```(\w*)/);
|
|
if (codeStart) {
|
|
out += flushList();
|
|
out += flushTable();
|
|
out += flushBlockquote();
|
|
inCodeBlock = true;
|
|
codeLang = codeStart[1] || '';
|
|
codeLines = [];
|
|
continue;
|
|
}
|
|
|
|
if (line.match(/^\s*\|.*\|\s*$/)) {
|
|
out += flushList();
|
|
out += flushBlockquote();
|
|
if (!inTable) inTable = true;
|
|
tableRows.push(splitTableRow(line));
|
|
continue;
|
|
} else if (inTable) {
|
|
out += flushTable();
|
|
}
|
|
|
|
if (line.match(/^>\s/)) {
|
|
out += flushList();
|
|
out += flushTable();
|
|
if (!inBlockquote) inBlockquote = true;
|
|
blockquoteLines.push(line.replace(/^>\s?/, ''));
|
|
continue;
|
|
} else if (inBlockquote) {
|
|
out += flushBlockquote();
|
|
}
|
|
|
|
var h1 = line.match(/^# (.+)/);
|
|
if (h1) {
|
|
out += flushList();
|
|
out += flushLabChallenge();
|
|
var slug = slugify(h1[1]);
|
|
out += '<h1 id="' + slug + '">' + inlineFormat(h1[1]) + '</h1>';
|
|
continue;
|
|
}
|
|
|
|
var h2 = line.match(/^## (.+)/);
|
|
if (h2) {
|
|
out += flushList();
|
|
var slug2 = slugify(h2[1]);
|
|
var sectionClass = '';
|
|
var txt = h2[1].toLowerCase();
|
|
if (txt.indexOf('build it') >= 0 || txt.indexOf('build ') === 0) sectionClass = ' section-build';
|
|
else if (txt.indexOf('use it') >= 0 || txt.indexOf('use ') === 0) sectionClass = ' section-use';
|
|
else if (txt.indexOf('ship it') >= 0 || txt.indexOf('ship ') === 0) sectionClass = ' section-ship';
|
|
if (txt.indexOf('learning objectives') >= 0) {
|
|
var objItems = [];
|
|
for (var lo = i + 1; lo < lines.length; lo++) {
|
|
var loLine = lines[lo];
|
|
if (loLine.match(/^[-*]\s+(.+)/)) {
|
|
objItems.push(loLine.replace(/^[-*]\s+/, ''));
|
|
} else if (loLine.trim() === '') {
|
|
continue;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
out += '<div class="learning-objectives"><div class="learning-objectives-title">🎯 Learning Objectives</div><ul>';
|
|
for (var oi = 0; oi < objItems.length; oi++) {
|
|
out += '<li>' + inlineFormat(objItems[oi]) + '</li>';
|
|
}
|
|
out += '</ul></div>';
|
|
i = lo - 1;
|
|
continue;
|
|
}
|
|
if (txt.indexOf('lab challenge') >= 0) {
|
|
out += flushLabChallenge();
|
|
inLabChallenge = true;
|
|
out += '<div class="lab-challenge">';
|
|
out += '<h2 id="' + slug2 + '">💫 Lab Challenge</h2>';
|
|
continue;
|
|
}
|
|
out += flushLabChallenge();
|
|
out += '<h2 id="' + slug2 + '" class="' + sectionClass + '">' + inlineFormat(h2[1]) + '</h2>';
|
|
continue;
|
|
}
|
|
|
|
var h3 = line.match(/^### (.+)/);
|
|
if (h3) {
|
|
out += flushList();
|
|
var slug3 = slugify(h3[1]);
|
|
out += '<h3 id="' + slug3 + '">' + inlineFormat(h3[1]) + '</h3>';
|
|
continue;
|
|
}
|
|
|
|
if (line.match(/^---+$/)) {
|
|
out += flushList();
|
|
out += '<hr>';
|
|
continue;
|
|
}
|
|
|
|
var ulMatch = line.match(/^[-*]\s+(.+)/);
|
|
if (ulMatch) {
|
|
out += flushTable();
|
|
out += flushBlockquote();
|
|
if (!inList || listType !== 'ul') {
|
|
out += flushList();
|
|
inList = true;
|
|
listType = 'ul';
|
|
}
|
|
listItems.push(ulMatch[1]);
|
|
continue;
|
|
}
|
|
|
|
var olMatch = line.match(/^\d+\.\s+(.+)/);
|
|
if (olMatch) {
|
|
out += flushTable();
|
|
out += flushBlockquote();
|
|
if (!inList || listType !== 'ol') {
|
|
out += flushList();
|
|
inList = true;
|
|
listType = 'ol';
|
|
}
|
|
listItems.push(olMatch[1]);
|
|
continue;
|
|
}
|
|
|
|
if (inList) {
|
|
out += flushList();
|
|
}
|
|
|
|
if (line.trim() === '') {
|
|
continue;
|
|
}
|
|
|
|
var paraClass = '';
|
|
if (firstBlockquoteDone && !firstParaAfterMottoDone) {
|
|
var isMetaLine = line.match(/^\*\*[^*]+\*\*\s*:/);
|
|
if (!isMetaLine) {
|
|
paraClass = ' class="drop-cap"';
|
|
firstParaAfterMottoDone = true;
|
|
}
|
|
}
|
|
|
|
var metaMatch = line.match(/^\*\*([^*]+)\*\*:\s*(.+)/);
|
|
if (metaMatch && !firstParaAfterMottoDone) {
|
|
out += '<div class="lesson-meta-tag"><strong>' + escapeHtml(metaMatch[1]) + ':</strong> ' + inlineFormat(metaMatch[2]) + '</div>';
|
|
continue;
|
|
}
|
|
|
|
out += '<p' + paraClass + '>' + inlineFormat(line) + '</p>';
|
|
}
|
|
|
|
out += flushList();
|
|
out += flushTable();
|
|
out += flushBlockquote();
|
|
out += flushLabChallenge();
|
|
|
|
return out;
|
|
}
|
|
|
|
function splitTableRow(line) {
|
|
// Split a markdown table row on cell delimiters while respecting
|
|
// escaped pipes (P(X\|Y)) and pipes inside inline-code spans (`a | b`).
|
|
var cells = [];
|
|
var cur = '';
|
|
var inCode = false;
|
|
for (var i = 0; i < line.length; i++) {
|
|
var ch = line.charAt(i);
|
|
if (ch === '\\' && line.charAt(i + 1) === '|') {
|
|
cur += '|';
|
|
i++;
|
|
continue;
|
|
}
|
|
if (ch === '`') {
|
|
inCode = !inCode;
|
|
cur += ch;
|
|
continue;
|
|
}
|
|
if (ch === '|' && !inCode) {
|
|
cells.push(cur);
|
|
cur = '';
|
|
continue;
|
|
}
|
|
cur += ch;
|
|
}
|
|
cells.push(cur);
|
|
if (cells.length && cells[0].trim() === '') cells.shift();
|
|
if (cells.length && cells[cells.length - 1].trim() === '') cells.pop();
|
|
return cells.map(function (c) { return c.trim(); });
|
|
}
|
|
|
|
function inlineFormat(text) {
|
|
text = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
text = text.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
|
|
text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
text = text.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function (m, label, href) {
|
|
if (/^https?:\/\/|^mailto:/i.test(href)) {
|
|
return '<a href="' + href + '" target="_blank" rel="noopener">' + label + '</a>';
|
|
}
|
|
return label;
|
|
});
|
|
return text;
|
|
}
|
|
|
|
function slugify(text) {
|
|
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
}
|
|
|
|
function renderCodeBlock(code, lang) {
|
|
var highlighted = highlightSyntax(escapeHtml(code), lang);
|
|
var langLabel = lang ? '<span class="code-lang">' + escapeHtml(lang) + '</span>' : '';
|
|
return '<pre>' + langLabel + '<button class="code-copy" data-code="' + escapeAttr(code) + '">Copy</button><code>' + highlighted + '</code></pre>';
|
|
}
|
|
|
|
function highlightSyntax(code, lang) {
|
|
var keywords, commentPattern;
|
|
|
|
if (lang === 'python' || lang === 'py') {
|
|
keywords = /\b(def|class|return|if|elif|else|for|while|in|import|from|as|with|try|except|finally|raise|yield|lambda|not|and|or|is|None|True|False|print|self|pass|break|continue|assert|global)\b/g;
|
|
commentPattern = /(#[^\n]*)/g;
|
|
} else if (lang === 'julia') {
|
|
keywords = /\b(function|end|if|elseif|else|for|while|in|using|import|return|struct|mutable|abstract|type|module|export|let|const|begin|do|try|catch|finally|throw|true|false|nothing|println)\b/g;
|
|
commentPattern = /(#[^\n]*)/g;
|
|
} else if (lang === 'javascript' || lang === 'js' || lang === 'typescript' || lang === 'ts') {
|
|
keywords = /\b(function|const|let|var|return|if|else|for|while|class|new|this|import|export|from|async|await|try|catch|throw|typeof|instanceof|true|false|null|undefined|console)\b/g;
|
|
commentPattern = /(\/\/[^\n]*|\/\*[\s\S]*?\*\/)/g;
|
|
} else if (lang === 'rust') {
|
|
keywords = /\b(fn|let|mut|struct|enum|impl|trait|pub|use|mod|self|super|crate|return|if|else|for|while|loop|match|async|await|true|false|None|Some|Ok|Err|println)\b/g;
|
|
commentPattern = /(\/\/[^\n]*)/g;
|
|
} else {
|
|
keywords = /\b(function|def|class|return|if|else|for|while|import|from|const|let|var|true|false|null|None|print|println)\b/g;
|
|
commentPattern = /(#[^\n]*|\/\/[^\n]*)/g;
|
|
}
|
|
|
|
var tokens = [];
|
|
function stash(match) {
|
|
var id = '\x00TOK' + tokens.length + '\x00';
|
|
tokens.push(match);
|
|
return id;
|
|
}
|
|
|
|
code = code.replace(commentPattern, function (m) { return stash('<span class="syn-comment">' + m + '</span>'); });
|
|
code = code.replace(/("(?:[^&]|&(?!quot;))*?"|'(?:[^&]|&(?!#39;))*?'|"""[\s\S]*?""")/g, function (m) { return stash('<span class="syn-string">' + m + '</span>'); });
|
|
code = code.replace(/"([^"]*?)"/g, function (m) { return stash('<span class="syn-string">' + m + '</span>'); });
|
|
|
|
code = code.replace(keywords, '<span class="syn-keyword">$1</span>');
|
|
code = code.replace(/\b(\d+\.?\d*)\b/g, '<span class="syn-number">$1</span>');
|
|
|
|
for (var ti = 0; ti < tokens.length; ti++) {
|
|
code = code.replace('\x00TOK' + ti + '\x00', tokens[ti]);
|
|
}
|
|
return code;
|
|
}
|
|
|
|
function initCodeCopy() {
|
|
document.querySelectorAll('.code-copy').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var code = btn.getAttribute('data-code');
|
|
navigator.clipboard.writeText(code).then(function () {
|
|
btn.textContent = 'Copied!';
|
|
setTimeout(function () { btn.textContent = 'Copy'; }, 1500);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function renderMermaidBlocks() {
|
|
var fontsReady = (document.fonts && document.fonts.load)
|
|
? Promise.all([
|
|
document.fonts.load('400 13px "JetBrains Mono"'),
|
|
document.fonts.load('500 13px "JetBrains Mono"'),
|
|
document.fonts.load('700 13px "JetBrains Mono"')
|
|
]).catch(function () {})
|
|
: Promise.resolve();
|
|
var check = setInterval(function () {
|
|
if (!window._mermaidReady) return;
|
|
clearInterval(check);
|
|
fontsReady.then(function () {
|
|
rerenderMermaidBlocks();
|
|
bindMermaidToolbar();
|
|
initMermaidModal();
|
|
});
|
|
}, 100);
|
|
setTimeout(function () { clearInterval(check); }, 10000);
|
|
}
|
|
|
|
function mermaidTheme() {
|
|
return document.documentElement.getAttribute('data-theme') === 'light' ? 'default' : 'dark';
|
|
}
|
|
|
|
// Lessons hard-code node fills (light pastels that vanish on the dark
|
|
// default theme, and dark navies that vanish in light mode). Rather than
|
|
// maintain a hex table, flip the lightness band while preserving hue, so
|
|
// color-coding survives and labels stay legible in either theme.
|
|
function hexToHsl(hex) {
|
|
var r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
var g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
var b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
var max = Math.max(r, g, b), min = Math.min(r, g, b), h = 0, s = 0, l = (max + min) / 2;
|
|
if (max !== min) {
|
|
var d = max - min;
|
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
|
|
else if (max === g) h = (b - r) / d + 2;
|
|
else h = (r - g) / d + 4;
|
|
h /= 6;
|
|
}
|
|
return [h, s, l];
|
|
}
|
|
|
|
function hslToHex(h, s, l) {
|
|
function hue(p, q, t) {
|
|
if (t < 0) t += 1;
|
|
if (t > 1) t -= 1;
|
|
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
if (t < 1 / 2) return q;
|
|
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
return p;
|
|
}
|
|
var r, g, b;
|
|
if (s === 0) {
|
|
r = g = b = l;
|
|
} else {
|
|
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
var p = 2 * l - q;
|
|
r = hue(p, q, h + 1 / 3);
|
|
g = hue(p, q, h);
|
|
b = hue(p, q, h - 1 / 3);
|
|
}
|
|
function c(x) {
|
|
x = Math.round(Math.max(0, Math.min(1, x)) * 255);
|
|
return ('0' + x.toString(16)).slice(-2);
|
|
}
|
|
return '#' + c(r) + c(g) + c(b);
|
|
}
|
|
|
|
function mermaidPreprocess(def) {
|
|
var dark = mermaidTheme() === 'dark';
|
|
return def.replace(/fill\s*:\s*(#[0-9a-fA-F]{6})/g, function (m, hex) {
|
|
var hsl = hexToHsl(hex.toLowerCase());
|
|
if (dark && hsl[2] > 0.65) return 'fill:' + hslToHex(hsl[0], hsl[1], 0.26);
|
|
if (!dark && hsl[2] < 0.30) return 'fill:' + hslToHex(hsl[0], Math.min(hsl[1], 0.5), 0.86);
|
|
return m;
|
|
});
|
|
}
|
|
|
|
function updateMermaidThemeAndRerender() {
|
|
if (!window._mermaidReady) return;
|
|
try {
|
|
window._mermaidReady.initialize({
|
|
startOnLoad: false,
|
|
theme: mermaidTheme(),
|
|
fontFamily: 'JetBrains Mono, ui-monospace, monospace',
|
|
themeVariables: { fontSize: '13px' },
|
|
flowchart: { useMaxWidth: true, htmlLabels: true, nodeSpacing: 40, rankSpacing: 50, padding: 12 }
|
|
});
|
|
} catch (_) {}
|
|
rerenderMermaidBlocks();
|
|
rerenderMermaidModal();
|
|
}
|
|
|
|
function rerenderMermaidBlocks() {
|
|
if (!window._mermaidReady) return;
|
|
var sources = document.querySelectorAll('pre.mermaid.mermaid-source');
|
|
sources.forEach(function (pre) {
|
|
var idxMatch = pre.id && pre.id.match(/mermaid-(\d+)/);
|
|
var idx = idxMatch ? idxMatch[1] : null;
|
|
if (!idx) return;
|
|
var target = document.getElementById('mermaid-render-' + idx);
|
|
if (!target) return;
|
|
var def = mermaidPreprocess(pre.textContent);
|
|
target.innerHTML = '<div class="panel-loading">Rendering diagram...</div>';
|
|
|
|
var token = ++mermaidRenderSeq;
|
|
target._mermaidRenderToken = token;
|
|
|
|
window._mermaidReady.render('mermaid-' + idx + '-svg', def).then(function (result) {
|
|
if (target._mermaidRenderToken !== token) return;
|
|
target.innerHTML = result.svg;
|
|
}).catch(function () {
|
|
if (target._mermaidRenderToken !== token) return;
|
|
target.innerHTML = '<p style="color:var(--text-muted);font-style:italic;">Diagram could not be rendered.</p>';
|
|
});
|
|
});
|
|
}
|
|
|
|
function bindMermaidToolbar() {
|
|
document.querySelectorAll('.mermaid-expand').forEach(function (btn) {
|
|
if (btn._bound) return;
|
|
btn._bound = true;
|
|
btn.addEventListener('click', function () {
|
|
var idx = btn.getAttribute('data-mermaid-index');
|
|
openMermaidModal(idx, btn);
|
|
});
|
|
});
|
|
}
|
|
|
|
function initMermaidModal() {
|
|
var overlay = document.getElementById('mermaidModalOverlay');
|
|
var closeBtn = document.getElementById('mermaidModalClose');
|
|
if (!overlay || !closeBtn) return;
|
|
if (overlay._bound) return;
|
|
overlay._bound = true;
|
|
|
|
closeBtn.addEventListener('click', closeMermaidModal);
|
|
overlay.addEventListener('click', function (e) {
|
|
if (e.target === overlay) closeMermaidModal();
|
|
});
|
|
document.addEventListener('keydown', function (e) {
|
|
if (!overlay.classList.contains('open')) return;
|
|
if (e.key === 'Escape') {
|
|
e.preventDefault();
|
|
closeMermaidModal();
|
|
return;
|
|
}
|
|
|
|
if (e.key === 'Tab') {
|
|
var focusables = getMermaidModalFocusables();
|
|
if (!focusables.length) return;
|
|
var first = focusables[0];
|
|
var last = focusables[focusables.length - 1];
|
|
var active = document.activeElement;
|
|
|
|
if (e.shiftKey) {
|
|
if (active === first || active === overlay) {
|
|
e.preventDefault();
|
|
last.focus();
|
|
}
|
|
} else {
|
|
if (active === last) {
|
|
e.preventDefault();
|
|
first.focus();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
var lastMermaidTrigger = null;
|
|
var openMermaidIndex = null;
|
|
var mermaidRenderSeq = 0;
|
|
|
|
function getMermaidModalFocusables() {
|
|
var modal = document.querySelector('#mermaidModalOverlay .mermaid-modal');
|
|
if (!modal) return [];
|
|
return Array.prototype.slice.call(modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'))
|
|
.filter(function (el) { return !el.disabled && el.offsetParent !== null; });
|
|
}
|
|
|
|
function openMermaidModal(idx, triggerEl) {
|
|
var overlay = document.getElementById('mermaidModalOverlay');
|
|
var body = document.getElementById('mermaidModalBody');
|
|
var center = document.getElementById('mermaidModalCenter');
|
|
var title = document.getElementById('mermaidModalTitle');
|
|
var closeBtn = document.getElementById('mermaidModalClose');
|
|
var pre = document.getElementById('mermaid-' + idx);
|
|
if (!overlay || !body || !center || !pre || !closeBtn) return;
|
|
|
|
lastMermaidTrigger = triggerEl || document.activeElement;
|
|
openMermaidIndex = idx;
|
|
|
|
if (title) title.textContent = 'Diagram';
|
|
|
|
overlay.classList.add('open');
|
|
overlay.setAttribute('aria-hidden', 'false');
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
closeBtn.focus();
|
|
|
|
rerenderMermaidModal();
|
|
}
|
|
|
|
function rerenderMermaidModal() {
|
|
var overlay = document.getElementById('mermaidModalOverlay');
|
|
var center = document.getElementById('mermaidModalCenter');
|
|
if (!overlay || !center) return;
|
|
if (!overlay.classList.contains('open')) return;
|
|
if (!openMermaidIndex) return;
|
|
if (!window._mermaidReady) return;
|
|
|
|
var pre = document.getElementById('mermaid-' + openMermaidIndex);
|
|
if (!pre) return;
|
|
var def = mermaidPreprocess(pre.textContent);
|
|
|
|
center.innerHTML = '<div class="panel-loading">Rendering diagram...</div>';
|
|
var token = ++mermaidRenderSeq;
|
|
center._mermaidRenderToken = token;
|
|
|
|
window._mermaidReady.render('mermaid-modal-svg', def).then(function (result) {
|
|
if (center._mermaidRenderToken !== token) return;
|
|
center.innerHTML = result.svg;
|
|
}).catch(function () {
|
|
if (center._mermaidRenderToken !== token) return;
|
|
center.innerHTML = '<p style="color:var(--text-muted);font-style:italic;">Diagram could not be rendered.</p>';
|
|
});
|
|
}
|
|
|
|
function closeMermaidModal() {
|
|
var overlay = document.getElementById('mermaidModalOverlay');
|
|
var center = document.getElementById('mermaidModalCenter');
|
|
if (!overlay) return;
|
|
overlay.classList.remove('open');
|
|
overlay.setAttribute('aria-hidden', 'true');
|
|
if (center) center.innerHTML = '';
|
|
document.body.style.overflow = '';
|
|
openMermaidIndex = null;
|
|
|
|
if (lastMermaidTrigger && typeof lastMermaidTrigger.focus === 'function') {
|
|
lastMermaidTrigger.focus();
|
|
}
|
|
}
|
|
|
|
function renderQuiz(data) {
|
|
var questions = data.questions || data;
|
|
if (!questions || !questions.length) return;
|
|
|
|
var article = document.querySelector('.lesson-article');
|
|
if (!article) return;
|
|
|
|
var conceptH2 = article.querySelector('#the-concept, #concept');
|
|
var buildH2 = article.querySelector('#build-it, #build');
|
|
var keyTermsH2 = article.querySelector('#key-terms');
|
|
|
|
var preQuizHtml = buildQuizHtml(questions.filter(function (q) { return q.stage === 'pre'; }), 'pre', 'Pre-Lesson Check');
|
|
var checkQuizHtml = buildQuizHtml(questions.filter(function (q) { return q.stage === 'check'; }), 'check', 'Mid-Lesson Check');
|
|
var postQuizHtml = buildQuizHtml(questions.filter(function (q) { return q.stage === 'post'; }), 'post', 'Post-Lesson Quiz');
|
|
|
|
if (!preQuizHtml && !checkQuizHtml && !postQuizHtml) {
|
|
var allHtml = buildQuizHtml(questions, 'all', 'Quiz');
|
|
if (allHtml && keyTermsH2) {
|
|
keyTermsH2.insertAdjacentHTML('beforebegin', allHtml);
|
|
} else if (allHtml) {
|
|
article.insertAdjacentHTML('beforeend', allHtml);
|
|
}
|
|
restoreQuizAnswers();
|
|
return;
|
|
}
|
|
|
|
if (preQuizHtml && conceptH2) {
|
|
conceptH2.insertAdjacentHTML('beforebegin', preQuizHtml);
|
|
} else if (preQuizHtml && buildH2) {
|
|
buildH2.insertAdjacentHTML('beforebegin', preQuizHtml);
|
|
}
|
|
|
|
// Mid-lesson check: land between Build It and Use It if possible,
|
|
// otherwise fall back to before Use It / Ship It / Key Terms.
|
|
var useH2 = article.querySelector('#use-it, #use');
|
|
var shipH2 = article.querySelector('#ship-it, #ship');
|
|
if (checkQuizHtml && useH2) {
|
|
useH2.insertAdjacentHTML('beforebegin', checkQuizHtml);
|
|
} else if (checkQuizHtml && shipH2) {
|
|
shipH2.insertAdjacentHTML('beforebegin', checkQuizHtml);
|
|
} else if (checkQuizHtml && keyTermsH2) {
|
|
keyTermsH2.insertAdjacentHTML('beforebegin', checkQuizHtml);
|
|
} else if (checkQuizHtml) {
|
|
article.insertAdjacentHTML('beforeend', checkQuizHtml);
|
|
}
|
|
|
|
if (postQuizHtml && keyTermsH2) {
|
|
keyTermsH2.insertAdjacentHTML('beforebegin', postQuizHtml);
|
|
} else if (postQuizHtml) {
|
|
article.insertAdjacentHTML('beforeend', postQuizHtml);
|
|
}
|
|
|
|
restoreQuizAnswers();
|
|
}
|
|
|
|
function buildQuizHtml(questions, id, title) {
|
|
if (!questions.length) return '';
|
|
var html = '<div class="quiz-section" id="quiz-' + id + '">';
|
|
html += '<div class="quiz-title">✅ ' + escapeHtml(title) + '</div>';
|
|
for (var qi = 0; qi < questions.length; qi++) {
|
|
var q = questions[qi];
|
|
var qid = id + '-q' + qi;
|
|
html += '<div class="quiz-question" data-correct="' + q.correct + '" id="' + qid + '">';
|
|
html += '<div class="quiz-question-text">' + (qi + 1) + '. ' + escapeHtml(q.question) + '</div>';
|
|
html += '<div class="quiz-options">';
|
|
for (var oi = 0; oi < q.options.length; oi++) {
|
|
html += '<div class="quiz-option" data-index="' + oi + '" onclick="handleQuizClick(this)">';
|
|
html += '<span class="quiz-marker"></span>';
|
|
html += '<span>' + escapeHtml(q.options[oi]) + '</span>';
|
|
html += '</div>';
|
|
}
|
|
html += '</div>';
|
|
if (q.explanation) {
|
|
html += '<div class="quiz-explanation" id="' + qid + '-exp">' + escapeHtml(q.explanation) + '</div>';
|
|
}
|
|
html += '</div>';
|
|
}
|
|
html += '<div class="quiz-score" id="quiz-' + id + '-score"></div>';
|
|
html += '</div>';
|
|
return html;
|
|
}
|
|
|
|
window.handleQuizClick = function (el) {
|
|
var questionDiv = el.closest('.quiz-question');
|
|
if (questionDiv.classList.contains('answered')) return;
|
|
applyQuizAnswer(questionDiv, parseInt(el.getAttribute('data-index')), true);
|
|
};
|
|
|
|
function applyQuizAnswer(questionDiv, chosen, persist) {
|
|
if (!questionDiv || isNaN(chosen)) return;
|
|
questionDiv.classList.add('answered');
|
|
|
|
var correct = parseInt(questionDiv.getAttribute('data-correct'));
|
|
var options = questionDiv.querySelectorAll('.quiz-option');
|
|
|
|
for (var i = 0; i < options.length; i++) {
|
|
options[i].classList.add('disabled');
|
|
if (i === correct) {
|
|
options[i].classList.add('correct');
|
|
var mc = options[i].querySelector('.quiz-marker');
|
|
if (mc) mc.textContent = '\u2713';
|
|
}
|
|
}
|
|
if (chosen !== correct) {
|
|
var picked = questionDiv.querySelector('.quiz-option[data-index="' + chosen + '"]');
|
|
if (picked) {
|
|
picked.classList.add('incorrect');
|
|
var mp = picked.querySelector('.quiz-marker');
|
|
if (mp) mp.textContent = '\u2717';
|
|
}
|
|
}
|
|
|
|
var exp = questionDiv.querySelector('.quiz-explanation');
|
|
if (exp) exp.style.display = 'block';
|
|
|
|
if (persist && window.AIFSProgress && lessonPath) {
|
|
window.AIFSProgress.recordAnswer(lessonPath, questionDiv.id, chosen, chosen === correct);
|
|
}
|
|
|
|
updateSectionScore(questionDiv.closest('.quiz-section'));
|
|
maybeMarkLessonComplete();
|
|
}
|
|
|
|
function updateSectionScore(section) {
|
|
if (!section) return;
|
|
var allQs = section.querySelectorAll('.quiz-question');
|
|
var answeredQs = section.querySelectorAll('.quiz-question.answered');
|
|
if (allQs.length !== answeredQs.length) return;
|
|
var correctCount = 0;
|
|
for (var j = 0; j < allQs.length; j++) {
|
|
if (!allQs[j].querySelector('.quiz-option.incorrect')) correctCount++;
|
|
}
|
|
var scoreEl = section.querySelector('.quiz-score');
|
|
if (scoreEl) {
|
|
scoreEl.textContent = correctCount + '/' + allQs.length + ' correct';
|
|
scoreEl.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
function maybeMarkLessonComplete() {
|
|
if (!window.AIFSProgress || !lessonPath) return;
|
|
var sections = document.querySelectorAll('.quiz-section');
|
|
if (!sections.length) return;
|
|
for (var s = 0; s < sections.length; s++) {
|
|
var qs = sections[s].querySelectorAll('.quiz-question');
|
|
var answered = sections[s].querySelectorAll('.quiz-question.answered');
|
|
if (qs.length === 0 || qs.length !== answered.length) return;
|
|
if (sections[s].querySelector('.quiz-option.incorrect')) return;
|
|
}
|
|
window.AIFSProgress.markLessonComplete(lessonPath);
|
|
}
|
|
|
|
function restoreQuizAnswers() {
|
|
if (!window.AIFSProgress || !lessonPath) return;
|
|
var lp = window.AIFSProgress.getLessonProgress(lessonPath);
|
|
if (!lp || !lp.answers) return;
|
|
for (var qid in lp.answers) {
|
|
var q = document.getElementById(qid);
|
|
if (q && !q.classList.contains('answered')) {
|
|
applyQuizAnswer(q, lp.answers[qid].picked, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
function addBottomNav(html) {
|
|
if (currentLessonIndex < 0) return html;
|
|
|
|
var prev = null;
|
|
for (var pi = currentLessonIndex - 1; pi >= 0; pi--) {
|
|
if (flatLessons[pi].isReadable) { prev = flatLessons[pi]; break; }
|
|
}
|
|
var next = null;
|
|
for (var ni = currentLessonIndex + 1; ni < flatLessons.length; ni++) {
|
|
if (flatLessons[ni].isReadable) { next = flatLessons[ni]; break; }
|
|
}
|
|
|
|
var nav = '<div class="lesson-nav-bottom">';
|
|
if (prev) {
|
|
nav += '<a class="lesson-nav-btn prev" href="lesson.html?path=' + prev.path + '">';
|
|
nav += '<span class="nav-label">← Previous</span>';
|
|
nav += '<span class="nav-title">' + escapeHtml(prev.lessonName) + '</span>';
|
|
nav += '</a>';
|
|
} else {
|
|
nav += '<div></div>';
|
|
}
|
|
if (next) {
|
|
nav += '<a class="lesson-nav-btn next" href="lesson.html?path=' + next.path + '">';
|
|
nav += '<span class="nav-label">Next →</span>';
|
|
nav += '<span class="nav-title">' + escapeHtml(next.lessonName) + '</span>';
|
|
nav += '</a>';
|
|
}
|
|
nav += '</div>';
|
|
return html + nav;
|
|
}
|
|
|
|
function renderAIPanels() {
|
|
var container = document.getElementById('aiPanels');
|
|
if (!container || !lessonPath) return;
|
|
|
|
renderOutputsPanel(container);
|
|
renderCodePanel(container);
|
|
renderQuizPanel(container);
|
|
renderLearningPathPanel(container);
|
|
renderContinuePanel(container);
|
|
}
|
|
|
|
var ghApiCache = {};
|
|
|
|
function ghApiFetch(apiPath, cb) {
|
|
if (ghApiCache[apiPath]) {
|
|
cb(null, ghApiCache[apiPath]);
|
|
return;
|
|
}
|
|
fetch('https://api.github.com/repos/rohitg00/ai-engineering-from-scratch/contents/' + apiPath, {
|
|
headers: { 'Accept': 'application/vnd.github.v3+json' }
|
|
}).then(function (res) {
|
|
if (!res.ok) throw new Error(res.status);
|
|
return res.json();
|
|
}).then(function (data) {
|
|
ghApiCache[apiPath] = data;
|
|
cb(null, data);
|
|
}).catch(function (err) {
|
|
cb(err, null);
|
|
});
|
|
}
|
|
|
|
function classifyOutput(name) {
|
|
var lower = name.toLowerCase();
|
|
if (lower.indexOf('prompt') >= 0) return 'prompt';
|
|
if (lower.indexOf('skill') >= 0) return 'skill';
|
|
return 'other';
|
|
}
|
|
|
|
function langIcon(filename) {
|
|
var ext = filename.split('.').pop().toLowerCase();
|
|
var map = { py: 'PY', python: 'PY', ts: 'TS', js: 'JS', rust: 'RS', rs: 'RS', jl: 'JL', julia: 'JL', sh: 'SH', bash: 'SH' };
|
|
return map[ext] || '··';
|
|
}
|
|
|
|
function langCommand(filename) {
|
|
var ext = filename.split('.').pop().toLowerCase();
|
|
if (ext === 'py') return 'python';
|
|
if (ext === 'ts') return 'npx tsx';
|
|
if (ext === 'js') return 'node';
|
|
if (ext === 'rs') return 'cargo run --';
|
|
if (ext === 'jl') return 'julia';
|
|
if (ext === 'sh') return 'bash';
|
|
return 'run';
|
|
}
|
|
|
|
function formatSize(bytes) {
|
|
if (bytes < 1024) return bytes + ' B';
|
|
return (bytes / 1024).toFixed(1) + ' KB';
|
|
}
|
|
|
|
function extractFrontmatterDesc(content) {
|
|
var match = content.match(/^---[\s\S]*?description:\s*(.+?)[\r\n]/m);
|
|
if (match) return match[1].replace(/^["']|["']$/g, '').trim();
|
|
var firstLine = content.split('\n').find(function (l) { return l.trim() && !l.match(/^[#-]/) && !l.match(/^---/); });
|
|
return firstLine ? firstLine.trim().substring(0, 120) : '';
|
|
}
|
|
|
|
function renderOutputsPanel(container) {
|
|
var panel = document.createElement('div');
|
|
panel.className = 'ai-panel';
|
|
panel.innerHTML = '<div class="ai-panel-header"><div class="ai-panel-icon">O</div><div class="ai-panel-title">What This Lesson Ships</div></div><div class="ai-panel-subtitle">Prompts, skills, and artifacts you can use right now</div><div id="outputsContent" class="panel-loading">Loading outputs...</div>';
|
|
container.appendChild(panel);
|
|
|
|
ghApiFetch(lessonPath + '/outputs', function (err, data) {
|
|
var el = document.getElementById('outputsContent');
|
|
if (err || !data || !Array.isArray(data) || data.length === 0) {
|
|
el.innerHTML = '<div class="panel-fallback"><a href="https://github.com/rohitg00/ai-engineering-from-scratch/tree/main/' + lessonPath + '" target="_blank" rel="noopener">View lesson on GitHub</a></div>';
|
|
return;
|
|
}
|
|
|
|
var html = '<div class="output-cards">';
|
|
data.forEach(function (file, idx) {
|
|
var type = classifyOutput(file.name);
|
|
var badgeClass = type;
|
|
var badgeText = type === 'prompt' ? 'Prompt' : type === 'skill' ? 'Skill' : 'Output';
|
|
var installId = 'install-hint-' + idx;
|
|
var installHint = '';
|
|
if (type === 'prompt') {
|
|
installHint = 'Paste into Claude, Cursor, Codex, OpenClaw, Hermes, or any agent that reads prompts';
|
|
} else if (type === 'skill') {
|
|
var skillName = file.name.replace(/\.md$/, '').replace(/^skill-/, '');
|
|
installHint = 'npx skills add rohitg00/ai-engineering-from-scratch --skill ' + skillName;
|
|
}
|
|
|
|
html += '<div class="output-card">';
|
|
html += '<div class="output-badge ' + badgeClass + '">' + badgeText + '</div>';
|
|
html += '<div class="output-card-name">' + escapeHtml(file.name) + '</div>';
|
|
html += '<div class="output-desc" id="desc-' + idx + '">Loading description...</div>';
|
|
html += '<div class="output-actions">';
|
|
html += '<a class="output-btn" href="' + file.html_url + '" target="_blank" rel="noopener">View on GitHub</a>';
|
|
if (installHint) {
|
|
html += '<button class="output-btn" onclick="var h=document.getElementById(\'' + installId + '\');h.classList.toggle(\'visible\')">Install</button>';
|
|
}
|
|
html += '</div>';
|
|
if (installHint) {
|
|
html += '<div class="output-install-hint" id="' + installId + '">' + escapeHtml(installHint) + '</div>';
|
|
}
|
|
html += '</div>';
|
|
|
|
fetch(file.download_url).then(function (r) { return r.text(); }).then(function (content) {
|
|
var descEl = document.getElementById('desc-' + idx);
|
|
if (descEl) {
|
|
var desc = extractFrontmatterDesc(content);
|
|
descEl.textContent = desc || file.name;
|
|
}
|
|
}).catch(function () {
|
|
var descEl = document.getElementById('desc-' + idx);
|
|
if (descEl) descEl.textContent = file.name;
|
|
});
|
|
});
|
|
html += '</div>';
|
|
el.innerHTML = html;
|
|
});
|
|
}
|
|
|
|
function renderCodePanel(container) {
|
|
var panel = document.createElement('div');
|
|
panel.className = 'ai-panel';
|
|
panel.innerHTML = '<div class="ai-panel-header"><div class="ai-panel-icon">C</div><div class="ai-panel-title">Run the Code</div></div><div class="ai-panel-subtitle">Executable files from this lesson</div><div id="codeContent" class="panel-loading">Loading code files...</div>';
|
|
container.appendChild(panel);
|
|
|
|
ghApiFetch(lessonPath + '/code', function (err, data) {
|
|
var el = document.getElementById('codeContent');
|
|
if (err || !data || !Array.isArray(data) || data.length === 0) {
|
|
el.innerHTML = '<div class="panel-fallback"><a href="https://github.com/rohitg00/ai-engineering-from-scratch/tree/main/' + lessonPath + '" target="_blank" rel="noopener">View lesson on GitHub</a></div>';
|
|
return;
|
|
}
|
|
|
|
var html = '<div class="code-cards">';
|
|
data.forEach(function (file) {
|
|
var icon = langIcon(file.name);
|
|
var cmd = langCommand(file.name);
|
|
var runCmd = cmd + ' ' + lessonPath + '/code/' + file.name;
|
|
html += '<div class="code-card">';
|
|
html += '<div class="code-card-header"><span class="code-lang-icon">' + icon + '</span><span class="code-card-name">' + escapeHtml(file.name) + '</span></div>';
|
|
html += '<div class="code-card-size">' + formatSize(file.size) + '</div>';
|
|
html += '<div class="code-card-run">' + escapeHtml(runCmd) + '</div>';
|
|
html += '<div class="code-card-actions">';
|
|
html += '<a class="code-card-btn" href="' + file.html_url + '" target="_blank" rel="noopener">View on GitHub</a>';
|
|
html += '<button class="code-card-btn code-card-copy" data-command="' + escapeAttr(runCmd) + '">Copy command</button>';
|
|
html += '</div>';
|
|
html += '</div>';
|
|
});
|
|
html += '</div>';
|
|
el.innerHTML = html;
|
|
el.querySelectorAll('.code-card-copy').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var command = btn.getAttribute('data-command');
|
|
navigator.clipboard.writeText(command).then(function () {
|
|
btn.textContent = 'Copied!';
|
|
setTimeout(function () { btn.textContent = 'Copy command'; }, 1500);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function getQuizQuestions() {
|
|
var pathLower = (lessonPath || '').toLowerCase();
|
|
var lessonName = '';
|
|
if (currentLessonIndex >= 0) lessonName = flatLessons[currentLessonIndex].lessonName;
|
|
|
|
if (pathLower.indexOf('math') >= 0 || pathLower.indexOf('linear-algebra') >= 0 || pathLower.indexOf('calculus') >= 0) {
|
|
return [
|
|
{ q: 'What does a dot product measure between two vectors?', opts: ['Their sum', 'How aligned they are', 'Their cross product', 'The distance between them'], answer: 1, explain: 'The dot product measures the similarity or alignment between two vectors. When it is zero, the vectors are orthogonal.' },
|
|
{ q: 'What does the gradient of a function point toward?', opts: ['The minimum', 'The steepest ascent', 'The nearest saddle point', 'A random direction'], answer: 1, explain: 'The gradient always points in the direction of steepest increase. Gradient descent moves opposite to the gradient to find minima.' },
|
|
{ q: 'A matrix with shape (3, 5) multiplied by (5, 2) produces what shape?', opts: ['(3, 2)', '(5, 5)', '(3, 5)', '(2, 3)'], answer: 0, explain: 'Matrix multiplication: (m, n) x (n, p) = (m, p). So (3, 5) x (5, 2) = (3, 2).' }
|
|
];
|
|
}
|
|
|
|
if (pathLower.indexOf('ml') >= 0 || pathLower.indexOf('regression') >= 0 || pathLower.indexOf('classification') >= 0 || pathLower.indexOf('supervised') >= 0) {
|
|
return [
|
|
{ q: 'What is the purpose of a loss function in machine learning?', opts: ['To generate data', 'To measure how wrong predictions are', 'To select features', 'To split data'], answer: 1, explain: 'A loss function quantifies the difference between predicted and actual values. The optimizer minimizes this value during training.' },
|
|
{ q: 'Why do we split data into train and test sets?', opts: ['To save memory', 'To evaluate generalization on unseen data', 'To make training faster', 'To reduce the dataset size'], answer: 1, explain: 'The test set acts as unseen data, revealing whether the model memorized training data or learned generalizable patterns.' },
|
|
{ q: 'What does overfitting mean?', opts: ['The model is too simple', 'The model memorizes training data but fails on new data', 'The model trains too slowly', 'The loss is too low'], answer: 1, explain: 'Overfitting occurs when a model performs well on training data but poorly on new data because it learned noise rather than signal.' }
|
|
];
|
|
}
|
|
|
|
if (pathLower.indexOf('deep-learning') >= 0 || pathLower.indexOf('neural') >= 0 || pathLower.indexOf('backprop') >= 0 || pathLower.indexOf('activation') >= 0) {
|
|
return [
|
|
{ q: 'What does backpropagation compute?', opts: ['Forward predictions', 'The gradient of the loss with respect to each weight', 'The learning rate', 'New training data'], answer: 1, explain: 'Backpropagation uses the chain rule to compute how much each weight contributed to the error, then adjusts weights accordingly.' },
|
|
{ q: 'Why do neural networks need non-linear activation functions?', opts: ['To speed up training', 'Without them, stacking layers is equivalent to a single linear layer', 'To reduce memory usage', 'To normalize outputs'], answer: 1, explain: 'Without non-linearities, any composition of linear layers collapses to a single linear transformation. Activations like ReLU let the network learn complex patterns.' },
|
|
{ q: 'What does the learning rate control?', opts: ['How many epochs to train', 'The size of each weight update step', 'The number of layers', 'The batch size'], answer: 1, explain: 'The learning rate scales the gradient update. Too large causes divergence, too small causes slow or stuck training.' }
|
|
];
|
|
}
|
|
|
|
if (pathLower.indexOf('llm') >= 0 || pathLower.indexOf('transformer') >= 0 || pathLower.indexOf('attention') >= 0 || pathLower.indexOf('tokeniz') >= 0 || pathLower.indexOf('fine-tun') >= 0) {
|
|
return [
|
|
{ q: 'What does self-attention allow a transformer to do?', opts: ['Process tokens in order', 'Weight the importance of every token relative to every other token', 'Reduce vocabulary size', 'Compress the model'], answer: 1, explain: 'Self-attention computes pairwise relevance scores across all positions, allowing the model to relate distant tokens without recurrence.' },
|
|
{ q: 'Why do LLMs use tokenizers instead of raw characters?', opts: ['Characters are too large', 'Tokens compress frequent patterns into single units, reducing sequence length', 'Tokenizers are faster to train', 'Characters cannot be embedded'], answer: 1, explain: 'Subword tokenizers like BPE balance vocabulary size against sequence length, making common words single tokens while handling rare words as pieces.' },
|
|
{ q: 'What is the key difference between pre-training and fine-tuning?', opts: ['Pre-training uses labeled data', 'Pre-training learns general language; fine-tuning adapts to a specific task', 'Fine-tuning uses more data', 'There is no difference'], answer: 1, explain: 'Pre-training learns language patterns from massive unlabeled text. Fine-tuning takes that foundation and specializes it on smaller, task-specific data.' }
|
|
];
|
|
}
|
|
|
|
if (pathLower.indexOf('rag') >= 0 || pathLower.indexOf('retrieval') >= 0 || pathLower.indexOf('embed') >= 0 || pathLower.indexOf('vector') >= 0) {
|
|
return [
|
|
{ q: 'What is the core idea behind RAG?', opts: ['Training a larger model', 'Retrieving relevant context before generating a response', 'Using more GPUs', 'Reducing model size'], answer: 1, explain: 'RAG (Retrieval-Augmented Generation) grounds LLM responses in retrieved documents, reducing hallucination and enabling knowledge updates without retraining.' },
|
|
{ q: 'What do embedding models produce?', opts: ['Text summaries', 'Dense vector representations of text', 'Token counts', 'Grammar corrections'], answer: 1, explain: 'Embedding models map text into fixed-dimensional vectors where semantic similarity corresponds to geometric proximity.' },
|
|
{ q: 'Why use cosine similarity for comparing embeddings?', opts: ['It is the only metric', 'It measures angular similarity regardless of vector magnitude', 'It is faster than dot product', 'It works with integers only'], answer: 1, explain: 'Cosine similarity normalizes for magnitude, focusing on direction. Two texts about the same topic will have high cosine similarity regardless of length.' }
|
|
];
|
|
}
|
|
|
|
if (pathLower.indexOf('agent') >= 0 || pathLower.indexOf('tool') >= 0 || pathLower.indexOf('mcp') >= 0) {
|
|
return [
|
|
{ q: 'What distinguishes an AI agent from a simple chatbot?', opts: ['Agents are faster', 'Agents can take actions and use tools autonomously', 'Agents use bigger models', 'There is no difference'], answer: 1, explain: 'Agents have a loop: observe, decide, act. They can call tools, read files, search the web, and chain multiple steps to complete complex tasks.' },
|
|
{ q: 'What is MCP (Model Context Protocol)?', opts: ['A model training format', 'A standardized protocol for connecting AI models to tools and data sources', 'A compression algorithm', 'A testing framework'], answer: 1, explain: 'MCP provides a universal interface between AI assistants and external tools/data, replacing one-off integrations with a standard protocol.' },
|
|
{ q: 'Why is tool-use important for LLMs?', opts: ['It reduces cost', 'It lets LLMs access real-time information and take actions beyond text generation', 'It makes responses shorter', 'It removes hallucinations entirely'], answer: 1, explain: 'Tools extend LLMs beyond their training data cutoff, enabling real-time lookups, calculations, code execution, and interaction with external systems.' }
|
|
];
|
|
}
|
|
|
|
if (pathLower.indexOf('eval') >= 0 || pathLower.indexOf('safety') >= 0 || pathLower.indexOf('alignment') >= 0 || pathLower.indexOf('deploy') >= 0) {
|
|
return [
|
|
{ q: 'Why are evals critical for production AI systems?', opts: ['To save money', 'To measure performance objectively before and after changes', 'To make the model bigger', 'Evals are optional'], answer: 1, explain: 'Evals are the tests of AI engineering. Without them, you cannot know if a change improved or regressed quality. They should run on every code change.' },
|
|
{ q: 'What does "alignment" mean in AI safety?', opts: ['Aligning text on screen', 'Ensuring AI systems act according to human intentions and values', 'Making models faster', 'Using the same training data'], answer: 1, explain: 'Alignment ensures that as AI systems become more capable, they remain helpful, honest, and harmless, acting in accordance with human goals.' },
|
|
{ q: 'What is a guardrail in the context of deployed LLMs?', opts: ['A physical barrier', 'A check that prevents harmful, off-topic, or policy-violating outputs', 'A backup model', 'A caching layer'], answer: 1, explain: 'Guardrails filter inputs and outputs at runtime, catching toxicity, prompt injection, PII leaks, and other risks before they reach the user.' }
|
|
];
|
|
}
|
|
|
|
return [
|
|
{ q: 'What does "from scratch" mean in this course?', opts: ['Using no computer', 'Building each concept by implementing it yourself, not just reading theory', 'Starting from assembly language', 'Only using pen and paper'], answer: 1, explain: 'This course follows a Build-Use-Ship methodology: first understand by building it, then apply it, then ship it as a real artifact.' },
|
|
{ q: 'Why does this course combine math, ML, and engineering?', opts: ['To make it longer', 'Because real AI engineering requires all three to build production systems', 'Math is just for fun', 'Engineering is optional'], answer: 1, explain: 'Production AI systems require mathematical foundations (for understanding), ML knowledge (for modeling), and engineering skills (for deployment and reliability).' },
|
|
{ q: 'What is the "Ship" step in the Build-Use-Ship framework?', opts: ['Mailing physical goods', 'Creating a reusable artifact like a prompt, skill, or tool from what you learned', 'Publishing a paper', 'Deleting your code'], answer: 2, explain: 'The Ship step turns your learning into something tangible: a prompt, skill file, MCP tool, or CLI utility that others (or future you) can use immediately.' }
|
|
];
|
|
}
|
|
|
|
function renderQuizPanel(container) {
|
|
var questions = getQuizQuestions();
|
|
var panel = document.createElement('div');
|
|
panel.className = 'ai-panel';
|
|
|
|
var phase = '';
|
|
if (currentLessonIndex >= 0) {
|
|
var fl = flatLessons[currentLessonIndex];
|
|
phase = fl.phaseSlug || '';
|
|
}
|
|
|
|
var html = '<div class="ai-panel-header"><div class="ai-panel-icon">Q</div><div class="ai-panel-title">Test Your Understanding</div></div>';
|
|
html += '<div class="ai-panel-subtitle">Did you get it?</div>';
|
|
html += '<div class="quiz-container" id="quizContainer">';
|
|
|
|
var letters = ['A', 'B', 'C', 'D'];
|
|
questions.forEach(function (q, qi) {
|
|
html += '<div class="quiz-question" id="quiz-q-' + qi + '">';
|
|
html += '<div class="quiz-question-num">Question ' + (qi + 1) + ' of ' + questions.length + '</div>';
|
|
html += '<div class="quiz-question-text">' + escapeHtml(q.q) + '</div>';
|
|
html += '<div class="quiz-options">';
|
|
q.opts.forEach(function (opt, oi) {
|
|
html += '<button class="quiz-option" data-qi="' + qi + '" data-oi="' + oi + '" onclick="handleQuizAnswer(' + qi + ',' + oi + ')">';
|
|
html += '<span class="opt-letter">' + letters[oi] + '</span>';
|
|
html += '<span>' + escapeHtml(opt) + '</span>';
|
|
html += '</button>';
|
|
});
|
|
html += '</div>';
|
|
html += '<div class="quiz-explanation" id="quiz-explain-' + qi + '">' + escapeHtml(q.explain) + '</div>';
|
|
html += '</div>';
|
|
});
|
|
|
|
html += '<div class="quiz-score" id="quizScore"><div class="quiz-score-number" id="quizScoreNum">0/' + questions.length + '</div><div class="quiz-score-label">Complete all questions to see your score</div></div>';
|
|
html += '<div class="quiz-deeper">Want a deeper quiz? Run <code>/check-understanding ' + escapeHtml(phase) + '</code> in Claude, Cursor, Codex, OpenClaw, Hermes, or any agent with the curriculum skills installed</div>';
|
|
html += '</div>';
|
|
|
|
panel.innerHTML = html;
|
|
container.appendChild(panel);
|
|
|
|
window._quizData = questions;
|
|
window._quizAnswered = 0;
|
|
window._quizCorrect = 0;
|
|
}
|
|
|
|
window.handleQuizAnswer = function (qi, oi) {
|
|
var questions = window._quizData;
|
|
if (!questions) return;
|
|
|
|
var qEl = document.getElementById('quiz-q-' + qi);
|
|
if (!qEl) return;
|
|
var opts = qEl.querySelectorAll('.quiz-option');
|
|
if (opts[0].classList.contains('disabled')) return;
|
|
|
|
var correct = questions[qi].answer;
|
|
opts.forEach(function (opt, i) {
|
|
opt.classList.add('disabled');
|
|
if (i === oi) {
|
|
opt.classList.add('selected');
|
|
opt.classList.add(i === correct ? 'correct' : 'wrong');
|
|
}
|
|
if (i === correct) {
|
|
opt.classList.add('is-answer');
|
|
}
|
|
});
|
|
|
|
var explain = document.getElementById('quiz-explain-' + qi);
|
|
if (explain) explain.classList.add('visible');
|
|
|
|
window._quizAnswered++;
|
|
if (oi === correct) window._quizCorrect++;
|
|
|
|
if (window._quizAnswered === questions.length) {
|
|
var scoreEl = document.getElementById('quizScore');
|
|
var scoreNum = document.getElementById('quizScoreNum');
|
|
if (scoreEl && scoreNum) {
|
|
scoreNum.textContent = window._quizCorrect + '/' + questions.length;
|
|
scoreEl.querySelector('.quiz-score-label').textContent =
|
|
window._quizCorrect === questions.length ? 'Perfect score!' :
|
|
window._quizCorrect >= 2 ? 'Great work!' : 'Keep studying!';
|
|
scoreEl.classList.add('visible');
|
|
}
|
|
}
|
|
};
|
|
|
|
function renderLearningPathPanel(container) {
|
|
if (currentLessonIndex < 0 || typeof PHASES === 'undefined') return;
|
|
|
|
var panel = document.createElement('div');
|
|
panel.className = 'ai-panel';
|
|
|
|
var current = flatLessons[currentLessonIndex];
|
|
var phaseIdx = current.phaseIndex;
|
|
var phase = PHASES[phaseIdx];
|
|
var lessonsInPhase = [];
|
|
var currentIdxInPhase = -1;
|
|
|
|
for (var i = 0; i < flatLessons.length; i++) {
|
|
if (flatLessons[i].phaseIndex === phaseIdx) {
|
|
if (i === currentLessonIndex) currentIdxInPhase = lessonsInPhase.length;
|
|
lessonsInPhase.push({ flat: flatLessons[i], flatIdx: i });
|
|
}
|
|
}
|
|
|
|
var html = '<div class="ai-panel-header"><div class="ai-panel-icon">P</div><div class="ai-panel-title">Learning Path</div></div>';
|
|
html += '<div class="ai-panel-subtitle">Phase ' + String(phase.id).padStart(2, '0') + ': ' + escapeHtml(phase.name) + '</div>';
|
|
|
|
// Window: 2 before current + current + 2 after, cap at 5 visible.
|
|
var windowSize = 5;
|
|
var startIdx = Math.max(0, currentIdxInPhase - 2);
|
|
var endIdx = Math.min(lessonsInPhase.length, startIdx + windowSize);
|
|
startIdx = Math.max(0, endIdx - windowSize);
|
|
var visible = lessonsInPhase.slice(startIdx, endIdx);
|
|
var hiddenBefore = startIdx;
|
|
var hiddenAfter = lessonsInPhase.length - endIdx;
|
|
|
|
html += '<div class="learning-timeline">';
|
|
if (hiddenBefore > 0) {
|
|
html += '<span class="timeline-ellipsis" title="' + hiddenBefore + ' earlier lessons">« ' + hiddenBefore + '</span>';
|
|
html += '<div class="timeline-line done"></div>';
|
|
}
|
|
visible.forEach(function (item, vIdx) {
|
|
var idx = startIdx + vIdx;
|
|
var isCurrent = idx === currentIdxInPhase;
|
|
var isPrev = idx < currentIdxInPhase;
|
|
var cls = isCurrent ? 'current' : isPrev ? 'prev' : '';
|
|
|
|
if (vIdx > 0) {
|
|
var lineCls = idx <= currentIdxInPhase ? (idx === currentIdxInPhase ? 'active' : 'done') : '';
|
|
html += '<div class="timeline-line ' + lineCls + '"></div>';
|
|
}
|
|
|
|
if (item.flat.isReadable) {
|
|
html += '<a class="timeline-item ' + cls + '" href="lesson.html?path=' + item.flat.path + '" title="' + escapeAttr(item.flat.lessonName) + '">';
|
|
} else {
|
|
html += '<span class="timeline-item ' + cls + ' disabled" title="' + escapeAttr(item.flat.lessonName) + '">';
|
|
}
|
|
html += '<div class="timeline-dot"></div>';
|
|
html += '<div class="timeline-label">' + escapeHtml(item.flat.lessonName) + '</div>';
|
|
html += item.flat.isReadable ? '</a>' : '</span>';
|
|
});
|
|
if (hiddenAfter > 0) {
|
|
html += '<div class="timeline-line"></div>';
|
|
html += '<span class="timeline-ellipsis" title="' + hiddenAfter + ' later lessons">' + hiddenAfter + ' »</span>';
|
|
}
|
|
html += '</div>';
|
|
|
|
var completedCount = currentIdxInPhase + 1;
|
|
var totalCount = lessonsInPhase.length;
|
|
var pct = Math.round((completedCount / totalCount) * 100);
|
|
|
|
html += '<div class="phase-progress-bar"><div class="phase-progress-fill" style="width:' + pct + '%"></div></div>';
|
|
html += '<div class="phase-progress-text">You\'ve completed ' + completedCount + ' of ' + totalCount + ' lessons in this phase</div>';
|
|
|
|
if (completedCount >= totalCount && phaseIdx < PHASES.length - 1) {
|
|
var nextPhase = PHASES[phaseIdx + 1];
|
|
html += '<div class="phase-complete-callout visible">Ready for Phase ' + String(nextPhase.id).padStart(2, '0') + ': ' + escapeHtml(nextPhase.name) + '</div>';
|
|
}
|
|
|
|
panel.innerHTML = html;
|
|
container.appendChild(panel);
|
|
}
|
|
|
|
function renderContinuePanel(container) {
|
|
if (currentLessonIndex < 0) return;
|
|
|
|
var panel = document.createElement('div');
|
|
panel.className = 'ai-panel';
|
|
|
|
var next = null;
|
|
for (var ci = currentLessonIndex + 1; ci < flatLessons.length; ci++) {
|
|
if (flatLessons[ci].isReadable) { next = flatLessons[ci]; break; }
|
|
}
|
|
var current = flatLessons[currentLessonIndex];
|
|
var phaseIdx = current.phaseIndex;
|
|
var phase = PHASES[phaseIdx];
|
|
|
|
var html = '<div class="ai-panel-header"><div class="ai-panel-icon">N</div><div class="ai-panel-title">Continue Learning</div></div>';
|
|
html += '<div class="continue-panel">';
|
|
|
|
if (!next || !next.path) {
|
|
html += '<div class="phase-finished">\u2705 You finished this phase!</div>';
|
|
}
|
|
|
|
html += '<div class="continue-links">';
|
|
|
|
var phaseSlug = extractPhaseSlug(phase, phaseIdx);
|
|
var firstLesson = phase.lessons[0] ? extractLessonSlug(phase.lessons[0], 0) : '';
|
|
html += '<a class="continue-link" href="lesson.html?path=phases/' + phaseSlug + '/' + firstLesson + '">Browse all Phase ' + String(phase.id).padStart(2, '0') + ' lessons</a>';
|
|
html += '<a class="continue-link" href="catalog.html">Full course catalog</a>';
|
|
html += '</div>';
|
|
|
|
html += '<div class="continue-callout">Run <code>/find-your-level</code> in Claude, Cursor, Codex, OpenClaw, Hermes, or any agent with the curriculum skills installed for a personalized learning path</div>';
|
|
html += '</div>';
|
|
|
|
panel.innerHTML = html;
|
|
container.appendChild(panel);
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
var div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function escapeAttr(str) {
|
|
return str.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|