Skip to content

Feat/UI deepseek - #174

Open
SX-ele wants to merge 11 commits into
StarTrail-org:mainfrom
SX-ele:feat/ui-deepseek
Open

SX-ele wants to merge 11 commits into
StarTrail-org:mainfrom
SX-ele:feat/ui-deepseek

Conversation

@SX-ele

@SX-ele SX-ele commented Oct 8, 2026

Copy link
Copy Markdown

No description provided.

SX-ele added 9 commits October 7, 2026 15:25
- pixelrag.yaml: local-source config to build the papers knowledge base index
- e2e_test/: sample PDF and matching config for end-to-end testing
…ient

- embed_cpu/api: use bfloat16 on CPU (fits 16GB RAM); .float() before numpy cast

- embed_cpu: persist/resume embed checkpoint so interrupted runs continue

- pipelines/pdf: explicit UTF-8 encoding for text/json I/O

- CLAUDE.md: expand layout/commands/architecture docs

- add local knowledge-base query client (query.py, pixelrag_client.py, *.bat, 使用说明)

- .gitignore: exclude papers/, knowledge_index/, e2e_test/index/
Retrieve Top-K page screenshots, send them as multi-image input to
DeepSeek-V4.1-Flash (deepseek-v4-flash) via the anthropic SDK, stream
the answer into the chat UI, and show citation cards below. Falls back
to citation-only display when the VLM is unavailable.
…esktop client

- split styling into pixelrag_theme (tokens) / pixelrag_widgets (PIL render +
  interaction primitives) / pixelrag_client (business + assembly)
- single FONT_SCALE knob drives both tk and Pillow text so they stay in sync
- query history: JSON store under ~/.pixelrag, left-hand panel, up/down recall
- VLM_MAX_TOKENS 2048 -> 8192: thinking tokens count against the cap, which is
  why deepseek-flash used to return an empty answer
- type scale: add bubble / answer / input / side tokens. The user's own
  question is now the largest thing on screen, while answers and the input
  box drop a step below body -- long-form text reads better smaller, and
  navigation should sit behind the content it navigates.
- history panel -> in-window left sidebar (260px). Hover reveals "ask
  again" in a fixed-width slot so the question text never shifts; each row
  has a right-click menu; the scrollbar shows only when the list overflows.
- history entries now keep their citation refs, so clicking one restores
  that whole exchange from disk -- no re-retrieval, no model call.
- window 1140x800 (min width 880) to make room for the sidebar.
对齐 chat.deepseek.com 的视觉规格,分四步落地:

