diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f9cb53b..8d4e7fa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # 印度占星 Skill 更新日志 +## 2026-09-26 — 首页开场小字拆开,今日星语和点数换图标(待验收) + +- 今日趋势(今日星语卡片那一句,每天生成)挪到问候语下面单独一行,浅色、最多两行;还没生成时写「今天的星语还没写出来。」,没有出生分钟时不显示。 +- 入口按钮下面平时不再有字,只在需要动手时出一句:有没做完的校正写「上次那次校正还没完成,可以接着做。」;当前人物不是本人写「生时校正暂时只支持本人。」。删掉「不确定出生时间时…」「上次已经校正完…」两句。 +- 修好已校正过的账户仍被当成第一次的问题(BUG-1041):首页读校正状态时一直读错字段名,所以从没出现过「再次校正」和未完成提示。 +- 今日星语入口图标换成带星的月亮,右上角点数换成硬币,两处不再是同一个图标;读屏名称不变。 +- Skill 版本不 bump。不改接口、今日星语生成逻辑和数据库;登录态真机与部署待验收。 + ## 2026-09-26 — 从其他页面回首页不再重放加载动画(待验收) - 同一次打开网页里,从星盘、星历、我的报告、星盘档案点「新建对话」、点侧栏里的历史对话、点左下角头像或按浏览器返回回到首页,不再出现「正在载入账户 / 正在准备对话」加载环,首页立刻可以输入。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 79bbf9e6..10fd40fe 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -13928,3 +13928,18 @@ - 相关记录:BUG-966(次级页模块缓存先例)、BUG-479(一次等待一次揭幕)、BUG-1021(不半揭幕,快照缺项回冷启动)、BUG-1038(新建意图与存根规则,暖路径沿用)、BUG-936(首屏兜底,`data-hydrated` 不变)。 - 复发自:无。 - 修复版本:`codex/home-warm-return-20260926`,未部署。 + +## BUG-1041 | 首页校正入口摘要解析读错键名:已校正账户仍显示首次文案、未完成的校正从不提示 + +- 状态:investigating(已修复待部署;登录态真机未复测) +- 首次发现 / 最近更新:2026-09-26 / 2026-09-26 +- 影响面:`frontend/src/lib/rectification-entry.ts` `entrySummaryFromResponse`(首页 `page.tsx` 启动 effect 与 `useRectificationSurface.refreshRectificationEntrySummary` 共用),进而影响首页生时校正入口的可访问名(`开始新的生时校正` / `再次校正`)与入口下方提示。 +- 用户现象:产品 09-26 真机截图:账户已经校正过,首页入口下方仍写首次文案「不确定出生时间时,用记得住的经历一步步缩小范围」;按钮可访问名也一直是「开始新的生时校正」,从未出现「再次校正」。 +- 触发条件:任何有未完成或已完成校正 Case 的账户打开首页。 +- 根因:`GET /api/rectification/cases/entry-summary` 返回 `getRectificationEntrySummary` 投影后的 **camelCase** 对象(`hasResumableCase` / `hasTerminalCaseWithTime` / `latestTerminal.caseId`…),只有底下的 RPC 行是 snake_case;而浏览器端 `entrySummaryFromResponse` 只读 snake_case 键(`has_resumable_case` 等),所以每一个真实响应都被解析成「什么都没有」。`resolveRectificationEntryAction` 恒为 `start`,`rectificationCardAction === "restart"` 与 `hasResumableCase` 分支从未命中。自 2026-08-11 `ca748466`(BUG-163 入口改造)起存在。测试没拦住:`rectification-v9-entry-routing.test.ts`、`home-warm-return-lifecycle.test.tsx`、`new-chat-from-people-lifecycle.test.tsx` 的 entry-summary mock 都是手写的 snake_case,不是路由真实输出;`rectification-v9-case-service.test.ts` 只测到服务端投影为止,两段之间没有往返合同。 +- 修复:`entrySummaryFromResponse` 以路由真实的 camelCase 形状为准,同时仍读 snake_case(RPC 形状不会再静默塌成空)。首页小字按 TASK-starter-home-polish-20260926 D2 改为只读 summary 的 `hasResumableCase` 与当前人物,不再依赖派生的 `restart`。 +- 验证:`frontend/tests/starter-home-polish-20260926.test.tsx`「BUG-1041: the homepage parser reads what the entry-summary route actually returns」:用 `getRectificationEntrySummary`(假 RPC)产出、`JSON` 往返后交给 `entrySummaryFromResponse`,断言 `hasTerminalCaseWithTime`、`latestTerminal`、`restart` 与未完成提示;修复前该条失败(解析结果全 false)。本地 `next start` + Chrome 151,`/api/*` 以 camelCase 形状回放:已校正账户可访问名「再次校正」、无小字;未完成账户显示「上次那次校正还没完成,可以接着做。」。全量 3960 条 / 61 失败,失败名单与基线逐条一致。 +- 防复发:往返合同测试锁住「路由输出 → 首页解析」一段;新的首页测试 fixture 用路由真实的 camelCase 形状。旧的 snake_case mock 仍能被解析,未改动。 +- 相关记录:BUG-163(入口摘要由服务端 Case 驱动)、BUG-1040(暖快照缓存的正是解析后的 summary,快照结构不变)。 +- 复发自:无。 +- 修复版本:`codex/starter-home-polish-20260926`,未部署。 diff --git a/docs/tasks/PROGRESS-starter-home-polish-20260926.md b/docs/tasks/PROGRESS-starter-home-polish-20260926.md new file mode 100644 index 00000000..4c88a89b --- /dev/null +++ b/docs/tasks/PROGRESS-starter-home-polish-20260926.md @@ -0,0 +1,69 @@ +# PROGRESS · 首页开场小字拆分 + 今日星语 / 点数图标(2026-09-26) + +任务书:`TASK-starter-home-polish-20260926.md`。执行方:Claude 子代理(直接执行模式)。分支 `codex/starter-home-polish-20260926`,worktree `.worktrees/starter-home-polish-20260926`,本地提交未推送。 + +## 基线 + +- `origin/staging` = `7475eb6e`(BUG-1038、BUG-1040 已合入)。 +- 基线全量前端测试(Node 20.19.2):3953 条,pass 3865 / fail 61 / skipped 27(61 条均为环境缺口:Node 20 `mock.module` 的 `ERR_MODULE_NOT_FOUND @/…`、无 Docker)。 +- 基线 `next build --webpack`:`/` ○ Static;rootMainFiles gzip 130933 B;`index.html` 引用的全部 js/css gzip 合计 656646 B。 +- BUG 编号:开工时 `docs/BUG_HISTORY.md` 最大 BUG-1040,远端各分支无更大号 → 本单用 BUG-1041。 + +## 改动 + +| 任务 | 文件 | 内容 | +| --- | --- | --- | +| T1 / D1 | `components/starter-home.tsx`、`app/(app)/page.tsx`、`globals.css` | `StarterGreeting` 加可选 `dailyTrend` / `dailyTrendSettled`,渲染 `.starter-greeting-sub`(`--color-ink-secondary`、`--type-body-sm`、两行截断)。`page.tsx` 传 `natalMinuteAvailable ? dailyStarlanguageTrend : null`,读的就是 BUG-1040 暖快照初始化的同一个 `dailyStarlanguage` state;未就绪沿用静态句「今天的星语还没写出来。」+ `role="status"`,无 `aria-busy`、无新等待态。 | +| T2 / D2 | `components/starter-home.tsx` | 新增纯函数 `rectificationEntryHint(forSelf, summary)`:非本人 →「生时校正暂时只支持本人。」;`summary.hasResumableCase` →「上次那次校正还没完成,可以接着做。」;其余 `null`,不渲染 `
`。删「不确定出生时间时…」「上次已经校正完…」「未完成的那次可以从左侧继续」「看看今天的整体节奏」四句与「·」拼接。`StarterEntries` 去掉不再用的 `dailyStarlanguage` / `dailyStarlanguageTrend` / `rectificationCardAction` 三个 prop。 | +| T2 / 分支误判 | `lib/rectification-entry.ts` | **真 bug(BUG-1041)**:`/api/rectification/cases/entry-summary` 返回 camelCase(`getRectificationEntrySummary` 的投影),`entrySummaryFromResponse` 只读 snake_case,每个真实响应都解析成全 false。改为优先读 camelCase、仍兼容 snake_case。结果:已校正账户可访问名变回「再次校正」,未完成提示能出现。 | +| T2 / D4 | — | 边界句「出生时间不足以支持的判断我会说明,不会补造具体分钟。」原位保留。 | +| T3 / D3 | `components/starter-home.tsx`、`app/(app)/page.tsx` | 今日星语入口 `Sparkles` → `MoonStar`;右上角点数 `Sparkles` → `Coins`(`className="credit-icon"`、`aria-hidden`、按钮 `aria-label` 不变)。已确认 lucide-react 1.24.0 同时导出 `MoonStar`、`Coins`。 | +| T4 | `CHANGELOG.md`、`frontend/DESIGN.md`(Starter home + §9)、`frontend/docs/VOICE.md`、`docs/BUG_HISTORY.md`(BUG-1041, investigating)、`docs/testing/starter-home-polish-20260926.md`、`docs/tasks/README.md` | 记录。 | + +红线核对:`Home()` 未新增 hook(只改 JSX 表达式);`page.tsx` 1373 → 1370 行;无语句压行;未改今日星语接口与生成逻辑;未改依赖、迁移、workflow。 + +## 测试 + +| 项 | 结果 | +| --- | --- | +| `tsc --noEmit` | 0 错 | +| `npm run lint` | 0 error(126 warning,均为既有文件的既有 warning,本单文件无新增) | +| 全量 `npm test` | 3960 条,pass 3872 / fail 61 / skipped 27 | +| 失败名单 vs 基线 | 逐条一致(`diff` 为空),新增失败 0 | +| 测试名单 vs 基线 | +7 条新名(下文),消失 0 条 | +| Python 源码合同 | `tests/test_daily_and_rectification_entrypoints.py`、`test_session_management_entrypoints.py`、`test_birth_time_journey_contract.py`、`test_supabase_user_data_contract.py` 全过 | +| `npm run build -- --webpack` | 通过;`/` ○ Static,`/chart` `/ephemeris` `/people` ○ | +| gzip | rootMainFiles 130933 → 130933 B(0);首屏引用全部 js/css 656646 → 656729 B(+83 B,+0.013%) | + +新增 `frontend/tests/starter-home-polish-20260926.test.tsx`(7 条):D2 首次 / 已校正无提示、未完成一句;D2 非本人(纯函数 + 真实 subject store 渲染,按钮禁用);D4 边界句;D1 副行三态(就绪 / 静态句 + status / 无分钟不渲染);D1 page.tsx 读暖快照同源 state;D3 图标与可访问名;BUG-1041 路由输出 JSON 往返后解析(修复前此条失败,已实测)。 + +### 改动既有断言(三栏同时写在测试文件内) + +| 文件 | 原值 | 新值 | 原因 | +| --- | --- | --- | --- | +| `frontend/tests/icon-motion-audit.test.ts` | `import { Clock3, Sparkles }`、``、doesNotMatch `/Moon\|MoonStar\|thinking-orbs/` | `import { Clock3, MoonStar }`、``、doesNotMatch `/Sparkles\|thinking-orbs/` | D3 产品拍板换 MoonStar;「无循环动画、无 thinking-orbs」原样保留,并反向锁住旧图标 | +| `frontend/tests/rectification-agentic-entry.test.ts` | match `/rectificationCardAction === "restart"/` | match `rectificationEntryHint(rectificationForSelf, rectificationEntrySummary)` 与 `summary?.hasResumableCase`;doesNotMatch `=== "restart"` | D2 删掉 restart 那句;BUG-1041 查明该分支从未命中。「由服务端 entry summary 驱动、不读会话列表」的意图不变 | + +## 浏览器验证(本地 `next start` + Chrome 151 headless,CDP 拦截 `/api/*` 回放虚构数据,entry-summary 用路由真实的 camelCase 形状) + +| 场景 | 副行 | 入口下方 | 其他 | +| --- | --- | --- | --- | +| 375 / 1280 × 明 / 暗,已校正账户 | 趋势句两行,明 `rgb(95,95,89)` / 暗 `rgb(179,175,164)`,14px | 无 | 生时校正可访问名「再次校正」;图标 moon-star / coins;无横向溢出 | +| 375 明,未完成校正 | 趋势句 | 「上次那次校正还没完成,可以接着做。」 | 可访问名「开始新的生时校正」 | +| 375 明,首次 | 趋势句 | 无 | — | +| 1280 明,今日星语 unavailable | 「今天的星语还没写出来。」`role="status"` | 无 | — | +| 375 明,无出生分钟 | 不渲染 | 仅边界句 | 入口「查看今日运势」 | +| 暖返回:首页 → /chart → 新建对话(今日星语接口压 2.5 s) | 首帧即有趋势句(点击后 17–20 ms) | — | 可交互 23–27 ms,加载环 0 次;控制台 0 error | + +截图(仓库内):`docs/testing/starter-home-polish-20260926/starter-375-light-resumable.png`、`starter-1280-dark.png`。其余截图在会话 scratchpad `shp/`(375/1280 明暗、静态句、无分钟、暖返回),未入库。 + +## 环境缺口 + +- 无登录态真机:真实账户的 entry-summary、真实今日星语文本长度、VoiceOver 朗读 → `docs/testing/starter-home-polish-20260926.md`。 +- Node 20:61 条 `mock.module` 类失败为既有环境缺口,名单与基线一致。 +- 无 Docker:DB 套件未跑(本单不动表)。 + +## 观察(未处理,留给产品 / Claude 定夺) + +- 首页「生时校正」按钮走 `intent=homepage`,服务端总是**新建** Case;未完成的那次要从侧栏继续。产品指定的新文案「上次那次校正还没完成,可以接着做。」没有说「从左侧」,用户可能以为点按钮就会接着做。按任务书原文实现,未改。 +- `useRectificationSurface.rectificationLimitedToSelf` 点击时的错误文案仍是无句号的「生时校正暂时只支持本人」(非本人时按钮已禁用,正常点不到),不在本单范围。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 92b0cfc9..9ced10fe 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -119,7 +119,7 @@ | 任务书 | 进度 | 主题 | 状态 | 落点 | | --- | --- | --- | --- | --- | -| `TASK-starter-home-polish-20260926.md` | — | **首页开场小字与图标**:今日趋势(每日模型生成,非写死)移到问候下方副行;入口下方提示只在有未完成校正或非本人时出现,删两句固定文案并修正已校正仍显示首次文案的分支;今日星语图标 MoonStar、点数图标 Coins。排在 BUG-1038、1040 之后 | 待领取 | — | +| `TASK-starter-home-polish-20260926.md` | `PROGRESS-starter-home-polish-20260926.md` | **首页开场小字与图标**:今日趋势(每日模型生成,非写死)移到问候下方副行;入口下方提示只在有未完成校正或非本人时出现,删两句固定文案并修正已校正仍显示首次文案的分支;今日星语图标 MoonStar、点数图标 Coins。排在 BUG-1038、1040 之后。分支误判实为入口摘要解析读错键名(BUG-1041) | 待验收(Claude 09-26 直接执行:子代理实现,tsc/lint 0、全量 3960 条 / 61 失败与基线逐条一致、`/` ○、首屏 gzip +0.01%;CDP 375/1280 明暗截图) | `codex/starter-home-polish-20260926`(本地未推) | | `TASK-home-warm-return-20260926.md` | — | **从其他页面回首页不再放加载动画**:首页按组件挂载计揭幕,客户端返回会重跑模型列表 / 后台状态 / 入口摘要 / 今日星语(3–5 次往返 + 最多 4 秒)。产品定:每次打开网页只揭幕一次;模块级暖快照按账户隔离、只存内存,暖返回首帧即就绪、后台静默刷新;缺项回落冷启动,不许半揭幕。**排在 BUG-1038 合入后**。BUG-1040 | 已验收(Claude 09-26 直接执行:子代理实现,Claude 独立复验 tsc/lint 0、全量 3953 条失败名单与基线逐条一致、四路由 ○、gzip 不变;CDP 实测回首页 5.5 s → ~20 ms、无加载环) | `638b6a60`(已部署 `7475eb6e`,health 一致) | | `TASK-new-chat-from-people-20260926.md` | `PROGRESS-new-chat-from-people-20260926.md` | **从星盘档案页点新建对话落回上一次生时校正**(产品 09-26 真机):标题显示旧校正会话、输入框「正在打开生时校正…」,中间却是新对话开场;另有两套新建入口(`?new=1` 与 `?newChat=1`)违反 BUG-1015 一处解析红线。先复现写失败测试、定位跨页残留状态再修,合并入口。BUG-1038 | 已验收(Claude 09-26 直接执行:子代理复现并定位 sessionStorage 登录返回存根;Claude 独立复验 tsc/lint 0、全量 3928 条失败名单与基线逐条一致、四路由 ○、gzip 不变、修复前后截图核对) | `0a8350cc`(已部署 `6a22626d`,health 一致) | | `TASK-people-archive-p1-fix-20260925.md` | `PROGRESS-people-archive-p1-fix-20260925.md` | **P0修复**:worker按持久化人物解析、真实删除预留退款、本人历史OR is、切人/消息时序及P2/P3;BUG-1031/1032,关联1030/990/1024。永久定向54/54、独立前端5探针与退款r4六场景通过;必须最终Node22 Linux全量+DB逐条基线对照 | 已验收(Claude 09-25:两 P0、两 P1、七 P2 通过;新 P2 删后跳错人、人物接口故障连累聊天 → 收尾单) | `4d801e53`(已部署,health 核对一致) | diff --git a/docs/testing/starter-home-polish-20260926.md b/docs/testing/starter-home-polish-20260926.md new file mode 100644 index 00000000..7c5438ad --- /dev/null +++ b/docs/testing/starter-home-polish-20260926.md @@ -0,0 +1,11 @@ +# 首页开场小字与图标 · 真机清单(2026-09-26) + +本轮在本地 `next start` + 无头 Chrome 里用虚构接口数据截过图(`starter-home-polish-20260926/`),没有登录态真机。下面留给真人,用自己的账户在 staging 上照做: + +1. 打开首页:问候语下面一行浅色小字是今天的趋势句,最多两行;入口按钮(今日星语、生时校正)下面没有字。 +2. 如果你有一次没做完的生时校正:入口按钮下面只有一句「上次那次校正还没完成,可以接着做。」。 +3. 如果你已经校正完、没有没做完的:按钮下面没有字;用读屏(VoiceOver)读生时校正按钮,应读「再次校正」。 +4. 在星盘档案里切到另一个人再回首页:按钮下面只有「生时校正暂时只支持本人。」,生时校正按钮是灰的。 +5. 今日星语按钮是带星的月亮图标,右上角点数是硬币图标,两处不一样。 +6. 从星盘页点「新建对话」回首页:没有加载环,问候语下面的趋势句一出来就在,不会先空着再跳出来。 +7. 375px 宽(手机)明暗两种主题下:趋势句不溢出、不遮挡输入框;深色下小字能看清。 diff --git a/docs/testing/starter-home-polish-20260926/starter-1280-dark.png b/docs/testing/starter-home-polish-20260926/starter-1280-dark.png new file mode 100644 index 00000000..2692212f Binary files /dev/null and b/docs/testing/starter-home-polish-20260926/starter-1280-dark.png differ diff --git a/docs/testing/starter-home-polish-20260926/starter-375-light-resumable.png b/docs/testing/starter-home-polish-20260926/starter-375-light-resumable.png new file mode 100644 index 00000000..5584312a Binary files /dev/null and b/docs/testing/starter-home-polish-20260926/starter-375-light-resumable.png differ diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 594d3404..0aa303a0 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -533,7 +533,7 @@ The empty state is the composer. It used to be a bordered hero card at sitting on top of the input, so the primary action was the last thing on the page. Three parts now, in reading order: -- **Greeting** (`.starter-greeting-block`): one line, no subtitle — a single h1 +- **Greeting** (`.starter-greeting-block`): one line — a single h1 at `clamp(24px, 2.8vw, 30px)`, weight 500, `line-height: 1.25`, centred. The salutation line above the question is gone; so is the question, which is the composer placeholder's job now («想聊什么都可以»). The line is drawn per visit @@ -543,6 +543,14 @@ page. Three parts now, in reading order: `{名字}` entries dropped when no name is set. It lives in the conversation area with `align-content: end`, so it sits directly above the composer rather than floating in the middle of the row. +- **Daily sub-line** (`.starter-greeting-sub`, 2026-09-26): today's trend sits + under the greeting as one quiet paragraph — `--color-ink-secondary`, + `--type-body-sm`, `line-height: 1.55`, `max-width: 560px`, clamped to two + lines with an ellipsis. It reads the same `dailyStarlanguage` state the warm + snapshot seeds (BUG-1040), so a warm return shows it on the first frame. Until + the card is ready it shows the existing static sentence「今天的星语还没写出来。」 + with `role="status"` and no `aria-busy`, spinner or placeholder; the reveal + rule does not change. Without a natal minute it is not rendered at all. - **Composer**: unchanged, and *in the same DOM position it occupies during a live session*. Nothing remounts when the first message lands, so an in-flight draft, focus, or queued card survives the transition. Its width is @@ -552,9 +560,14 @@ page. Three parts now, in reading order: - **Entry pills** (`.starter-entry`): the two product entrances, rendered inside `.composer-wrap` *below* the composer. The visual pill is 36px tall (`min-height: 36px`). The transparent hit area is the `::after` box with - `inset: -4px`, so the touch target is 44px. One word each, accent icon 15px. - Below them one line merges the daily trend with the rectification hint, and - an accuracy-boundary line appears when no natal minute is available. + `inset: -4px`, so the touch target is 44px. One word each, accent icon 15px: + lucide `MoonStar` for 今日星语 (was `Sparkles`, the same glyph as the credits + button) and `Clock3` for 生时校正. The header credits button uses lucide + `Coins`; its accessible name is unchanged. Below the pills there is normally + **nothing**: a hint line (`.starter-entry-hint`) appears only when the user has + something to do, and an accuracy-boundary line appears when no natal minute is + available. The daily trend and the rectification hint are no longer joined + with「·」. - **States, measured:** - Default border is `1px solid var(--starter-entry-border)`, and that token is @@ -585,15 +598,19 @@ page. Three parts now, in reading order: entrypoint identifier and expanded only on the server. There is no “从一个主题开始” grid; leftover `guided_topic` values in stored sessions still pin domains when replayed. -- **Rectification hint** keeps three server-driven branches — an unfinished case, - a finished one that can be redone (`rectificationCardAction === "restart"`), - and a first run. The branch comes from the entry summary, never the session - list. +- **Rectification hint** (`rectificationEntryHint`, 2026-09-26) has two + visible branches and reads the entry summary itself, never the session list + and never the derived start/restart label: a non-self current person → + 「生时校正暂时只支持本人。」; otherwise `hasResumableCase` → + 「上次那次校正还没完成,可以接着做。」. First runs and finished cases show no + hint. The old first-run and "redo" sentences are gone. (The summary parser had + been reading the wrong key casing since 2026-08-11, so every account looked + like a first run — BUG-1041.) - **Responsive:** the pill row wraps and stays centred; there is no phone-specific layout, because there is no fixed-height card to collapse. - **Accessibility:** each pill is one native button with a descriptive accessible - name (the full action label, not the one-word face). The daily hint keeps - `role="status"` while the trend is still arriving. + name (the full action label, not the one-word face). The daily sub-line keeps + `role="status"` while the trend has not arrived. ### Account popover @@ -703,7 +720,7 @@ Agent 的 live 标记只有 `InlineSpinner` 一种。曾经并存的 canvas 小 **每次打开网页只揭幕一次,客户端返回首页走暖快照(2026-09-26,BUG-1040)。** 整页加载(首次打开、刷新、登录后跳转)照旧放下面这一次加载屏。同一次打开里从星盘 / 星历 / 我的报告 / 星盘档案经侧栏「新建对话」、历史会话行、账户页脚或浏览器返回回到 `/`,不再出现加载环:第一次冷启动成功后,模型目录、校正入口摘要、会话分页游标和今日星语记进模块级暖快照(`lib/home-warm-snapshot.ts`,按账户隔离、只在内存、不落任何存储;账户、资料与会话列表本来就在布局层的会话列表 provider 里跨页存活)。首页重挂时若快照齐全,首帧即可用,落点按冷启动同一套规则同步算出(`?new=1` 新建本地空对话、`?c=` 打开该会话、登录返回存根与其人物范围同 BUG-1038),随后后台刷新模型目录、账户、后台回答恢复、入口摘要与今日星语,结果到了静默替换;消息没缓存的历史会话沿用首页内切换会话的留白方式。快照缺任何一项、落点需要去服务端查、或客户端导航没留下目标地址(Next 先渲染新页面、后写地址栏,`AppLink` 在点击时记下目标),一律回到冷启动,不半揭幕。账户变化、退出、任一 401 清空快照;今日星语跨日或换人按新键重取,卡片先用静态句。 -首页只揭幕一次。揭幕前的加载屏先并行取账户 / 会话、模型目录和咨询状态,再等待 lookup、会话详情等依赖数据全部到齐;不设 8 秒硬超时,超过 `HOME_BOOTSTRAP_SLOW_MS = 20_000` 才进入现有全屏错误屏。错误屏的「重试」只取消旧请求、重载 provider 并局部重跑 bootstrap,不调用 `location.reload()`。揭幕后不得再出现任何阻塞等待或组件级 spinner:今日星语未到显示静态文案「今天的星语还没写出来。」(不带 `aria-busy`),校正卡用无摘要文案,内容到达后静默替换。登录后的空首页只剩问候语、今日星语卡、生时校正卡和输入框,没有主题卡。切换到消息尚未缓存的会话时消息区留白并只给 `sr-only` 文案,不转圈;揭幕后按侧栏顺序后台预取最近 5 条会话(`SESSION_PREFETCH_COUNT`)让常见切换零等待。轨道环消失后不得再换一套动效继续等。 +首页只揭幕一次。揭幕前的加载屏先并行取账户 / 会话、模型目录和咨询状态,再等待 lookup、会话详情等依赖数据全部到齐;不设 8 秒硬超时,超过 `HOME_BOOTSTRAP_SLOW_MS = 20_000` 才进入现有全屏错误屏。错误屏的「重试」只取消旧请求、重载 provider 并局部重跑 bootstrap,不调用 `location.reload()`。揭幕后不得再出现任何阻塞等待或组件级 spinner:今日星语未到时问候下方副行显示静态文案「今天的星语还没写出来。」(不带 `aria-busy`),校正卡用无摘要文案,内容到达后静默替换。登录后的空首页只剩问候语、今日星语卡、生时校正卡和输入框,没有主题卡。切换到消息尚未缓存的会话时消息区留白并只给 `sr-only` 文案,不转圈;揭幕后按侧栏顺序后台预取最近 5 条会话(`SESSION_PREFETCH_COUNT`)让常见切换零等待。轨道环消失后不得再换一套动效继续等。 ### 首屏死屏兜底 diff --git a/frontend/docs/VOICE.md b/frontend/docs/VOICE.md index cd8cf273..97902c37 100644 --- a/frontend/docs/VOICE.md +++ b/frontend/docs/VOICE.md @@ -18,6 +18,8 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、 首页开场语只有一行,不带追问句,不用星星、月亮、陪伴一类比喻。 +首页开场语下面可以有一行今日趋势(今日星语卡片的 trend,每日生成);还没生成时写「今天的星语还没写出来。」,没有出生分钟时不写。入口按钮下面平时不写字,只在用户需要动手时写一句:有没做完的校正写「上次那次校正还没完成,可以接着做。」;当前人物不是本人写「生时校正暂时只支持本人。」。不再写「不确定出生时间时,用记得住的经历一步步缩小范围」「上次已经校正完,可以拿最新资料再来一次」,也不要用「·」把两句不相干的提示拼成一行。 + ## 星盘空状态、0 年大运、星历切日期 这三句只描述眼前缺的数据,不解释运势,也不许写成「正在加载」: @@ -177,7 +179,7 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、 账户与点数的页签只用:会员套餐 / 点数包 / 兑换码 / 订单记录。购买中按钮写「跳转收银台…」,轮询中写「等待支付结果」,不要写「正在加载」。 -星盘档案不写关系。人数到顶时只说「最多保存 5 个人的星盘」。当前人物不是本人时,生时校正入口禁用,提示「生时校正暂时只支持本人」。删除一个人之前必须写出「将同时删除 N 个对话和 M 份报告,无法恢复。」本人不能删除,不要给删除按钮配一句绕过去的确认。 +星盘档案不写关系。人数到顶时只说「最多保存 5 个人的星盘」。当前人物不是本人时,生时校正入口禁用,提示「生时校正暂时只支持本人。」删除一个人之前必须写出「将同时删除 N 个对话和 M 份报告,无法恢复。」本人不能删除,不要给删除按钮配一句绕过去的确认。 余额不足时输入框占位写「余额不足,可在账户与点数中补充」,不要叫用户去会员页。 diff --git a/frontend/src/app/(app)/page.tsx b/frontend/src/app/(app)/page.tsx index 21be2d9e..311789ed 100644 --- a/frontend/src/app/(app)/page.tsx +++ b/frontend/src/app/(app)/page.tsx @@ -2,7 +2,7 @@ import dynamic from "next/dynamic"; import { loadBillingPanel } from "@/lib/billing-panel-loader"; -import { Sparkles } from "lucide-react"; +import { Coins } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import type { FormEvent, KeyboardEvent } from "react"; import { AccountDialogOverlay, type AccountOverlayModel } from "@/components/account-dialog-overlay"; @@ -1210,7 +1210,7 @@ export default function Home() {
+ {dailyTrend} +
+ ) : null} ); } +/* The line under the pills only appears when there is something to do about + it. First runs and finished cases say nothing: the pill already names the + action. The branch reads the entry summary itself, not the derived + start/restart label, and a non-self subject wins because the case summary + belongs to the account owner. */ +export function rectificationEntryHint( + rectificationForSelf: boolean, + summary: RectificationEntrySummary | null, +): string | null { + if (!rectificationForSelf) return "生时校正暂时只支持本人。"; + if (summary?.hasResumableCase) return "上次那次校正还没完成,可以接着做。"; + return null; +} + export type StarterEntriesProps = { - readonly dailyStarlanguage: DailyStarlanguageState; readonly natalMinuteAvailable: boolean; readonly dailyStarlanguageQuestion: string; - readonly dailyStarlanguageTrend: string; readonly productEntrypointsDisabled: boolean; readonly startDailyStarlanguageConsultation: () => void; readonly rectificationCardLabel: string; - readonly rectificationCardAction: string; readonly rectificationLoading: boolean; readonly rectificationMutationPending: boolean; readonly openRectificationFromHomepage: () => void; @@ -53,13 +75,10 @@ export type StarterEntriesProps = { export function StarterEntries({ natalMinuteAvailable, - dailyStarlanguage, dailyStarlanguageQuestion, - dailyStarlanguageTrend, productEntrypointsDisabled, startDailyStarlanguageConsultation, rectificationCardLabel, - rectificationCardAction, rectificationLoading, rectificationMutationPending, openRectificationFromHomepage, @@ -71,20 +90,7 @@ export function StarterEntries({ const subjectId = useCurrentSubject(); const rectificationForSelf = subjectId === "self"; const dailyLabel = natalMinuteAvailable ? "今日星语" : "每日运势"; - /* The trend line is the only part that arrives late. It stays a live region so - a screen reader hears it settle, but it is a tooltip-rank hint now, not a - card body — the pill is actionable from the first paint either way. */ - const dailyHint = natalMinuteAvailable ? dailyStarlanguageTrend : "看看今天的整体节奏"; - /* Three branches, same as the card it replaces: an unfinished case, a finished - one that can be redone, and a first run. The label itself is server-driven - from the entry summary (`rectificationCardAction`), not from the session list. */ - const rectificationHint = !rectificationForSelf - ? "生时校正暂时只支持本人" - : rectificationEntrySummary?.hasResumableCase - ? "未完成的那次可以从左侧继续" - : rectificationCardAction === "restart" - ? "上次已经校正完,可以拿最新资料再来一次" - : "不确定出生时间时,用记得住的经历一步步缩小范围"; + const rectificationHint = rectificationEntryHint(rectificationForSelf, rectificationEntrySummary); return (- {dailyHint} · {rectificationHint} -
+ {rectificationHint &&{rectificationHint}
} {!natalMinuteAvailable && (出生时间不足以支持的判断我会说明,不会补造具体分钟。
)} diff --git a/frontend/src/lib/rectification-entry.ts b/frontend/src/lib/rectification-entry.ts index 1b6cbc9f..04a397da 100644 --- a/frontend/src/lib/rectification-entry.ts +++ b/frontend/src/lib/rectification-entry.ts @@ -69,32 +69,45 @@ export function isTerminalRectificationStatus(status: string): boolean { return TERMINAL_STATUSES.has(status); } +/* `GET /api/rectification/cases/entry-summary` answers with the camelCase + object `getRectificationEntrySummary` returns; only the RPC underneath speaks + snake_case. This parser used to read the snake_case keys alone, so every real + response parsed as "nothing on record" and the homepage never saw a resumable + or finished case (BUG-1041). The route's camelCase shape wins; snake_case is + still read so an RPC-shaped payload does not silently collapse either. */ +function field(row: Record([^<]*)<\/p>/)?.[1] ?? null;
+}
+
+test("D2: the line under the pills only appears when the user has something to do", () => {
+ // First run and an already-rectified account: no hint at all.
+ for (const summary of [null, summaries.firstRun, summaries.rectified]) {
+ const markup = renderToStaticMarkup(
出生时间不足以支持的判断我会说明,不会补造具体分钟。<\/p>/);
+ assert.doesNotMatch(markup, REMOVED_HINTS);
+ const withMinute = renderToStaticMarkup(
虚构的今日趋势句。<\/p>/);
+ assert.doesNotMatch(ready, /role="status"|aria-busy/);
+
+ const pending = renderToStaticMarkup(
+
今天的星语还没写出来。<\/p>/);
+ assert.doesNotMatch(pending, /aria-busy|spinner|inline-spin|正在加载/);
+
+ for (const markup of [
+ renderToStaticMarkup(早上好