项目8 · 个人 React 与 AI 学习空间
把八个项目学到的页面、数据、AI 与交付能力收进一个真正能用的个人学习空间:记笔记、练练习、问 AI、看进度。
- 提交需求验收表:把学习台、课程笔记、练习记录、AI 问答、进度统计五个页面与 notes / progress / ask 三类接口拆成可逐条勾选的验收项,写清完成标准与不做什么
- 独立完成关键决策:在 SQLite 持久化、LIKE 检索加片段引用、mock / 真实模型双通道、JSON 接口与纯 CSS 图表之间做取舍,并在架构说明里给出理由与代价
- 用 Next.js 16 App Router 组织五个页面,划清 Server Component 直接读库、Client Component 负责交互、Route Handler 负责写入的渲染边界
- 用 node:sqlite 建 notes / progress / chat 三张表,写基于 PRAGMA user_version 的迁移与可重复执行的 seed,不依赖外部数据库服务
- 完成笔记 CRUD、关键词搜索、标签筛选与搜索评分排序,搜索命中后能截取可读片段
- 完成练习记录的列表、状态流转与进度聚合,把 react / nextjs / ai / python 四条学习线放进同一张进度视图
- 实现基于笔记检索的 AI 问答:无 key 走本地 mock,有 key 走 OpenAI 兼容接口;回答附编号引用,无命中时明确说笔记不足而不是编造
- 写架构说明与运行脚本:记录渲染边界、数据流、关键决策和失败兜底,用一条 verify 命令跑通单测与结构检查,solution 构建通过、starter 保留缺口
- 01学习台 /:今日继续、笔记与练习统计、今日投入、四条学习线进度、最近笔记
- 02课程笔记 /notes:笔记列表、新建、编辑、删除、关键词搜索、标签筛选、课程编号标记
- 03练习记录 /practice:练习列表、状态筛选、todo / doing / done 状态流转、新增练习记录、投入分钟数
- 04AI 问答 /ask:基于笔记检索的问答、mock / 真实模型模式标识、编号引用与片段摘录、历史问答、无命中提示
- 05进度统计 /progress:完成率、累计与今日投入、按学习线完成度、状态分布、最近 7 天投入,全部用纯 CSS 条形
- 06接口 /api/notes 与 /api/notes/[id]:列表 / 搜索 / 标签过滤、新建、更新、删除
- 07接口 /api/progress 与 /api/progress/[id]:列表与聚合摘要、新增、状态更新
- 08接口 /api/ask:POST 问题,返回 answer / mode / citations,并写入 chat 历史;采用 JSON 响应而非 SSE
docs/requirements.md 覆盖五个页面、三类接口与十项里程碑,每条写清用户动作、可观察结果、完成标准和明确不做的事;至少包含空状态、错误状态、无 AI key、无检索命中四类边界。
/、/notes、/practice、/ask、/progress 均可访问,导航可切换,未知路径进入 not-found;页面有统一页头与空状态,starter 保持静态壳和 TODO,solution 的 npm run build 通过。
lib/db.ts 使用 node:sqlite 打开 data/study.db,通过 PRAGMA user_version 执行迁移,建出 notes / progress / chat 三张表;npm run seed 读取 fixtures/seed-notes.json 与 seed-progress.json,重复执行按标题 / exercise_slug 跳过,不产生重复数据。
首页展示笔记数、完成练习数、累计投入、今日投入;今日继续同时给出最近编辑的笔记与正在进行的练习,没有数据时显示引导空状态;四条学习线的完成度条形与真实数据一致。
笔记页可新建、编辑、删除;标题与正文必填,标签可逗号分隔;搜索同时匹配标题、正文、标签和课程编号,按评分排序并显示命中片段;标签栏可筛选,搜索与标签可组合,URL 参数 q / tag 能还原当前筛选。
练习页按 todo / doing / done 筛选,状态按钮可互相切换并立即持久化;新增练习校验标题、学习线与分钟数;页头统计完成数、进行中与累计投入,刷新后状态不丢。
POST /api/ask 先用 LIKE 检索笔记并评分,取前 4 条生成 [1]…[n] 引用;无 AI_API_KEY 时返回基于片段的 mock 答案,配置后调用 {AI_BASE_URL}/chat/completions 并把片段注入 system;回答与引用写入 chat,无命中时明确提示笔记不足且不调用模型。
进度页展示总完成率、累计 / 今日投入、按学习线的完成度、todo / doing / done 分布与最近 7 天投入;所有图表用 div + width / height 的纯 CSS 条形实现,不引入图表库,空数据不出现 NaN。
docs/architecture.md 说明渲染边界(Server Component 读库、Client Component 交互、Route Handler 写入)、三条数据流(笔记 CRUD、进度流转、问答与引用)、关键决策及代价、失败兜底与目录职责;能据此定位任意一个页面请求从点击到落库的路径。
README 给出一条可复制的 npm install && npm run seed && npm run dev;npm run verify 依次跑纯函数单测与结构检查;solution 的 npm run build 通过、单测全过、node tests/check.mjs 判定 solution 全过且 starter 存在缺口,验收表逐项签字并记录遗留项。
# 个人 React 与 AI 学习空间(p8-capstone)
八个项目里的最后一个:用 Next.js 16 + React 19 + `node:sqlite` 做一个单用户的个人学习空间。学习台、课程笔记、练习记录、AI 问答、进度统计五个页面都接真实数据;AI 问答先用笔记检索,再把片段编号交给 mock 或 OpenAI 兼容接口。
这个项目的重点不是堆功能,而是独立完成一次「需求 → 数据 → 页面 → 验收 → 交付」的闭环:写需求验收表、做关键决策、写架构说明、留下能一条命令跑通的运行脚本。
## 学习目标
- 用 App Router 组织五个页面,划清 Server Component 读库、Client Component 交互、Route Handler 写入的边界。
- 用 `node:sqlite` 建 `notes` / `progress` / `chat` 三张表,写 `PRAGMA user_version` 迁移与可重复执行的 seed。
- 完成笔记 CRUD、LIKE 检索、评分排序、标签筛选与片段截取。
- 完成练习状态流转与进度聚合,把 react / nextjs / ai / python 四条学习线放进同一张视图。
- 实现基于笔记检索的 AI 问答:无 key 走本地 mock,有 key 走 OpenAI 兼容接口,回答附 `[n]` 引用。
- 用纯 CSS 条形做统计图表,不引入图表库。
- 写需求验收表、架构说明和运行脚本,完成毕业作品交付。
## 环境要求
- Node.js **24+**(`node:sqlite` 在此版本稳定;不需要安装数据库服务)
- npm 10+
- 不依赖 Google Fonts,构建不需要访问外网字体
## 快速开始
### 运行参考实现(solution)
```bash
cd solution
npm install --no-audit --no-fund
npm run seed
npm run dev
```
打开 `http://localhost:3000`。一条命令版本:
```bash
cd solution && npm install --no-audit --no-fund && npm run seed && npm run dev
```
**AI key 说明**:不配置 `AI_API_KEY` 也能完整走通问答,`/api/ask` 会返回基于笔记片段的本地 mock 回答。要接真实模型:
```bash
cp .env.example .env.local
# 填入 AI_API_KEY,必要时改 AI_BASE_URL / AI_MODEL
```
配置后调用 `{AI_BASE_URL}/chat/completions`,默认 `https://api.openai.com/v1` 与 `gpt-4o-mini`。模型失败会自动回退到 mock,并在响应里带 `warning`。
### 运行起点(starter)
```bash
cd starter
npm install --no-audit --no-fund
npm run seed
npm run dev
```
起点只有静态学习台、导航外观和 `TODO.md`。`npm run seed` 只打印提示,不会写数据库;按 `TODO.md` 的 m1–m10 逐项补。
### 常用命令
| 命令 | 作用 |
| --- | --- |
| `npm run dev` | 启动开发服务器 |
| `npm run seed` | 建库、迁移并导入 `../fixtures/seed-notes.json` 与 `seed-progress.json`,已存在则跳过 |
| `npm run seed:reset` | 清空 notes / progress / chat 后重新导入 |
| `npm run build` | 生产构建 |
| `npm run start` | 启动生产服务器 |
| `npm run typecheck` | TypeScript 检查(solution) |
| `npm run verify` | 一条命令跑纯函数单测 + 结构检查(solution) |
## 目录结构
```text
p8-capstone/
├── README.md
├── starter/ # 可启动起点:静态学习台 + TODO.md
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── page.tsx # 静态壳 + 里程碑清单
│ │ └── globals.css
│ ├── scripts/seed.mjs # 只打印提示,等 m3 替换
│ ├── TODO.md # m1–m10 逐项缺口
│ ├── next.config.ts
│ └── package.json
├── solution/ # 完整参考实现
│ ├── app/
│ │ ├── page.tsx # 学习台:统计 + 今日继续 + 最近笔记
│ │ ├── notes/page.tsx # 笔记:CRUD + 搜索 + 标签
│ │ ├── practice/page.tsx # 练习:列表 + 状态流转
│ │ ├── ask/page.tsx # AI 问答:mock / 模型 + 引用
│ │ ├── progress/page.tsx # 统计:纯 CSS 条形
│ │ ├── api/
│ │ │ ├── notes/route.ts # GET / POST
│ │ │ ├── notes/[id]/route.ts # GET / PATCH / DELETE
│ │ │ ├── progress/route.ts # GET / POST
│ │ │ ├── progress/[id]/route.ts
│ │ │ └── ask/route.ts # POST,JSON 响应
│ │ └── globals.css
│ ├── components/
│ │ ├── site-nav.tsx
│ │ ├── page-head.tsx
│ │ ├── stat-card.tsx
│ │ ├── progress-charts.tsx # 纯 CSS 条形
│ │ ├── notes-workbench.tsx
│ │ ├── practice-board.tsx
│ │ └── ask-workbench.tsx
│ ├── lib/
│ │ ├── db.ts # node:sqlite + 迁移 + seed
│ │ ├── notes.ts # 笔记 SQL / LIKE 搜索
│ │ ├── practice.ts # 练习 SQL
│ │ ├── chats.ts # 问答历史
│ │ ├── search.ts # 评分 / 片段(纯函数)
│ │ ├── progress.ts # 聚合 / 按日(纯函数)
│ │ ├── citations.ts # 引用 / mock(纯函数)
│ │ ├── ask.ts # 问答编排 / 模型调用
│ │ ├── api.ts # 请求体与错误映射
│ │ ├── errors.ts
│ │ ├── format.ts
│ │ ├── study.ts
│ │ └── types.ts
│ ├── scripts/
│ │ ├── seed.ts
│ │ └── verify.mjs # 一条命令验收
│ ├── .env.example
│ ├── next.config.ts
│ └── package.json
├── tests/
│ ├── check.mjs # 结构 / API 特征 + 临时数据库检查
│ └── unit-study.test.mjs # 搜索 / 进度 / 引用纯函数单测
├── docs/
│ ├── requirements.md # 需求验收表
│ └── architecture.md # 渲染边界 / 数据流 / 关键决策
└── fixtures/
├── seed-notes.json # 6 条示例笔记
└── seed-progress.json # 10 条示例练习
```
`node_modules/`、`.next/`、`data/*.db` 是运行产物,不提交。
## 验收流程
### 自动检查
```bash
# 在 p8-capstone 根目录
node tests/unit-study.test.mjs # 纯函数单测,node 直跑
node tests/check.mjs # solution 全过,starter 保留缺口
```
或者从 solution 目录:
```bash
npm run verify
```
### 手动走查
1. **m1 需求验收表**:打开 `docs/requirements.md`,确认五个页面、三类接口、十条里程碑、四类边界都在。
2. **m2 页面骨架**:依次访问 `/`、`/notes`、`/practice`、`/ask`、`/progress`,导航高亮正确,随便输一个未知路径进 404。
3. **m3 数据层**:删掉 `solution/data/study.db`,跑两次 `npm run seed`;第一次新增 6 条笔记 / 10 条练习,第二次全部跳过。
4. **m4 学习台**:统计卡与数据库一致;「今日继续」显示最近笔记和 doing 练习;四条学习线条形有数据。
5. **m5 笔记**:新建一条带标签的笔记;搜索「状态」看排序;点标签筛选;编辑后刷新;删除并确认。
6. **m6 练习**:新增一条练习;在 todo / doing / done 之间切换;刷新页面状态仍在。
7. **m7 AI 问答**:不配 key 问「渲染边界怎么记的」看 mock + 引用;再问「火星天气」看「笔记不足」;刷新看历史还在。
8. **m8 统计**:检查完成率、状态分布、7 天柱状图;空数据不出现 `NaN`。
9. **m9 架构说明**:打开 `docs/architecture.md`,能按文档找到一条请求从点击到落库的路径。
10. **m10 运行脚本**:`npm run build`、`npm run verify` 通过;starter 的 `npm run build` 也能通过但检查有缺口。
逐项验收标准见 `docs/requirements.md`。
## 实现要点
- **渲染边界**:页面 Server Component 直接读 SQLite,首屏就是完整 HTML;只有表单、搜索、状态切换和问答走 Route Handler。页面统一 `dynamic = "force-dynamic"`,避免构建时把某一次数据库结果静态化。
- **搜索**:SQL `LIKE` 先筛候选,`scoreNote` 再按标题 / 标签 / 课程编号 / 正文出现次数评分,`buildSnippet` 截命中片段。
- **AI 问答**:`answerQuestion` 先检索、再装配 `[1]…[4]` 引用;无命中直接说笔记不足,不调用模型;无 key 走 mock,有 key 走 OpenAI 兼容;每次问答写入 `chat`。
- **引用**:`citations` 存的是快照(noteId、标题、摘录、标签、课程编号),笔记删除后历史回答仍可读。
- **进度**:`summarizeProgress` 与 `minutesByDay` 是纯函数,页面、API、单测共用同一口径;图表只用 `div` 的宽度 / 高度。
## 交付物
- `docs/requirements.md`:需求验收表。
- `docs/architecture.md`:架构说明。
- `solution/scripts/verify.mjs`:一条命令跑单测与结构检查。
- `solution`:完整实现,`npm run build` 通过。
- `starter`:可启动起点,保留 m1–m10 缺口。
notes(id, title, content, tags, course_code, created_at, updated_at) 保存课程笔记,tags 用逗号分隔的文本存储,course_code 可关联 R / N / A / E 课程编号;progress(id, exercise_slug, title, track, status, minutes, updated_at) 保存练习记录,exercise_slug 唯一,track 取 react / nextjs / ai / python,status 取 todo / doing / done,minutes 为该练习累计投入;chat(id, question, answer, mode, citations, created_at) 保存问答历史,mode 取 mock / model / none,citations 为 JSON 字符串,每条包含 marker、noteId、title、quote、tags、courseCode、score。notes 与 chat 没有外键关系,引用通过 citations 里的 noteId 保留快照式关联,笔记删除后历史引用仍可读。搜索先用 SQL LIKE 对标题、正文、标签、课程编号做候选过滤,再用纯函数评分排序;进度页由 progress 行聚合,不额外建统计表。