1. 色板换成 DeepSeek 一套(冷灰侧栏 #F9F9F9 + 白底 + #4D6BFE),深色板同步;
   主色拆成 accent(填充/边框)与 accent_text(白底上的文字)—— #4D6BFE 落白底
   只有 4.33:1,不够正文级,文字另取 #3D5AE0(5.6:1)。
2. 字号改 px 原生:TYPE 直接存 DeepSeek 的像素值,pt() 负责 px→pt,肉眼可比。
   同时修掉 Pillow 侧字号小 31% 的 DPI bug —— font_px() 原先按 96dpi 写死 4/3,
   本机 tk scaling 实为 1.93,导致用户气泡与所有按钮的字一直偏小。现在改问 tk 实测值。
3. 行高令牌 LINE_HEIGHT=1.6。tk 的 spacing2 会被对半分给折点两侧,所以只有
   spacing1=ceil(gap/2) / spacing2=gap / spacing3=floor(gap/2) 这一组能让段首行、
   折行、硬换行的行盒完全相等(实测均为 37px)。
4. 消息列宽 768 且居中(原先只在答案里做了内缩,其余块通栏左对齐),消息间距
   收敛到单一来源 SPACE["msg"]=24,引用卡片组内保持 sm。

顺带修 font_mono 的语义:上一轮它已从"点"改成"设计像素",代码块还写着
font_mono(9),实际只剩 8.7 物理像素,改为 14。
5. 用户气泡浅蓝 #E8EEFF、圆角 8、内边距 10·14,右对齐;助手答案左对齐、无气泡,
   正文色 16px/1.6 —— 提问与答案同号,只靠气泡区分(严格照 DeepSeek 的层级)。
6. 引用卡片 / 代码块 / 骨架条改走令牌:卡片标题在白底上用 accent_text(小字不加粗
   时 4.33:1 的 accent 不够),代码块走 font_mono(px_size("code")) 与同一套行距。
7. 输入框从「裸 Text + 一条下划线」改成圆角方框:Canvas 铺圆角底图 + create_window
   放 Text,聚焦时描边换 accent。发送 / 停止按钮改成浮在框内右下角的胶囊(半径
   RADIUS_PILL,绘制时自动夹到高度的一半),发送按钮用 min_w 钉住宽度、位置预留
   _btn_reserve 在建模时一次算死,所以按钮在「发送 / 生成中 / 停止」之间切换时,
   输入框里的文字不会跟着伸缩。底栏也不再通栏:按侧栏开合与滚动条宽度让位,
   和消息列严格对齐(实测左右各差 0 / 1px)。
8. 侧栏条目换成 DeepSeek 的浅灰圆角块(hover=nav_hover、当前=nav_active,不再是
   蓝底蓝字),「+ 新对话」同样是整条胶囊,底色始终是正文色。圆角靠「铺满整行的
   Canvas + 沉到最底层 + 贴一张圆角图」实现,挖空色传列表底色,所以四角透出的是
   底色而不是方角 —— 这一点用截图像素逐角断言过,不是"看着像"。

验证:9 套自检全绿(侧栏 41 项、侧栏像素 45 项、对比度 40 项、字号 13 项、行高、
布局、输入框、交互链路、截图断言),深浅两套主题各出一组截图肉眼复核,真实启动
一次通过。ruff check / format 干净。
模型输出的答案里带着 Markdown 行内标记(`**小标题**` 和行首 `* 条目`),
直接进 Text 就是一串星号,读起来很吵。真机历史里实际只出现这两类标记
(5 处 `**N. 标题**` + 11 处 `*   条目`,没有 `#`、没有斜体、没有行内反引号),
所以只处理这两类:

- `**粗体**` → 去掉星号、套 tk 的 bold 标签(tmp 的粗体只借字重,字号与行高
  仍归 token 管,所以加粗后行盒不会变高);行首 `#`/`##` 标题同理去 `#` 并加粗。
- 行首 `*   条目` → 换成圆点 `•`,保住列表形态(星号消失但层次还在)。
- 不动的:`-` 列表、`a*b*c` 里的单个星号、`*.tmp` 通配符、代码块内的 `**`
  —— 代码块先被 `_split_code_blocks` 切走,正文里再出现 `**` 才当标记处理。

流式生成走同一条富文本管线,所以生成过程中也不会闪星号。测试里用**独立实现**
的 `strip_marks()` 与渲染结果逐字比对,证明只去掉了标记字符、正文一个不少。

验证:11 套自检全绿(新增 mdcheck 一套,含"渲染结果 == 去标记后的原文"的
逐字比对 + 截图肉眼复核),ruff check / format 干净。

注:复制答案仍是原始 Markdown(带星号),这是有意的 —— 粘到别处还得能用。
点开一条旧问答,答案在、引用卡片却一张不剩。查下来是两种情况:

1. 存引用页的代码是后加的,之前留下的记录里 refs 是空的 —— 数据从来没存过,
   光读是读不出来的;
2. 检索成功但没走完生成的那一轮(没配 API key / 截图加载失败,只降级显示引用
   卡片),回填历史的调用挂在生成成功那条路上,这些轮次只留下 hits=None,侧栏
   显示「未完成」。引用页是检索的结果,不该跟着答案一起丢。

改法:
- 抽 refs_of() / tiles_from():命中列表 → 引用页 / 引用卡片,按 (文章, 页, 段)
  去重。存进历史的仍然是数据(article_id / tile_index / url 五个字段)不是图片,
  缩略图照旧由 history_tiles() 从本地 tiles 现读,历史文件不会因此变大。
- 降级路径也回填历史:_show_results() 顺带把这轮检索到的页记下来,所以"没生成
  出答案"不等于"没引用过"。
- 老记录点开时按原问题补跑一次检索,把卡片补上。只问本地检索服务,没有模型调用,
  所以打开旧问答依然不花钱;补到的页写回历史,补一次就够。补不到(服务没起来、
  也没截图)就明说没补到 —— 空着不吭声会让人以为这条记录本来就没有引用。
- 补检索来的卡片上方加一行说明:这些页是按原问题重新检索的,**未必是当时那几页**,
  要还原当时那一轮请点「重问」。不能把重新检索的结果冒充成原本的出处。
- 补检索期间用户点开别的记录:数据照样写回(那条记录的事实,与此刻在看哪条无关),
  但卡片不往新对话里插。

验证:新增 _uicheck/citcheck.py(26 项,覆盖新记录还原 / 老记录补检索 / 补失败 /
中途走开 / 降级轮次回填),其余 12 套自检全绿。另用真实检索服务打了一次 BFO 问题:
5 条命中、refs_of 与 tiles_from 各 5 条、PDF 与 tile 文件都在磁盘上 —— 用户那条
历史记录下次点开就会自动补上。ruff check / format 干净(仅 query.py 是既有问题)。
@vercel

vercel Bot commented Oct 8, 2026

Copy link
Copy Markdown

@SX-ele is attempting to deploy a commit to the andylizf's projects Team on Vercel.

A member of the Team first needs to authorize it.

SX-ele added 2 commits October 9, 2026 00:57
往 papers\ 里加完论文后双击一次,把"重建索引 -> 停旧服务 -> 起新服务"
串起来。第 2 步是重点:serve 只在启动时把索引读进内存,之后不重载,
不重启就还在用老索引回答,重建等于白等。

- 脚本按 GBK(cp936)存、不带 chcp:UTF-8 码页下 cmd 的批处理读取器
  遇到多字节汉字会随机错位,把 echo 的内容当命令执行(实测踩到)。
- 数 PDF 用纯 cmd 的 for + set /a,不调 dir/find —— git 自带的同名
  find 会被优先命中,报一堆无关错误。
- 等待用 ping 而不是 timeout:timeout 需要控制台 stdin,重定向时报
  "不支持输入重定向"。
- 杀进程只挑 30001 上 LISTENING 的那个,不会误伤连上来的客户端连接;
  本来就没有服务在跑时也照常往下走。
- build 失败走 :failed 分支,不碰旧服务,旧索引保持完好。

验证:做了个干跑副本(真实命令换成 echo、杀进程换成打印)在仓库目录下
按 %~dp0 实跑,中文显示正常、退出码 0、正确抓到 30001 上的旧服务 PID。
嵌入阶段全量重跑、CPU 上约 125s/页,而进度全在内存里 —— 只有断点文件
能活过进程被杀(内存不足保护的常见后果)。原先每 10 个 chunk 才存一次,
一次 kill 最多丢约 20 分钟。

- 抽出 --checkpoint-every(默认 1),间隔交给 CLI,默认值收紧到每 1 个
  chunk,一次 kill 最多丢 1 个约 2 分钟。
- 启动时打印一行说明断点落点与间隔,不用等模型加载完才知道会不会续跑。
- 存盘逻辑没动,仍是原子写(.tmp.npz + os.replace):写一半被杀不会弄坏
  已有断点。

代价可忽略:全程约写 860MB / 22h。

注意 index build --force 会 rmtree 掉 embeddings/,断点文件正好在里面,
会被一起删掉 —— 重建时别带 -f。

验证:另写了个只测断点文件本身的脚本(仓库外,不提交),覆盖原子写、
只存已完成行、重复存盘覆盖不回退、坏 .tmp 不影响正式文件,7 项断言全过;
--help 可见新参数。完整的"被杀后重启续跑"闭环未跑 —— 它要加载 2B 模型
(约 4~5GB),而本机当前仅剩 2.8GB 可用,留待下次 rebuild 时看日志里是否
出现 "Resuming from checkpoint: N/M"。

ruff check / format 干净。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant