8db71aaf81
- 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
250 lines
13 KiB
Markdown
250 lines
13 KiB
Markdown
# 任务书 · 客户端交互与 UI 收尾(2026-08-30)
|
||
|
||
基线:`origin/staging` @ `ea0fbd44`。
|
||
|
||
本轮四条任务全部来自一次对着代码实测的审计,每条都附了实测数据。**先读完「硬红线」再动手。**
|
||
|
||
---
|
||
|
||
## 硬红线
|
||
|
||
1. **不得改变任何元素的视觉尺寸。** 任务 1 是扩大热区,不是把按钮画大。改完截图对比,图标和留白必须和改前一模一样。
|
||
2. **不得手写 `useCallback` / `useMemo`。**
|
||
3. **不得修改既有测试断言** —— 除非该断言锁住的正是本轮要修的缺陷本身;那种情况下必须在断言上方写注释说明「原值是什么、为什么它是错的」,并在 PROGRESS 里单列。
|
||
4. 推 staging 前必须 `./node_modules/.bin/tsc --noEmit` 通过。**不要用 `npx tsc`**,本仓库环境下会装到空包 `tsc@2.0.4`。
|
||
5. 测试数不得低于基线,且 `fail=0`、`skipped=0`(在有 Docker 的环境里跑)。本机没有 Docker 时会有 23 条数据库/部署类失败 + 10 条 skipped,那是既有环境缺口,不是你引入的 —— 但**必须逐条比对失败清单,确认没有新增**。
|
||
6. **浅色和深色两套都要验。** 本仓库已有深色主题(`prefers-color-scheme` + `data-theme`),任何配色改动必须同时在两套下检查对比度。
|
||
7. 不得改 `.gitea/workflows/**`。不得在有未提交改动的工作树上切分支。不得自行把 staging 提升到 main。
|
||
|
||
让步顺序:功能与测试不回归 > 可验证的修复 > 视觉一致 > 代码整洁。
|
||
|
||
## 开工前置
|
||
|
||
```bash
|
||
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` 的深色对照表里都有,**必须一起换**,否则边界页的行动色会和主站不一致:
|
||
|
||
```bash
|
||
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,失败信息里要打印出具体是哪一对、实际多少。**不要放宽阈值来让测试通过。**
|
||
|
||
### 验收
|
||
|
||
```bash
|
||
./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` 新增一条(用户可见的可读性缺陷)。**编号前先确认远端最大号**,别抢号:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```css
|
||
.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。
|
||
|
||
## 交付物清单
|
||
|
||
1. 两个深色块各改两处色值,`#d4785a` 全仓 15 处清零
|
||
2. `dark-theme-contract.test.ts` 扩成 32 组对比度断言 + 反向验证证据
|
||
3. 触达尺寸改动,附改前改后截图(浅色+深色)证明视觉零差异
|
||
4. 每个可点元素的实测命中矩形与相邻间距
|
||
5. 字数计数提示,附阈值行为录制与读屏播报验证
|
||
6. 发送键统一(或补提示)+ 中文输入法选词不误发的逐字验证
|
||
7. 三个输入框行为对照表
|
||
8. 新增合同测试(触达尺寸、`isComposing`)
|
||
9. `docs/BUG_HISTORY.md` 两条新条目(编号已对远端确认)
|
||
10. `BLOCKED.md`:任何触发止损的项
|
||
11. `PROGRESS-frontend-interaction-20260830.md`
|
||
12. `tsc --noEmit` / `eslint`(0 error)/ `tsx --test`(失败清单与基线逐条比对)/ `next build` 四条命令的实际输出
|