Files
2026-07-13 12:09:03 +08:00

476 lines
16 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 Catalog - 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="Full catalog of 503 AI engineering lessons. Search, filter, and sort every lesson across all 20 phases.">
<link rel="canonical" href="https://aiengineeringfromscratch.com/catalog.html">
<meta property="og:title" content="Catalog · AI Engineering from Scratch">
<meta property="og:description" content="Search and filter 503 lessons across 20 phases. Python, TypeScript, Rust, Julia.">
<meta property="og:image" content="https://aiengineeringfromscratch.com/og-image.png?v=3">
<meta property="og:url" content="https://aiengineeringfromscratch.com/catalog.html">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Catalog · AI Engineering from Scratch">
<meta name="twitter:description" content="Search and filter 503 lessons across 20 phases.">
<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>
.catalog-page {
padding: 100px 0 80px;
min-height: 100vh;
}
.catalog-header {
text-align: center;
margin-bottom: 32px;
}
.catalog-header h1 {
font-size: clamp(2rem, 5vw, 3rem);
margin-bottom: 8px;
}
.catalog-header p {
color: var(--text-muted);
font-size: 1.1rem;
}
.catalog-controls {
display: flex;
gap: 12px;
margin-bottom: 24px;
flex-wrap: wrap;
align-items: center;
}
.catalog-search {
flex: 1;
min-width: 200px;
font-family: var(--font-body);
font-size: 1rem;
padding: 12px 16px;
background: var(--bg-surface);
border: 2px solid var(--border);
border-radius: 0;
color: var(--text);
outline: none;
transition: border-color 0.2s;
}
.catalog-search::placeholder {
color: var(--text-muted);
}
.catalog-search:focus {
border-color: var(--accent);
}
.catalog-search:focus-visible,
.catalog-filter:focus-visible {
outline: 2px solid var(--blueprint);
outline-offset: 2px;
}
.catalog-filter {
font-family: var(--font-body);
font-size: 1rem;
padding: 12px 16px;
background: var(--bg-surface);
border: 2px solid var(--border);
border-radius: 0;
color: var(--text);
outline: none;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
}
.catalog-count {
font-family: var(--font-mono);
font-size: 0.85rem;
color: var(--text-muted);
margin-bottom: 16px;
}
.catalog-table-wrap {
overflow-x: auto;
border: 2px solid var(--border);
border-radius: 12px;
background: var(--bg-surface);
}
.catalog-table {
width: 100%;
border-collapse: collapse;
font-size: 0.95rem;
min-width: 640px;
}
.catalog-table th {
font-family: var(--font-heading);
font-weight: 700;
font-size: 0.9rem;
text-align: left;
padding: 14px 16px;
border-bottom: 2px solid var(--border);
color: var(--text-muted);
cursor: pointer;
user-select: none;
white-space: nowrap;
transition: color 0.2s;
}
.catalog-table th:hover {
color: var(--accent);
}
.catalog-table th .sort-arrow {
font-size: 0.7rem;
margin-left: 4px;
}
.catalog-table td {
padding: 10px 16px;
border-bottom: 1px solid var(--border);
vertical-align: middle;
}
.catalog-table tr:last-child td {
border-bottom: none;
}
.catalog-table tr:hover td {
background: var(--bg-surface-hover);
}
.catalog-table a {
color: var(--secondary);
}
.catalog-table a:hover {
color: var(--accent);
}
.catalog-status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.catalog-status-dot.complete {
background: var(--complete);
}
.catalog-status-dot.planned {
background: var(--planned);
}
.catalog-lang {
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--text-muted);
}
.catalog-phase-label {
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--accent);
}
.catalog-empty {
text-align: center;
padding: 48px 24px;
color: var(--text-muted);
font-size: 1.1rem;
}
@media (max-width: 768px) {
.catalog-page {
padding: 80px 0 48px;
}
.catalog-controls {
flex-direction: column;
}
.catalog-filter {
width: 100%;
}
.catalog-table {
font-size: 0.85rem;
}
.catalog-table th,
.catalog-table td {
padding: 8px 10px;
}
}
</style>
</head>
<body>
<a href="#main" class="skip-link">Skip to content</a>
<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="about.html">About</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>
<main class="catalog-page" id="main">
<div class="container">
<div class="catalog-header">
<h1>Lesson Catalog</h1>
<p>Every lesson across all 20 phases. Search, filter, sort.</p>
</div>
<div class="catalog-controls">
<input type="text" class="catalog-search" id="catalogSearch" placeholder="Search lessons...">
<select class="catalog-filter" id="catalogPhase">
<option value="">All Phases</option>
</select>
<select class="catalog-filter" id="catalogStatus">
<option value="">All Status</option>
<option value="complete">Complete</option>
<option value="planned">Planned</option>
</select>
</div>
<div class="catalog-count" id="catalogCount"></div>
<div class="catalog-table-wrap">
<table class="catalog-table" id="catalogTable">
<thead>
<tr>
<th data-sort="phase">Phase <span class="sort-arrow"></span></th>
<th data-sort="name">Lesson <span class="sort-arrow"></span></th>
<th data-sort="type">Type <span class="sort-arrow"></span></th>
<th data-sort="lang">Language <span class="sort-arrow"></span></th>
<th data-sort="status">Status <span class="sort-arrow"></span></th>
</tr>
</thead>
<tbody id="catalogBody"></tbody>
</table>
</div>
</div>
</main>
<footer class="site-footer">
<div class="container footer-inner">
<p>AI Engineering from Scratch · open source · free forever.</p>
<div class="footer-links">
<a href="index.html">Home</a>
<a href="https://github.com/rohitg00/ai-engineering-from-scratch" target="_blank" rel="noopener">GitHub</a>
<a href="glossary.html">Glossary</a>
<a href="https://github.com/rohitg00/ai-engineering-from-scratch/issues/new/choose" target="_blank" rel="noopener">Report / Suggest</a>
</div>
</div>
</footer>
<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>
(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');
}
function updateIcon() {
var icon = document.getElementById('themeIcon');
if (!icon) return;
icon.textContent = root.getAttribute('data-theme') === 'light' ? 'N' : 'D';
}
updateIcon();
document.addEventListener('DOMContentLoaded', function () {
updateIcon();
var btn = document.getElementById('themeToggle');
if (btn) {
btn.addEventListener('click', function () {
var next = root.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
root.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
updateIcon();
});
}
var allRows = [];
var phaseSelect = document.getElementById('catalogPhase');
for (var i = 0; i < PHASES.length; i++) {
var p = PHASES[i];
var opt = document.createElement('option');
opt.value = String(p.id);
opt.textContent = String(p.id).padStart(2, '0') + ' - ' + p.name;
phaseSelect.appendChild(opt);
for (var j = 0; j < p.lessons.length; j++) {
var l = p.lessons[j];
allRows.push({
phase: p.id,
phaseName: p.name,
name: l.name,
type: l.type,
lang: l.lang,
status: l.status,
url: l.url || ''
});
}
}
var searchInput = document.getElementById('catalogSearch');
var statusSelect = document.getElementById('catalogStatus');
var bodyEl = document.getElementById('catalogBody');
var countEl = document.getElementById('catalogCount');
var sortCol = 'phase';
var sortDir = 1;
function escapeHtml(str) {
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function render() {
var query = (searchInput.value || '').toLowerCase().trim();
var phaseFilter = phaseSelect.value;
var statusFilter = statusSelect.value;
var rows = allRows.filter(function (r) {
if (phaseFilter && String(r.phase) !== phaseFilter) return false;
if (statusFilter && r.status !== statusFilter) return false;
if (query) {
return r.name.toLowerCase().indexOf(query) !== -1 ||
r.phaseName.toLowerCase().indexOf(query) !== -1 ||
r.type.toLowerCase().indexOf(query) !== -1 ||
r.lang.toLowerCase().indexOf(query) !== -1;
}
return true;
});
rows.sort(function (a, b) {
var va, vb;
if (sortCol === 'phase') {
va = a.phase;
vb = b.phase;
} else if (sortCol === 'name') {
va = a.name.toLowerCase();
vb = b.name.toLowerCase();
} else if (sortCol === 'type') {
va = a.type.toLowerCase();
vb = b.type.toLowerCase();
} else if (sortCol === 'lang') {
va = a.lang.toLowerCase();
vb = b.lang.toLowerCase();
} else if (sortCol === 'status') {
va = a.status;
vb = b.status;
}
if (va < vb) return -1 * sortDir;
if (va > vb) return 1 * sortDir;
return 0;
});
countEl.textContent = rows.length + ' of ' + allRows.length + ' lessons';
if (rows.length === 0) {
bodyEl.innerHTML = '<tr><td colspan="5" class="catalog-empty">No lessons match your filters.</td></tr>';
return;
}
var html = '';
for (var i = 0; i < rows.length; i++) {
var r = rows[i];
html += '<tr>';
html += '<td><span class="catalog-phase-label">' + String(r.phase).padStart(2, '0') + '</span></td>';
if (r.url) {
html += '<td><a href="' + r.url + '" target="_blank" rel="noopener">' + escapeHtml(r.name) + '</a></td>';
} else {
html += '<td>' + escapeHtml(r.name) + '</td>';
}
html += '<td>' + escapeHtml(r.type) + '</td>';
html += '<td><span class="catalog-lang">' + escapeHtml(r.lang) + '</span></td>';
html += '<td><span class="catalog-status-dot ' + r.status + '"></span>' + r.status + '</td>';
html += '</tr>';
}
bodyEl.innerHTML = html;
}
var urlQuery = new URLSearchParams(window.location.search).get('q');
if (urlQuery) {
searchInput.value = urlQuery;
try { searchInput.setSelectionRange(urlQuery.length, urlQuery.length); } catch (_) {}
try { searchInput.focus({ preventScroll: true }); } catch (_) { searchInput.focus(); }
}
searchInput.addEventListener('input', render);
phaseSelect.addEventListener('change', render);
statusSelect.addEventListener('change', render);
document.querySelectorAll('.catalog-table th[data-sort]').forEach(function (th) {
th.addEventListener('click', function () {
var col = th.getAttribute('data-sort');
if (sortCol === col) {
sortDir = sortDir * -1;
} else {
sortCol = col;
sortDir = 1;
}
document.querySelectorAll('.catalog-table th .sort-arrow').forEach(function (a) {
a.textContent = '';
});
th.querySelector('.sort-arrow').textContent = sortDir === 1 ? ' \u25B2' : ' \u25BC';
render();
});
});
render();
});
})();
</script>
<script defer src="https://va.vercel-scripts.com/v1/script.js"></script>
</body>
</html>