Skip to content

Commit 69f3aaa

Browse files
change: Playground hosts the six library sandboxes; lab grid back on the course home
As requested: - /playground/ is now the library sandboxes from the merged playgrounds branch: NumPy, matplotlib, pandas, PyTorch, Keras and TensorFlow as tabs (arrow keys), each in a full-width two-column layout with a link to its note. The library is kept in the URL hash (survives full screen) and is prefetched with the page. - The home page gets its Course map / Lab grid switch back (remembered); the lab grid is the toolkit grid, now with its own heading. Also: pandas NaN cells and inline code in notes reach AA contrast; the editor's shortcut hint moves under the editor so the header doesn't wrap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent bcf5b53 commit 69f3aaa

16 files changed

Lines changed: 295 additions & 25 deletions

File tree

‎CLAUDE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ GitHub Actions(`.github/workflows/deploy.yml`)只执行 Node 22 下的 `npm
8484

8585
- `src/lib/sitemap.ts` —— 唯一页面清单:模块(讲次 / 试卷)、笔记、拓展、URL、旧链接映射(`#/course/x` → `/x/`,`/lab/*.html` 跳转页)。`vite.config.js` 据此生成 HTML 入口。
8686
- `src/site/watch/` —— 讲解视频:`Player.tsx`(时间轴播放器)、`stage.tsx`(1600×900 舞台坐标、配色、`At`/`Tex`)、`episodes/*.tsx`(每集一组场景,`render(p)` 为场景进度的纯函数)。新增一集:计算放 `src/lib/` 配测试 → 写 episode → 在 `episodes/index.ts`、`useEpisode.ts` 的 `LOADERS` 与 `sitemap.ts` 的 `EPISODES` 注册 → `watch.<id>.*` 文案(字幕键 `scenes.<scene>.c<k>`,数量与 `cues` 对齐)。
87-
- `src/site/playground/` + `src/data/playgrounds/` —— 资料包里 matplotlib / PyTorch / Keras / TensorFlow / pandas 笔记旁的实验台:`Playground.tsx`(目录、编辑器、步骤、输出、全屏)、`views.tsx`(按事件类型画调用网格 / 图 / DataFrame / 形状流 / 计算图 / 训练曲线)、`FigureView.tsx`、`loaders.ts`(笔记 id → 配置 + `playground.<id>` 文案包);示例配置在 `src/data/playgrounds/<库>.ts`,`playgrounds.test.ts` 会运行全部示例。库实现在 `src/lib/pyPlot.ts`、`pyTorch.ts`、`pyKeras.ts`(含 tensorflow)、`pyPandas.ts`,张量与自动求导在 `tensorCore.ts`;它们通过 minipy 的 `PyLib` / `PyObj` / `HostClass` 接入,改输出格式时对照真实库(各 `*.test.ts` 存有真实输出)。
87+
- `src/site/playground/` + `src/data/playgrounds/` —— 库实验台:`/playground/`(`pages/PlaygroundPage.tsx`,库清单与加载在 `playground/libs.ts`,NumPy 用 `NumpyApiPanel`,其余五个库用 `Playground` 的 `wide` 两栏布局,所选库在 URL hash)以及资料包里 matplotlib / PyTorch / Keras / TensorFlow / pandas 笔记旁的实验台:`Playground.tsx`(目录、编辑器、步骤、输出、全屏)、`views.tsx`(按事件类型画调用网格 / 图 / DataFrame / 形状流 / 计算图 / 训练曲线)、`FigureView.tsx`、`loaders.ts`(笔记 id → 配置 + `playground.<id>` 文案包);示例配置在 `src/data/playgrounds/<库>.ts`,`playgrounds.test.ts` 会运行全部示例。库实现在 `src/lib/pyPlot.ts`、`pyTorch.ts`、`pyKeras.ts`(含 tensorflow)、`pyPandas.ts`,张量与自动求导在 `tensorCore.ts`;它们通过 minipy 的 `PyLib` / `PyObj` / `HostClass` 接入,改输出格式时对照真实库(各 `*.test.ts` 存有真实输出)。
8888
- `src/data/drill.ts`、`src/data/formulas.ts` —— 概念自测(`/drill/`)与公式表(`/formulas/`)的数据,文案在 `drill.*`、`formulas.*`;`/papers/` 考点地图由 `MODULES[].exams` 反推。对应测试会用库函数核对可计算的答案、并检查 KaTeX 能排版每条公式。
8989
- `src/site/` —— 全站共用入口 `main.tsx`(按 `<body data-page>` 懒加载页面)、`Shell.tsx`(顶栏 / 页脚 / 语言)、`ui.tsx`(PageBar、LabPage、Workspace、`<Ans>` 自测格等)、`pages/`(Home / Playground / Module / Notes / Extend / Watch / Drill / Formulas / Papers / 404;导航为「课程 · 实验场 · 更多」)、`modules/`(12 个考点实验页,含 Affine 与 PyTorch)。新增实验页:计算放 `src/lib/` 配测试 → `site/modules/Xxx.tsx`(用 `LabPage`)→ `sitemap.ts` 的 `ModuleId` 与 `MODULES` → `pageLoaders.ts` 的 `LAB_LOADERS` → `site.modules.<id>` 与 `lab.<id>` 文案。`/lab/*.html` 旧跳转只覆盖最初 10 个原型。
9090
- `src/modules/*.tsx` —— 8 个经典课程模块 + Package / Extend 视图(感知机与 PyTorch 由 `site/modules/` 的实验页承担)。动画一律用 `m.*`,不要用 `motion.*`;用到 `m.*` 的页面要包在 `components/Motion.tsx` 里(入口不再全局提供 LazyMotion)。

