校正的选项卡片换成组件库(#110)。
题目、计分、扣点和保存没有改。明确说要校正的人仍然直接开始,没做完的仍然接着做。选项改用单选标签,确定程度改用圆点,其余按钮改用已有组件。动效只淡入。
自测:tsc 通过;frontend lint 0 error(122 条原有警告);校正前端测试 2291 条,通过 2270、失败 21,与改前名单一致;Python test_rectification_*.py 26 个文件退出码 0;next build 后 / 仍是 Static;首屏 JS gzip-9 644683,相对 1516ad56 记下的 642174 增加 0.39%。隐私标记测试通过。
Co-Authored-By: Grok (agent loop)
This commit is contained in:
co-authored by
Grok (agent loop)
parent
1516ad56ec
commit
728ead8844
@@ -1,5 +1,13 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-25 — 生时校正的选项卡片换成组件库
|
||||
|
||||
- 校正里的选项仍是圆角卡片。点中之后是浅底描边。确定程度仍是三个圆点,大概范围仍是一排标签。
|
||||
- 盘面的关闭仍是一枚叉。重新打开、采用和其余确认都换成了同一套按钮。
|
||||
- 题目出现时只淡入。系统开了减少动态效果时同样只淡入,没有位移,也没有来回循环。
|
||||
- 题目、计分、扣点和保存都没有改。明确说要校正的人仍然直接开始,没做完的仍然接着做。
|
||||
- Skill 版本不变(BUG-1438)。
|
||||
|
||||
## 2026-10-25 — 报告、星历和登录页换成组件库
|
||||
|
||||
- 报告的中文和 English 是一排分段。切换之后,页面上不留下上一种语言的正文。
|
||||
|
||||
@@ -19494,4 +19494,20 @@
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/ui-kit-5-pages-20261023`。
|
||||
|
||||
## BUG-1438 | 生时校正界面换成组件库
|
||||
|
||||
- 状态:enhancement
|
||||
- 首次发现:2026-10-25
|
||||
- 最近更新:2026-10-25
|
||||
- 影响面:校正里的选项卡片、确定程度、大概范围、经历草稿的精度、盘面的关闭和重新打开、采用按钮。题目怎么出、怎么计分、怎么落库、怎么扣点,都不在这次的改动里。校正入口也不在这次:明确要求校正的人仍然直接开始,没做完的仍然接着做。
|
||||
- 用户现象:选项仍是原来的圆角卡片,点中后浅底描边。确定程度仍是三个圆点。范围仍是一排标签。盘面关闭仍是一枚叉。题目出现时,如果动效已经载入,只淡入;系统开了减少动态效果时同样只淡入,没有位移,也没有循环。
|
||||
- 触发条件:打开一次生时校正,或在建档时填写出生时间的确定程度。
|
||||
- 根因:不是缺陷。组件库迁移的生时校正这一张(任务书 K6,决策 D4、D7、D9)。
|
||||
- 修复:选项卡片和大概范围改用单选标签组。确定程度改用单选圆点。其余确认、关闭、重新打开和采用改用已有按钮。经历草稿的精度改用已有下拉,可见文字仍是「记得的精度」。动效用已有的展开预设,只淡入。校正对话仍在首页里,没有为了体积把整段校正拆出首页。盘面探头仍挂在顶栏原来的位置,没有改成弹窗。
|
||||
- 验证:见 `docs/tasks/PROGRESS-ui-kit-6-rectification-20261023.md`。没有登录态,不把浏览器里走完一次校正写成已经做过。
|
||||
- 防复发:选项卡片的合同仍锁住选中态、不出现「已选择」、不在卡片上抢焦点。白名单测试锁住名单只减不增。入口行为仍由 #103 的合同锁住,本张不改那条路径。
|
||||
- 相关记录:issue #110。任务书 `docs/tasks/TASK-ui-kit-migration-20261023.md` 的 K6。校正入口沿用 #103,不另开编号。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/ui-kit-6-rectification-20261023`。
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# PROGRESS · 组件库 K6 生时校正界面(2026-10-25)
|
||||
|
||||
工单 #110。分支 `codex/ui-kit-6-rectification-20261023`。基线 `origin/staging` `1516ad56ec66c5e80ef2a1f0ef4d82152b163699`(#109 报告、星历、登录和 #103 校正入口都在这个提交里)。只做任务书 **K6 生时校正界面**。K7 没收尾。没有重做校正入口、报告、登录、星盘页。
|
||||
|
||||
## 结论
|
||||
|
||||
校正的选项卡片换成了组件库的单选标签。题目、计分、扣点和保存都没改。明确说要校正的人仍然直接开始,没做完的仍然接着做。
|
||||
|
||||
## 做了什么
|
||||
|
||||
- 选项卡片(引导题和对话里的判断卡)改用 `ToggleGroup`,一次只选一个。原来的圆角、浅底描边、题干和选项原文都留着。不写「已选择」,卡片自己也不抢焦点。
|
||||
- 「你对这个时间有多确定?」改用 `RadioGroup`,仍是三个圆点。「选一个最接近的范围」改用 `ToggleGroup`。
|
||||
- 其余确认、暂停、重试、关闭、重新打开、采用,改用已有的 `Button`。关闭盘面仍是 Lucide 的叉,读屏名仍是「关闭盘面」。没有用字符当图标。
|
||||
- 经历草稿的精度改用已有下拉。可见文字仍是「记得的精度」,触发按钮用 `aria-labelledby` 指到这句,方便读屏。对照 `frontend/docs/VOICE.md`:没有新文案,没有改题目。
|
||||
- 题目出现时,若 Motion 在这次挂载之前已经载入,用已有的 `disclosure` 预设只淡入。减少动态效果时时长用预设里的 0.15 秒淡入,没有位移。没有新增循环。动效还没载入时不包一层,避免已经在屏幕上的内容再闪一次。
|
||||
- 盘面探头仍用原来的 `createPortal` 挂到顶栏,没有改成弹窗。`rectification-agentic-chat.tsx` 因此仍留在白名单里。
|
||||
- 白名单去掉 12 个校正文件,没有加长。测试里写死的基线名单没有改。
|
||||
- 没有改 `page.tsx`,没有把整段校正拆出首页。`Home()` 的 useState / useRef 没有增长。没有给 `scripts/jyotish_api_server.py` 加方法。没有改 `.gitea/**`,没有改生产部署,没有改数据库,没有升级依赖。
|
||||
|
||||
移出白名单的 12 个文件:`birth-time-candidate-result.tsx`、`birth-time-choice-question.tsx`、`birth-time-evidence-draft-card.tsx`、`birth-time-guide-turn.tsx`、`birth-time-intake.tsx`、`birth-time-legacy-rectification.tsx`、`birth-time-rectification.tsx`、`rectification-board.tsx`、`rectification-choice-card.tsx`、`rectification-composer-reopen.tsx`、`rectification-range-delivery.tsx`、`rectification-segment-delivery.tsx`。
|
||||
|
||||
## 断言改动(原值 / 新值 / 原因)
|
||||
|
||||
| 位置 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| `frontend/tests/rectification-agentic-entry.test.ts` 持久化选择题的选项 class | `class="birth-time-choice-option is-primary"` 整段恰好 4 次 | 同一串作为 class 的一部分出现 4 次 | K6 选项改用 ToggleGroup,组件库会合并上自己的类。选项文案和读屏名称不变 |
|
||||
|
||||
选中态源码 `data-selected={selectedKey === option.key ? "true" : "false"}` 没有改。卡片仍不得出现 `Check`、「已选择」和 `.focus(`。
|
||||
|
||||
## 自测
|
||||
|
||||
改前先在同一批测试上记过一笔。Windows 上本来就有 21 条失败。改完再跑,失败名单没有变长,通过的没有变少。
|
||||
|
||||
| 检查 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 类型 | 仓库根 `frontend\node_modules\.bin\tsc --noEmit -p frontend` | 退出码 0 |
|
||||
| 检查 | 在 `frontend` 下 `npm run lint` | 退出码 0。122 个警告,0 error。警告数和上一张记下的一样 |
|
||||
| 校正前端 | 在 `frontend` 下 `node node_modules/tsx/dist/cli.mjs --test`,233 个 `rectification-*` / `birth-time-*` 测试文件加 `ui-kit-whitelist.test.ts` | 2291 条。通过 2270,失败 21,取消 0。与改前一致 |
|
||||
| Python | `.venv\Scripts\python.exe -m pytest`,`tests/test_rectification_*.py` 共 26 个文件 | 退出码 0,进度到 100% |
|
||||
| 构建 | 在 `frontend` 下 `npm run build` | 退出码 0。编译 10.3s,TypeScript 30.5s。79 个静态页。`/` 仍是 ○ Static。5 条既有 skill-package-registry 警告,没有改 |
|
||||
| 首屏体积 | 首页 `index.html` 引用的 `/_next/static/**.js`,gzip-9 相加,不含 CSS | 基线是 #109 在 `1516ad56` 记下的 35 个文件、642174 字节。本张 37 个文件,缺失 0,644683 字节,多 2509(+0.39%)。±2% 大约是 629331 到 655017。没有把校正拆出首页 |
|
||||
|
||||
改前就失败、改完仍是这 21 条(没有新增):
|
||||
|
||||
1. public code never imports the internal dynamic choice contract(路径被拼成 `G:\G:\...`)
|
||||
2. candidate scores stay out of the specified client ownership boundary(同上)
|
||||
3. A · typed answer to a pending choice(BUG-1045,`getComputedStyle`)
|
||||
4. A · collect denial with a next choice(BUG-1045)
|
||||
5. B · failed choice submit 409(BUG-1046,`window is not defined`)
|
||||
6. B · failed choice submit network error(BUG-1046)
|
||||
7. B · typed answer that never reaches the server
|
||||
8. D3 · live focus moves to the latest message(BUG-635)
|
||||
9. D3 · persisted question with no asked turn
|
||||
10. D3 · standalone block fallback
|
||||
11. D4 · willContinue
|
||||
12. D4 · send() settle
|
||||
13. C · superseded focus(BUG-1135)
|
||||
14. C · client guard(BUG-1135)
|
||||
15. C · stem left in the body(BUG-1135)
|
||||
16. Skill bump 10.0.32 keeps Cases pinned(BUG-621)
|
||||
17. D3 · first stage line
|
||||
18. D3 · turn.rejected
|
||||
19. opening on the real surface · body only
|
||||
20. opening on the real surface · body + stem
|
||||
21. agent pins the dedicated rectification skill and its fixed version
|
||||
|
||||
没有跑前端全量 `npm test`,没有跑 Python 快速门,没有跑 `test:db`。没有为了让测试通过去改计分或落库。
|
||||
|
||||
## 环境缺口
|
||||
|
||||
- 没有登录态,没有模型凭据。没有在浏览器里走完一次校正。不写成通过。可照做的步骤在 `docs/testing/ui-kit-migration-20261023.md` 的「生时校正」一节。
|
||||
@@ -87,3 +87,14 @@
|
||||
2. 走到填写六位验证码时,是六格分开的输入。恢复码仍是一条输入框。
|
||||
3. 注册账号、忘记密码、返回登录仍是这几个字。没勾同意就继续,仍会提示先阅读并勾选。
|
||||
4. 打开那一刻的天空。星星仍会汇聚到定格。左上关闭、右上分享仍在。没有重画星星。
|
||||
|
||||
## 生时校正
|
||||
|
||||
没有登录态,没有在浏览器里走完一次校正,也没有模型凭据。下面这些步骤留给有账号的人照做。
|
||||
|
||||
1. 在对话里明确说要校正出生时间。界面直接开始一次新的校正,不再先出一张卡让你点。已经开始、还没做完的,接着上次做,不另开一轮。
|
||||
2. 题目仍是原来的句子。选项是圆角卡片,点中后变成浅底描边。读屏能读到题干和选项原文,没有多出来的「已选择」。
|
||||
3. 建档或改出生时间时,「你对这个时间有多确定?」仍是三个圆点。选了大概时间之后,「选一个最接近的范围」仍是一排标签。
|
||||
4. 校正进行中可以打开当前盘面,再关掉。关闭是一枚叉,读屏名是「关闭盘面」。窄屏上的探头仍在顶栏,不是弹窗。
|
||||
5. 系统开了减少动态效果时,新出现的题目只淡入,选项不飞,也没有一直转的动画。
|
||||
6. 做完或停下来之后,已经记下的范围还在。扣点和保存跟换皮之前一样。
|
||||
|
||||
@@ -49,6 +49,8 @@
|
||||
|
||||
不新增等待动画;打开校正沿用既有一次揭幕。
|
||||
|
||||
**选项和按钮换成组件库(2026-10-25,K6)**:选项卡片仍是原来的圆角卡片,点中后浅底描边,读屏仍读题干和选项原文,不另写「已选择」。确定程度仍是三个圆点。大概范围仍是一排标签。盘面关闭仍是一枚叉,探头仍挂在顶栏原来的位置,不是弹窗。其余确认、重新打开和采用用组件库按钮,样子沿用原来的实心、描边和文字按钮。题目出现时,若动效已经载入,只淡入;减少动态效果时同样只淡入,没有位移,也没有循环。题目、计分、扣点和保存没有改。明确要求校正仍直接开始,没做完的接着做。
|
||||
|
||||
**明确要求则直接打开(2026-10-12,BUG-1306)**:用户这一轮明说要做校正时,不再先出卡再点。回答只留一句过渡,界面沿用既有一次揭幕直接打开校正面。没有新的按钮、输入框、等待动画或 Home 状态。推得不准时的主动提议仍用 `.rectification-offer-card`。做不了时只显示一句话,不静默,也不在对话里假装校正。开案还没成功时,同一张提议卡仍是普通提议(不说「已经开始」,按钮仍是「开始核对出生时间」)。开案成功并绑定来源后才换成已开始的文案。打开失败时按钮仍可点。
|
||||
|
||||
## 用量页分段计时(2026-10-05,在途)
|
||||
|
||||
@@ -3,21 +3,9 @@
|
||||
"src/components/ayanamsa-preference-field.tsx",
|
||||
"src/components/birth-place-picker.tsx",
|
||||
"src/components/birth-sky/birth-sky-converge.tsx",
|
||||
"src/components/birth-time-candidate-result.tsx",
|
||||
"src/components/birth-time-choice-question.tsx",
|
||||
"src/components/birth-time-evidence-draft-card.tsx",
|
||||
"src/components/birth-time-guide-turn.tsx",
|
||||
"src/components/birth-time-intake.tsx",
|
||||
"src/components/birth-time-legacy-rectification.tsx",
|
||||
"src/components/birth-time-rectification.tsx",
|
||||
"src/components/consultation-run-timeline.tsx",
|
||||
"src/components/home-bootstrap-error.tsx",
|
||||
"src/components/home-onboarding-shell.tsx",
|
||||
"src/components/personal-report/personal-report-markdown-view.tsx",
|
||||
"src/components/rectification-agentic-chat.tsx",
|
||||
"src/components/rectification-board.tsx",
|
||||
"src/components/rectification-choice-card.tsx",
|
||||
"src/components/rectification-composer-reopen.tsx",
|
||||
"src/components/rectification-range-delivery.tsx",
|
||||
"src/components/rectification-segment-delivery.tsx"
|
||||
"src/components/rectification-agentic-chat.tsx"
|
||||
]
|
||||
|
||||
@@ -5,14 +5,15 @@
|
||||
.birth-time-choice-question legend { width: 100%; margin: 0 0 var(--space-3); color: var(--color-ink); font-family: var(--font-display); font-size: var(--type-title-md); font-weight: 400; line-height: 1.55; text-wrap: pretty; word-break: auto-phrase; }
|
||||
.birth-time-primary-choices { display: grid; gap: var(--space-2); }
|
||||
.birth-time-special-choices, .birth-time-reframe-actions, .birth-time-journey-controls { display: flex; flex-wrap: wrap; gap: var(--space-2); }
|
||||
.birth-time-choice-option { min-height: 52px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); cursor: pointer; font-size: var(--type-body-sm); line-height: 1.5; text-align: left; text-wrap: pretty; word-break: auto-phrase; transition: border-color 120ms ease-out, background-color 120ms ease-out, transform 120ms ease-out; }
|
||||
.birth-time-choice-option { min-height: 52px; height: auto; justify-content: flex-start; white-space: normal; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); cursor: pointer; font-size: var(--type-body-sm); font-weight: 400; line-height: 1.5; text-align: left; text-wrap: pretty; word-break: auto-phrase; transition: border-color 120ms ease-out, background-color 120ms ease-out, transform 120ms ease-out; }
|
||||
.birth-time-choice-option.is-primary { width: 100%; padding: var(--space-3) var(--space-4); }
|
||||
.birth-time-choice-option.is-secondary { min-height: 44px; flex: 1 1 0; padding: var(--space-2) var(--space-4); color: var(--color-ink-secondary); text-align: center; }
|
||||
.birth-time-choice-option:hover:not(:disabled) { border-color: var(--color-border-strong); background: var(--color-canvas-soft); }
|
||||
.birth-time-choice-option.is-secondary { min-height: 44px; flex: 1 1 0; justify-content: center; padding: var(--space-2) var(--space-4); color: var(--color-ink-secondary); text-align: center; }
|
||||
.birth-time-choice-option:hover:not(:disabled) { border-color: var(--color-border-strong); background: var(--color-canvas-soft); color: var(--color-ink); }
|
||||
.birth-time-choice-option.is-secondary:hover:not(:disabled) { color: var(--color-ink-secondary); }
|
||||
.birth-time-choice-option:active:not(:disabled) { transform: scale(.96); }
|
||||
.birth-time-choice-option[data-selected="true"] { border-color: var(--color-action); background: var(--color-action-soft); color: var(--color-ink); }
|
||||
.birth-time-choice-option:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -2px; }
|
||||
.birth-time-choice-option[data-selected="true"]:focus-visible { outline: none; }
|
||||
.birth-time-choice-option:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -2px; box-shadow: none; border-color: var(--color-border); }
|
||||
.birth-time-choice-option[data-selected="true"]:focus-visible { outline: none; box-shadow: none; border-color: var(--color-action); }
|
||||
@media (forced-colors: active) {
|
||||
.birth-time-choice-option[data-selected="true"]:focus-visible { outline: 2px solid Highlight; outline-offset: -2px; }
|
||||
}
|
||||
|
||||
@@ -1261,14 +1261,17 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
|
||||
.birth-time-source-option { min-height: 64px; display: grid; grid-template-columns: 18px minmax(0, 1fr); align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); cursor: pointer; transition: border-color 120ms ease-out, background-color 120ms ease-out, transform 120ms ease-out; }
|
||||
.birth-time-source-option.is-selected { border-color: var(--color-action); background: var(--color-action-soft); }
|
||||
.birth-time-source-option input { width: 16px; height: 16px; margin: 0; accent-color: var(--color-action); }
|
||||
.birth-time-source-option [data-slot="radio-group-item"] { width: 16px; height: 16px; margin: 0; accent-color: var(--color-action); }
|
||||
.birth-time-source-option > span { display: grid; gap: 3px; }
|
||||
.birth-time-source-option b { color: var(--color-ink); font-size: var(--type-body-sm); font-weight: 600; }
|
||||
.birth-time-source-option small, .birth-time-detail-note, .birth-time-legacy-note { margin: 0; color: var(--color-ink-secondary); font-size: var(--type-caption); line-height: 1.5; }
|
||||
.birth-time-uncertainty-fieldset { margin: var(--space-3) 0 0; padding: 0; border: 0; }
|
||||
.birth-time-uncertainty-fieldset legend { margin-bottom: var(--space-2); color: var(--color-ink-secondary); font-size: var(--type-caption); font-weight: 600; }
|
||||
.birth-time-uncertainty-list { display: flex; flex-wrap: wrap; gap: var(--space-2); }
|
||||
.birth-time-uncertainty-option { min-height: 44px; padding: 0 var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); font-size: var(--type-body-sm); cursor: pointer; }
|
||||
.birth-time-uncertainty-option.is-selected { border-color: var(--color-action); background: var(--color-action-soft); }
|
||||
.birth-time-uncertainty-list { display: flex; flex-wrap: wrap; gap: var(--space-2); width: 100%; }
|
||||
.birth-time-uncertainty-option { min-height: 44px; height: auto; justify-content: center; white-space: normal; padding: 0 var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); font-size: var(--type-body-sm); font-weight: 400; cursor: pointer; }
|
||||
.birth-time-uncertainty-option:hover { background: var(--color-canvas); color: var(--color-ink); }
|
||||
.birth-time-uncertainty-option.is-selected,
|
||||
.birth-time-uncertainty-option.is-selected:hover { border-color: var(--color-action); background: var(--color-action-soft); color: var(--color-ink); }
|
||||
.birth-time-detail-grid { display: grid; grid-template-columns: minmax(0, 180px) minmax(0, 1fr); align-items: end; gap: var(--space-3); }
|
||||
.birth-time-detail-grid > label { min-width: 0; }
|
||||
.birth-time-detail-note { padding-bottom: 12px; }
|
||||
@@ -1327,8 +1330,9 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
|
||||
.birth-time-guide-composer textarea { min-height: var(--space-24); resize: vertical; line-height: 1.55; }
|
||||
.birth-time-guide-hint, .birth-time-scoring-status p, .birth-time-candidate-terminal p, .birth-time-success-note { margin: 0; color: var(--color-ink-secondary); font-size: var(--type-body-sm); line-height: 1.6; text-wrap: pretty; word-break: auto-phrase; }
|
||||
.birth-time-guided-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-2); }
|
||||
.birth-time-guided-action { min-height: 44px; padding-inline: var(--space-4); font-size: var(--type-body-sm); }
|
||||
.birth-time-guided-action { min-height: 44px; height: auto; padding-inline: var(--space-4); font-size: var(--type-body-sm); white-space: normal; }
|
||||
.button-text.birth-time-guided-action { border: 0; background: transparent; color: var(--color-action); cursor: pointer; }
|
||||
.button-text.birth-time-guided-action:hover { background: transparent; color: var(--color-action); }
|
||||
.birth-time-draft-domain { margin: 0; padding: var(--space-3) var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); }
|
||||
.birth-time-draft-domain > div { display: grid; gap: var(--space-1); }
|
||||
.birth-time-draft-domain dt { color: var(--color-ink-tertiary); font-size: var(--type-overline); }
|
||||
@@ -2893,6 +2897,10 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
.birth-time-source-option.is-selected { border-color: var(--color-action); background: var(--color-action-soft); box-shadow: inset 0 0 0 1px var(--color-action); }
|
||||
.birth-time-source-option input { appearance: none; align-self: center; justify-self: center; position: static; box-sizing: border-box; width: 16px; min-width: 16px; max-width: 16px; height: 16px; min-height: 16px; max-height: 16px; margin: 0; overflow: visible; clip: auto; clip-path: none; border: 1.5px solid var(--color-border-strong); border-radius: 50%; background: var(--color-canvas); }
|
||||
.birth-time-source-option input:checked { border-color: var(--color-action); background: var(--color-action-strong); box-shadow: inset 0 0 0 4px var(--color-action-soft); }
|
||||
.birth-time-source-option [data-slot="radio-group-item"] { appearance: none; align-self: center; justify-self: center; position: static; box-sizing: border-box; width: 16px; min-width: 16px; max-width: 16px; height: 16px; min-height: 16px; max-height: 16px; margin: 0; overflow: visible; clip: auto; clip-path: none; border: 1.5px solid var(--color-border-strong); border-radius: 50%; background: var(--color-canvas); box-shadow: none; }
|
||||
.birth-time-source-option [data-slot="radio-group-item"][data-checked] { border-color: var(--color-action); background: var(--color-action-strong); box-shadow: inset 0 0 0 4px var(--color-action-soft); }
|
||||
.birth-time-source-option [data-slot="radio-group-item"]:focus-visible { outline: none; box-shadow: none; }
|
||||
.birth-time-source-option [data-slot="radio-group-indicator"] { display: none; }
|
||||
.birth-time-source-option > span { gap: 4px; }
|
||||
.birth-time-source-option b { font-size: var(--type-body-sm); line-height: 1.35; }
|
||||
.birth-time-source-option small { font-size: var(--type-caption); line-height: 1.5; }
|
||||
@@ -2906,7 +2914,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
font-size: var(--type-caption);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.birth-time-window-details .birth-time-skip-button { grid-column: 1 / -1; justify-self: start; min-height: 44px; padding-inline: 13px; border-color: transparent; background: transparent; color: var(--color-action); font-size: var(--type-caption); }
|
||||
.birth-time-window-details .birth-time-skip-button { grid-column: 1 / -1; justify-self: start; min-height: 44px; height: auto; padding-inline: 13px; border-color: transparent; background: transparent; color: var(--color-action); font-size: var(--type-caption); font-weight: 400; white-space: normal; }
|
||||
.birth-time-window-details .birth-time-skip-button:hover { background: color-mix(in srgb, var(--color-action-soft) 72%, transparent); }
|
||||
.birth-time-detail-grid > label { gap: var(--space-2); }
|
||||
.birth-time-detail-grid > label > span { color: var(--color-ink-secondary); font-size: var(--type-caption); font-weight: 600; }
|
||||
@@ -3500,6 +3508,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 44px;
|
||||
margin: 0 0 var(--space-2);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
@@ -3507,7 +3516,9 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-canvas-soft);
|
||||
color: var(--color-ink);
|
||||
font-weight: 400;
|
||||
text-align: start;
|
||||
white-space: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
.rectification-board-peek strong { font-size: var(--type-body-sm); }
|
||||
@@ -3805,6 +3816,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
}
|
||||
.rectification-range-delivery__accept {
|
||||
justify-self: stretch;
|
||||
height: auto;
|
||||
min-height: 44px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
@@ -3813,8 +3825,13 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
background: var(--color-canvas);
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
white-space: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
.rectification-range-delivery__accept:hover {
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-action);
|
||||
}
|
||||
.rectification-range-delivery__accept[aria-pressed="true"] {
|
||||
border-color: color-mix(in srgb, var(--color-action) 55%, var(--color-border));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { JourneyClientResponse } from "@/lib/birth-time-journey-client";
|
||||
import type { BirthTimeGuidedController } from "@/hooks/use-birth-time-guided-journey";
|
||||
import { guidedTerminalPath } from "@/lib/birth-time-guided-terminal";
|
||||
import { ErrorToast } from "@/components/error-toast";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type CandidateResultProps = {
|
||||
readonly journey: JourneyClientResponse;
|
||||
@@ -44,7 +45,7 @@ export function BirthTimeCandidateResult({ journey, controller, error }: Candida
|
||||
<div className="birth-time-candidate-result" aria-live="polite">
|
||||
<p className="birth-time-assessment-unavailable">尚未进入分钟计算:还没有可评分的关键经历资料。</p>
|
||||
<p className="birth-time-evidence-boundary">当前范围仅为填报范围,不是校正结果;请补充跨领域、可注明年月的重大事件后重新评估。</p>
|
||||
{terminalPath && <button className="button-secondary birth-time-guided-action" disabled={controller.pending} type="button" onClick={controller.editBirthTimeDetails}>补充资料并重新评估</button>}
|
||||
{terminalPath && <Button className="button-secondary birth-time-guided-action" disabled={controller.pending} type="button" variant="outline" onClick={controller.editBirthTimeDetails}>补充资料并重新评估</Button>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -110,9 +111,9 @@ export function BirthTimeCandidateResult({ journey, controller, error }: Candida
|
||||
{action.kind === "present_medium_result" && winner && !dynamic && (
|
||||
<div className="birth-time-candidate-terminal">
|
||||
<p>可以保存候选时间范围,代表时间不会<span className="phrase-nowrap">用于</span>排盘。</p>
|
||||
<button className="button-primary birth-time-guided-action" disabled={controller.pending} type="button" onClick={() => controller.saveCandidate(result.resultId)}>
|
||||
<Button className="button-primary birth-time-guided-action" disabled={controller.pending} type="button" onClick={() => controller.saveCandidate(result.resultId)}>
|
||||
{controller.pending ? "保存中…" : "保存候选范围"}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{action.kind === "candidate_saved" && (
|
||||
@@ -125,15 +126,15 @@ export function BirthTimeCandidateResult({ journey, controller, error }: Candida
|
||||
<div className="birth-time-confirmation-panel">
|
||||
<b>确认<span className="phrase-nowrap">当前排盘使用时间</span></b>
|
||||
<p><span className="phrase-nowrap">候选时间</span>为 {winner.representativeTime}。确认后它会成为<span className="phrase-nowrap">当前排盘使用时间</span>,<span className="phrase-nowrap">原始填报时间</span>仍会保留。</p>
|
||||
<button className="button-primary birth-time-guided-action" disabled={controller.pending} type="button" onClick={() => controller.confirmCandidate(result.resultId, winner.representativeTime)}>
|
||||
<Button className="button-primary birth-time-guided-action" disabled={controller.pending} type="button" onClick={() => controller.confirmCandidate(result.resultId, winner.representativeTime)}>
|
||||
{controller.pending ? "确认中…" : `确认使用 ${winner.representativeTime}`}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{action.kind === "ready" && (
|
||||
<div className="birth-time-candidate-terminal" role="status">
|
||||
<p className="birth-time-success-note"><span className="phrase-nowrap">当前排盘使用时间</span>已更新为 {action.activeTime},<span className="phrase-nowrap">原始填报时间</span>仍已保留。</p>
|
||||
<button className="button-primary birth-time-guided-action" disabled={controller.pending} type="button" onClick={controller.acknowledgeReady}>继续使用此排盘</button>
|
||||
<Button className="button-primary birth-time-guided-action" disabled={controller.pending} type="button" onClick={controller.acknowledgeReady}>继续使用此排盘</Button>
|
||||
</div>
|
||||
)}
|
||||
{!terminalPath ? <ErrorToast message={error} /> : null}
|
||||
@@ -150,7 +151,7 @@ function TerminalAction({ controller, error, path }: {
|
||||
<div className="birth-time-new-assessment">
|
||||
<b>尚未达到采用条件</b>
|
||||
<p>候选范围已保留,但当前证据不足以将具体分钟写入当前排盘时间。补充经历后可重新评估。</p>
|
||||
<button className="button-secondary birth-time-guided-action" disabled={controller.pending} onClick={controller.editBirthTimeDetails} type="button">开始新的评估</button>
|
||||
<Button className="button-secondary birth-time-guided-action" disabled={controller.pending} onClick={controller.editBirthTimeDetails} type="button" variant="outline">开始新的评估</Button>
|
||||
<ErrorToast message={error} />
|
||||
<small>会建立新的记录,当前结果仍会保留。</small>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { CharacterRemaining } from "@/components/character-remaining";
|
||||
import { RectificationFade } from "@/components/rectification-fade";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { characterRemainingVisible } from "@/lib/character-remaining";
|
||||
import {
|
||||
choiceQuestionGroups,
|
||||
@@ -62,41 +65,55 @@ export function BirthTimeChoiceQuestion(props: ChoiceQuestionProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<RectificationFade>
|
||||
<div className="birth-time-choice-surface" aria-busy={props.pending}>
|
||||
<BirthTimeQuestionProgress progress={props.progress} />
|
||||
<fieldset className="birth-time-choice-question" disabled={props.pending}>
|
||||
<legend>{props.question.prompt}</legend>
|
||||
<div className="birth-time-primary-choices">
|
||||
{groups.primary.map((option, index) => (
|
||||
<button
|
||||
className="birth-time-choice-option is-primary"
|
||||
data-selected={selectedId === option.optionId}
|
||||
key={option.optionId}
|
||||
onClick={() => select(option)}
|
||||
ref={index === 0 ? firstChoiceRef : undefined}
|
||||
type="button"
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="birth-time-special-choices">
|
||||
{[groups.unknown, groups.unmatched].map((option) => (
|
||||
<button
|
||||
className="birth-time-choice-option is-secondary"
|
||||
data-selected={selectedId === option.optionId}
|
||||
key={option.optionId}
|
||||
onClick={() => select(option)}
|
||||
type="button"
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<ToggleGroup
|
||||
className="contents"
|
||||
style={{ display: "contents" }}
|
||||
value={selectedId ? [selectedId] : []}
|
||||
onValueChange={(next) => {
|
||||
const id = next[0];
|
||||
if (!id || id === selectedId) return;
|
||||
const option = [...groups.primary, groups.unknown, groups.unmatched].find((item) => item.optionId === id);
|
||||
if (option) select(option);
|
||||
}}
|
||||
>
|
||||
<div className="birth-time-primary-choices">
|
||||
{groups.primary.map((option, index) => (
|
||||
<ToggleGroupItem
|
||||
className="birth-time-choice-option is-primary"
|
||||
data-selected={selectedId === option.optionId}
|
||||
key={option.optionId}
|
||||
ref={index === 0 ? firstChoiceRef : undefined}
|
||||
type="button"
|
||||
value={option.optionId}
|
||||
>
|
||||
{option.label}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</div>
|
||||
<div className="birth-time-special-choices">
|
||||
{[groups.unknown, groups.unmatched].map((option) => (
|
||||
<ToggleGroupItem
|
||||
className="birth-time-choice-option is-secondary"
|
||||
data-selected={selectedId === option.optionId}
|
||||
key={option.optionId}
|
||||
type="button"
|
||||
value={option.optionId}
|
||||
>
|
||||
{option.label}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</div>
|
||||
</ToggleGroup>
|
||||
</fieldset>
|
||||
{props.pending && <p className="birth-time-choice-pending" role="status">正在缩小候选范围…</p>}
|
||||
<JourneyControls {...props} />
|
||||
</div>
|
||||
</RectificationFade>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,6 +128,7 @@ export function BirthTimeUnmatchedClarification(props: ClarificationProps) {
|
||||
}
|
||||
}, [props.error, props.pending, props.progress.answeredCount]);
|
||||
return (
|
||||
<RectificationFade>
|
||||
<div className="birth-time-choice-surface" aria-busy={props.pending}>
|
||||
<BirthTimeQuestionProgress progress={props.progress} />
|
||||
<fieldset className="birth-time-choice-question" disabled={props.pending}>
|
||||
@@ -129,13 +147,14 @@ export function BirthTimeUnmatchedClarification(props: ClarificationProps) {
|
||||
/>
|
||||
</label>
|
||||
<div className="birth-time-reframe-actions">
|
||||
<button className="button-secondary birth-time-guided-action" onClick={() => props.onReframe("")} ref={reframeRef} type="button">换一道题</button>
|
||||
<button className="button-primary birth-time-guided-action" onClick={() => props.onReframe(normalizeUnmatchedNote(note))} type="button">提交补充并换题</button>
|
||||
<Button className="button-secondary birth-time-guided-action" onClick={() => props.onReframe("")} ref={reframeRef} type="button" variant="outline">换一道题</Button>
|
||||
<Button className="button-primary birth-time-guided-action" onClick={() => props.onReframe(normalizeUnmatchedNote(note))} type="button">提交补充并换题</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
{props.pending && <p className="birth-time-choice-pending" role="status">正在换一道更合适的问题…</p>}
|
||||
<JourneyControls {...props} />
|
||||
</div>
|
||||
</RectificationFade>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,8 +162,8 @@ function JourneyControls(props: SharedQuestionProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="birth-time-journey-controls">
|
||||
<button className="button-text birth-time-guided-action" disabled={props.pending} onClick={props.onPause} type="button">暂停,稍后继续</button>
|
||||
<button className="button-text birth-time-guided-action" disabled={props.pending} onClick={props.onFinish} type="button">结束并保存当前范围</button>
|
||||
<Button className="button-text birth-time-guided-action" disabled={props.pending} onClick={props.onPause} type="button" variant="ghost">暂停,稍后继续</Button>
|
||||
<Button className="button-text birth-time-guided-action" disabled={props.pending} onClick={props.onFinish} type="button" variant="ghost">结束并保存当前范围</Button>
|
||||
</div>
|
||||
<ErrorToast message={props.error} />
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useId, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { lifeEventSchema } from "@/lib/birth-time-evidence";
|
||||
import type { EvidenceDraft } from "@/lib/birth-time-journey-turn";
|
||||
import type { EvidenceDatePrecision, LegacyGuidedEvidenceDomain } from "@/lib/birth-time-question-planner";
|
||||
@@ -21,12 +23,8 @@ type DraftCardProps = {
|
||||
readonly onSkip: () => void;
|
||||
};
|
||||
|
||||
function parsePrecision(value: string): EvidenceDatePrecision {
|
||||
if (value === "year" || value === "month" || value === "day") return value;
|
||||
throw new TypeError("Unsupported evidence date precision");
|
||||
}
|
||||
|
||||
export function BirthTimeEvidenceDraftCard(props: DraftCardProps) {
|
||||
const precisionLabelId = useId();
|
||||
const [precision, setPrecision] = useState<EvidenceDatePrecision>(props.draft.precision ?? "year");
|
||||
const [date, setDate] = useState(props.draft.date ?? "");
|
||||
const isValid = lifeEventSchema.safeParse({
|
||||
@@ -43,12 +41,25 @@ export function BirthTimeEvidenceDraftCard(props: DraftCardProps) {
|
||||
<dl className="birth-time-draft-domain"><div><dt>经历领域</dt><dd>{domainLabels[props.draft.domain]}</dd></div></dl>
|
||||
<div className="birth-time-draft-fields">
|
||||
<label>
|
||||
<span>记得的精度</span>
|
||||
<select disabled={props.pending} value={precision} onChange={(event) => { setPrecision(parsePrecision(event.target.value)); setDate(""); }}>
|
||||
<option value="year">只记得年份</option>
|
||||
<option value="month">记得月份</option>
|
||||
<option value="day">记得具体日期</option>
|
||||
</select>
|
||||
<span id={precisionLabelId}>记得的精度</span>
|
||||
<Select
|
||||
disabled={props.pending}
|
||||
value={precision}
|
||||
onValueChange={(next) => {
|
||||
if (next !== "year" && next !== "month" && next !== "day") return;
|
||||
setPrecision(next);
|
||||
setDate("");
|
||||
}}
|
||||
>
|
||||
<SelectTrigger aria-labelledby={precisionLabelId}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="year">只记得年份</SelectItem>
|
||||
<SelectItem value="month">记得月份</SelectItem>
|
||||
<SelectItem value="day">记得具体日期</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</label>
|
||||
<label>
|
||||
<span>发生时间</span>
|
||||
@@ -66,10 +77,10 @@ export function BirthTimeEvidenceDraftCard(props: DraftCardProps) {
|
||||
</div>
|
||||
{!isValid && <p className="form-error" role="alert">请填写与所选精度一致的有效日期;<span className="phrase-nowrap">不会自动补猜缺失时间</span>。</p>}
|
||||
<div className="birth-time-guided-actions">
|
||||
<button className="button-secondary birth-time-guided-action" disabled={props.pending} type="button" onClick={props.onSkip}>放弃这条并跳过</button>
|
||||
<button className="button-primary birth-time-guided-action" disabled={props.pending || !isValid} type="button" onClick={() => props.onConfirm(precision, date)}>
|
||||
<Button className="button-secondary birth-time-guided-action" disabled={props.pending} type="button" variant="outline" onClick={props.onSkip}>放弃这条并跳过</Button>
|
||||
<Button className="button-primary birth-time-guided-action" disabled={props.pending || !isValid} type="button" onClick={() => props.onConfirm(precision, date)}>
|
||||
{props.pending ? "确认中…" : "确认并用于校正"}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { CharacterRemaining } from "@/components/character-remaining";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { characterRemainingVisible } from "@/lib/character-remaining";
|
||||
import type { JourneyProgress } from "@/lib/birth-time-journey-turn";
|
||||
import { protectOnboardingPhrases } from "@/lib/onboarding-copy";
|
||||
@@ -57,11 +58,11 @@ export function BirthTimeGuideTurn(props: BirthTimeGuideTurnProps) {
|
||||
</label>
|
||||
<p id="birth-time-guide-hint" className="birth-time-guide-hint">Enter 发送,Shift+Enter 换行。说出大概年份也可以,不需要为了校正而猜测。</p>
|
||||
<div className="birth-time-guided-actions">
|
||||
<button className="button-text birth-time-guided-action" disabled={props.pending} type="button" onClick={props.onPause}>暂停,稍后继续</button>
|
||||
<button className="button-secondary birth-time-guided-action" disabled={props.pending} type="button" onClick={props.onSkip}>不记得,跳过</button>
|
||||
<button className="button-primary birth-time-guided-action" disabled={props.pending || !message.trim()} type="button" onClick={() => props.onSubmit(message.trim())}>
|
||||
<Button className="button-text birth-time-guided-action" disabled={props.pending} type="button" variant="ghost" onClick={props.onPause}>暂停,稍后继续</Button>
|
||||
<Button className="button-secondary birth-time-guided-action" disabled={props.pending} type="button" variant="outline" onClick={props.onSkip}>不记得,跳过</Button>
|
||||
<Button className="button-primary birth-time-guided-action" disabled={props.pending || !message.trim()} type="button" onClick={() => props.onSubmit(message.trim())}>
|
||||
{props.pending ? "整理中…" : "整理为经历草稿"}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
import { useId, useState } from "react";
|
||||
import { BirthDatePicker } from "@/components/birth-date-picker";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { birthTimeConsultationOptionsCopy } from "@/lib/birth-time-consultation-consent";
|
||||
import {
|
||||
approximateUncertaintyOptions,
|
||||
@@ -83,7 +86,8 @@ export function BirthTimeIntakeFields({ value, onPatch, lockReportedTime = false
|
||||
if (lockReportedTime) return;
|
||||
onPatch(patch);
|
||||
};
|
||||
const groupId = useId();
|
||||
const sourceLegendId = useId();
|
||||
const uncertaintyLegendId = useId();
|
||||
const source = value.birthTimeSource;
|
||||
const isConfirmed = value.birthTimeStatus === "confirmed";
|
||||
const displayState = birthTimeDisplayState(value);
|
||||
@@ -143,33 +147,36 @@ export function BirthTimeIntakeFields({ value, onPatch, lockReportedTime = false
|
||||
)}
|
||||
|
||||
{!isConfirmed && <fieldset className="birth-time-source-fieldset">
|
||||
<legend>你对这个时间有多确定?</legend>
|
||||
<legend id={sourceLegendId}>你对这个时间有多确定?</legend>
|
||||
<p className="birth-time-source-intro">只用在这里问一次。校正会按你选的范围去比,不再追问你有多确定。</p>
|
||||
<div className="birth-time-source-list">
|
||||
<RadioGroup
|
||||
aria-labelledby={sourceLegendId}
|
||||
className="birth-time-source-list"
|
||||
value={selectedSourceOption}
|
||||
onValueChange={(next) => {
|
||||
const option = birthTimeSourceOptions.find((item) => item.value === next);
|
||||
if (!option) return;
|
||||
emitTime({
|
||||
birthTimeSource: option.value,
|
||||
birthTimeStatus: "reported",
|
||||
time: "",
|
||||
...birthTimeSourceDefaults[option.value],
|
||||
});
|
||||
}}
|
||||
>
|
||||
{birthTimeSourceOptions.map((option) => (
|
||||
<label
|
||||
className={`birth-time-source-option ${option.value === selectedSourceOption ? "is-selected" : ""}`}
|
||||
key={option.value}
|
||||
>
|
||||
<input
|
||||
checked={option.value === selectedSourceOption}
|
||||
name={`birth-time-source-${groupId}`}
|
||||
type="radio"
|
||||
value={option.value}
|
||||
onChange={() => emitTime({
|
||||
birthTimeSource: option.value,
|
||||
birthTimeStatus: "reported",
|
||||
time: "",
|
||||
...birthTimeSourceDefaults[option.value],
|
||||
})}
|
||||
/>
|
||||
<RadioGroupItem value={option.value} />
|
||||
<span>
|
||||
<b>{option.label}</b>
|
||||
<small>{option.hint}</small>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</fieldset>}
|
||||
|
||||
{usesClockTime && (
|
||||
@@ -187,28 +194,38 @@ export function BirthTimeIntakeFields({ value, onPatch, lockReportedTime = false
|
||||
)}
|
||||
{(source === "approximate" || source === "family_exact") && (
|
||||
<fieldset className="birth-time-uncertainty-fieldset">
|
||||
<legend>选一个最接近的范围</legend>
|
||||
<div className="birth-time-uncertainty-list">
|
||||
<legend id={uncertaintyLegendId}>选一个最接近的范围</legend>
|
||||
<ToggleGroup
|
||||
aria-labelledby={uncertaintyLegendId}
|
||||
className="birth-time-uncertainty-list"
|
||||
value={approximateRadius == null ? [] : [String(approximateRadius)]}
|
||||
onValueChange={(next) => {
|
||||
const minutes = Number(next[0]);
|
||||
const option = approximateUncertaintyOptions.find((item) => item.minutes === minutes);
|
||||
if (!option) return;
|
||||
emitTime(
|
||||
option.minutes === 15 && source === "family_exact"
|
||||
? {
|
||||
uncertaintyBeforeMinutes: 0,
|
||||
uncertaintyAfterMinutes: 0,
|
||||
}
|
||||
: {
|
||||
birthTimeSource: "approximate",
|
||||
uncertaintyBeforeMinutes: option.minutes,
|
||||
uncertaintyAfterMinutes: option.minutes,
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
{approximateUncertaintyOptions.map((option) => (
|
||||
<button
|
||||
<ToggleGroupItem
|
||||
className={`birth-time-uncertainty-option ${approximateRadius === option.minutes ? "is-selected" : ""}`}
|
||||
key={option.minutes}
|
||||
type="button"
|
||||
onClick={() => emitTime(
|
||||
option.minutes === 15 && source === "family_exact"
|
||||
? {
|
||||
uncertaintyBeforeMinutes: 0,
|
||||
uncertaintyAfterMinutes: 0,
|
||||
}
|
||||
: {
|
||||
birthTimeSource: "approximate",
|
||||
uncertaintyBeforeMinutes: option.minutes,
|
||||
uncertaintyAfterMinutes: option.minutes,
|
||||
},
|
||||
)}
|
||||
>{option.label}</button>
|
||||
value={String(option.minutes)}
|
||||
>{option.label}</ToggleGroupItem>
|
||||
))}
|
||||
</div>
|
||||
</ToggleGroup>
|
||||
</fieldset>
|
||||
)}
|
||||
</div>
|
||||
@@ -239,9 +256,10 @@ export function BirthTimeIntakeFields({ value, onPatch, lockReportedTime = false
|
||||
onChange={(declaredWindowEnd) => emitTime({ declaredWindowEnd })}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
<Button
|
||||
className="button-secondary birth-time-skip-button"
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => emitTime({
|
||||
birthTimeSource: "unknown",
|
||||
reportedTime: "",
|
||||
@@ -254,7 +272,7 @@ export function BirthTimeIntakeFields({ value, onPatch, lockReportedTime = false
|
||||
birthTimeStatus: "reported",
|
||||
time: "",
|
||||
})}
|
||||
>完全不清楚,跳过出生时间</button>
|
||||
>完全不清楚,跳过出生时间</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { LegacyJourneyClientResponse } from "@/lib/birth-time-journey-respo
|
||||
import { guidedTurnIdentity } from "@/lib/birth-time-guided-turn-identity";
|
||||
import type { NextAction } from "@/lib/birth-time-journey-turn";
|
||||
import { ErrorToast } from "@/components/error-toast";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type LegacyRectificationProps = {
|
||||
readonly journey: LegacyJourneyClientResponse;
|
||||
@@ -75,8 +76,8 @@ export function BirthTimeLegacyRectification(props: LegacyRectificationProps) {
|
||||
/>
|
||||
)}
|
||||
{action.kind === "score_pending" && <p className="birth-time-scoring-status" role="status">正在使用已确认的关键经历评分…</p>}
|
||||
{action.kind === "retry_scoring" && <button className="button-primary birth-time-guided-action" disabled={props.controller.pending} onClick={props.controller.retryScoring} type="button">重新评分</button>}
|
||||
{action.kind === "paused" && <button className="button-primary birth-time-guided-action" disabled={props.controller.pending} onClick={props.controller.resume} type="button">继续校正</button>}
|
||||
{action.kind === "retry_scoring" && <Button className="button-primary birth-time-guided-action" disabled={props.controller.pending} onClick={props.controller.retryScoring} type="button">重新评分</Button>}
|
||||
{action.kind === "paused" && <Button className="button-primary birth-time-guided-action" disabled={props.controller.pending} onClick={props.controller.resume} type="button">继续校正</Button>}
|
||||
{showsCandidate ? (
|
||||
<BirthTimeCandidateResult
|
||||
controller={props.controller}
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { BirthTimeGuidedController } from "@/hooks/use-birth-time-guided-jo
|
||||
import type { JourneyClientResponse } from "@/lib/birth-time-journey-client";
|
||||
import type { DynamicNextAction } from "@/lib/birth-time-journey-turn-protocol";
|
||||
import { ErrorToast } from "@/components/error-toast";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type BirthTimeRectificationProps = {
|
||||
readonly journey: JourneyClientResponse;
|
||||
@@ -108,16 +109,16 @@ export function BirthTimeRectification(props: BirthTimeRectificationProps) {
|
||||
/>
|
||||
)}
|
||||
{(action.kind === "generate_dynamic_question" || action.kind === "retry_question_generation") && error && (
|
||||
<button className="button-secondary birth-time-guided-action" disabled={props.controller.pending} onClick={props.controller.retryQuestionGeneration} type="button">重试当前问题</button>
|
||||
<Button className="button-secondary birth-time-guided-action" disabled={props.controller.pending} onClick={props.controller.retryQuestionGeneration} type="button" variant="outline">重试当前问题</Button>
|
||||
)}
|
||||
{action.kind === "score_pending" && (
|
||||
<div className="birth-time-scoring-status" aria-live="polite">
|
||||
<b>正在缩小候选范围…</b>
|
||||
{props.controller.pollRecoverable && <button className="button-secondary birth-time-guided-action" onClick={props.controller.retryScoring} type="button">重新检查评分状态</button>}
|
||||
{props.controller.pollRecoverable && <Button className="button-secondary birth-time-guided-action" onClick={props.controller.retryScoring} type="button" variant="outline">重新检查评分状态</Button>}
|
||||
</div>
|
||||
)}
|
||||
{action.kind === "retry_scoring" && <button className="button-primary birth-time-guided-action" disabled={props.controller.pending} onClick={props.controller.retryScoring} type="button">重新评分</button>}
|
||||
{action.kind === "paused" && <button className="button-primary birth-time-guided-action" disabled={props.controller.pending} onClick={props.controller.resume} type="button">继续本次评估</button>}
|
||||
{action.kind === "retry_scoring" && <Button className="button-primary birth-time-guided-action" disabled={props.controller.pending} onClick={props.controller.retryScoring} type="button">重新评分</Button>}
|
||||
{action.kind === "paused" && <Button className="button-primary birth-time-guided-action" disabled={props.controller.pending} onClick={props.controller.resume} type="button">继续本次评估</Button>}
|
||||
{showsCandidate ? (
|
||||
<BirthTimeCandidateResult
|
||||
controller={props.controller}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { X } from "lucide-react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
groupWindowTransitions,
|
||||
houseTableToNorthIndianChart,
|
||||
@@ -99,14 +100,15 @@ function RectificationBoardBody({
|
||||
? <span className="rectification-board__clock is-declared">{emptyCopy.clock}</span>
|
||||
: null}
|
||||
{compact && (
|
||||
<button
|
||||
<Button
|
||||
className="rectification-board__close"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
aria-label="关闭盘面"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{stage ? <p className="rectification-board__stage">{engineMeaningToDisplayCopy(stage)}</p> : null}
|
||||
@@ -338,9 +340,10 @@ export function RectificationBoardPeek({
|
||||
}>) {
|
||||
const peek = rectificationBoardPeekCopy(result, declaredTime, birthTimeSource);
|
||||
return (
|
||||
<button
|
||||
<Button
|
||||
className="rectification-board-peek"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
aria-label={peek.detail ? `${peek.title},${peek.detail}` : peek.title}
|
||||
aria-expanded={expanded}
|
||||
aria-controls={boardId}
|
||||
@@ -348,6 +351,6 @@ export function RectificationBoardPeek({
|
||||
>
|
||||
<strong>{peek.title}</strong>
|
||||
<span>{peek.detail}</span>
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { InlineSpinner } from "@/components/inline-spinner";
|
||||
import { RectificationFade } from "@/components/rectification-fade";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import {
|
||||
CHOICE_STOP_LABEL,
|
||||
type RectificationChoiceCard as ChoiceCard,
|
||||
@@ -40,6 +42,7 @@ export function RectificationChoiceCard(props: RectificationChoiceCardProps) {
|
||||
const hidePrompt = embedded || props.hidePrompt === true;
|
||||
|
||||
return (
|
||||
<RectificationFade>
|
||||
<section
|
||||
className={`rectification-choice-card${embedded ? " is-embedded" : ""}${props.pending ? " is-pending" : ""}${answered ? " is-answered" : ""}`}
|
||||
aria-label={props.card.scoring ? "校正判断" : "盘外核对"}
|
||||
@@ -66,30 +69,46 @@ export function RectificationChoiceCard(props: RectificationChoiceCardProps) {
|
||||
<p>{props.card.why_user}</p>
|
||||
</details>
|
||||
) : null}
|
||||
<div className="birth-time-primary-choices">
|
||||
{props.card.options.map((option) => (
|
||||
<button
|
||||
key={option.key}
|
||||
type="button"
|
||||
className="birth-time-choice-option is-primary"
|
||||
data-selected={selectedKey === option.key ? "true" : "false"}
|
||||
onClick={() => select(option.key)}
|
||||
>
|
||||
<strong>{option.key}.</strong> {option.label}
|
||||
</button>
|
||||
))}
|
||||
{props.card.stop_label !== CHOICE_STOP_LABEL ? (
|
||||
<button
|
||||
type="button"
|
||||
className="birth-time-choice-option is-primary"
|
||||
data-selected={selectedKey === "stop" || selectedKey === "skip_probe" ? "true" : "false"}
|
||||
onClick={stop}
|
||||
>
|
||||
{props.card.stop_label}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<ToggleGroup
|
||||
className="contents"
|
||||
style={{ display: "contents" }}
|
||||
value={selectedKey ? [selectedKey] : []}
|
||||
onValueChange={(next) => {
|
||||
const key = next[0];
|
||||
if (!key || key === selectedKey) return;
|
||||
if (key === "stop") {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
if (props.card.options.some((option) => option.key === key)) select(key as ChoiceKey);
|
||||
}}
|
||||
>
|
||||
<div className="birth-time-primary-choices">
|
||||
{props.card.options.map((option) => (
|
||||
<ToggleGroupItem
|
||||
key={option.key}
|
||||
type="button"
|
||||
value={option.key}
|
||||
className="birth-time-choice-option is-primary"
|
||||
data-selected={selectedKey === option.key ? "true" : "false"}
|
||||
>
|
||||
<strong>{option.key}.</strong> {option.label}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
{props.card.stop_label !== CHOICE_STOP_LABEL ? (
|
||||
<ToggleGroupItem
|
||||
type="button"
|
||||
value="stop"
|
||||
className="birth-time-choice-option is-primary"
|
||||
data-selected={selectedKey === "stop" || selectedKey === "skip_probe" ? "true" : "false"}
|
||||
>
|
||||
{props.card.stop_label}
|
||||
</ToggleGroupItem>
|
||||
) : null}
|
||||
</div>
|
||||
</ToggleGroup>
|
||||
</fieldset>
|
||||
</section>
|
||||
</RectificationFade>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RECTIFICATION_REOPEN_LABEL } from "@/lib/rectification-session-composer-guard";
|
||||
|
||||
export function RectificationComposerReopen({
|
||||
@@ -10,9 +11,9 @@ export function RectificationComposerReopen({
|
||||
return (
|
||||
<div className="error-message" role="alert">
|
||||
<p>{message}</p>
|
||||
<button type="button" className="button-secondary" onClick={onReopen}>
|
||||
<Button type="button" className="button-secondary" variant="outline" onClick={onReopen}>
|
||||
{RECTIFICATION_REOPEN_LABEL}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
|
||||
import { loadDomAnimation, loadMotionReact, peekMotionReact, type MotionReact } from "@/lib/motion/load-react";
|
||||
import { motionPresets } from "@/lib/motion/presets";
|
||||
|
||||
function useMotionReact(): MotionReact | null {
|
||||
const [mod, setMod] = useState<MotionReact | null>(() => peekMotionReact());
|
||||
useEffect(() => {
|
||||
if (mod) return;
|
||||
let live = true;
|
||||
void loadMotionReact().then((next) => {
|
||||
if (live) setMod(next);
|
||||
}, () => undefined);
|
||||
return () => {
|
||||
live = false;
|
||||
};
|
||||
}, [mod]);
|
||||
return mod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fade once, and only when Motion was already loaded before this mount.
|
||||
* Otherwise return the children unchanged so a late chunk does not wrap
|
||||
* content that is already on screen. Reduced motion stays a short fade.
|
||||
*/
|
||||
export function RectificationFade({ children }: { readonly children: ReactNode }) {
|
||||
const [readyAtMount] = useState(() => peekMotionReact() !== null);
|
||||
const motion = useMotionReact();
|
||||
if (!readyAtMount || motion === null) return children;
|
||||
return <FadeIn motion={motion}>{children}</FadeIn>;
|
||||
}
|
||||
|
||||
function FadeIn({ motion, children }: { readonly motion: MotionReact; readonly children: ReactNode }) {
|
||||
const reduced = motion.useReducedMotion();
|
||||
const preset = motionPresets.disclosure;
|
||||
const duration = reduced ? preset.reduced.duration : preset.duration;
|
||||
return (
|
||||
<motion.LazyMotion features={loadDomAnimation} strict>
|
||||
<motion.m.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration, ease: [...preset.ease] }}
|
||||
>
|
||||
{children}
|
||||
</motion.m.div>
|
||||
</motion.LazyMotion>
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { RectificationVerificationReport } from "@/components/rectification-verification-report";
|
||||
import { RectificationSegmentDelivery } from "@/components/rectification-segment-delivery";
|
||||
import { sessionOutcomeAllowsAdopt } from "@/lib/rectification-agentic/core/rectification-decision";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import { deliveryWindowFromReceipt, protectVargaReport } from "@/lib/rectification-agentic/v9/delivery-window";
|
||||
|
||||
@@ -159,8 +160,9 @@ export function RectificationRangeDelivery({
|
||||
)}
|
||||
{adoptAllowed && !adoptLocked ? (
|
||||
<>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="rectification-range-delivery__accept"
|
||||
aria-pressed={adopted}
|
||||
disabled={busy || adopted}
|
||||
@@ -173,7 +175,7 @@ export function RectificationRangeDelivery({
|
||||
: recordConflict
|
||||
? SWITCH_TO_RECTIFIED_LABEL
|
||||
: RECTIFICATION_USER_COPY.rangeDeliveryMoreLikeThis}
|
||||
</button>
|
||||
</Button>
|
||||
{recordConflict && !adopted ? (
|
||||
<p className="rectification-range-delivery__switch-note">
|
||||
{switchToRectifiedConfirmCopy(recordConflict.reported_time, column.time)}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { SWITCH_TO_RECTIFIED_LABEL } from "@/lib/rectification-agentic/birth-tim
|
||||
import { RectificationVerificationReport } from "@/components/rectification-verification-report";
|
||||
import { protectVargaReport } from "@/lib/rectification-agentic/v9/delivery-window";
|
||||
import { unknownProductDeliveryCopy } from "@/lib/rectification-agentic/v9/product-delivery";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function RectificationSegmentDelivery({ summary, delivery, rangeLabel, selection, resultId, busy, accepting, onAccept, adoptLocked, markdown, candidateDate, canAdopt = false }: Readonly<{
|
||||
summary: SegmentSummary | null;
|
||||
@@ -57,12 +58,12 @@ export function RectificationSegmentDelivery({ summary, delivery, rangeLabel, se
|
||||
{showAdopt && adoption && candidateId ? (
|
||||
<>
|
||||
<p className="rectification-range-delivery__muted">采用后按 {adoption.date ? `${adoption.date} ` : ""}{adoption.time} 排盘;原填报资料保留。</p>
|
||||
<button type="button" className="rectification-range-delivery__accept" disabled={busy || adopted}
|
||||
<Button type="button" variant="outline" className="rectification-range-delivery__accept" disabled={busy || adopted}
|
||||
aria-pressed={adopted} onClick={() => onAccept(candidateId)}>
|
||||
{accepting ? RECTIFICATION_USER_COPY.rangeDeliveryAdopting : adopted ? RECTIFICATION_USER_COPY.rangeDeliveryAdopted
|
||||
: readopt && priorAdoption ? `改用 ${readopt}`
|
||||
: delivery.record_conflict ? SWITCH_TO_RECTIFIED_LABEL : `改用 ${charts.map((chart) => chart.chart).join(" + ")} 盘解读`}
|
||||
</button>
|
||||
</Button>
|
||||
{readopt && priorAdoption && !adopted ? (
|
||||
<p className="rectification-range-delivery__switch-note">现在按 {priorAdoption.time} 排盘;改用后按 {readopt} 排盘,原填报资料保留。</p>
|
||||
) : null}
|
||||
|
||||
@@ -786,7 +786,10 @@ test("time-selection cards use server adoption state and stay mutually exclusive
|
||||
}));
|
||||
assert.match(persistedHtml, /data-testid="persisted-question"/);
|
||||
assert.match(persistedHtml, /class="rectification-choice-card is-embedded"/);
|
||||
assert.equal(persistedHtml.match(/class="birth-time-choice-option is-primary"/g)?.length, 4);
|
||||
// 原值: class="birth-time-choice-option is-primary" 整段 class 恰好出现 4 次。
|
||||
// 新值: 同一串作为 class 的一部分出现 4 次。
|
||||
// 原因: K6 选项卡片改用 ToggleGroup,组件库会合并上自己的类。选项文案和读屏名称不变。
|
||||
assert.equal(persistedHtml.match(/birth-time-choice-option is-primary/g)?.length, 4);
|
||||
assert.doesNotMatch(messageLoop, /rectification-question-slot/);
|
||||
assert.doesNotMatch(persistedHtml, /rectification-question-slot/);
|
||||
assert.doesNotMatch(messageEntry, /rectification-question-slot/);
|
||||
|
||||
Reference in New Issue
Block a user