- README.md is now the product/repo front door (architecture, repo map, local dev, test tiers, delivery flow, doc map). Engine positioning, VedAstro/Codex setup and the oracle/benchmark command reference move verbatim to docs/engine/README.md, docs/engine/vedastro-gateway.md and docs/benchmark/README.md. Capability badges realigned with the registry (91/78/8/0); tests/test_readme_badges.py was red on staging. - AGENTS.md: Part A (environment truth, delivery, worktrees, record placement, bug workflow, growth freeze, frontend red lines, privacy, pre-work check, test tiers) and Part B (reading-rigor constraints). GitHub issue-tracker/triage boilerplate removed: GitHub is a read-only mirror. All strings locked by tests/ are preserved. - CLAUDE.md added: roles, three working modes, task-brief sections, acceptance criteria, session discipline; imports AGENTS.md. - 50 tracked TASK-*/PROGRESS-* files and 3 never-committed briefs move to docs/tasks/ with an index; REPO_LAYOUT.md merged into README. Docs-only change (no gated path touched). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
13 KiB
任务书 · 客户端交互与 UI 收尾(2026-08-30)
基线:origin/staging @ ea0fbd44。
本轮四条任务全部来自一次对着代码实测的审计,每条都附了实测数据。先读完「硬红线」再动手。
硬红线
- 不得改变任何元素的视觉尺寸。 任务 1 是扩大热区,不是把按钮画大。改完截图对比,图标和留白必须和改前一模一样。
- 不得手写
useCallback/useMemo。 - 不得修改既有测试断言 —— 除非该断言锁住的正是本轮要修的缺陷本身;那种情况下必须在断言上方写注释说明「原值是什么、为什么它是错的」,并在 PROGRESS 里单列。
- 推 staging 前必须
./node_modules/.bin/tsc --noEmit通过。不要用npx tsc,本仓库环境下会装到空包tsc@2.0.4。 - 测试数不得低于基线,且
fail=0、skipped=0(在有 Docker 的环境里跑)。本机没有 Docker 时会有 23 条数据库/部署类失败 + 10 条 skipped,那是既有环境缺口,不是你引入的 —— 但必须逐条比对失败清单,确认没有新增。 - 浅色和深色两套都要验。 本仓库已有深色主题(
prefers-color-scheme+data-theme),任何配色改动必须同时在两套下检查对比度。 - 不得改
.gitea/workflows/**。不得在有未提交改动的工作树上切分支。不得自行把 staging 提升到 main。
让步顺序:功能与测试不回归 > 可验证的修复 > 视觉一致 > 代码整洁。
开工前置
git fetch origin --prune
git worktree add -b codex/interaction-20260830 \
../.worktrees/interaction-20260830 origin/staging
基线必须是 origin/staging,不是任何本地 ref。读 pre_work_error_ledger.md,跑 scripts/pre_work_check.py,读 frontend/AGENTS.md(Next.js 版本与训练数据不同,写代码前先看 node_modules/next/dist/docs/)。改前先在 docs/BUG_HISTORY.md 检索同类记录。
下面所有行号只是线索,请按选择器定位,origin/staging 上的行号可能有偏移。
任务 0(P0)· 深色下两处对比度不达标
事实
深色调色板的字色逐个对底色实测,两组低于 WCAG AA 4.5:1:
| 组合 | 实测 |
|---|---|
--color-ink-tertiary #928e84 on --color-canvas-muted #30302d |
4.05:1 ✗ |
--color-action #d4785a on --color-canvas-muted #30302d |
4.18:1 ✗ |
frontend/tests/dark-theme-contract.test.ts 现有的对比度断言只验了「字色 vs --color-canvas」,漏掉了卡片底 --color-canvas-muted,所以这两组没被拦住。代码块的语言标签(.markdown-code-language)正是 ink-tertiary 落在 .markdown-code 的暖卡片底上,是已在线上的实例。
做法
两个深色块(@media (prefers-color-scheme: dark) 内的 :root:not([data-theme="light"]),以及 :root[data-theme="dark"])各改一处,共两处:
--color-ink-tertiary: #928e84 → #9c988e
--color-action: #d4785a → #d78064
#d78064 与原值色相 15°、饱和度均不变,只提亮 2.5% 明度 —— 观感仍是同一个黏土橙,不要顺手换色相。
#d4785a 在仓库里共 15 处,四个根边界页(error.tsx / not-found.tsx / forbidden.tsx / global-error.tsx,它们各自内联了一份深色 token)和 DESIGN.md 的深色对照表里都有,必须一起换,否则边界页的行动色会和主站不一致:
grep -rn '#d4785a' frontend/src frontend/DESIGN.md
--color-focus 在深色块里跟随 --color-action,一并确认。
测试
把 dark-theme-contract.test.ts 里那条对比度断言从「1 种底色」扩成 8 种字色 × 4 种底色:
- 字色:
color-ink、color-ink-strong、color-ink-secondary、color-ink-tertiary、color-action、color-danger、color-success、color-warning - 底色:
color-canvas、color-canvas-soft、color-canvas-muted、color-sidebar-solid
全部 32 组必须 ≥ 4.5:1,失败信息里要打印出具体是哪一对、实际多少。不要放宽阈值来让测试通过。
验收
./node_modules/.bin/tsx --test tests/dark-theme-contract.test.ts
grep -rc '#d4785a' frontend/src frontend/DESIGN.md # 期望全 0
反向验证:把 --color-ink-tertiary 改回 #928e84,测试必须变红。
建档
docs/BUG_HISTORY.md 新增一条(用户可见的可读性缺陷)。编号前先确认远端最大号,别抢号:
git fetch origin --prune && git show origin/staging:docs/BUG_HISTORY.md \
| grep -o '^## BUG-[0-9]*' | sort -t- -k2 -n | tail -3
防复发条写清楚:新增字色/底色 token 时必须同时进这张 32 组矩阵,不得只验阅读面。
任务 1(P0)· 触达尺寸普遍低于文档承诺的 44px
事实
frontend/DESIGN.md:165 写着 "Touch targets are at least 44px"。实测违反:
| 选择器 | 实际 | 位置 |
|---|---|---|
.message-actions button(globals.css:1253 附近) |
26×26px,padding: 0 |
每条回答下方的赞/踩/复制/重新生成 |
.chart-nav-chip(:732) |
32px | 侧栏星盘 chip |
.auth-links button(:1657) |
32px,padding: 0 |
登录页链接按钮 |
.select-item(:2507) |
40px | 模型选择器每一项 |
.birth-time-window-details .birth-time-skip-button(:2450) |
40px | |
.report-center-section-heading button |
40px |
26px 那条优先级最高 —— 它是全产品点得最勤的控件,出现在每一条回答下方,且完全没有 padding 撑开热区。
做法
扩大热区,不改视觉尺寸。 用透明的伪元素把可点区域撑到 44px:
.message-actions button { position: relative; }
.message-actions button::after {
position: absolute;
content: "";
inset: 50% 50% 50% 50%;
translate: -50% -50%;
width: 44px;
height: 44px;
}
(上面是示意,具体写法自己定,但必须满足:视觉尺寸不变、热区 ≥44px、不影响相邻元素布局、不遮挡兄弟元素的点击。)
对 40px 那几个,直接把 min-height 提到 44px 更简单 —— 但前提是不撑破所在容器。.select-item 在下拉里有 max-height 限制,改高之后一屏能显示的条目变少,需要目视确认下拉不会变得难用;如果变差,改用伪元素方案。
.chart-nav-chip 是 inline-flex 的胶囊,直接提高会改变视觉;用伪元素。
止损
- 如果某一处用伪元素会造成热区互相重叠(相邻按钮间距小于 44px 时必然发生),不要硬撑到 44px:把该处的实际间距和重叠范围写进
BLOCKED.md,只做到不重叠的最大值,并说明还差多少。宁可 36px 不重叠,也不要 44px 互相吃点击。 .message-actions一行有 4 个按钮,请先量一下它们的间距再决定。
验收
- 改前改后各截一张图(浅色 + 深色),确认视觉零差异
- 用浏览器 devtools 量出每个按钮的实际命中矩形,列表给出
- 相邻按钮热区不得重叠 —— 给出实测间距
- 新增合同测试:断言这些选择器要么
min-height ≥ 44px,要么带扩大热区的伪元素规则
建档
同任务 0,另建一条 BUG_HISTORY。防复发:新增可点元素必须满足 44px 命中区,视觉尺寸小于 44px 时用伪元素补。
任务 2(P1)· 字数上限是静默的
事实
| 位置 | 上限 | 反馈 |
|---|---|---|
主对话输入框(page.tsx 传给 chat-composer.tsx) |
500 | 无 |
birth-time-guide-turn.tsx:34 |
500 | 无 |
birth-time-choice-question.tsx:113 |
240 | 无 |
姓名输入(page.tsx:909) |
80 | 无 |
只有 maxLength 属性。到顶之后继续打字,浏览器直接不接收,界面毫无反应 —— 这是最难自我诊断的一类问题,用户只会觉得"键盘坏了"。
做法
给这几处加接近上限时才出现的计数提示:
- 平时不显示,剩余字数低于某个阈值(建议 50,短输入按比例调)才出现,避免常态噪声
- 用
--color-ink-tertiary;到达上限时才转--color-danger - 计数区必须
aria-live="polite"且aria-atomic="true",但只在阈值内挂载,不要让读屏用户每敲一个字都被播报 - 输入框加
aria-describedby指向计数区
主输入框的计数放哪需要判断:.composer-footer 已经存在,优先复用,不要新造一层容器把输入框顶高。
止损
如果加计数会让移动端输入区高度变化导致布局跳动,先只做主对话输入框,其余三处登记 BLOCKED.md。布局跳动比没有计数更糟。
验收
- 四处(或已做的那几处)各录一段:从阈值外打到上限,说明何时出现、何时变红
- 读屏验证:阈值外不播报,进入阈值后播报一次,不是每字一次
- 移动端确认输入区高度不跳
任务 3(P1)· 三个输入框的发送键不一致,且无任何提示
事实
| 位置 | 发送方式 |
|---|---|
主对话输入框(page.tsx:3701) |
Enter(Shift+Enter 换行),已正确处理 isComposing |
生时校正对话(rectification-agentic-chat.tsx:1233) |
Enter,已处理 isComposing |
birth-time-guide-turn.tsx:39 |
⌘/Ctrl+Enter |
第三处不但与前两处相反,界面上没有一个字告诉用户。它有可见的「整理为经历草稿」按钮所以不会卡死,但用户在主对话养成的 Enter 习惯到这里只会换行。
做法
统一为 Enter 发送、Shift+Enter 换行,与前两处一致。改 birth-time-guide-turn.tsx:
- 必须同时加
!event.nativeEvent.isComposing判断 —— 这是中文输入法的必要条件,前两处都有,第三处因为原本需要修饰键所以没加。漏了这条会导致中文用户选词时误发送,比现状严重得多。 - 保留「整理为经历草稿」按钮不动。
那是一个 rows={3} 的多行框,改成 Enter 发送后换行只能靠 Shift+Enter。如果你判断这个框的使用场景更需要自由换行(用户在描述一段经历),可以反过来选择保留 ⌘+Enter 但补上可见提示(放在既有的 #birth-time-guide-hint 里)。两种都可接受,但必须二选一并在 PROGRESS 里写明理由 —— 不允许维持"既不一致又无提示"的现状。
验收
- 中文输入法下逐字验证:输入拼音 → 按 Enter 选词 → 不得发送;选完词再按 Enter → 发送
- 三个输入框的行为写成一张对照表放进 PROGRESS
- 新增合同测试:三处的
onKeyDown必须都带isComposing判断
不在本轮范围
- 会话列表虚拟化 /
page.tsx:1212的visibleSessions每次 render 重排:与已修好的消息列表同类,但会话数通常只有几十。没有渲染基准之前不得动手 —— 需要先按frontend/scripts/home-streaming-render-benchmark.mts的模式出一份会话列表基准,证明它是热点。红线也禁止手写useMemo。 - 代码块语法高亮:产品已明确不做。
- 会话搜索、编辑自己发出的消息:不在本轮。
- 15 个无 CSS 规则的死类名:见
frontend/tests/class-name-definition-contract.test.ts的knownUnstyled白名单,都是有意的空修饰符,不要顺手清理。
收尾
- PROGRESS 文件名写
PROGRESS-frontend-interaction-20260830.md。不要写成PROGRESS.md—— 根目录已有受版本控制的progress.md,大小写不敏感文件系统上会互相覆盖。 - 四条任务合成一次 staging 推送(staging push 触发全量构建+部署,没有路径过滤,不要分多次推)。
- 推送后核对
https://staging.jyotisha.chat/api/health的.deployment.gitCommit等于新 SHA。流水线是validate→publish→ 才派发deploy,全链路约 20 分钟,不要在十几分钟内就断定部署失败。 - 不自行提升 main。
交付物清单
- 两个深色块各改两处色值,
#d4785a全仓 15 处清零 dark-theme-contract.test.ts扩成 32 组对比度断言 + 反向验证证据- 触达尺寸改动,附改前改后截图(浅色+深色)证明视觉零差异
- 每个可点元素的实测命中矩形与相邻间距
- 字数计数提示,附阈值行为录制与读屏播报验证
- 发送键统一(或补提示)+ 中文输入法选词不误发的逐字验证
- 三个输入框行为对照表
- 新增合同测试(触达尺寸、
isComposing) docs/BUG_HISTORY.md两条新条目(编号已对远端确认)BLOCKED.md:任何触发止损的项PROGRESS-frontend-interaction-20260830.mdtsc --noEmit/eslint(0 error)/tsx --test(失败清单与基线逐条比对)/next build四条命令的实际输出