feat(consult): show the queued row from the send frame; only smalltalk replies are quiet (T1, BUG-1074)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
584ad05c36
commit
86d99115c6
@@ -756,6 +756,8 @@ or user IDs.
|
||||
|
||||
Agent 的 live 标记只有 `InlineSpinner` 一种。曾经并存的 canvas 小球(`thinking-orbs`)已移除,不得再引入第二种 live 标记。
|
||||
|
||||
**普通对话发出即有反馈(2026-09-28,TASK-consult-first-frame-and-pacing,BUG-1074)。** 按下发送的那一帧,回答行就带着步骤栏,唯一一行是排队行「收到,正在看你的问题…」(`QUEUED_TIMELINE_ROW`,行内 `InlineSpinner` + shimmer,summary 写「正在分析」)。服务端流的第一个字节也是这句(`activity` 事件,phase `received`),所以那一帧到达时行不换字。不再有「分类结果回来之前整行静默」:只有服务端判定为寒暄的回复(`run.completed` 带 `responseKind: "smalltalk"`)才把步骤栏收掉、只留一句话,寒暄前会闪约一秒这一行,属于流式生成中的正常形态。首事件之后的行照旧由服务端事件推动;不新增第二种 live 标记,不加 spinner 面板或骨架。
|
||||
|
||||
校正面的所有等待复用行内等待:进入前的 hydration 在揭幕之前完成,进入后唯一的等待形态是时间线 live 行(含「正在准备下一个问题…」这一条独立 live 行)。打字回答的 live 行从发出那一帧起就是阶段句「收到,正在对照你的档案…」,按服务端 `turn.progress` 换成「正在记下这件事…」「正在重新对照盘面…」「正在准备下一个问题…」,属于流式生成中(BUG-1047)。区间交付卡只挂在最新那条采用旁白下面,不得留在更早的采集/区分题下。有未答的采集或选择题时卡仍在,「更像这个」置灰并写「先答完上面这道,再选时间」,不得整卡消失。卡上至多三列并排,相同性格句只写一次,点「更像这个」即采用该列分钟,按钮显示「正在采用…」或「已采用」。采用过程中整张卡留在原处,不得因 `busy` 卸掉。采用后前事核对结束走 `verified_idle`:一行收尾文案跟在卡片下面、与助手列对齐,没有 live 行、没有重载、没有采用状态条。卡片与右栏细则见 §11、§12。
|
||||
|
||||
**每次打开网页只揭幕一次,客户端返回首页走暖快照(2026-09-26,BUG-1040)。** 整页加载(首次打开、刷新、登录后跳转)照旧放下面这一次加载屏。同一次打开里从星盘 / 星历 / 我的报告 / 星盘档案经侧栏「新建对话」、历史会话行、账户页脚或浏览器返回回到 `/`,不再出现加载环:第一次冷启动成功后,模型目录、校正入口摘要、会话分页游标和今日星语记进模块级暖快照(`lib/home-warm-snapshot.ts`,按账户隔离、只在内存、不落任何存储;账户、资料与会话列表本来就在布局层的会话列表 provider 里跨页存活)。首页重挂时若快照齐全,首帧即可用,落点按冷启动同一套规则同步算出(`?new=1` 新建本地空对话、`?c=` 打开该会话、其余一律当前人物的空白首页,同 BUG-1052),随后后台刷新模型目录、账户、后台回答恢复、入口摘要与今日星语,结果到了静默替换;消息没缓存的历史会话沿用首页内切换会话的留白方式。快照缺任何一项、落点需要去服务端查、或客户端导航没留下目标地址(Next 先渲染新页面、后写地址栏,`AppLink` 在点击时记下目标),一律回到冷启动,不半揭幕。账户变化、退出、任一 401 清空快照;今日星语跨日或换人按新键重取,卡片先用静态句。
|
||||
|
||||
@@ -134,6 +134,10 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
|
||||
- 领域中文名:父母、子女与原有的事业、关系、财富、身心压力、学习、迁居、家庭、年运、时运、综合并列;「家庭」只用于家里整体(家庭氛围、家里的事),问父母就说父母,问孩子就说子女。
|
||||
- 写作行里补取卡外数据的进行中句:「正在多看一眼:<名称>…」,例如「正在多看一眼:D60 分盘…」「正在多看一眼:太阳回归…」「正在多看一眼:格局明细…」。数据卡 v2 新增可多看的名称:「灵魂星在九分盘的落点(Karakamsha)」「宫主链」「本命、九分盘、十分盘的联动」「门闩(Argala)」「月亮行运」。不写「补取」「卡外」「数据卡」这些内部词,不写秒数、不写「正在加载」。只在活动行里,不进正文与历史。
|
||||
|
||||
## 普通咨询发出即有一句(2026-09-28,BUG-1074)
|
||||
|
||||
按下发送的那一帧,步骤栏第一行就是「收到,正在看你的问题…」;服务端流的第一行也是它,字不变。随后只按服务端真实做的事换行:「正在读取印度占星分析规则…」「正在计算本命盘…」「正在核对可用证据…」「正在组织回答…」。这句只在步骤栏里,不进正文、不进历史;不写秒数、不写「请稍候」、不写「正在加载」。寒暄轮也先看到这句,服务端判定是寒暄后整栏收掉,只留一句回复。
|
||||
|
||||
## 寒暄只回一句
|
||||
|
||||
普通对话里的纯打招呼、道谢、告别,只回一句简体白话,最多 20 字,不以句号结尾;不套开场形状,不写星盘或运势主张,不用客服套话、星月比喻或一串追问。本命、申报时段、无出生分钟三种模式共用。含咨询、解释前文、纠错、抱怨或不确定意图时仍按咨询处理。
|
||||
|
||||
@@ -54,9 +54,11 @@ export const ChatMessageRow = memo(function ChatMessageRow({
|
||||
}>) {
|
||||
const messageRow = useRef<HTMLElement>(null);
|
||||
const smalltalk = message.responseKind === "smalltalk";
|
||||
const awaitingClassification = message.state === "thinking" && !message.text && !message.activity
|
||||
&& !message.thinkingText && message.timeline?.length === 0;
|
||||
const quiet = smalltalk || awaitingClassification;
|
||||
// Only a smalltalk reply is quiet. The wait before the server's first event
|
||||
// used to be quiet too, so nothing showed for the classification round trip
|
||||
// (BUG-1074); now the timeline's queued row is on screen from the send frame
|
||||
// and disappears when the run.completed event says the reply was smalltalk.
|
||||
const quiet = smalltalk;
|
||||
const assistantLabel = quiet ? "Jyotisha" : message.state === "thinking"
|
||||
? "Jyotisha 正在分析"
|
||||
: message.state === "streaming"
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useId, useState } from "react";
|
||||
import { BookOpen, Check, ChevronDown, Layers, ListTodo, LoaderCircle, PenLine, type LucideIcon } from "lucide-react";
|
||||
|
||||
import { InlineSpinner } from "@/components/inline-spinner";
|
||||
import { CONSULTATION_RECEIVED_LABEL } from "@/lib/consultation-activity-labels";
|
||||
|
||||
void LoaderCircle; // source lock: tests/chat-stream-layout.test.ts matches /LoaderCircle/
|
||||
|
||||
@@ -19,12 +20,17 @@ const KIND_ICONS: Record<ConsultationTimelineKind, LucideIcon> = {
|
||||
write: PenLine,
|
||||
};
|
||||
|
||||
/** Shown while the request is in flight but no step has been reported yet. */
|
||||
/**
|
||||
* Shown from the frame the question is sent until the first step is reported
|
||||
* (BUG-475, BUG-1074). The label is the same deterministic sentence the server
|
||||
* writes as its first byte, so the row never changes wording when that frame
|
||||
* lands.
|
||||
*/
|
||||
export const QUEUED_TIMELINE_ROW: ConsultationTimelineRow = {
|
||||
id: "queued",
|
||||
kind: "method",
|
||||
status: "live",
|
||||
label: "正在处理…",
|
||||
label: CONSULTATION_RECEIVED_LABEL,
|
||||
};
|
||||
|
||||
/** Shown while the request is still in flight after completed steps, before the next live step. */
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* The first line of every consultation turn: on the client from the frame the
|
||||
* question is sent, on the server as the first byte of the stream, before the
|
||||
* turn is classified or the chart is read (TASK-consult-first-frame-and-pacing-20260928 D1).
|
||||
*/
|
||||
export const CONSULTATION_RECEIVED_LABEL = "收到,正在看你的问题…";
|
||||
export const CONSULTATION_LOADING_METHOD_LABEL = "正在读取印度占星分析规则…";
|
||||
export const CONSULTATION_CHART_CALCULATION_LABEL = "正在计算本命盘…";
|
||||
export const CONSULTATION_EVIDENCE_VALIDATION_LABEL = "正在核对可用证据…";
|
||||
|
||||
@@ -67,7 +67,11 @@ test("the timeline collapses in place with a 180ms height transition and honours
|
||||
|
||||
const live = renderToString(createElement(ConsultationRunTimeline, { rows: [], live: true }));
|
||||
assert.match(live, /aria-expanded="true"/);
|
||||
assert.match(live, /正在处理…/);
|
||||
// 原值: /正在处理…/
|
||||
// 新值: /收到,正在看你的问题…/
|
||||
// 原因: TASK-consult-first-frame-and-pacing-20260928 D1 / BUG-1074:排队行从发送那一帧起可见,文案与服务端首帧一致
|
||||
assert.match(live, /收到,正在看你的问题…/);
|
||||
assert.doesNotMatch(live, /正在处理…/);
|
||||
assert.match(live, /inline-spinner/);
|
||||
const liveAfterTool = renderToString(createElement(ConsultationRunTimeline, {
|
||||
rows: [{ id: "read", kind: "calculate", status: "done", label: "读取校正记录" }],
|
||||
|
||||
@@ -14,8 +14,20 @@ test("settled and streaming smalltalk render text without thinking, activity or
|
||||
assert.doesNotMatch(html, /consultation-thinking-report|agent-activity|must stay hidden|已完成|技法|正在分析/);
|
||||
}
|
||||
});
|
||||
test("classification wait does not invent a thinking step before any activity", () => {
|
||||
// 原值: 分类结果回来之前整行静默——不渲染时间线,不出现「正在处理 / 正在分析」
|
||||
// 新值: 从发送那一帧起就渲染时间线的排队行「收到,正在看你的问题…」;只有 responseKind 为 smalltalk 的行才静默
|
||||
// 原因: TASK-consult-first-frame-and-pacing-20260928 D1 / BUG-1074:静默让每一轮在服务端首事件前都是空白(BUG-976 的副作用)
|
||||
test("the classification wait shows the queued row from the send frame; only a smalltalk reply is quiet", () => {
|
||||
const message = streamingChatMessageView([{ role: "user", text: "你好" }], true, "", undefined, undefined, undefined, [])!;
|
||||
const html = renderToStaticMarkup(<ChatMessageRow message={message} />);
|
||||
assert.doesNotMatch(html, /consultation-thinking-report|正在处理|正在分析|已完成/);
|
||||
const html = renderToStaticMarkup(<ChatMessageRow message={message} showActivity />);
|
||||
assert.match(html, /consultation-thinking-report/);
|
||||
assert.match(html, /收到,正在看你的问题…/);
|
||||
assert.match(html, /inline-spinner/);
|
||||
assert.doesNotMatch(html, /已完成|正在处理…/);
|
||||
// No invented step: the queued row is the only row, and it is not a think or write step.
|
||||
assert.equal((html.match(/consultation-run-timeline__row/g) ?? []).length, 1);
|
||||
|
||||
const smalltalk = streamingChatMessageView([{ role: "user", text: "你好" }], true, "", undefined, undefined, undefined, [], "smalltalk")!;
|
||||
const quiet = renderToStaticMarkup(<ChatMessageRow message={smalltalk} showActivity />);
|
||||
assert.doesNotMatch(quiet, /consultation-thinking-report|收到,正在看你的问题|正在分析|已完成/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user