diff --git a/AGENTS.md b/AGENTS.md index 1d7e6c84..b77987c7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -125,7 +125,19 @@ Deployment safety rules: 3. 不得在未查看当前 `git status --short --branch` 时覆盖或重置本地变更。 4. 新发现的重复错误、阻塞、碎片目录、远端验证失败,必须追加到错误台账或当轮 sweep 文档。 -## 6. Bug History Workflow Hard Constraint +## 6. Git Branch Delivery Hard Constraint + +当用户要求 push,且开发发生在非 `main` 分支时,默认交付目标是远端 +`main` 已包含本次变更,而不是仅把功能分支推到远端: + +1. 先 fetch 并同步最新 `origin/main`,不得基于过期的 `main` 合并。 +2. 在保护本地未提交修改的前提下,将功能分支合并到最新 `main`。 +3. 完成与风险相称的测试并确认合并结果后,push `main`。 +4. 除非用户明确要求只推功能分支,否则不得把“功能分支已 push”当作最终交付,也不得要求用户再去 GitHub 手动寻找分支或创建 PR。 +5. 推送后必须核对远端 SHA,并验证 `origin/main` 已包含目标提交。 +6. 工作树有无关脏文件时,使用独立 worktree 完成 `main` 合并;不得 stash、reset、覆盖或顺带提交用户修改。 + +## 7. Bug History Workflow Hard Constraint 任何包含“Bug、报错、失败、异常、回归、无法保存、无法删除、状态码错误、线上故障”等现象的任务,开始诊断前必须完整读取并搜索: diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index e7118981..cc894144 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -345,3 +345,99 @@ - 相关记录:BUG-017、BUG-018 - 复发自:无 - 修复版本:待提交 + +## BUG-020 | 生时校正把语言问答包装成高阻力卡片表单 + +- 状态:resolved +- 首次发现:2026-07-22 +- 最近更新:2026-07-22 +- 影响面:首页生时校正会话、经历录入、候选核对与更正 +- 用户现象:每轮同时出现候选卡、领域按钮、年份与月份下拉、经历回顾卡和确认卡;用户需要理解多层控件才能回答一个问题,移动端尤为费力。 +- 触发条件:进入生时校正并提交或更正一条真实经历。 +- 根因:后端已有自由文本证据契约,但前端仍用结构化表单和多卡片包装;校正叙事 Agent 也未加载 Jyotish Skill 来生成自然、逐问式的取证措辞。 +- 修复:改为一段助理提问加一个自然语言输入框;候选状态和证据历史收进可展开进度区;保留明确确认、更正、持久化、计费、计算和分钟验证门禁;叙事 Agent 加载 Jyotish Skill,但服务端技术包仍是候选事实和确认权限的唯一来源。 +- 验证:语言优先组件静态与真实 Chromium 390px 回归 8/8 通过;校正路由与叙事契约 23/23 通过;目标文件 ESLint 和 Next.js production build 通过。 +- 防复发:语言问答不得重新拆成领域按钮或日期下拉;Skill 只能改进问题策略与措辞,不得生成、重算或确认候选时间。 +- 相关记录:BUG-008、BUG-009、BUG-015、BUG-016、BUG-017 +- 复发自:无 +- 修复版本:待提交(本地可测) + +## BUG-021 | 自然语言真实事件在分钟评分入口被静默丢弃 + +- 状态:resolved +- 首次发现:2026-07-22 +- 最近更新:2026-07-22 +- 影响面:生时校正自然语言抽取、历史事件持久化、分钟评分、首轮与兜底叙事 +- 用户现象:用户已经提供带日期的真实经历,系统仍反复索取证据;首轮回答展示完整 D 层技术清单,却没有用一个具体问题推进取证。19 世纪事件、疾病、手术、事故和丧亲尤其容易无法参与评分。 +- 触发条件:日期早于 1900 年;事件属于健康或重大压力;累计有效事件超过 6 条;或叙事模型未满足首轮全量技术层合同而进入确定性兜底。 +- 根因:前端日期正则写死 1900–2099;健康类关键词落入 `other`,并在路由中被过滤,尽管后端已有 `health_pressure` 与 D30 评分支持;路由另有独立的 6 条截断;叙事校验强制首轮正文列出全部稳定层、敏感层和值。 +- 修复:日期抽取支持 1000–2099 的四位历史年份;健康、事故、丧亲映射到既有 `health_pressure`/D30 合同并贯通持久化、旧数据导入和公开 turn schema;评分截断统一复用 8 条收敛上限;技术包和 validation receipt 继续完整保存,但非最终用户正文只呈现候选范围、未确认边界和一个高信息量问题,确定性兜底也遵守相同语言交互。 +- 验证:抽取、叙事、路由聚焦测试 59/59 通过;真实案例回放、编排和端到端契约 65/65 通过;目标文件 ESLint、Python replay/compilation、`git diff --check` 和 Next.js production build 通过。公开 smoke 中 Steve Jobs、Einstein、Marie Curie 分别保留 4、3、4 条可评分事件,产品过滤结果与结构化 oracle 一致。 +- 防复发:新增 18xx 中文与 ISO 日期、健康/事故/丧亲、8 条事件上限和单问题兜底断言;技术层可进入服务端证据包,不得重新进入普通会话正文;family/other 仍是持久化背景,不得伪装成已评分领域。 +- 相关记录:BUG-008、BUG-009、BUG-015、BUG-016、BUG-020 +- 复发自:BUG-020 +- 修复版本:待提交(本地可测) + +## BUG-022 | 生时校正入口等待首轮请求完成后才切换会话 + +- 状态:resolved +- 首次发现:2026-07-22 +- 最近更新:2026-07-22 +- 影响面:首页生时校正入口、普通咨询中的“先完成生时校正”、侧栏恢复校正会话 +- 用户现象:点击生时校正后长时间停留在首页或原咨询 session,直到恢复、建案、计算和会话持久化全部完成才突然跳转,用户容易误以为点击无效并重复操作。 +- 触发条件:`start`、`resume`、durable handoff 或 session persistence 任一请求响应较慢。 +- 根因:`openBirthTimeRectification` 虽然先设置了 loading,但专属 session 的插入、激活和校正 surface 的开放都位于首个异步请求之后;surface 还要求首个 turn 已存在,因此 loading 状态无法在目标会话中显示。 +- 修复:创建或复用专属 session 后,在任何 `await` 之前立即插入并激活该 session;校正 surface 允许以空 turn 渲染既有“正在建立校正记录…”状态;增加同步 in-flight 门禁防止快速重复点击发出多个启动请求;失败时新建 session 回滚到来源会话,复用 session 则保留可见错误与重试入口。 +- 验证:`frontend/tests/consultation-entrypoint.test.ts` 21/21 通过,锁定 session 插入、激活和 loading surface 必须早于首个异步请求,并锁定单请求门禁;目标文件 ESLint、`git diff --check` 和 Next.js production build 通过;真实应用内 Chromium 从首页点击时,在后台恢复完成前已经显示“正在建立校正记录…”,从普通会话切回校正 session 也立即显示目标会话。 +- 防复发:首页卡片、普通 session 建议和侧栏恢复必须继续共用同一启动函数;不得重新用首个 turn 是否返回作为 session 可见性的条件。 +- 相关记录:BUG-016、BUG-020、BUG-021 +- 复发自:无 +- 修复版本:待提交(本地可测) + +## BUG-023 | 初始化地址保存后的加载提示仍指向生时评估 + +- 状态:resolved +- 首次发现:2026-07-22 +- 最近更新:2026-07-22 +- 影响面:新用户初始化流程、出生地点保存后的全屏加载提示 +- 用户现象:用户填完出生地址后,页面实际准备进入首页,但加载提示仍显示正在生成生时评估,造成流程去向与界面文案不一致。 +- 触发条件:初始化流程完成出生时间填写,并提交最后一步出生地点。 +- 根因:出生时间保存和出生地点保存共用 `saving_profile` 展示阶段;该阶段文案仍按旧流程描述为即将生成生时评估。 +- 修复:新增独立的 `entering_home` 展示阶段,仅在 `saveOnboardingPlace` 提交地址时使用,显示“正在进入首页 / 出生资料已保存,正在为你准备首页。”;出生时间保存继续使用原 `saving_profile` 阶段。 +- 验证:聚焦测试锁定地址提交与 `entering_home` 的绑定及目标文案;目标文件 ESLint 与 `git diff --check` 通过。 +- 防复发:初始化步骤的加载文案必须绑定实际导航结果;不得通过修改共享 `saving_profile` 文案改变出生时间提交阶段的语义。 +- 相关记录:BUG-022 +- 复发自:无 +- 修复版本:待提交(本地可测) + +## BUG-024 | 侧栏会话标题与更多操作被拆成两块 + +- 状态:resolved +- 首次发现:2026-07-22 +- 最近更新:2026-07-22 +- 影响面:聊天记录侧栏、当前会话选中态与更多操作入口 +- 用户现象:会话标题显示在一块选中背景中,右侧更多操作却以独立圆形按钮悬在外侧,看起来像两个不一致的控件。 +- 触发条件:侧栏展开并选中任意会话。 +- 根因:选中背景只应用在左侧 `.session-main`,右侧 `.session-menu-trigger` 又单独使用 `50%` 圆角和悬停背景。 +- 修复:把选中、悬停和聚焦背景统一应用到整行 `.session-row`;子按钮保持透明,并移除更多操作按钮的独立圆形底色。 +- 验证:侧栏契约测试锁定整行选中背景、透明标题按钮和非圆形更多操作按钮;目标文件 ESLint 与 `git diff --check` 通过。 +- 防复发:会话标题和行内操作必须共享同一个行级状态面,不得分别绘制互相竞争的选中背景。 +- 相关记录:无 +- 复发自:无 +- 修复版本:待提交(本地可测) + +## BUG-025 | 健康压力追问被数据库误判为校正操作冲突 + +- 状态:resolved +- 首次发现:2026-07-22 +- 最近更新:2026-07-22 +- 影响面:生时校正首条自然语言回答、`POST /api/birth-time-conversation`、Supabase 对话持久化契约 +- 用户现象:用户提交一条带年月的真实经历后等待约一分钟,接口返回 `409 action_conflict`,页面提示加载最新进度后重试;重复提交仍无法进入下一问。 +- 触发条件:技术包为下一轮选择 `health_pressure`(健康与重大压力)作为待补证据领域。 +- 根因:应用层已把 `health_pressure` 作为正式领域,并允许语言问答每轮只追问一个重点领域;durable SQL 既缺少该领域,又要求 evidence request 至少包含两个领域。完整计算和叙事生成完成后,`save_conversational_rectification_turn` 才以 `conversational_action_conflict` 拒绝公开 turn。 +- 修复:新增向前迁移,将 `health_pressure` 同步加入四个 durable validator 和事件证据表约束,并把 evidence request 基数从 2–4 对齐为应用契约的 1–4;保留现有幂等、版本和候选确认门禁。 +- 验证:真实浏览器复现确认只有 `conversational_rectification_valid_evidence_request` 失败,其余公开 turn 子结构、事件、回执和私有候选均通过;迁移应用到测试 Supabase 后,用同一条“2023 年 3 月离家来北京工作”重放,`POST /api/birth-time-conversation` 返回 `200`,日志为 `actionKind: answer`、`resultCategory: success`,事件规范化保存为 `2023-03 · 离家来北京工作`,页面继续自然追问学业经历且输入框恢复可用;聚焦迁移测试锁定四个 validator 与表约束必须共同支持该领域。 +- 防复发:应用 evidence domain 枚举新增值时,必须同时更新 TypeScript schema、公开 recap/request、私有候选、事件表约束和迁移契约测试;不得把正常领域扩展映射为通用 409。 +- 相关记录:BUG-007、BUG-020、BUG-021 +- 复发自:BUG-007 +- 修复版本:待提交(测试 Supabase smoke 通过) diff --git a/docs/research/conversational_minute_rectification_change_plan_2026_07_22.md b/docs/research/conversational_minute_rectification_change_plan_2026_07_22.md new file mode 100644 index 00000000..258e276a --- /dev/null +++ b/docs/research/conversational_minute_rectification_change_plan_2026_07_22.md @@ -0,0 +1,131 @@ +# 对话式生时纠正修改方案与真实回放门槛 + +日期:2026-07-22 + +## 结论先行 + +当前系统可以改成普通 session 式的一问一答,但不能把“交互更顺”描述成“已经能准确到分钟”。现有公开开发案例的分钟评分仍不稳定;产品入口还会丢弃健康、事故、家庭类证据,并且实际只把最近 6 条事件送入评分。 + +本方案把修改拆成两个互不混淆的目标: + +1. 让用户通过自然语言持续补充经历,系统每轮只问一个最有区分力的问题。 +2. 用开发集和独立冻结盲测证明分钟排序是否真的收敛;没有通过门槛时只返回候选范围和证据缺口。 + +## 已确认的问题 + +### 1. 首轮正文被技术回执合同绑架 + +`narrative-agent.ts` 当前要求首轮正文复述所有稳定层、敏感层、具体值和领域映射。模型未满足时,fallback 又会输出同一份 D 层清单。技术包本身可以保留,但不应该成为用户正文。 + +### 2. 自然语言证据域不完整 + +`evidence-extractor.ts` 只显式识别事业、学业、迁移、关系、家庭、财富。疾病、手术、事故和丧亲会落到 `other`;`route.ts` 又会排除 `family` 和 `other`,所以 D30、家庭与部分重大人生事件无法参与评分。 + +日期正则还被限制为 1900–2099 年。公开历史人物案例中的 18xx 年事件会被保存成“日期未知”,无法评分;这也意味着现有历史 AA 开发案例并没有真正通过当前产品的自然语言入口。 + +### 3. 事件数量合同互相矛盾 + +- 会话收敛层:3 条开始排序,8 条停止继续取证。 +- 路由评分层:只保留最近 6 条。 +- Jyotish Skill:至少 5 条 dated events 才适合定框;高置信度通常需要 10–15 条多领域事件。 + +3 条可以产生早期候选反馈,但不得升级成准确分钟。停止条件也不能仅由事件数量触发。 + +### 4. 当前问题选择仍偏“让用户交材料” + +系统应从候选差异中选择一个信息增益最高、用户容易回忆的问题,例如“第一次正式工作是哪年哪月”,而不是一次要求用户提交多个领域的事件。 + +## 最小实施方案 + +### 阶段 A:修正文合同,不改评分器 + +建议改动: + +- `frontend/src/lib/conversational-rectification/narrative-agent.ts` + - 首轮正文只要求:当前范围、尚未确认边界、证据进度、一条具体问题。 + - 取消首轮必须公开全部 stable/sensitive layer values 的校验。 + - fallback 同样只输出一条问题。 +- 技术层继续完整保存在 technical packet / receipt 中,在 UI 中放入可折叠“计算依据”。 +- 用户消息区、输入框、滚动和留白完全复用普通 session 布局,不保留卡片式问卷。 + +验收:首轮正文不出现 D1/D9/D10 技术 dump;每轮只有一个问号和一个待回答主题。 + +### 阶段 B:补齐事件入口 + +建议改动: + +- 在持久化和技术包域中新增 `health_pressure`,不要把它降级成 `other`。 +- 抽取器识别:确诊、住院、手术、事故、受伤、创伤、重大疾病、康复。 +- 日期解析支持出生之后的合法公历年份,而不是写死 19xx/20xx;继续用 `asOfDate` 排除未来事件。 +- 家庭事件不要整体丢弃;按父母、子女、生育、丧亲等映射到后端支持的主题证据,若评分器尚无对应合同则明确存储为 context-only,而不是静默消失。 +- 移除 `.slice(-6)` 的隐式截断。建立一个共享常量;建议开发回放先测 5、8、10、12 条,再决定产品上限。 +- UI 显示“已记录 / 可评分 / 需补日期 / 仅作背景”的数量,避免用户以为回答已评分。 + +验收:真实测试集中所有标记为可评分的自然语言回答都能保留正确日期、精度和领域;任何被排除的事件都有可见原因。 + +### 阶段 C:一次问一个高信息量问题 + +复用现有 `candidateDifferences`、`suggestedDomains` 和 fact difference opportunities: + +1. 过滤已经问过、用户表示不记得、或缺少后端支持的主题。 +2. 优先选择能把当前候选分成最均衡分区的事件领域。 +3. 将领域转成生活化问题,并要求“年份/月份 + 发生了什么”。 +4. 用户回答后重新排序,再选择下一题。 + +问题模板必须具体,例如: + +> 你第一次明显的工作转折发生在什么时候?可以是入职、离职、换行业、升职或创业。直接告诉我大约哪一年哪一月和发生了什么;记不清月份也可以先说年份。 + +### 阶段 D:安全收敛 + +每轮都可以展示“当前领先范围”,但只有同时满足以下条件才允许提示确认: + +- 至少 5 条可评分事件,且至少 3 个领域;高置信度目标使用 10–15 条。 +- 真实候选形成唯一分钟或预先规定的窄范围。 +- ±1/2/5 分钟邻近稳定性通过。 +- leave-one-event-out 通过,不能由单一事件决定结果。 +- 没有缺失的强制计算层。 +- 独立冻结盲测达到发布指标。 + +未满足时输出:候选范围、支持事件、冲突事件、未评分事件和下一条问题,不允许写“准确时间为”。 + +## 测试集分层 + +### 对话开发回放集 + +文件:`references/real_case_calibration/conversational_rectification_development_v1.json` + +当前 v1 是入口 smoke fixture,不是分钟收敛 benchmark:TypeScript 测试验证 fixture 中自然语言在现有抽取器下的日期、精度和领域;Python 回放使用同一 fixture 的人工 `expected_route_scoreable` 标签模拟现有路由过滤,再测试逐轮排名。它不是自然语言到评分的端到端回放,且每例不足 5 条可评分事件,不能用于判断 Skill 级收敛。 + +下一版 development benchmark 必须把每例扩充至至少 5 条、目标 10–15 条多领域事件,并直接调用抽取器和路由投影,覆盖记不清、拒答、否定、纠正、重复和一条消息多事件。它引用仓库已有公开 AA 开发案例,永久排除在 holdout 之外,可以用于修 bug 和调问题排序。 + +### 独立冻结 holdout v4 + +开发集不能证明发布准确率。正式验证需要另一位 reviewer 收集至少 20 个未参与调参的公开 AA 案例,在评分前完成:出生来源复核、事件来源复核、假分钟承诺、manifest hash、实现 hash 和真值密封。只允许盲回放一次;失败后整批降为 development,下一版重新收集 fresh holdout。 + +## 指标 + +对话入口: + +- 日期和精度提取准确率。 +- 领域提取准确率。 +- 应评分事件保留率。 +- 每轮问题数必须等于 1。 +- 不能回答、记不清、纠正旧答案时仍能继续。 + +分钟能力: + +- 3、5、8、10 条事件时的 Top-1 / Top-3。 +- 逐轮真实分钟排名和误差轨迹。 +- 唯一分钟率、邻近稳定性、leave-one-out。并列范围未破时 `predicted_time` 必须为空;如保留确定性 tie-break,只能单列为诊断 MAE,不能当成分钟能力。 +- false confirmation rate 和 confirmation coverage 必须同时报告。 + +建议继续沿用当前发布门槛:Top-1 ≥ 60%,Top-3 ≥ 85%,MAE ≤ 2 分钟,false confirmation ≤ 5%,insufficient-evidence rejection ≥ 90%。confirmation coverage 不能为 0,否则只是“永不确认”而非可用收敛。 + +## 实施顺序 + +1. 先合并阶段 A 与入口可观测性,不改变“准确分钟”声明。 +2. 用本开发回放集修阶段 B,确保事件没有在入口丢失。 +3. 接入阶段 C 的单问题排序并做多轮 replay。 +4. 当开发集达到稳定目标后,冻结实现并收集 fresh holdout v4。 +5. 只有 holdout 通过后才开放分钟确认;否则产品始终停在候选范围。 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 052b5920..4527f605 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@base-ui/react": "^1.6.0", + "@gsap/react": "^2.1.2", "@mastra/core": "^1.50.1", "@supabase/ssr": "^0.12.3", "@supabase/supabase-js": "^2.110.5", @@ -18,6 +19,7 @@ "clsx": "^2.1.1", "date-fns": "^4.4.0", "drizzle-orm": "^0.45.2", + "gsap": "^3.15.0", "lucide-react": "^1.24.0", "next": "16.2.10", "pg": "^8.22.0", @@ -29,6 +31,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.2", + "thinking-orbs": "^0.1.1", "tw-animate-css": "^1.4.0", "zod": "^3.25.76" }, @@ -1285,6 +1288,16 @@ "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "license": "MIT" }, + "node_modules/@gsap/react": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@gsap/react/-/react-2.1.2.tgz", + "integrity": "sha512-JqliybO1837UcgH2hVOM4VO+38APk3ECNrsuSM4MuXp+rbf+/2IG2K1YJiqfTcXQHH7XlA0m3ykniFYstfq0Iw==", + "license": "SEE LICENSE AT https://gsap.com/standard-license", + "peerDependencies": { + "gsap": "^3.12.5", + "react": ">=17" + } + }, "node_modules/@hono/node-server": { "version": "1.19.14", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", @@ -6564,6 +6577,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/gsap": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz", + "integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -10834,6 +10853,16 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/thinking-orbs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/thinking-orbs/-/thinking-orbs-0.1.1.tgz", + "integrity": "sha512-nLvLTGJtk74K13MmP7XiRdzFiQx7UsoZAIyBZNgXyl7Q3h2mdz0r3eKn9LCsuzb3DGOVjwDvMhtnAkIqJJRVQA==", + "license": "MIT", + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", diff --git a/frontend/package.json b/frontend/package.json index 090aca40..bff01bc2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@base-ui/react": "^1.6.0", + "@gsap/react": "^2.1.2", "@mastra/core": "^1.50.1", "@supabase/ssr": "^0.12.3", "@supabase/supabase-js": "^2.110.5", @@ -26,6 +27,7 @@ "clsx": "^2.1.1", "date-fns": "^4.4.0", "drizzle-orm": "^0.45.2", + "gsap": "^3.15.0", "lucide-react": "^1.24.0", "next": "16.2.10", "pg": "^8.22.0", @@ -37,6 +39,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.2", + "thinking-orbs": "^0.1.1", "tw-animate-css": "^1.4.0", "zod": "^3.25.76" }, diff --git a/frontend/src/app/api/birth-time-conversation/route.ts b/frontend/src/app/api/birth-time-conversation/route.ts index 30d4299e..0b549734 100644 --- a/frontend/src/app/api/birth-time-conversation/route.ts +++ b/frontend/src/app/api/birth-time-conversation/route.ts @@ -1,4 +1,5 @@ import { randomUUID } from "node:crypto"; +import path from "node:path"; import { conversationalRectificationCommandSchema, type ConversationalRectificationCommand, @@ -25,6 +26,7 @@ import type { BirthTimeJourneyEngine, RectificationQuestionnaire } from "../../. import type { CandidateResult, LifeEvent } from "../../../lib/birth-time-evidence.ts"; import type { CandidateDifferenceBuild } from "../../../lib/birth-time-dynamic-choice-internal.ts"; import { conversationalRectificationCreationPolicyFromEnvironment } from "../../../lib/conversational-rectification/creation-policy.ts"; +import { MAXIMUM_SCOREABLE_EVENTS } from "../../../lib/conversational-rectification/convergence.ts"; import { conversationalRectificationLatencyBucket, createConversationalRectificationTelemetry, @@ -37,6 +39,9 @@ import { export const runtime = "nodejs"; export const maxDuration = 60; +const jyotishSkillPath = process.env.JYOTISH_SKILL_PATH?.trim() + || path.resolve(process.cwd(), "..", "skills", "jyotish-vedic-astrology"); + type AuthenticatedRequest = Readonly<{ userId: string; context: unknown; @@ -349,7 +354,7 @@ function scoreableLifeEvents( precision: item.datePrecision as "day" | "month" | "year", date: item.dateValue, } as LifeEvent]; - }).slice(-6); + }).slice(-MAXIMUM_SCOREABLE_EVENTS); } function sampleTimes(scan: RectificationQuestionnaire): readonly { readonly sampleIndex: number; readonly time: string }[] { @@ -665,7 +670,8 @@ async function productionNarrativeGenerator(): Promise svg { width: var(--space-3); height: var(--space-3); flex: 0 0 auto; color: var(--sidebar-ring); } .session-main small { color: inherit; line-height: 1.3; opacity: .72; font-size: var(--type-overline); } -.session-menu-trigger { width: 44px; height: 44px; display: grid; place-items: center; padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--sidebar-muted-foreground); cursor: pointer; opacity: .64; transition: opacity 120ms ease-out, background-color 120ms ease-out, color 120ms ease-out; } +.session-menu-trigger { width: 44px; height: 44px; display: grid; place-items: center; padding: 0; border: 0; border-radius: 0; background: transparent; color: inherit; cursor: pointer; opacity: .64; transition: opacity 120ms ease-out, color 120ms ease-out; } .session-menu-trigger > svg { width: 18px; height: 18px; } .session-row:hover .session-menu-trigger, .session-row:focus-within .session-menu-trigger, .session-menu-trigger[aria-expanded="true"] { opacity: 1; } -.session-menu-trigger:hover, .session-menu-trigger[aria-expanded="true"] { background: var(--sidebar-accent); color: var(--sidebar-accent-foreground); } +.session-menu-trigger:hover, .session-menu-trigger[aria-expanded="true"] { background: transparent; color: inherit; } .session-actions { position: absolute; z-index: 4; top: calc(100% - var(--space-1)); right: 0; min-width: calc(var(--space-24) + var(--space-12)); display: grid; gap: var(--space-1); padding: var(--space-2); border: 1px solid var(--sidebar-border); border-radius: var(--radius-md); background: var(--color-canvas); box-shadow: var(--shadow-elevated); } .session-actions button { width: 100%; min-height: 44px; display: flex; align-items: center; gap: var(--space-2); padding: 0 var(--space-3); border: 0; border-radius: var(--radius-md); background: transparent; color: var(--color-ink-secondary); cursor: pointer; text-align: left; font-size: var(--type-body-sm); } .session-actions button > svg { width: 16px; height: 16px; } @@ -464,13 +466,6 @@ button:disabled { cursor: default; opacity: .45; } .conversational-narrative .message-markdown > :first-child { margin-top: 0; } .conversational-narrative .message-markdown > :last-child { margin-bottom: 0; } .conversational-narrative .message-markdown h2, .conversational-narrative .message-markdown h3 { margin: var(--space-4) 0 var(--space-2); font-family: var(--font-sans); font-size: var(--type-body-md); font-weight: 600; letter-spacing: 0; } -.conversational-domain-picker { display: grid; gap: var(--space-3); margin: 0; padding: 0; border: 0; } -.conversational-domain-picker .conversational-domain-question { margin-bottom: var(--space-2); color: var(--color-ink); font-size: var(--type-body-md); font-weight: 600; line-height: 1.5; } -.conversational-domain-picker > div { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: var(--space-2); } -.conversational-domain-picker button { display: grid; gap: var(--space-1); padding: var(--space-2) var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); cursor: pointer; text-align: left; } -.conversational-domain-picker button > span { font-size: var(--type-body-sm); font-weight: 600; } -.conversational-domain-picker button > small { color: var(--color-ink-secondary); font-size: var(--type-overline); font-weight: 400; } -.conversational-domain-picker button[aria-pressed="true"] { border-color: var(--color-action); background: var(--color-action-soft); color: var(--color-action); } .conversational-answer-pending { min-width: 0; display: grid; grid-template-columns: 40px minmax(0, 1fr); align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border: 1px solid color-mix(in srgb, var(--color-action) 24%, var(--color-border)); border-radius: var(--radius-lg); background: var(--color-action-soft); } .conversational-answer-pending .app-loading-symbol { width: 40px; height: 40px; } .conversational-answer-pending .app-loading-mark { width: 22px; height: 22px; } @@ -480,8 +475,6 @@ button:disabled { cursor: default; opacity: .45; } .conversational-composer { display: grid; gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas); } .conversational-composer label { display: grid; gap: var(--space-1); font-size: var(--type-body-sm); font-weight: 600; } .conversational-composer label span { color: var(--color-ink-secondary); font-size: var(--type-caption); font-weight: 400; line-height: 1.5; } -.conversational-event-date { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); } -.conversational-event-date label { gap: var(--space-2); color: var(--color-ink-secondary); font-size: var(--type-caption); font-weight: 500; } .conversational-composer textarea { width: 100%; min-width: 0; min-height: 112px; resize: vertical; padding: var(--space-3); border: 1px solid var(--color-border-strong); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); line-height: 1.55; } .conversational-composer-footer { min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); } .conversational-composer-footer small { color: var(--color-ink-tertiary); font-size: var(--type-overline); } @@ -542,14 +535,6 @@ button:disabled { cursor: default; opacity: .45; } .product-entrypoint-action .starter-arrow { width: 15px; height: 15px; color: currentColor; } .starter-loading { color: var(--color-ink-secondary); margin-left: 0; padding: var(--space-5); border-radius: var(--radius-lg); background: var(--color-canvas-muted); font-size: 14px; } .starter-note { margin: 10px 0 0; color: var(--color-ink-secondary); line-height: 1.5; grid-column: 1 / -1; font-size: 13px; } -.parameter-freeze-panel { display: grid; grid-column: 1 / -1; gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas); } -.parameter-freeze-panel > div { display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--space-2); color: var(--color-ink-secondary); font-size: var(--type-caption); } -.parameter-freeze-panel b { color: var(--color-ink); font-size: var(--type-title-sm); } -.parameter-freeze-panel dl { display: grid; grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); gap: var(--space-2); margin: 0; } -.parameter-freeze-panel dl > div { min-width: 0; padding: var(--space-2); border-radius: var(--radius-md); background: var(--color-canvas-soft); } -.parameter-freeze-panel dt { color: var(--color-ink-tertiary); font-size: 11px; } -.parameter-freeze-panel dd { overflow: hidden; margin: 2px 0 0; color: var(--color-ink); text-overflow: ellipsis; white-space: nowrap; font-size: 12px; } - .message-list { margin: 0 auto; width: min(900px, 100%); padding: var(--space-8) var(--space-8) var(--space-16); } .message { display: flex; animation: message-enter 160ms var(--ease-out) both; padding: var(--space-2) 0; } .agent-avatar { width: 32px; height: 32px; display: block; flex: 0 0 32px; margin-top: var(--space-2); border-radius: 50%; background: var(--color-canvas) url("/jyotish-logo.png") center / contain no-repeat; box-shadow: 0 0 0 1px var(--color-border); } @@ -565,7 +550,6 @@ button:disabled { cursor: default; opacity: .45; } .message-markdown strong { color: var(--color-ink); font-weight: 500; } .message-markdown a { text-underline-offset: 3px; color: var(--color-action); } .message-markdown code { padding: 2px 5px; font-size: .9em; border-radius: var(--radius-xs); background: var(--color-canvas-muted); font-family: var(--font-mono); } -.thinking i { width: 5px; height: 5px; border-radius: 50%; animation: pulse 850ms ease-in-out infinite alternate; background: var(--color-action); } .error-message { margin: 12px 0 0; padding: 12px 14px; border-left: 3px solid var(--color-danger); background: var(--color-danger-muted); color: var(--color-danger); font-size: 13px; line-height: 1.6; border-color: var(--color-danger); border-radius: 0 var(--radius-md) var(--radius-md) 0; } .composer-wrap { position: sticky; bottom: 0; z-index: 2; min-width: 0; border-top: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent); padding: var(--space-3) var(--space-6) var(--space-4); background: var(--color-frosted); backdrop-filter: saturate(130%) blur(20px); } @@ -790,7 +774,599 @@ input:disabled, select:disabled { color: var(--color-ink-tertiary); background: [data-sonner-toaster] { right: var(--space-4) !important; left: var(--space-4) !important; width: auto !important; } .conversational-rectification { width: 100%; max-width: 100%; gap: var(--space-4); overflow-x: clip; } .conversational-narrative-body, .conversational-composer, .conversational-evidence-recap, .conversational-candidate, .conversational-confirmation, .conversational-original-question, .conversational-empty-state { padding: var(--space-3); } - .conversational-domain-picker > div, .conversational-candidate dl { grid-template-columns: minmax(0, 1fr); } + .conversational-candidate dl { grid-template-columns: minmax(0, 1fr); } .conversational-composer-footer { align-items: stretch; flex-direction: column; } .conversational-composer-footer button, .conversational-confirmation button, .conversational-original-question button { width: 100%; } } + +/* Conversational rectification shell */ +.conversation.is-rectification { overflow: hidden; padding-bottom: 0; } +.rectification-chat { height: 100%; min-height: 0; display: flex; flex-direction: column; color: var(--color-ink); } +.rectification-message-list { min-height: 0; flex: 1; overflow-y: auto; overscroll-behavior: contain; padding-bottom: var(--space-6); } +.rectification-message-details { margin-top: var(--space-3); padding-top: var(--space-2); border-top: 1px solid var(--color-border); color: var(--color-ink-secondary); font-size: var(--type-caption); } +.rectification-message-details summary { min-height: 44px; display: flex; align-items: center; cursor: pointer; font-size: var(--type-body-sm); font-weight: 600; } +.rectification-message-details p { margin: 0 0 var(--space-2); font-size: var(--type-caption); line-height: 1.6; } +.rectification-message-details ul { display: grid; gap: var(--space-2); margin: var(--space-3) 0 0; padding: 0; list-style: none; } +.rectification-message-details li { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--space-3); padding-top: var(--space-2); border-top: 1px solid var(--color-border); } +.rectification-message-details button, .rectification-correction-target button { min-height: 44px; padding-inline: var(--space-3); border: 0; background: transparent; color: var(--color-action); cursor: pointer; } +.rectification-composer-wrap { position: static; flex: 0 0 auto; } +.rectification-correction-target { width: min(760px, 100%); display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin: 0 auto var(--space-2); color: var(--color-ink-secondary); font-size: var(--type-caption); } +.rectification-correction-target p { margin: 0; } +.conversational-empty-state { width: min(760px, 100%); margin: var(--space-8) auto; color: var(--color-ink-secondary); } + +@media (max-width: 430px) { + .rectification-message-details li { align-items: stretch; grid-template-columns: minmax(0, 1fr); } + .rectification-correction-target { align-items: stretch; display: grid; } +} + +/* Starter workbench */ +.welcome { + width: min(1040px, 100%); + padding: var(--space-8) 0 var(--space-12); +} + +.starter-list { + width: min(1040px, 100%); + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--space-6); + border: 0; +} + +.starter-hero, +.product-entrypoints, +.starter-themes { + width: 100%; + min-width: 0; +} + +.starter-hero { + position: relative; + min-height: 220px; + overflow: hidden; + display: grid; + grid-template-columns: minmax(0, 1fr) 250px; + align-items: center; + gap: var(--space-8); + padding: var(--space-10); + border: 1px solid color-mix(in srgb, var(--color-action) 14%, var(--color-border)); + border-radius: var(--radius-xl); + background: radial-gradient(circle at 82% 36%, rgba(133, 67, 47, .12), transparent 33%), linear-gradient(135deg, #fbfaf7 0%, #f5f1eb 100%); +} + +.starter-hero::after { + position: absolute; + inset: 0; + opacity: .2; + pointer-events: none; + background-image: radial-gradient(rgba(29, 29, 31, .24) .45px, transparent .45px); + background-size: 6px 6px; + content: ""; + mask-image: linear-gradient(to right, transparent 8%, #000 72%); +} + +.starter-hero-copy { + position: relative; + z-index: 1; + max-width: 720px; +} + +.starter-greeting { + margin: 0 0 var(--space-3); + color: var(--color-action); + font-size: var(--type-body-sm); + font-weight: 600; + letter-spacing: .02em; +} + +.starter-hero h1 { + max-width: 980px; + margin: 0; + color: var(--color-ink); + font-family: var(--font-display); + font-size: clamp(42px, 5.4vw, 68px); + font-weight: 400; + letter-spacing: -.055em; + line-height: .98; + text-wrap: balance; +} + +.starter-hero-note { + max-width: 590px; + margin: var(--space-5) 0 0; + color: var(--color-ink-secondary); + font-size: var(--type-body-sm); + line-height: 1.7; + text-wrap: pretty; +} + +.starter-celestial-visual { + position: relative; + z-index: 1; + width: 232px; + height: 178px; + justify-self: end; +} + +.starter-orbit { + position: absolute; + border: 1px solid color-mix(in srgb, var(--color-action) 34%, transparent); + border-radius: 50%; +} + +.starter-orbit-outer { + inset: 8px 10px; + transform: rotate(-12deg); +} + +.starter-orbit-inner { + inset: 38px 50px; + border-color: color-mix(in srgb, var(--color-ink-tertiary) 42%, transparent); + transform: rotate(20deg); +} + +.starter-orbit-dot { + position: absolute; + width: 9px; + height: 9px; + border-radius: 50%; + background: var(--color-action); + box-shadow: 0 0 0 5px color-mix(in srgb, var(--color-action-soft) 72%, transparent); +} + +.starter-orbit-dot-primary { + top: 26px; + right: 37px; +} + +.starter-orbit-dot-secondary { + bottom: 31px; + left: 31px; + width: 6px; + height: 6px; + background: var(--color-ink-tertiary); + box-shadow: none; +} + +.starter-celestial-mark { + position: absolute; + top: 50%; + left: 50%; + width: 54px; + height: 54px; + border: 1px solid color-mix(in srgb, var(--color-action) 18%, var(--color-border)); + border-radius: 50%; + background: color-mix(in srgb, var(--color-canvas) 88%, transparent) url("/jyotish-logo.png") center / 32px 32px no-repeat; + box-shadow: 0 14px 36px -24px rgba(29, 29, 31, .48); + transform: translate(-50%, -50%); +} + +.product-entrypoints { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0; + overflow: hidden; + border: 1px solid var(--color-border); + border-radius: var(--radius-xl); + background: var(--color-canvas); +} + +.product-entrypoints small { + color: var(--color-ink-secondary); + line-height: 1.5; + font-size: var(--type-caption); +} + +.daily-starlanguage-card, +.birth-rectification-card { + min-height: 228px; + display: grid; + grid-template-rows: minmax(0, 1fr) auto; + gap: var(--space-6); + padding: var(--space-8); + border: 0; + border-radius: 0; + background: transparent; +} + +.birth-rectification-card { + border-left: 1px solid var(--color-border); +} + +.product-entrypoint-card { + position: relative; + overflow: hidden; + transition: background-color 180ms ease-out; +} + +.product-entrypoint-card::after { + position: absolute; + inset: 0; + opacity: 0; + pointer-events: none; + background: linear-gradient(112deg, transparent 22%, rgba(255, 255, 255, .54) 46%, transparent 68%); + content: ""; + transform: translateX(-48%); + transition: opacity 220ms ease-out, transform 520ms var(--ease-out); +} + +.product-entrypoint-hitarea { + position: absolute; + z-index: 3; + inset: 0; + width: 100%; + min-height: 0; + padding: 0; + border: 0; + border-radius: inherit; + background: transparent; + cursor: pointer; +} + +.product-entrypoint-card > :not(.product-entrypoint-hitarea) { + position: relative; + z-index: 1; + pointer-events: none; +} + +.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover) { + border-color: transparent; + background: color-mix(in srgb, var(--color-action-soft) 58%, var(--color-canvas)); + transform: none; +} + +.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover)::after { + opacity: .65; + transform: translateX(48%); +} + +.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover) .product-entrypoint-action { + transform: translateX(3px); +} + +.product-entrypoint-card:has(.product-entrypoint-hitarea:focus-visible) { + outline: 3px solid color-mix(in srgb, var(--color-focus) 56%, transparent); + outline-offset: -3px; +} + +.product-entrypoint-card:has(.product-entrypoint-hitarea:disabled) { + opacity: .65; +} + +.product-entrypoint-hitarea:disabled { + cursor: not-allowed; +} + +.product-entrypoint-copy { + max-width: 520px; +} + +.product-entrypoint-kicker { + display: block; + margin-bottom: var(--space-4); + color: var(--color-action); + font-size: var(--type-overline); + font-weight: 600; + letter-spacing: .12em; +} + +.product-entrypoint-copy h2 { + margin: 0; + color: var(--color-ink); + font-family: var(--font-display); + font-size: clamp(26px, 2.6vw, 34px); + font-weight: 400; + letter-spacing: -.035em; + line-height: 1.05; +} + +.product-entrypoint-copy p { + max-width: 460px; + margin: var(--space-3) 0 0; + color: var(--color-ink-secondary); + font-size: var(--type-body-sm); + line-height: 1.65; + text-wrap: pretty; +} + +.product-entrypoint-footer { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: var(--space-5); + padding-top: var(--space-5); + border-top: 1px solid color-mix(in srgb, var(--color-border) 76%, transparent); +} + +.product-entrypoint-footer small { + min-width: 0; + flex: 1; +} + +.product-entrypoint-action { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + justify-content: flex-end; + gap: var(--space-1); + color: var(--color-action); + font-size: var(--type-caption); + font-weight: 600; + white-space: nowrap; + transition: transform 180ms var(--ease-out); +} + +.product-entrypoint-action .starter-arrow { + width: 15px; + height: 15px; + color: currentColor; +} + +.starter-themes { + display: grid; + gap: var(--space-4); +} + +.starter-section-heading { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var(--space-5); +} + +.starter-section-heading h2 { + margin: 0; + color: var(--color-ink); + font-family: var(--font-display); + font-size: var(--type-title-lg); + font-weight: 400; + letter-spacing: -.025em; +} + +.starter-section-heading p { + margin: 0; + color: var(--color-ink-tertiary); + font-size: var(--type-caption); +} + +.starter-theme-accordion { + display: flex; + min-width: 0; + overflow: hidden; + border: 1px solid var(--color-border); + border-radius: var(--radius-xl); + background: var(--color-canvas-soft); +} + +.starter-theme-card { + position: relative; + min-width: 0; + min-height: 170px; + flex: 1 1 0; + display: grid; + grid-template-columns: minmax(0, 1fr) 20px; + align-items: end; + gap: var(--space-3); + padding: var(--space-5); + border: 0; + border-right: 1px solid var(--color-border); + background: transparent; + cursor: pointer; + text-align: left; + transition: background-color 220ms ease-out, flex-grow 360ms var(--ease-out); +} + +.starter-theme-card:last-child { + border-right: 0; +} + +.starter-theme-card:not(:disabled):hover, +.starter-theme-card:focus-visible { + flex-grow: 1.62; + background: color-mix(in srgb, var(--color-action-soft) 56%, var(--color-canvas)); +} + +.starter-theme-accordion:has(.starter-theme-card:not(:disabled):hover) .starter-theme-card:not(:hover), +.starter-theme-accordion:has(.starter-theme-card:focus-visible) .starter-theme-card:not(:focus-visible) { + flex-grow: .78; +} + +.starter-theme-card:not(:disabled):hover .starter-arrow, +.starter-theme-card:focus-visible .starter-arrow { + color: var(--color-action); + transform: translate(2px, -2px); +} + +.starter-content { + min-width: 0; + display: grid; + gap: var(--space-3); +} + +.starter-content b { + color: var(--color-action); + font-size: var(--type-body-sm); + font-weight: 600; + letter-spacing: .04em; +} + +.starter-content span { + max-width: 245px; + overflow: visible; + color: var(--color-ink); + font-family: var(--font-display); + font-size: var(--type-title-md); + line-height: 1.38; + text-overflow: clip; + text-wrap: pretty; + white-space: normal; +} + +.starter-arrow { + width: 17px; + height: 17px; + color: var(--color-ink-secondary); + transition: color 180ms ease-out, transform 180ms var(--ease-out); +} + +.starter-loading { + width: min(1040px, 100%); + margin-left: 0; + padding: var(--space-6); + border: 1px solid var(--color-border); + border-radius: var(--radius-xl); + background: var(--color-canvas-soft); + color: var(--color-ink-secondary); + font-size: var(--type-body-sm); +} + +.starter-note { + margin: calc(var(--space-2) * -1) 0 0; + color: var(--color-ink-secondary); + line-height: 1.5; + font-size: var(--type-caption); +} + +.composer-wrap-starter .composer, +.composer-wrap-starter .composer-footer, +.composer-wrap-starter .composer-suggestions { + width: min(1040px, 100%); +} + +@media (min-width: 768px) and (max-width: 900px) { + .starter-hero { + grid-template-columns: minmax(0, 1fr) 180px; + padding: var(--space-8); + } + + .starter-celestial-visual { + width: 180px; + height: 150px; + } + + .product-entrypoints { + grid-template-columns: 1fr; + } + + .birth-rectification-card { + border-top: 1px solid var(--color-border); + border-left: 0; + } + + .starter-theme-accordion { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .starter-theme-card { + min-height: 136px; + border-bottom: 1px solid var(--color-border); + } + + .starter-theme-card:nth-child(2n) { + border-right: 0; + } + + .starter-theme-card:nth-last-child(-n + 2) { + border-bottom: 0; + } +} + +@media (max-width: 767px) { + .welcome { + padding: var(--space-4) 0 var(--space-8); + } + + .starter-list { + grid-template-columns: 1fr; + grid-template-rows: auto; + gap: var(--space-5); + } + + .starter-hero { + min-height: 0; + grid-template-columns: 1fr; + padding: var(--space-8) var(--space-6); + } + + .starter-hero h1 { + font-size: clamp(38px, 11vw, 54px); + } + + .starter-celestial-visual { + position: absolute; + top: var(--space-4); + right: calc(var(--space-8) * -1); + width: 180px; + height: 150px; + opacity: .34; + } + + .product-entrypoints { + grid-template-columns: 1fr; + } + + .daily-starlanguage-card, + .birth-rectification-card { + min-height: 210px; + padding: var(--space-6); + } + + .birth-rectification-card { + border-top: 1px solid var(--color-border); + border-left: 0; + } + + .starter-section-heading { + display: grid; + gap: var(--space-1); + } + + .starter-theme-accordion { + display: grid; + } + + .starter-theme-card { + min-height: 116px; + border-right: 0; + border-bottom: 1px solid var(--color-border); + } + + .starter-theme-card:last-child { + border-bottom: 0; + } + + .starter-theme-accordion:has(.starter-theme-card:not(:disabled):hover) .starter-theme-card:not(:hover), + .starter-theme-accordion:has(.starter-theme-card:focus-visible) .starter-theme-card:not(:focus-visible), + .starter-theme-card:not(:disabled):hover, + .starter-theme-card:focus-visible { + flex-grow: 1; + } + + .starter-content span { + max-width: 440px; + } +} + +@media (max-width: 480px) { + .starter-hero { + padding: var(--space-6) var(--space-5); + } + + .starter-hero-note { + margin-top: var(--space-4); + } + + .product-entrypoint-footer { + display: grid; + gap: var(--space-4); + } + + .product-entrypoint-action { + justify-self: start; + } + + .starter-content span { + font-size: var(--type-title-sm); + } +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index ee6bff5e..3eb02dc7 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -3,6 +3,8 @@ import Link from "next/link"; import dynamic from "next/dynamic"; import { ArrowUp, ArrowUpRight, Sparkles, Square, X } from "lucide-react"; +import { useGSAP } from "@gsap/react"; +import gsap from "gsap"; import { useEffect, useRef, useState } from "react"; import type { FormEvent, KeyboardEvent } from "react"; import { AppSidebar } from "@/components/app-sidebar"; @@ -15,7 +17,6 @@ import { ConversationalBirthTimeRectification } from "@/components/conversationa import { ChatMessageContent } from "@/components/chat-message-content"; import { AgentAvatar, ChatMessageRow } from "@/components/chat-message-row"; import { ModelSelector } from "@/components/model-selector"; -import { ParameterFreezePanel, type ParameterFreezeRow } from "@/components/parameter-freeze-panel"; import { Button } from "@/components/ui/button"; import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { Textarea } from "@/components/ui/textarea"; @@ -72,7 +73,6 @@ import { consultationReportMarkdown } from "@/lib/consultation-report-export"; import { OnboardingAuthenticationError, type OnboardingContent, - createOnboardingFallbackGreeting, createStartGreeting, isCurrentOnboardingRequest, onboardingProfileFingerprint, @@ -92,6 +92,8 @@ import { import { selfHostedOtpActions } from "@/modules/identity/client"; import { createBrowserSupabaseClient } from "@/lib/supabase/client"; +gsap.registerPlugin(useGSAP); + const BirthTimeRectification = dynamic( () => import("@/components/birth-time-rectification").then((module) => module.BirthTimeRectification), { @@ -239,6 +241,12 @@ const dailyStarlanguageCards: DailyStarlanguageCard[] = [ { trend: "适合观察资源流向:时间、注意力、金钱都算。", action: "检查一个正在消耗你的习惯,并给它设上限。", caution: "不要为了短期安心做长期成本高的选择。" }, ]; +function greetingForHour(hour: number): string { + if (hour < 11) return "早上好"; + if (hour < 18) return "下午好"; + return "晚上好"; +} + const emptyProfile: Profile = { name: "", date: "", @@ -836,6 +844,7 @@ export default function Home() { const closeButton = useRef(null); const redeemInput = useRef(null); const composerInput = useRef(null); + const starterWorkbench = useRef(null); const pendingConsultation = useRef(null); const cancellationRequests = useRef(new Map>()); const cancellationFeedbackRequest = useRef(null); @@ -856,6 +865,7 @@ export default function Home() { createDurableRectificationQuestionHandoffClient(), ); const resumeRectificationSession = useRef<(session: ChatSession) => void>(() => undefined); + const rectificationOpenInFlight = useRef(false); const rectificationContinuationInFlight = useRef(false); const uiPreview = useRef(false); const uiPreviewMode = useRef(null); @@ -873,7 +883,8 @@ export default function Home() { const visibleRectificationTurn = activeSession?.id === rectificationSessionId ? rectificationInitialTurn : null; - const rectificationSurfaceOpen = activeRectificationSession && visibleRectificationTurn !== null; + const rectificationSurfaceOpen = activeRectificationSession + && activeSession.id === rectificationSessionId; const visibleSessions = sessions .filter((session) => showArchivedSessions ? archivedSessionIds.includes(session.id) : !archivedSessionIds.includes(session.id)) .sort((left, right) => Number(pinnedSessionIds.includes(right.id)) - Number(pinnedSessionIds.includes(left.id))); @@ -1004,17 +1015,6 @@ export default function Home() { }, [accountId, profile]); const profileComplete = isProfileComplete(profile); - const birthTimeDisplay = birthTimeDisplayState(profile); - const profileBirthPlace = selectedBirthPlace(profile); - const parameterFreezeRows: ParameterFreezeRow[] = profileComplete && profileBirthPlace ? [ - { label: "出生时间", value: `${profile.date} ${profile.time || "未定"}` }, - { label: "出生地点", value: profileBirthPlace.label }, - { label: "经纬度", value: `${profileBirthPlace.lat.toFixed(4)}, ${profileBirthPlace.lon.toFixed(4)}` }, - { label: "时区", value: `UTC+${profileBirthPlace.tz}` }, - { label: "Ayanamsa", value: "Lahiri / Sidereal" }, - { label: "Node mode", value: "True Node" }, - { label: "出生时间精度", value: birthTimeDisplay?.kind === "candidate" ? "候选时间" : birthTimeDisplay?.kind === "confirmed" ? "已确认" : "待校正" }, - ] : []; const dailyStarlanguage = dailyStarlanguageCard ?? (profileComplete ? buildDailyStarlanguageCard(profile) : null); const onboardingPending = profileComplete && !onboarding && !onboardingError; const currentOnboardingMessage = onboardingJustCompleted @@ -1029,6 +1029,14 @@ export default function Home() { const shouldStreamOnboarding = !profileComplete; const presetMessageFinished = !shouldStreamOnboarding || presetMessageLength >= currentOnboardingMessage.length; const onboardingCardReady = presetMessageFinished || birthTimeAssessmentPhase !== null; + const starterSuggestions = themes.map((theme) => onboarding?.suggestions.find((item) => item.theme === theme.id) + ?? { theme: theme.id, text: theme.prompt }); + const starterHomeVisible = profileComplete + && presetMessageFinished + && !onboardingPending + && !rectificationSurfaceOpen + && !activeSession?.messages.length; + const daypartGreeting = greetingForHour(new Date().getHours()); useEffect(() => { const controller = new AbortController(); @@ -1745,7 +1753,7 @@ export default function Home() { event.preventDefault(); if (!selectedBirthPlace(profileDraft) || !account || profileSaving) return; setProfileSaving(true); - setBirthTimeAssessmentPhase("saving_profile"); + setBirthTimeAssessmentPhase("entering_home"); setAccountError(""); try { await persistProfile(profileDraft); @@ -1896,7 +1904,8 @@ export default function Home() { pendingConsultationQuestion: string | null = null, sourceSessionOverride: ChatSession | null = null, ) { - if (!account || !modelCatalog || creatingSession || rectificationLoading || rectificationMutationPending + if (!account || !modelCatalog || creatingSession || rectificationLoading || rectificationOpenInFlight.current + || rectificationMutationPending || rectificationContinuationInFlight.current) return; const sourceSession = sourceSessionOverride ?? activeSession; if (!sourceSession) return; @@ -1926,6 +1935,7 @@ export default function Home() { ? null : rectificationQuestionHandoff.current.peek()?.question) ?? null; + rectificationOpenInFlight.current = true; setDraft(""); setDraftTheme(null); setDraftEntrypoint(null); @@ -1933,6 +1943,18 @@ export default function Home() { setRectificationInitialTurn(null); setRectificationError(""); setRectificationLoading(true); + if (!reusingRectificationSession) { + setSessions((current) => [ + rectificationSession, + ...current.filter((session) => session.id !== rectificationSession.id), + ]); + } + if (sourceSession.id !== rectificationSession.id) { + setRectificationReturnSessionId(sourceSession.id); + } + setRectificationSessionId(rectificationSession.id); + activeSessionIdRef.current = rectificationSession.id; + setActiveSessionId(rectificationSession.id); try { let turn: ConversationalRectificationTurn; if (action !== "resume" || !account.rectificationCase) { @@ -1998,12 +2020,6 @@ export default function Home() { } setRectificationInitialTurn(turn); synchronizeRectificationQuestion(turn, sourceSession); - if (sourceSession.id !== boundSession.id) { - setRectificationReturnSessionId(sourceSession.id); - } - setRectificationSessionId(boundSession.id); - activeSessionIdRef.current = boundSession.id; - setActiveSessionId(boundSession.id); setComposerNotice(sessionSyncFailed ? "校正已经开始,但会话关联暂时未同步到云端。" : ""); } catch (caught) { const message = caught instanceof Error @@ -2011,7 +2027,14 @@ export default function Home() { : "生时校正暂时无法继续,请稍后重试。"; setRectificationError(message); setComposerNotice(message); + if (!reusingRectificationSession) { + setSessions((current) => current.filter((session) => session.id !== rectificationSession.id)); + setRectificationSessionId(null); + activeSessionIdRef.current = sourceSession.id; + setActiveSessionId(sourceSession.id); + } } finally { + rectificationOpenInFlight.current = false; setRectificationLoading(false); } } @@ -2691,6 +2714,39 @@ export default function Home() { continueRectificationQuestion.current(question); }, [rectificationInitialTurn, rectificationLoading, rectificationMutationPending]); + useGSAP(() => { + if (!starterHomeVisible || !starterWorkbench.current) return; + const motion = gsap.matchMedia(); + motion.add("(prefers-reduced-motion: no-preference)", () => { + const timeline = gsap.timeline({ defaults: { ease: "power3.out" } }); + timeline + .from(".starter-hero-copy > *", { + autoAlpha: 0, + duration: 0.62, + stagger: 0.07, + y: 18, + }) + .from(".starter-celestial-visual", { + autoAlpha: 0, + duration: 0.76, + scale: 0.94, + }, 0.08) + .from(".product-entrypoint-card", { + autoAlpha: 0, + duration: 0.54, + stagger: 0.08, + y: 20, + }, 0.2) + .from(".starter-theme-card", { + autoAlpha: 0, + duration: 0.46, + stagger: 0.055, + y: 14, + }, 0.34); + }); + return () => motion.revert(); + }, { dependencies: [activeSession?.id, starterHomeVisible], scope: starterWorkbench }); + function submit(event: FormEvent) { event.preventDefault(); if (!profileComplete) { @@ -2831,7 +2887,7 @@ export default function Home() { -
+
{!rectificationSurfaceOpen && (!activeSession?.messages.length ? (
{!profileComplete ? ( @@ -2846,12 +2902,7 @@ export default function Home() { {profileComplete && onboardingJustCompleted && selectedBirthPlace(profileDraft) && } {profileComplete && onboardingJustCompleted && } - ) : ( - - )} + ) : null} {!profileComplete && onboardingStep === "birth" && onboardingCardReady && (
@@ -2910,10 +2961,24 @@ export default function Home() { {profileComplete && presetMessageFinished && !rectificationSurfaceOpen && (onboardingPending ? (
正在准备三个入门问题…
) : ( -
- {parameterFreezeRows.length > 0 && } -
-
+
+
+
+

{daypartGreeting},{profile.name.trim()}。

+

今天想先理清什么?

+

从此刻最在意的事开始,我会结合你的星盘证据,帮你把问题拆得更清楚。

+
+ +
+ +
+
-
+
-
- {(onboarding?.suggestions ?? themes.map((item) => ({ theme: item.id, text: item.prompt }))).map((item) => { - const theme = themes.find((candidate) => candidate.id === item.theme); - return ( - - ); - })} - {onboardingError &&

Agent 的个性化入门问题暂时不可用,已显示安全的默认问题。

} + + +
+
+

或者,从一个主题开始

+

选择最接近你当下的问题。

+
+
+ {starterSuggestions.map((item) => { + const theme = themes.find((candidate) => candidate.id === item.theme); + return ( + + ); + })} +
+
+ {onboardingError &&

个性化问题暂时不可用,已显示安全的默认问题。

}
))}
@@ -2994,21 +3062,26 @@ export default function Home() {
))} - {rectificationSurfaceOpen && visibleRectificationTurn && ( -
- void continueRectificationOriginalQuestion(question)} - /> -
- )} + {rectificationSurfaceOpen && (!visibleRectificationTurn && rectificationError ? ( +
+

{rectificationError}

+ +
+ ) : ( + void continueRectificationOriginalQuestion(question)} + /> + ))}
- {!rectificationSurfaceOpen &&
+ {!rectificationSurfaceOpen &&
{activeSuggestions.length > 0 && (
{activeSuggestions.map((question) => ( diff --git a/frontend/src/components/agent-activity-status.tsx b/frontend/src/components/agent-activity-status.tsx new file mode 100644 index 00000000..3b3d748e --- /dev/null +++ b/frontend/src/components/agent-activity-status.tsx @@ -0,0 +1,25 @@ +import { ThinkingOrb, type OrbState } from "thinking-orbs"; + +const labels = { + working: "正在处理任务…", + searching: "正在搜索相关信息…", + solving: "正在分析问题…", + listening: "正在聆听…", + composing: "正在组织回答…", + shaping: "正在生成结果…", +} as const satisfies Record; + +export function AgentActivityStatus({ + state, + label = labels[state], +}: Readonly<{ + state: OrbState; + label?: string; +}>) { + return ( +
+
+ ); +} diff --git a/frontend/src/components/birth-time-assessment-overlay.tsx b/frontend/src/components/birth-time-assessment-overlay.tsx index f44a23a7..4496f734 100644 --- a/frontend/src/components/birth-time-assessment-overlay.tsx +++ b/frontend/src/components/birth-time-assessment-overlay.tsx @@ -1,4 +1,4 @@ -export type BirthTimeAssessmentPhase = "saving_profile" | "assessing"; +export type BirthTimeAssessmentPhase = "saving_profile" | "assessing" | "entering_home"; const progressCopy = { saving_profile: { @@ -9,6 +9,10 @@ const progressCopy = { title: "正在生成生时评估", detail: "正在读取已有资料并准备下一步,不需要重复操作。", }, + entering_home: { + title: "正在进入首页", + detail: "出生资料已保存,正在为你准备首页。", + }, } as const satisfies Record; export function BirthTimeAssessmentOverlay({ phase }: { readonly phase: BirthTimeAssessmentPhase | null }) { diff --git a/frontend/src/components/chat-message-row.tsx b/frontend/src/components/chat-message-row.tsx index 5abd93c3..0cd4eb8f 100644 --- a/frontend/src/components/chat-message-row.tsx +++ b/frontend/src/components/chat-message-row.tsx @@ -1,3 +1,4 @@ +import { AgentActivityStatus } from "@/components/agent-activity-status"; import { ChatMessageContent } from "@/components/chat-message-content"; import type { ChatMessageView } from "@/lib/chat-message-view"; @@ -22,8 +23,11 @@ export function ChatMessageRow({ message }: { readonly message: ChatMessageView
{message.role === "assistant" ? ( message.state === "thinking" - ?
- : + ? + : <> + {message.state === "streaming" && } + + ) :

{message.text}

}
diff --git a/frontend/src/components/conversational-birth-time-rectification.tsx b/frontend/src/components/conversational-birth-time-rectification.tsx index f5d3703a..07e257e8 100644 --- a/frontend/src/components/conversational-birth-time-rectification.tsx +++ b/frontend/src/components/conversational-birth-time-rectification.tsx @@ -1,11 +1,11 @@ "use client"; -import { - useEffect, - useRef, - useState, -} from "react"; +import { ArrowUp } from "lucide-react"; +import { useEffect, useRef } from "react"; import { ChatMessageContent } from "./chat-message-content.tsx"; +import { AgentAvatar, ChatMessageRow } from "./chat-message-row.tsx"; +import { Button } from "./ui/button.tsx"; +import { Textarea } from "./ui/textarea.tsx"; import { useConversationalRectification, type ConversationalRectificationController, @@ -20,46 +20,52 @@ const domainLabels = { career: "事业与身份", education: "学业与学习", finance: "收入与资产", + health_pressure: "健康与重大压力", relocation: "搬迁与居住地", relationship: "重要关系", family: "家庭变化", other: "其他关键经历", } as const satisfies Readonly>; -function orderedEvidenceDomains(turn: ConversationalRectificationTurn): EvidenceDomain[] { +function nextEvidenceDomains(turn: ConversationalRectificationTurn): EvidenceDomain[] { const provided = new Set(turn.evidenceRecap.flatMap((item) => item.domain ? [item.domain] : [])); - return [...(turn.evidenceRequest?.domains ?? [])].sort((left, right) => - Number(provided.has(left)) - Number(provided.has(right))); + return [...(turn.evidenceRequest?.domains ?? [])] + .sort((left, right) => Number(provided.has(left)) - Number(provided.has(right))) + .slice(0, 2); } function visibleTurnNarrative(turn: ConversationalRectificationTurn): string { - if ( - turn.status !== "active" - || turn.evidenceRecap.length === 0 - || /(?:已记录|已修订):/.test(turn.narrative) - ) { + if (["paused", "abandoned", "completed"].includes(turn.status)) { return turn.narrative; } - const latestEvidence = turn.evidenceRecap.at(-1)!; - const candidate = turn.candidate; - const candidateRange = candidate.rangeStart && candidate.rangeEnd - ? `${candidate.rangeStart}–${candidate.rangeEnd}` - : candidate.representativeTime ?? "当前候选范围"; - const nextDomains = orderedEvidenceDomains(turn) - .slice(0, 2) - .map((domain) => domainLabels[domain]); - const nextStep = candidate.status === "ready_for_confirmation" - ? "当前证据已形成候选总结;请先核对上方候选时间,再决定是否确认或继续补充经历。" - : nextDomains.length > 0 - ? `下一步:请优先补充一件${nextDomains.join("或")}领域已经发生的事件,并选择发生年月。` - : "下一步:请继续补充一件已经发生并带有年月的真实经历。"; + const suggestedDomains = nextEvidenceDomains(turn).map((domain) => domainLabels[domain]); + if (turn.evidenceRecap.length === 0) { + const examples = suggestedDomains.length > 0 + ? suggestedDomains.join("或") + : "工作、搬迁、关系或学业"; + return `为了帮助校正出生时间,请先说一件${examples}方面已经发生的重要经历。最好带上年月,直接像聊天一样描述即可。`; + } + const latestEvidence = turn.evidenceRecap.at(-1)!; + if (turn.candidate.status === "ready_for_confirmation") { + return [ + `${latestEvidence.isCorrection ? "已修订" : "已记录"}:${latestEvidence.dateLabel} · ${latestEvidence.summary}。`, + "目前已经形成一个待确认候选。你可以展开下方详情核对,也可以继续补充一件带年月的真实经历。", + ].join("\n\n"); + } + + const candidateRange = turn.candidate.rangeStart && turn.candidate.rangeEnd + ? `${turn.candidate.rangeStart}–${turn.candidate.rangeEnd}` + : turn.candidate.representativeTime ?? "当前候选范围"; + const nextQuestion = suggestedDomains.length > 0 + ? `接下来请说一件${suggestedDomains.join("或")}方面已经发生的事,尽量带上年月。` + : "接下来请再说一件已经发生的真实经历,尽量带上年月。"; return [ `${latestEvidence.isCorrection ? "已修订" : "已记录"}:${latestEvidence.dateLabel} · ${latestEvidence.summary}。`, - `候选范围当前为 ${candidateRange};范围暂未变化不代表提交失败,系统会结合后续经历继续比较相邻分钟。`, - nextStep, - ].join("\n"); + `候选范围现在是 ${candidateRange}。范围暂未变化不代表提交失败,我会结合后续经历继续比较相邻分钟。`, + nextQuestion, + ].join("\n\n"); } type SurfaceProps = Readonly<{ @@ -73,53 +79,11 @@ function safely(request: Promise) { void request.catch(() => undefined); } -function CandidateSummary({ turn }: { readonly turn: ConversationalRectificationTurn }) { - const candidate = turn.candidate; - const confirmed = candidate.status === "confirmed" && turn.status === "completed"; - const status = confirmed - ? "已明确确认" - : turn.status === "completed" - ? "范围已保存 · 分钟未确认" - : candidate.status === "ready_for_confirmation" - ? "待确认 · 未验证" - : "待验证 · 未确认"; - const recordedCount = turn.evidenceRecap.length; - const nextDomains = orderedEvidenceDomains(turn) - .slice(0, 2) - .map((domain) => domainLabels[domain]); - - return ( -
-
-

候选时间

- {status} -
- {candidate.representativeTime ? ( -
-
-
代表时间
-
-
-
-
候选范围
-
{candidate.rangeStart && candidate.rangeEnd - ? `${candidate.rangeStart}—${candidate.rangeEnd}` - : "尚未缩小"}
-
-
- ) :

尚未形成可供确认的具体时间。

} - {!confirmed &&

候选仍待真实经历验证,未经你的明确确认不会成为当前排盘时间。

} - {!confirmed && recordedCount > 0 && turn.status !== "completed" && ( -
- 已记录 {recordedCount} 条经历 - 系统至少需要 3 条时间明确、可评分的经历后才会可靠缩小范围;范围不变不代表提交失败。 - {nextDomains.length > 0 && ( - 下一步优先补充:{nextDomains.join("或")}领域已经发生的事件。 - )} -
- )} -
- ); +function candidateStatus(turn: ConversationalRectificationTurn): string { + if (turn.status === "completed" && turn.candidate.status === "confirmed") return "已确认"; + if (turn.status === "completed") return "范围已保存,分钟未确认"; + if (turn.candidate.status === "ready_for_confirmation") return "待确认,尚未验证"; + return "待验证"; } export function ConversationalRectificationSurface({ @@ -129,14 +93,11 @@ export function ConversationalRectificationSurface({ onContinueOriginalQuestion, }: SurfaceProps) { const composer = useRef(null); - const [eventYear, setEventYear] = useState(""); - const [eventMonth, setEventMonth] = useState(""); - const currentYear = new Date().getFullYear(); - const eventYears = Array.from({ length: 101 }, (_, index) => currentYear - index); const turn = controller.turn; const pendingQuestion = turn?.status === "completed" ? turn.pendingConsultationQuestion : turn?.pendingConsultationQuestion ?? pendingConsultationQuestion ?? null; + if (!turn) { return (
@@ -147,262 +108,166 @@ export function ConversationalRectificationSurface({ } const canAnswer = turn.actions.includes("answer") && turn.status !== "abandoned" && turn.status !== "completed"; - const requestedDomains = orderedEvidenceDomains(turn); - const providedDomains = new Set( - turn.evidenceRecap.flatMap((item) => item.domain ? [item.domain] : []), - ); - const priorityDomain = requestedDomains.find((domain) => !providedDomains.has(domain)) - ?? requestedDomains[0] - ?? null; + const canConfirm = turn.actions.includes("confirm") + && turn.candidate.status === "ready_for_confirmation" + && Boolean(turn.candidate.representativeTime); + const canContinue = turn.actions.includes("continue_original_question") + && Boolean(pendingQuestion) + && Boolean(onContinueOriginalQuestion); const submit = async () => { if (!canAnswer || !controller.draft.trim() || controller.pending) return; - const dateLabel = eventYear - ? `${eventYear} 年${eventMonth ? ` ${Number(eventMonth)} 月` : ""}` - : "时间不确定"; - const answer = `发生时间:${dateLabel}\n事件详情:${controller.draft.trim()}`; try { - await controller.answer(undefined, answer); - setEventYear(""); - setEventMonth(""); + await controller.answer(undefined, controller.draft.trim()); + composer.current?.focus(); } catch { - // The controller owns the visible request error and keeps the draft available for retry. + // The controller keeps the draft and owns the visible error. } }; - const focusComposer = () => composer.current?.focus(); return ( -
- - -
-
- -
-
- - {requestedDomains.length > 0 && ( -
- 这轮想先补充哪个领域? -
- {requestedDomains.map((domain) => ( - - ))} -
-
- )} - - {controller.pending && canAnswer && ( -
- -
- 正在核对这段经历 - 正在比较相邻候选分钟;完成后会继续提问或更新候选范围。 -
-
- )} - - {canAnswer &&
{ - event.preventDefault(); - void submit(); - }} - > - {controller.correctionTarget && ( -
-

- 正在更正:{controller.correctionTarget.dateLabel} · {controller.correctionTarget.summary} -

- 一次只更正一条事件。提交后会保留原记录用于审计,但候选评分只使用更正后的有效证据。 - -
+
+
+ {controller.pending ? "Jyotisha 正在核对经历" : ""} + {turn.evidenceRecap.map((entry) => ( + + ))} + {controller.pending && controller.draft.trim() && ( + )} - -
- - -
-