fix(web): 聊天列表恢复项目符号,收紧定义列表,压思考条间距(BUG-962/963/964)

This commit is contained in:
jesse-ux
2026-09-18 18:45:19 +08:00
parent 53d78137ff
commit dccedf372e
12 changed files with 214 additions and 9 deletions
+9 -1
View File
@@ -186,7 +186,7 @@ One documented exception: the thinking text inside a timeline step (`.consultati
## 4. Spacing & Layout
The base unit is 4px. Tokens are `--space-1: 4px`, `--space-2: 8px`, `--space-3: 12px`, `--space-4: 16px`, `--space-5: 20px`, `--space-6: 24px`, `--space-8: 32px`, `--space-10: 40px`, `--space-12: 48px`, `--space-16: 64px`, and `--space-24: 96px`.
The base unit is 4px. Tokens are `--space-1: 4px`, `--space-2: 8px`, `--space-3: 12px`, `--space-4: 16px`, `--space-5: 20px`, `--space-6: 24px`, `--space-8: 32px`, `--space-10: 40px`, `--space-12: 48px`, `--space-16: 64px`, and `--space-24: 96px`. `--consult-think-answer-gap` is `var(--space-3)` (12px): it is the only gap between the thinking bar and the answer body. Both `.consultation-thinking-report` and `.message-stage-and-answer` use it; do not write a second value on either path.
Radii have two visual steps. Controls use 8px (`--radius-md`; `--radius-xs` and `--radius-sm` alias that value). Cards and sheets use 12px (`--radius-lg`; `--radius-xl` aliases it). Circles stay `50%`; pills stay `999px`.
@@ -755,6 +755,14 @@ Admin 的 antd `<Spin>` 是独立设计系统,不在此表。
- **窄屏表格:** 三列表(技法审计)继续卡片化。四列及以上(Yoga、统一参数)在 `.conversation` 里横向滚动,右缘用 canvas 渐隐提示还可滑;`.conversation` 自身 `overflow-x: hidden`,页面不得出现横向滚动条。个人报告表格不走这套规则。
- **表单输入 16px** 全局 `input` / `select``--type-body-md`16px)。iOS Safari 对小于 16px 的字段聚焦时会整页放大且不缩回。`.otp-input` 仍是 20px`.composer textarea` 仍是 16px。
### Chat body lists
Three product rules for assistant markdown in `.message-markdown` (BUG-962 / 963 / 964). Personal-report markdown (`personal-report-markdown-view`) is a different surface and is not restyled here.
- **Lists have markers.** `.message-markdown ul.markdown-list` is `list-style-type: disc`; `ol.markdown-list` is `decimal`. `::marker` uses `--color-ink-secondary`, not ink. Tailwind v4 preflight zeros `ul, ol { list-style }`; this surface must restore it. Keep `display: grid` — markers render with grid.
- **Prose is not auto-promoted to lists.** `promoteDefinitionLists` still lifts consecutive short `termbody` blocks (BUG-356: parallel transit / advice items). A block promotes only when every one of these holds, and still only for ≥2 consecutive blocks: the term has no sentence-end punctuation (`。!?;!?;`) and no newlines; the body is a single sentence (no sentence-end, or exactly one at the end); the body is ≤ 40 characters; fences and existing lists are skipped. Spoken four-heading prose with a colon inside a long paragraph stays paragraphs.
- **Thinking-bar-to-body spacing is one token.** `--consult-think-answer-gap` (`var(--space-3)` = 12px) is the gap on both `.consultation-thinking-report` and `.message-stage-and-answer`. In conversation scope, `.message-markdown > *:first-child { margin-top: 0 }` so an opening `h2` does not add `--space-8` on top of that gap. Conversation `.markdown-list` item gap is `--space-2` (8px).
## 14. 断点
宽度切点只允许出现在 `globals.css` 顶部注释的白名单里。壳层切点以 `sidebarViewportForWidth` 为准:小于 768 移动,小于 1024 平板,否则桌面。组件级栅格(会员方案卡、校正候选)用 640/641。校正独立问题块(`.rectification-message-question.is-standalone`)与助手正文共用 `--assistant-content-inset`。报告目录在 860 塌成抽屉,因为那是第三栏的内容宽度,不是设备宽度。
+20 -3
View File
@@ -163,6 +163,10 @@
--space-12: 48px;
--space-16: 64px;
--space-24: 96px;
/* Gap between the thinking bar and the answer body. Both stacked paths
(timeline report and stage-and-answer) must use this token — do not
write a second value on either (BUG-964). */
--consult-think-answer-gap: var(--space-3);
--composer-reserve: 124px;
--ease-out: cubic-bezier(.22, 1, .36, 1);
/* The personal report's paper surface. Declared here so the dark blocks can
@@ -556,7 +560,7 @@ button:disabled { cursor: default; opacity: .45; }
.agent-thinking-panel + .message-answer { margin-top: var(--space-2); }
.message-stage-and-answer {
display: grid;
gap: var(--space-2);
gap: var(--consult-think-answer-gap);
}
.message-stage-and-answer .agent-thinking-panel,
.message-stage-and-answer .agent-activity-status {
@@ -1196,7 +1200,7 @@ button:disabled { cursor: default; opacity: .45; }
}
.consultation-thinking-report {
display: grid;
gap: var(--space-6);
gap: var(--consult-think-answer-gap);
}
.consultation-report-analysis {
min-width: 0;
@@ -1340,6 +1344,15 @@ button:disabled { cursor: default; opacity: .45; }
margin: var(--space-3) 0 var(--space-5);
padding-inline-start: 1.35em;
}
.message-markdown ul.markdown-list {
list-style-type: disc;
}
.message-markdown ol.markdown-list {
list-style-type: decimal;
}
.message-markdown .markdown-list ::marker {
color: var(--color-ink-secondary);
}
.message-markdown .markdown-list > li {
padding-block: 0;
padding-inline-start: 6px;
@@ -2343,7 +2356,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
}
.conversation:not(.is-empty):not(.is-rectification) .message-markdown .markdown-list {
gap: var(--space-4);
gap: var(--space-2);
}
.conversation:not(.is-empty):not(.is-rectification) .markdown-table {
@@ -2355,6 +2368,10 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
margin: var(--space-8) 0 var(--space-3);
}
.conversation:not(.is-empty):not(.is-rectification) .message-markdown > *:first-child {
margin-top: 0;
}
.conversation:not(.is-empty):not(.is-rectification) .message-markdown h2 {
font-size: var(--type-title-lg);
}
+10 -1
View File
@@ -1,17 +1,26 @@
const FENCE = /(```[\s\S]*?```)/;
const DEFINITION_LINE = /^(.{2,80}?)[:](.+)$/u;
const SENTENCE_END = /[!?;]/u;
const BODY_MAX_CHARS = 40;
function isSkippableLine(line: string): boolean {
return /^(#{1,6}\s|>\s|[-*+]\s|\d+[.)]\s|\|)/.test(line) || line.startsWith("```");
}
function isSingleSentence(text: string): boolean {
return !SENTENCE_END.test(text.replace(/[!?;]$/u, ""));
}
function isDefinitionLine(block: string): boolean {
const trimmed = block.trim();
if (!trimmed || trimmed.includes("\n") || isSkippableLine(trimmed)) return false;
const match = DEFINITION_LINE.exec(trimmed);
if (!match) return false;
const term = match[1]?.trim() ?? "";
const body = match[2]?.trim() ?? "";
return body.length >= 4;
if (body.length < 4 || body.length > BODY_MAX_CHARS) return false;
if (SENTENCE_END.test(term)) return false;
return isSingleSentence(body);
}
function toListItem(block: string): string {
@@ -29,3 +29,16 @@ test("code fences and existing lists are left alone", () => {
].join("\n");
assert.equal(promoteDefinitionLists(source), source);
});
test("spoken four-heading prose with a colon stays paragraphs", () => {
const prose = [
"你这半年的事业是「外刚内不承」:表面硬气是真的硬——你确实扛得住场面,底下却承不住。别人看见的是硬气,盘上撑着的是另一层。",
"",
"推的是火星:事情由你自己起动、自己顶上去。别人帮不上忙,你也没法把责任分出去,更别指望场面自己圆。",
"",
"所以别去应火星的「硬顶」,也别去应土星弱位的「拖」。去扮演太阳的「署名」:把事做成一件能签字的作品,而不是一场硬顶。",
].join("\n");
const result = promoteDefinitionLists(prose);
assert.equal(result, prose);
assert.equal(result.includes("- **"), false);
});
+18 -1
View File
@@ -80,7 +80,24 @@ test("shows honest agent activity states before and during streamed text", () =>
assert.doesNotMatch(messageRowSource, /vargaSentence=\{showThinkingPanel \? null : vargaSentence\}/);
assert.match(messageRowSource, /<ConsultationRunTimeline[\s\S]*vargaSentence=\{vargaSentence\}/);
assert.match(globalStyles, /\.message-stage-and-answer/);
assert.match(globalStyles, /\.message-stage-and-answer \{[\s\S]*gap: var\(--space-2\)/);
// 原值: gap: var(--space-2)
// 新值: gap: var(--consult-think-answer-gap)
// 原因: BUG-964 思考条与正文间距由同一 token 决定(12px),不再各写各的
assert.match(globalStyles, /\.message-stage-and-answer \{[\s\S]*gap: var\(--consult-think-answer-gap\)/);
assert.match(globalStyles, /\.consultation-thinking-report \{[\s\S]*gap: var\(--consult-think-answer-gap\)/);
assert.match(globalStyles, /--consult-think-answer-gap:\s*var\(--space-3\)/);
assert.match(
globalStyles,
/\.conversation:not\(\.is-empty\):not\(\.is-rectification\) \.message-markdown > \*:first-child \{[\s\S]*margin-top:\s*0/,
);
assert.match(globalStyles, /\.message-markdown ul\.markdown-list \{[\s\S]*list-style-type:\s*disc/);
assert.match(globalStyles, /\.message-markdown ol\.markdown-list \{[\s\S]*list-style-type:\s*decimal/);
assert.match(globalStyles, /\.markdown-list[^{]*\{[^}]*list-style/);
assert.match(
globalStyles,
/\.conversation:not\(\.is-empty\):not\(\.is-rectification\) \.message-markdown \.markdown-list \{[\s\S]*gap:\s*var\(--space-2\)/,
);
assert.match(globalStyles, /\.markdown-list ::marker \{[\s\S]*color:\s*var\(--color-ink-secondary\)/);
assert.match(globalStyles, /\.agent-activity-status \+ \.message-answer/);
// Former lock: `<ThinkingOrb aria-hidden="true" state={state} size={20}` — the canvas orb was a
// second live-marker vocabulary next to the timeline's InlineSpinner (BUG-476). One marker now.
@@ -349,7 +349,10 @@ test("Agentic rectification follows the conversation tail only while the reader
assert.doesNotMatch(styles, /\.rectification-jump-latest/);
assert.match(styles, /\.jump-to-latest \{[^}]*justify-content: center/);
assert.match(styles, /--rectification-jump-clearance/);
assert.match(styles, /\.message-stage-and-answer \{[\s\S]*gap: var\(--space-2\)/);
// 原值: gap: var(--space-2)
// 新值: gap: var(--consult-think-answer-gap)
// 原因: BUG-964 思考条与正文间距由同一 token 决定
assert.match(styles, /\.message-stage-and-answer \{[\s\S]*gap: var\(--consult-think-answer-gap\)/);
assert.doesNotMatch(chat, /conversationEnd/);
// 原值: 改选滚到出卡消息
// 新值: 无 offerSectionRef;点其他行即改选