‎docs/log.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77

88
## 进展时间线
99

10+
### 2026-10-08 — 合并库实验台;实验场改为六个库的沙盒;实验网格回到课程首页(按用户要求)
11+
- **merge**:合并 `worktree-playgrounds`(matplotlib / PyTorch / Keras / TensorFlow / pandas 实验台、Matplotlib 笔记、解释器扩展,测试 627 个)。`docs/design.md` 的改动未合入(用户要求忽略该文件)。
12+
- **change**:`/playground/` 现在是库沙盒页:顶部六张库卡片(NumPy、matplotlib、pandas、PyTorch、Keras、TensorFlow,按讲次排序,tablist + 方向键),下面是整宽两栏的实验台(左:目录、编辑器、输出;右:步骤与图),附「阅读对应笔记」链接。所选库写在网址 hash(`/playground/#keras`),全屏退出后恢复;入口在加载页面代码的同时预取该库。笔记页旁的实验台保留。
13+
- **change**:首页恢复「课程地图 / 实验网格」切换(记在 `knowcs-home-view`),实验网格为上一版放在实验场的工具网格,新增标题。
14+
- **a11y**:pandas 表格的 NaN、笔记行内代码颜色加深到 AA。浏览器检查:六个库在 1440 / 390 宽度无控制台错误、无横向滚动、无原始键,繁体文案正常。
15+
1016
### 2026-10-08 — 导航、实验场、Alpha-Beta 布局、思考暂停修复(按用户要求)
1117
- **change**:顶部导航只剩「课程 · 实验场 · 更多」;「更多」依次为视频、资料包、自测、拓展(当前所在栏目会显示在按钮上)。新增 `/playground/` 实验场页:仿参考图的工具网格(按领域筛选、带计数的标签、每个实验一张线稿插图卡片、等宽英文副标题、试卷标签、READY / 已打开 与「进入实验 →」)。首页去掉「58 道判断自测」「公式表」入口与离线保存按钮(连同离线清单、Service Worker 保存逻辑与相关文案)。经典模块的考试提示移入交互卡片顶部,模块占满宽度;中文标签改为「考试提示」。
1218
- **change**:Alpha-Beta 页仿用户提供的 HTML 重排:深色工作区、标题与状态图例、左侧预设与树画布(MAX / MIN / 叶子层图例)、右侧控制台与实时解释(α / β 与当前剪枝条件)、整宽叶子滑块沙盒,并新增可点击跳转的「算法运行追踪链」。

