fix(consult): blank-line paragraphs, drop the H2 contract line, remove the assistant avatar
Independent Staging Quality Gate / validate (push) Failing after 18m5s
Independent Staging Quality Gate / publish (push) Skipped

Acceptance fix for 28113fa0 (TASK-consult-conversational-answer-fix-20261006):
- F1: chat-answer-detail no longer pins 'Every H2 must start its own line'
  (first turns carry no H2), assertion change noted in three columns.
- F2: Good examples separate paragraphs with a blank line and the shape says
  so; .message p is pre-wrap, so a single newline only wraps.
- Product request: remove the assistant avatar on every surface (consult,
  rectification, onboarding); --assistant-content-inset back to 0px.

Full suite 4941 / fail 24, identical to baseline 86cafe68 minus a timing flake;
build keeps / Static; first-load gzip -0.1%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-10-06 15:58:40 +08:00
co-authored by Claude Opus 5.5
parent 5a589a9058
commit da226db1d4
14 changed files with 82 additions and 49 deletions
+8 -25
View File
@@ -483,7 +483,7 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
.birth-time-assessment-progress strong { margin-top: var(--space-2); color: var(--color-ink); font-family: var(--font-display); font-size: var(--type-title-md); font-weight: 500; }
.birth-time-assessment-progress > span { font-size: var(--type-body-sm); line-height: 1.55; text-wrap: pretty; word-break: auto-phrase; }
.message-assistant { align-items: flex-start; justify-content: flex-start; gap: var(--space-3); }
.message-assistant { align-items: flex-start; justify-content: flex-start; }
.message-user { justify-content: flex-end; }
.message-user .message-content { display: flex; flex-direction: column; align-items: flex-end; }
.message-assistant .message-content { width: auto; max-width: none; flex: 1; }
@@ -1243,9 +1243,10 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
.message-list {
/* Follow-up chips and the run timeline line up with the assistant's text,
which starts past the avatar and its gap. */
--assistant-content-inset: calc(32px + var(--space-3));
/* The assistant reply starts at the column edge (no avatar since
2026-10-06), so follow-up chips and the run timeline line up with the
text itself. */
--assistant-content-inset: 0px;
margin: 0 auto;
width: min(900px, 100%);
padding: var(--space-8) var(--space-8) var(--space-16);
@@ -1258,8 +1259,6 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
color: var(--color-ink-secondary);
}
.message { display: flex; padding: var(--space-2) 0; }
/* Restored after real-device review; see chat-message-row.tsx for why. */
.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(--ring-hairline); }
.message-content { min-width: 0; max-width: min(80%, 680px); }
.message-bubble { overflow: hidden; border: 0; padding: var(--space-3) var(--space-4); border-radius: var(--radius-lg); background: var(--color-canvas-muted); }
.message-assistant .message-bubble { border-radius: 0; background: transparent; padding: var(--space-3) 0; }
@@ -2581,18 +2580,6 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
margin-top: var(--space-8);
}
.conversation:not(.is-empty):not(.is-rectification) .message-assistant {
gap: var(--space-4);
}
.conversation:not(.is-empty):not(.is-rectification) .message-assistant .agent-avatar {
width: 30px;
height: 30px;
flex-basis: 30px;
margin-top: 2px;
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-border) 82%, transparent);
}
.conversation:not(.is-empty):not(.is-rectification) .message-assistant .message-bubble {
padding: 0 0 var(--space-2);
@@ -2676,10 +2663,6 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
margin-top: var(--space-6);
}
.conversation:not(.is-empty):not(.is-rectification) .message-assistant {
gap: var(--space-3);
}
.conversation:not(.is-empty):not(.is-rectification) .message-user .message-content {
max-width: 88%;
@@ -2888,9 +2871,9 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
The pending state keeps the same box for the same reason. */
.rectification-workspace__chat {
--rectification-timeline-height: 64px;
/* Same token as `.message-list`: avatar 32px + gap. The bar sits outside the
transcript, so it cannot inherit that custom property from there. */
--assistant-content-inset: calc(32px + var(--space-3));
/* Same token as `.message-list`. The bar sits outside the transcript, so it
cannot inherit that custom property from there. */
--assistant-content-inset: 0px;
}
.rectification-timeline {
height: var(--rectification-timeline-height);
@@ -31,14 +31,6 @@ function motionPreferred() {
if (motionPreferred()) prefetchOnIdle(loadGsap);
/* Restored after real-device review: R3 removed it on the grounds that it was
the most repeated element in a long transcript, but without it the assistant
turn has no visual owner — in the rectification surface especially, where
narration, a question and its options all stack as plain text. */
export function AgentAvatar() {
return <span className="agent-avatar" aria-hidden="true" />;
}
export const ChatMessageRow = memo(function ChatMessageRow({
message,
showActivity = message.state !== "settled",
@@ -138,7 +130,6 @@ export const ChatMessageRow = memo(function ChatMessageRow({
className={`message message-${message.role}`}
aria-label={message.role === "assistant" ? assistantLabel : "你"}
>
{message.role === "assistant" && <AgentAvatar />}
<div className="message-content">
<div className="message-bubble">
{message.role === "assistant" ? (
@@ -1,7 +1,6 @@
"use client";
import { ChatMessageContent } from "@/components/chat-message-content";
import { AgentAvatar } from "@/components/chat-message-row";
import { protectOnboardingPhrases } from "@/lib/onboarding-copy";
import type { Message } from "@/lib/home-types";
@@ -10,7 +9,6 @@ export function OnboardingChatMessage({ role, text, streaming = false, length =
const protectedVisibleText = protectOnboardingPhrases(visibleText);
return (
<article className={`message message-${role} onboarding-message${phraseSafe ? " is-phrase-safe" : ""}`} aria-label={role === "assistant" ? "Jyotisha" : "你"}>
{role === "assistant" && <AgentAvatar />}
<div className="message-content">
<div className="message-bubble">
{role === "assistant" ? (
@@ -26,7 +26,7 @@ export const REPORT_HEADING = {
* the shape quotes this, and the Chinese user-turn instruction below says the
* same thing.
*/
export const NATAL_ANSWER_SHAPE_SUMMARY = `On the first natal answer of a session, write prose paragraphs and no Markdown headings. The first sentence is the conclusion. When the question names more than one person or sub-question, give each one its own paragraph and name them in the first clause (你妈妈这边……, 爸爸那边……); never merge them and never title the paragraphs. One person or one matter stays one stretch of prose. Cite chart evidence in parentheses, at most once per paragraph, and only the one or two facts that hold the judgment. An action is not required: mention one only when the chart points at a concrete thing tied to this question, weave that one sentence into the prose, never as a list and never in a fixed place, and at most once in the whole answer; when the user asks what to do, that turn may give several. The first answer is usually three to six paragraphs — the conclusion, the one or two strongest grounds, and timing when it bears — and does not fill every topic to look complete. There is no hard cutoff. Say each thing once. Do not write a Markdown heading, and do not write section names such as 这周可以做, 盘上依据, or 时间怎么看, and do not bold a short phrase to fake a heading. Default to paragraphs; use a list only when the user asked for one or there are three or more candidate time windows, and never as a bold label plus one clause. Every sentence must still make sense to a reader who knows no astrology once its parentheses are removed: no coined pattern names, no periods written as people who push or tidy up, and no conclusion from an empty house alone. Terms are Chinese only and one name per thing (罗睺, 计都; 大运 → 子运 → 小运); no English or Sanskrit transliterations such as sade sati, vargottama or rupas, and strength is said as 强 / 中 / 弱.`;
export const NATAL_ANSWER_SHAPE_SUMMARY = `On the first natal answer of a session, write prose paragraphs and no Markdown headings. The first sentence is the conclusion. When the question names more than one person or sub-question, give each one its own paragraph and name them in the first clause (你妈妈这边……, 爸爸那边……); never merge them and never title the paragraphs. One person or one matter is not split by person. Separate paragraphs with a blank line; a single line break does not start a new paragraph on screen. Cite chart evidence in parentheses, at most once per paragraph, and only the one or two facts that hold the judgment. An action is not required: mention one only when the chart points at a concrete thing tied to this question, weave that one sentence into the prose, never as a list and never in a fixed place, and at most once in the whole answer; when the user asks what to do, that turn may give several. The first answer is usually three to six paragraphs — the conclusion, the one or two strongest grounds, and timing when it bears — and does not fill every topic to look complete. There is no hard cutoff. Say each thing once. Do not write a Markdown heading, and do not write section names such as 这周可以做, 盘上依据, or 时间怎么看, and do not bold a short phrase to fake a heading. Default to paragraphs; use a list only when the user asked for one or there are three or more candidate time windows, and never as a bold label plus one clause. Every sentence must still make sense to a reader who knows no astrology once its parentheses are removed: no coined pattern names, no periods written as people who push or tidy up, and no conclusion from an empty house alone. Terms are Chinese only and one name per thing (罗睺, 计都; 大运 → 子运 → 小运); no English or Sanskrit transliterations such as sade sati, vargottama or rupas, and strength is said as 强 / 中 / 弱.`;
/** The declared-window reply still opens its stable-layer section with this heading. */
export const WINDOW_OPEN_HEADING = "先回答你的问题";
@@ -429,7 +429,7 @@ export function natalAnswerShapeBody(): string {
`${NATAL_SECTION_RULE}每段的判断后在括号里给撑住它的盘上依据,每段最多一处括号。`,
"行动不强制:只有盘上确实指向一件和问题相关的具体事情时,才在话里顺口说一句,不列清单、不放固定位置、全篇最多一处;用户问「我该怎么办」或「怎么做」时,那一轮可以给几条。",
"首轮通常三到六段:讲清结论、最关键的一两条依据,相关时带上时间;不为显得周全把各方面写满。不设硬截断。同一件事只说一遍。",
"正文写成段落。只有用户要求列举,或确有三个以上候选时间窗时才用列表,且不用加粗标签加一句的格式。",
"正文写成段落,段落之间空一行(单个换行在界面上不分段)。只有用户要求列举,或确有三个以上候选时间窗时才用列表,且不用加粗标签加一句的格式。",
"首轮正文不写 ##,不写「这周可以做」「盘上依据」「时间怎么看」这类节名,也不用加粗短语冒充小标题。",
"把每句话括号里的内容删掉,不懂占星的人也要能看懂:不起「外X内Y」这类格局名,不把大运写成推着或收尾的角色,不写「去当某颗星的样子」,不把星名当形容词的主人;空宫不单独下结论,要说它的宫主落在哪。名词只用中文、全篇一个叫法(罗睺、计都;大运、子运、小运),不写英文或梵文音译(sade sati、vargottama、rupas 之类),力量只说强、中、弱。",
"不要写「先回答你的问题」这个标题,不要写「统一参数与原始结构」,不要写技法审计表,不要写思考过程清单,不要复述内部 JSON 字段。",
+7 -1
View File
@@ -27,7 +27,7 @@ ANSWER SHAPE (natal / general / declared-window 三种模式共用)
2. 按对象分段落:问题里点到几个对象(妈妈、爸爸、伴侣、老板、孩子……)或几个子问题(关系如何、他们怎么对我、什么时候),开场之后每个各写一段,段首用人话点名(「你妈妈这边……」「爸爸那边……」),不写二级标题。问「父母」时妈妈和爸爸分开写,不揉成一段。只有一个对象、一个子问题时不拆段。
3. ${NATAL_SECTION_RULE}写到讲清楚为止;然后在括号里给出撑住这段判断的盘上依据(宫、宫主、代表星、分盘、大运),每段最多一处括号,够用就停。问的是别人时,写那个人在你盘上对应的宫位与代表星说明了什么,不写那个人的心思。
4. 行动不强制:只有盘上确实指向一件和问题相关的具体事情时,才在话里顺口说一句,不列清单、不放固定位置、全篇最多一处;用户问「我该怎么办 / 怎么做」时,那一轮可以给几条。无出生分钟与申报时段同样不交固定行动清单。
并列的点默认写成段落。只有用户要求列举,或确有三个以上候选时间窗时才用列表,且不用「加粗标签 + 一句」的格式。首轮正文不写 ##,不写「这周可以做」「盘上依据」「时间怎么看」这类节名,也不用加粗短语冒充小标题。
段落之间空一行(单个换行在界面上不分段)。并列的点默认写成段落。只有用户要求列举,或确有三个以上候选时间窗时才用列表,且不用「加粗标签 + 一句」的格式。首轮正文不写 ##,不写「这周可以做」「盘上依据」「时间怎么看」这类节名,也不用加粗短语冒充小标题。
人话自检:把每句话括号里的内容删掉,一个完全不懂占星的人仍然能看懂它在说什么;做不到就重写这句。禁止:
- 自造格局名、四字概括(「外X内Y」这一类);
- 把大运、行运写成拟人的角色(「推这一段的是某某大运」「修形式的是某某子运」「交给某某去收尾」),以及「去当某颗星的某种样子」「别去应某颗星的某个象」这类角色修辞;
@@ -41,14 +41,20 @@ ANSWER SHAPE (natal / general / declared-window 三种模式共用)
Good (父母题首轮,分段落、段首点名;括号里是依据的占位,只演示口气与结构,不是读法):
你和妈妈这条线近、来往多;爸爸这条线盘上有距离的迹象。
你妈妈这边,她离你近,对你的事上心,你们之间来往多。(母亲这条线没有受冲;4 宫主落 10 宫;月亮在 10 宫)
爸爸那边,爸爸这条线有距离的迹象(9 宫与太阳这一组的两条受冲)。现实里可能是他不常在你身边,也可能人在、只是话少、关系紧。实际是哪一种,告诉我一句,我按真实情况再看。
两边给你的不一样:妈妈这一头来往多,爸爸这一头要看实际情况再说。
现在这十年(月亮大运,到 2034 年 2 月)和妈妈的来往会比以前多。
Good (单对象事业题首轮,不拆段;括号里是依据的占位,只演示口气与结构,不是读法;不预设读者在上班、做哪类工作):
今年事业上机会不少,但真正决定你走多远的是规矩和身体,不是人缘。
机会和帮你的人今年都在;同时事情多、要求严,身体也容易被拖累。(木星行运 10 宫;土星行运 6 宫)
9 月木星换宫之前,机会多、事情也不稳。
Bad (多对象题,改写示意): 你和父母是「外热内冷」:推着这段关系的是金星大运,土星子运负责收尾;别去应火星的「硬」,去当金星的「和」。(自造格局名、大运写成角色、星名当形容词,爸妈揉成一段;括号外没有一句不懂占星的人能看懂。)