打开一个装论文的文件夹,论文上架子。选中原文或讲解里的任何一句: 追问 → 小窗问清楚 → 收起继续读。 主文永远不变长;每个知识点的讨论单独保存、锚定出处、按书隔离,随时重开。
读论文时卡住的地方,通常在正文里只占一句话——但为了搞懂它,你要么在聊天窗口里另起一段对话(然后丢失上下文),要么把解释直接塞进正文(然后正文越读越长)。论文书房把这个循环反过来:问题留在原处,答案单独存,正文保持原样。
原文(永不变长) 讨论(锚定出处,独立保存)
───────────────── ──────────────────────────
门控循环单元通过更新门…
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ← 荧光笔标记 ┐
决定保留多少历史信息。 └─→ 「这句话里的更新门具体
怎么算的?」 ← 你问的
「更新门是一个 0~1 的
门控信号,公式是…」 ← 模型的回答
界面是纸与墨的语言:一张摊在浅灰书桌上的白纸,墨色正文,铅笔灰批注,唯一的颜色是划过纸面的荧光笔。没有侧边栏聊天流,没有气泡,没有蓝紫渐变。
前端 React + TypeScript + Vite,后端 FastAPI(同目录 backend/)。前端启动时自动探测后端:可达即进入 live 模式(真实模型,数据全在后端),不可达则退回 mock 演示模式(假流式,行为与真实一致,用来做交互开发和演示)。
先起后端(首次运行会在根目录自动生成 .env.local:本地 token、端口 8765、PAPERLEARN_PROVIDER=demo):
scripts/backend-dev.sh # → http://127.0.0.1:8765再起前端(另一个终端):
npm install
npm run dev -- --port 5179浏览器打开 http://localhost:5179/?layout=a。不起后端也能玩,会自动退回 mock 演示模式。
?layout=a:方案 A,贴近选区的浮窗?layout=b:方案 B,页边批注式侧栏- 顶栏的「追问布局」开关随时切换,两套方案共用同一份讨论数据
后端默认 PAPERLEARN_PROVIDER=demo(不调外部模型)。要接真实模型,在 .env.local 里显式配置——协议、地址、模型名、key 四项都要给,没有默认供应商:
PAPERLEARN_PROVIDER=messages # 或 chat_completions
PAPERLEARN_MODEL_BASE_URL=https://your-provider.example/anthropic
PAPERLEARN_MODEL_NAME=your-model
PAPERLEARN_MODEL_API_KEY=your-key.env.local 已被 .gitignore 覆盖,key 不会进仓库;后端也不会把密钥写进数据库或日志。
在 WSL 里跑、模型却在 Windows 宿主上?
后端出于安全只允许 loopback 的 http 上游(HTTPS 或 localhost/127.0.0.1/::1)。WSL NAT 下 Windows 宿主在网关 IP 上,不算 loopback,用 scripts/model-relay.sh 把它搬进 WSL 的 127.0.0.1 即可(需要 socat):
scripts/model-relay.sh # 127.0.0.1:8317 → Windows 宿主 :8317scripts/backend-dev.sh 会自动把 http://<WSL-IP>:5179 加进后端的来源白名单,不用手工配。
其他命令:npm run typecheck(tsc)、npm run lint(oxlint)、npm run build。
Windows 上直接跑也行,但要先在 Windows 侧重新
npm install一次(Vite 8 的 rolldown 是原生二进制,按平台安装)。
书架:顶栏「书架」或首次进入即是。点「选择文件夹导入」挑一个装论文的文件夹(.txt / .md / .markdown,递归扫描),勾选后上架;卡片显示每本书的阅读进度和疑问数,点封面继续读,拖文件进来也能上架。正文内容存在本地 IndexedDB,源文件不受影响。
阅读与追问:
- 在正文里选中任意一句话,松开鼠标后旁边出现「追问」;点它(或按 Enter)。
- 小窗打开,顶部钉着你选的那句话;直接输入问题,Enter 发送(Shift+Enter 换行),生成中再按一次 Esc 旁的圆钮可停止。
- 按 Esc 或「收起」;出处留下荧光笔标记和计数角标,页边有一个小圆点。
- 点标记、角标、页边点,或顶栏「讨论」列表,都能重开这条讨论。
- 讨论开着往下读,滚远了底部会浮出「回到出处」。
阅读体验:阅读进度发丝线、章节目录弹层(当前节高亮 + 各节追问计数)、字号三档(记忆)、阅读位置记忆与恢复。
假流式的演示暗号(写在问题里;只在 mock 演示模式生效,live 模式下是真实模型的回答):
| 暗号 | 效果 |
|---|---|
长 / 详细 / 展开 |
返回一段很长的回答,测小窗内部滚动 |
例子 / 比喻 |
类比式回答 |
失败 / error |
900ms 后报错,重试成功 |
慢 |
首字延迟 4 秒,看等待态 |
src/
data/paper.ts 样例讲解 + 两条预置讨论
data/idb.ts IndexedDB 封装(书的内容存这里,localStorage 只放索引)
data/parse.ts 文档解析:.md/.txt → 可锚定的 sections(标题/章节正则/兜底切块)
data/library.ts 书架状态:多本书、当前书、阅读位置(useSyncExternalStore)
lib/types.ts Anchor / Thread / ThreadMessage —— 与后端共享的数据形状
lib/selection.ts 选区 ↔ 文本锚点(TextQuoteSelector)↔ Range 的往返
lib/geometry.ts 锚点几何:矩形、布局变化重算
lib/mockStream.ts 假流式(后端不可达时的演示回退)
lib/sse.ts SSE 解析器(fetch + ReadableStream,支持 POST 起流与 Last-Event-ID)
lib/api.ts 后端门面客户端(ApiError)
store/threads.ts zustand:双模式(live/mock)、讨论、草稿、生成状态、持久化
components/ Library(书架)· TopBar · ReadingPane · MarksLayer · AskTrigger
TocButton · TextSizeButton · ReadingResume · usePopover
ThreadShell(共用)· ThreadWindow(A)· ThreadPanel(B)
SnapBackPill · ThreadList
styles/ tokens.css(设计 token)· base · app · reading · thread · library
docs/
ROADMAP.md 产品愿景、阶段计划与决策记录
DESIGN.md 设计方向、token、状态清单、「丝滑」验收证据
design/PRODUCT.md 产品总纲:领域模型、学习旅程、功能域详案
design/READER.md 阅读器设计(书架 → 阅读 → 追问)
design/CLIENT.md 桌面客户端形态与双栏对照布局方案
design/PHASE1-LEARNING.md 学习记忆设计稿(三态掌握标记)
API-CONTRACT.md 前后端接口契约(v0.3)
scripts/ backend-dev.sh · backend-smoke.sh · frontend-smoke.sh · model-relay.sh
backend/ 后端(FastAPI + SQLite)
- 锚点只依赖文本,不依赖 DOM:采用 W3C Web Annotation 的
TextQuoteSelector(exact+prefix+suffix)加消息 id。重新渲染、换布局、换字号、换字体之后,标记依然能找到原来那句话;同一句话出现多次时用前后文消歧。 - 主讲解与原文是两层:论文原文用衬线(Noto Serif SC),模型讲解与界面用无衬线(Noto Sans SC)——一眼分清「论文说的」和「模型解释的」。
- 讨论是非模态浮层:不遮罩、不锁滚动,背景照样读;浮窗用 Floating UI 的
offset → flip → shift → size逐级避让,永不伸出视口或压到顶栏。 - 收起不中断生成:
stop()与close()是两个独立操作;草稿持久化,刷新后原样回来。 - 公式不假装:起止点落在 KaTeX 内部的选区不允许追问(DOM 文本与可见文本不一致),跨公式的选区可以。
MIT。
字体 Noto Sans SC / Noto Serif SC / Inter 均为 OFL 开源字体;组件库 Semi Design;公式渲染 KaTeX;浮层定位 Floating UI;图标为自绘。