‎src/index.css‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
.note-prose thead { @apply bg-slate-50; }
2525
.note-prose th { @apply px-3 py-2 text-left font-bold text-slate-700 border-b border-slate-200 whitespace-nowrap; }
2626
.note-prose td { @apply px-3 py-2 text-slate-600 border-b border-slate-100 align-top; }
27-
.note-prose :not(pre) > code { @apply px-1.5 py-0.5 bg-slate-100 text-rose-600 rounded text-[0.85em] font-mono; }
27+
.note-prose :not(pre) > code { @apply px-1.5 py-0.5 bg-slate-100 text-rose-700 rounded text-[0.85em] font-mono; }
2828
.note-prose .np-block { @apply relative; }
2929
.note-prose .np-run { @apply absolute right-2 top-2 rounded-lg bg-blue-600 px-2.5 py-1 font-sans text-xs font-bold text-white shadow hover:bg-blue-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-300; }
3030
.note-prose code.np-link { @apply cursor-pointer bg-blue-50 text-blue-700 underline decoration-blue-300 decoration-dotted underline-offset-2 hover:bg-blue-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400; }

‎src/locales/en.json‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -881,11 +881,26 @@
881881
"neural": "Neural networks",
882882
"vision": "Images & CNNs",
883883
"search": "Game search"
884-
}
885-
}
884+
},
885+
"title": "Every lab, by topic"
886+
},
887+
"view_label": "Home page layout",
888+
"view_map": "Course map",
889+
"view_lab": "Lab grid"
886890
},
887891
"playground": {
888-
"title": "Playground"
892+
"title": "Playground",
893+
"lead": "Real Python in your browser. Pick a library, edit the code, and every call becomes a step you can open: the array cells it used, the figure it drew, the layer shapes or the gradient graph.",
894+
"libs_label": "Libraries",
895+
"read_note": "Read the {{name}} note",
896+
"libs": {
897+
"numpy": "Arrays, indexing, broadcasting",
898+
"matplotlib": "Plots, images, subplots",
899+
"pandas": "Tables, selection, groupby",
900+
"pytorch": "Tensors, autograd, training loops",
901+
"keras": "Models, summary(), fit",
902+
"tensorflow": "Tensors, Variables, GradientTape"
903+
}
889904
}
890905
},
891906
"lab": {

‎src/locales/zh-HK.json‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -881,11 +881,26 @@
881881
"neural": "神經網絡",
882882
"vision": "圖像與 CNN",
883883
"search": "博弈搜索"
884-
}
885-
}
884+
},
885+
"title": "按主題瀏覽全部實驗"
886+
},
887+
"view_label": "首頁佈局",
888+
"view_map": "課程地圖",
889+
"view_lab": "實驗網格"
886890
},
887891
"playground": {
888-
"title": "實驗場"
892+
"title": "實驗場",
893+
"lead": "在瀏覽器裏執行真正的 Python。選一個庫、改程式碼,每次調用都會變成一個可以點開的步驟:用到了哪些陣列元素、畫出了什麼圖、各層的形狀或梯度計算圖。",
894+
"libs_label": "庫",
895+
"read_note": "閲讀 {{name}} 筆記",
896+
"libs": {
897+
"numpy": "陣列、索引、廣播",
898+
"matplotlib": "折線圖、圖像、子圖",
899+
"pandas": "表格、選取、分組",
900+
"pytorch": "張量、自動求導、訓練循環",
901+
"keras": "模型、summary()、fit",
902+
"tensorflow": "張量、Variable、GradientTape"
903+
}
889904
}
890905
},
891906
"lab": {

‎src/locales/zh.json‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -881,11 +881,26 @@
881881
"neural": "神经网络",
882882
"vision": "图像与 CNN",
883883
"search": "博弈搜索"
884-
}
885-
}
884+
},
885+
"title": "按主题浏览全部实验"
886+
},
887+
"view_label": "首页布局",
888+
"view_map": "课程地图",
889+
"view_lab": "实验网格"
886890
},
887891
"playground": {
888-
"title": "实验场"
892+
"title": "实验场",
893+
"lead": "在浏览器里运行真正的 Python。选一个库、改代码,每次调用都会变成一个可以点开的步骤:用到了哪些数组元素、画出了什么图、各层的形状或梯度计算图。",
894+
"libs_label": "库",
895+
"read_note": "阅读 {{name}} 笔记",
896+
"libs": {
897+
"numpy": "数组、索引、广播",
898+
"matplotlib": "折线图、图像、子图",
899+
"pandas": "表格、选取、分组",
900+
"pytorch": "张量、自动求导、训练循环",
901+
"keras": "模型、summary()、fit",
902+
"tensorflow": "张量、Variable、GradientTape"
903+
}
889904
}
890905
},
891906
"lab": {

‎src/modules/NumpyApiPanel.tsx‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ interface PanelProps {
2020
initialEntry?: string | null
2121
/** Shown when the note linked to a namespace outside the sandbox */
2222
missing?: string | null
23+
/** two columns on wide screens (the Playground page) instead of the narrow panel beside the note */
24+
wide?: boolean
2325
}
2426

25-
export const NumpyApiPanel = ({ initialCat, initialEntry, missing }: PanelProps) => {
27+
export const NumpyApiPanel = ({ initialCat, initialEntry, missing, wide }: PanelProps) => {
2628
const { t } = useTranslation()
2729
const start = NUMPY_APIS.find((e) => e.id === initialEntry)
2830
const [cat, setCat] = useState<ApiCat>(start?.cat ?? initialCat ?? 'reduce')
@@ -225,6 +227,22 @@ export const NumpyApiPanel = ({ initialCat, initialEntry, missing }: PanelProps)
225227
)
226228
}
227229

230+
if (wide) {
231+
return (
232+
<section ref={ref} aria-label={t('numpy_api.title')} className="rounded-[1.5rem] border border-slate-200 bg-white p-4 shadow-xl shadow-slate-200/40 sm:p-6">
233+
{header}
234+
<div className="mt-4 grid grid-cols-[minmax(0,1fr)] gap-5 lg:grid-cols-[minmax(0,5fr)_minmax(0,7fr)]">
235+
<div className="min-w-0 space-y-4">
236+
{left}
237+
{output}
238+
</div>
239+
<div className="min-w-0 space-y-4 lg:border-l lg:border-slate-100 lg:pl-5">{right}</div>
240+
</div>
241+
<p className="mt-4 text-[11px] leading-relaxed text-slate-500">{t('numpy_api.sandbox_note')}</p>
242+
</section>
243+
)
244+
}
245+
228246
return (
229247
<aside ref={ref} aria-label={t('numpy_api.title')} className="space-y-4 rounded-[1.5rem] border border-slate-200 bg-white p-4 shadow-xl shadow-slate-200/40 sm:p-5">
230248
{header}

‎src/site/pageLoaders.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export const PAGE_LOADERS = {
5959
export function preloadPage(page: string, id: string): void {
6060
const kind = page === 'note' ? 'notes' : page === 'extend-item' ? 'extend' : page === 'watch-item' ? 'watch' : page
6161
void (PAGE_LOADERS as Record<string, (() => Promise<unknown>) | undefined>)[kind]?.()
62+
if (page === 'playground') void import('./playground/libs').then((m) => m.loadLib(m.libFromHash()))
6263
if (page !== 'module') return
6364
const lab = LAB_LOADERS[id as ModuleId]
6465
if (lab) void lab()

‎src/site/pages/HomeLab.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,8 @@ export default function HomeLab() {
271271
)
272272
return (
273273
<div className="mx-auto max-w-[1280px] px-4 sm:px-8">
274-
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 pb-5">
274+
<h1 className="text-[clamp(26px,3.2vw,36px)] font-black tracking-tight text-slate-900">{t('site.home.lab.title')}</h1>
275+
<div className="mt-2 flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 pb-5">
275276
<p className="max-w-3xl text-[15px] text-slate-600">{t('site.home.lab.lead')}</p>
276277
<span className="flex items-center gap-2 font-mono text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">
277278
<i className="inline-block h-2 w-2 rounded-full bg-emerald-500" aria-hidden />

‎src/site/pages/HomePage.tsx‎

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,47 @@
11
import { Suspense, lazy, useEffect, useState } from 'react'
22
import { useTranslation } from 'react-i18next'
3+
import { LayoutGrid, List } from 'lucide-react'
34
import { EPISODES, LECTURES, MODULES, PAPERS, lectureScope, papersUrl, moduleUrl, noteUrl, watchUrl } from '../../lib/sitemap'
45
import type { ModuleInfo } from '../../lib/sitemap'
56
import { readVisited } from '../ui'
67

78
// the explainer thumbnails pull in the animation code and KaTeX: load them after the rest of the page
89
const WatchStrip = lazy(() => import('../watch/WatchStrip'))
10+
const HomeLab = lazy(() => import('./HomeLab'))
11+
12+
type View = 'map' | 'lab'
13+
const VIEW_KEY = 'knowcs-home-view'
14+
const readView = (): View => {
15+
try {
16+
return localStorage.getItem(VIEW_KEY) === 'lab' ? 'lab' : 'map'
17+
} catch {
18+
return 'map'
19+
}
20+
}
21+
const GRID = { backgroundImage: 'linear-gradient(#e7ebf1 1px, transparent 1px), linear-gradient(90deg, #e7ebf1 1px, transparent 1px)', backgroundSize: '32px 32px' }
22+
23+
/** Course map (lectures in order) or lab grid (every lab by area); the choice is remembered. */
24+
function ViewSwitch({ view, onChange }: { view: View; onChange: (v: View) => void }) {
25+
const { t } = useTranslation()
26+
return (
27+
<div className="mx-auto flex max-w-[1280px] justify-end px-4 pt-4 sm:px-8">
28+
<div className="inline-flex gap-1 rounded-xl border border-slate-200 bg-white p-1 text-sm font-bold shadow-sm" role="group" aria-label={t('site.home.view_label')}>
29+
{(['map', 'lab'] as const).map((v) => (
30+
<button
31+
key={v}
32+
type="button"
33+
onClick={() => onChange(v)}
34+
aria-pressed={view === v}
35+
className={`flex items-center gap-1.5 rounded-lg px-3 py-1.5 transition ${view === v ? 'bg-slate-900 text-white' : 'text-slate-600 hover:bg-slate-100'}`}
36+
>
37+
{v === 'map' ? <List size={15} aria-hidden /> : <LayoutGrid size={15} aria-hidden />}
38+
{t(`site.home.view_${v}`)}
39+
</button>
40+
))}
41+
</div>
42+
</div>
43+
)
44+
}
945

1046
type Scope = 'all' | 'mid' | 'final'
1147

@@ -43,15 +79,36 @@ export default function HomePage() {
4379
const [scope, setScope] = useState<Scope>('all')
4480
const [q, setQ] = useState('')
4581
const [visited] = useState(readVisited)
82+
const [view, setViewState] = useState<View>(readView)
83+
const setView = (v: View) => {
84+
setViewState(v)
85+
try {
86+
localStorage.setItem(VIEW_KEY, v)
87+
} catch {
88+
// private mode: the choice just isn't remembered
89+
}
90+
}
4691
const query = q.trim().toLowerCase()
4792
useEffect(() => {
4893
document.title = `KnowCS · ${t('site.home.tab_title')}`
4994
}, [t])
5095
const matches = (m: ModuleInfo) => !query || `${t(`site.modules.${m.id}.title`)} ${t(`site.modules.${m.id}.blurb`)}`.toLowerCase().includes(query)
5196

97+
if (view === 'lab') {
98+
return (
99+
<main className="min-h-screen pb-10" style={GRID}>
100+
<ViewSwitch view={view} onChange={setView} />
101+
<Suspense fallback={<div className="min-h-screen" />}>
102+
<HomeLab />
103+
</Suspense>
104+
</main>
105+
)
106+
}
107+
52108
return (
53109
<main className="pb-10">
54-
<section className="mx-auto max-w-[1280px] px-4 pb-6 pt-8 sm:px-8">
110+
<ViewSwitch view={view} onChange={setView} />
111+
<section className="mx-auto max-w-[1280px] px-4 pb-6 pt-2 sm:px-8">
55112
<div className="grid items-end gap-6 lg:grid-cols-[minmax(0,1fr)_420px]">
56113
<div>
57114
<p className="text-xs font-extrabold uppercase tracking-[0.14em] text-blue-600">{t('site.home.eyebrow')}</p>

0 commit comments

Comments
 (0)