Compare commits
2 Commits
d3833356c7
...
5fdabfae89
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fdabfae89 | |||
| 8b8e5214f9 |
+49
-1
@@ -4966,7 +4966,7 @@
|
||||
- 根因:BUG-328 把窄屏盘面改成工作区第二行(约 46% 高)且未设叠层;输入框 `.composer-wrap` 仍是 `position: relative; z-index: 2`。盘面与输入框在同一工作区网格里重叠时,输入框画在盘面标题之上。
|
||||
- 修复:仅在视口宽度小于 768px 的移动端,把盘面改成工作区内 `z-index: 20` 的底部 sheet 覆盖层;桌面网页仍是右侧分栏,不出现底部弹出。遮罩点击、Escape、44px 关闭按钮可关;聊天列在打开时 `inert`。
|
||||
- 验证:`frontend/tests/rectification-agentic-entry.test.ts`。
|
||||
- 防复发:底部 sheet 只允许 `matchMedia(max-width: 767px)` 触发,不得用聊天区 `clientWidth` 把桌面侧栏挤窄误判成移动端;桌面必须保持 `grid-template-columns: minmax(0, 1fr) minmax(18rem, 22.5rem)`。窄屏盘面必须是覆盖层(absolute、高于输入框的 z-index、不透明背景)。
|
||||
- 防复发:底部 sheet 只允许 `matchMedia(max-width: 767px)` 触发;工作区必须 `isolation: isolate` 且 `z-index: 0`,打开时隐藏 composer(含 `backdrop-filter`),覆盖层 z-index 必须高于输入框。不得用聊天区 `clientWidth` 把桌面侧栏挤窄误判成移动端。
|
||||
- 相关记录:BUG-322、BUG-326、BUG-328
|
||||
- 复发自:BUG-328(当时为了露出聊天改成下半屏分栏,叠层低于输入框)
|
||||
- 修复版本:9670b661
|
||||
@@ -5019,6 +5019,54 @@
|
||||
- 复发自:无
|
||||
- 修复版本:9c4b64c9
|
||||
|
||||
## BUG-334 | 移动端盘面覆盖层仍被输入框 backdrop-filter 盖住
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-08-20
|
||||
- 最近更新:2026-08-20
|
||||
- 影响面:生时纠正窄屏盘面、`.composer-wrap`、`.rectification-board-overlay`
|
||||
- 用户现象:打开“当前盘面”后,输入区预览条仍叠在弹层标题上;覆盖层和输入框像同一层,聊天列表也被挡住看不清。
|
||||
- 触发条件:移动端视口打开宫位表。iOS Safari 上更明显。
|
||||
- 根因:`.composer-wrap` 带 `z-index: 2` 和 `backdrop-filter`。iOS 会把该滤镜层合成到覆盖层之上。BUG-330 只把 overlay 设为 `z-index: 20`,工作区没有独立层叠上下文,也没有在打开时关掉输入框滤镜。
|
||||
- 修复:工作区 `isolation: isolate; z-index: 0` 锁住输入框层叠;覆盖层 `z-index: 50`。打开盘面时隐藏 composer(`visibility: hidden`、关掉 `backdrop-filter`),并收起预览条。sheet 高度改为 72%,上方聊天列表可从遮罩后看见。
|
||||
- 验证:`frontend/tests/rectification-agentic-entry.test.ts`。
|
||||
- 防复发:窄屏覆盖层必须高于 composer;打开时 composer 不得再带 `backdrop-filter`。源码合同锁定 `isolation: isolate`、overlay `z-index: 50`、`visibility: hidden`。展开入口不得放在输入框上方,必须挂在页头右上角。
|
||||
- 相关记录:BUG-328、BUG-330、BUG-335
|
||||
- 复发自:BUG-330(覆盖层 z-index 未锁住 iOS backdrop-filter 合成层)
|
||||
- 修复版本:8b8e5214
|
||||
|
||||
## BUG-335 | 移动端当前盘面入口仍在输入框上方
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-08-20
|
||||
- 最近更新:2026-08-20
|
||||
- 影响面:生时纠正窄屏盘面入口、`.chat-header-actions`、`.rectification-board-peek`
|
||||
- 用户现象:点开盘面的按钮在输入框上方,占掉打字区;希望放到右上角。
|
||||
- 触发条件:窄屏生时纠正会话。
|
||||
- 根因:`RectificationBoardPeek` 渲染在 `.composer-wrap` 里,输入框上方整条通栏。
|
||||
- 修复:页头 `chat-header-actions` 增加挂载点;窄屏未打开盘面时把预览按钮 portal 到余额按钮左侧。输入框上方不再放展开入口。
|
||||
- 验证:`frontend/tests/rectification-agentic-entry.test.ts`。
|
||||
- 防复发:源码合同锁定 `createPortal`、`data-rectification-header-slot`,并禁止 composer 内出现 `RectificationBoardPeek`。
|
||||
- 相关记录:BUG-330、BUG-334
|
||||
- 复发自:BUG-330(窄屏入口一开始就放在输入区)
|
||||
- 修复版本:8b8e5214
|
||||
|
||||
## BUG-336 | 初始化对话列表与资料卡不在同一栏
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-08-20
|
||||
- 最近更新:2026-08-20
|
||||
- 影响面:首页 onboarding `.welcome`、出生资料卡、输入框
|
||||
- 用户现象:新用户初始化时,对话气泡和出生日期卡片左右对不齐;称呼阶段输入框也比上面的对话更窄。
|
||||
- 触发条件:未完成资料的新会话,尤其是填写出生日期与时间的步骤。
|
||||
- 根因:`.welcome` 被后续规则拉到 1040px 且无水平居中;资料卡 `max-width: 680/760px` 靠左。会话正文和输入框已经收成 760px 栏,初始化没有走同一套栏宽。
|
||||
- 修复:未完成资料时给会话加上 `is-onboarding`。欢迎区和资料卡使用 `--session-column-width`,与正式会话、输入框同一栏。
|
||||
- 验证:`frontend/tests/session-conversation-layout.test.ts`。
|
||||
- 防复发:源码合同锁定 `is-onboarding`、欢迎区栏宽和资料卡 `width: 100%; max-width: none`。
|
||||
- 相关记录:无
|
||||
- 复发自:无
|
||||
- 修复版本:8b8e5214
|
||||
|
||||
## BUG-329 | 生时纠正 Agent 回答在结算后一次性出现,推理中无法停止
|
||||
|
||||
- 状态:resolved
|
||||
|
||||
@@ -1763,8 +1763,24 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
.onboarding-card-heading b { font-size: clamp(22px, 2.6vw, 30px); letter-spacing: -.03em; }
|
||||
.onboarding-card-heading small { max-width: 260px; color: var(--color-ink-secondary); line-height: 1.55; font-size: var(--type-caption); text-wrap: pretty; }
|
||||
.birth-time-intake { gap: var(--space-6); }
|
||||
.conversation.is-onboarding .welcome {
|
||||
box-sizing: border-box;
|
||||
width: min(calc(var(--session-column-width) + (var(--session-column-gutter) * 2)), 100%);
|
||||
margin-inline: auto;
|
||||
}
|
||||
.conversation.is-onboarding.is-empty .welcome {
|
||||
width: min(var(--session-column-width), 100%);
|
||||
}
|
||||
.conversation.is-onboarding .onboarding-card,
|
||||
.conversation.is-onboarding .onboarding-card.birth-time-transition-card {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
margin-inline: 0;
|
||||
}
|
||||
.conversation.is-onboarding-form { align-content: start; padding-bottom: var(--space-8); }
|
||||
.conversation.is-onboarding-form .welcome { padding-top: var(--space-6); padding-bottom: var(--space-8); }
|
||||
.conversation.is-onboarding-form .welcome {
|
||||
padding: var(--space-6) var(--session-column-gutter) var(--space-8);
|
||||
}
|
||||
.conversation.is-onboarding-form .welcome > .onboarding-message:first-child { padding-bottom: var(--space-3); }
|
||||
.conversation.is-onboarding-form .welcome > .onboarding-message:first-child .message-bubble p {
|
||||
max-width: 680px;
|
||||
@@ -1876,6 +1892,8 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
|
||||
.rectification-workspace {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
isolation: isolate;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
@@ -1908,6 +1926,12 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.rectification-workspace.is-compact.is-board-open .rectification-workspace__chat .composer-wrap {
|
||||
z-index: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
.rectification-board {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
@@ -1920,10 +1944,9 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
.rectification-workspace.is-compact .rectification-board-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
z-index: 50;
|
||||
display: grid;
|
||||
align-items: end;
|
||||
isolation: isolate;
|
||||
}
|
||||
.rectification-board-scrim {
|
||||
position: absolute;
|
||||
@@ -1937,7 +1960,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 92%;
|
||||
height: 72%;
|
||||
max-height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
@@ -2102,6 +2125,33 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
color: var(--color-ink);
|
||||
font-size: var(--type-caption);
|
||||
}
|
||||
.chat-header-rectification:empty {
|
||||
display: none;
|
||||
}
|
||||
.chat-header-rectification .rectification-board-peek {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: auto;
|
||||
max-width: min(11.5rem, 42vw);
|
||||
min-height: 44px;
|
||||
margin: 0;
|
||||
padding: 0 var(--space-3);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.chat-header-rectification .rectification-board-peek strong,
|
||||
.chat-header-rectification .rectification-board-peek span {
|
||||
display: inline;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.chat-header-rectification .rectification-board-peek span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.rectification-board-peek {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -3559,6 +3559,7 @@ export default function Home() {
|
||||
<strong>{activeSession?.title || "新对话"}</strong>
|
||||
</div>
|
||||
<div className="chat-header-actions">
|
||||
<div className="chat-header-rectification" data-rectification-header-slot="" />
|
||||
<button className="credit-button" type="button" onClick={() => router.push(membershipHref("credits"))} aria-label={account ? `余额 ${account.credits} 点,会员与充值` : accountError || "读取余额中"}>
|
||||
<Sparkles className="credit-icon" aria-hidden="true" />
|
||||
<span>{account ? account.credits : "—"}</span>
|
||||
@@ -3567,7 +3568,7 @@ export default function Home() {
|
||||
</header>
|
||||
|
||||
{!rectificationSurfaceOpen && (
|
||||
<div ref={conversation} className={`conversation ${!activeSession?.messages.length && !onboardingFormActive ? "is-empty" : ""} ${onboardingFormActive ? "is-onboarding-form" : ""}`}>
|
||||
<div ref={conversation} className={`conversation${!activeSession?.messages.length && !onboardingFormActive ? " is-empty" : ""}${!profileComplete ? " is-onboarding" : ""}${onboardingFormActive ? " is-onboarding-form" : ""}`}>
|
||||
{!activeSession?.messages.length ? (
|
||||
<div className="welcome">
|
||||
{!profileComplete ? (
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { ArrowUp, Square } from "lucide-react";
|
||||
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { parseAgentReply } from "@/lib/agent-reply";
|
||||
import type { ChatMessage, ChatMessageView } from "@/lib/chat-message-view";
|
||||
import {
|
||||
@@ -262,9 +263,14 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
const [compactBoard, setCompactBoard] = useState(false);
|
||||
const [boardOpen, setBoardOpen] = useState(false);
|
||||
const [boardDiff, setBoardDiff] = useState(() => diffRectificationBoard(null, null));
|
||||
const [headerSlot, setHeaderSlot] = useState<HTMLElement | null>(null);
|
||||
const boardId = useId();
|
||||
const boardTitleId = useId();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setHeaderSlot(document.querySelector("[data-rectification-header-slot]"));
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const query = window.matchMedia(`(max-width: ${RECTIFICATION_BOARD_SPLIT_MIN_PX - 1}px)`);
|
||||
const update = () => {
|
||||
@@ -682,11 +688,21 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
: undefined;
|
||||
const canSend = !busy && !readonly && !regeneratingMessageKey;
|
||||
|
||||
const boardPeek = compactBoard && !boardOpen ? (
|
||||
<RectificationBoardPeek
|
||||
result={candidateResult}
|
||||
expanded={boardOpen}
|
||||
boardId={boardId}
|
||||
onOpen={toggleBoard}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={workspace}
|
||||
className={`rectification-workspace${compactBoard ? " is-compact" : ""}${boardOpen ? " is-board-open" : ""}`}
|
||||
>
|
||||
{headerSlot && boardPeek ? createPortal(boardPeek, headerSlot) : null}
|
||||
<div className="rectification-workspace__chat" inert={compactBoard && boardOpen ? true : undefined}>
|
||||
<section ref={conversation} className="conversation is-rectification" aria-label="生时校正对话" aria-busy={busy || regeneratingMessageKey !== null}>
|
||||
<div className="message-list">
|
||||
@@ -777,14 +793,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
</section>
|
||||
|
||||
<div className="composer-wrap">
|
||||
{compactBoard && (
|
||||
<RectificationBoardPeek
|
||||
result={candidateResult}
|
||||
expanded={boardOpen}
|
||||
boardId={boardId}
|
||||
onOpen={toggleBoard}
|
||||
/>
|
||||
)}
|
||||
<form className="composer" onSubmit={submit}>
|
||||
<Textarea
|
||||
ref={composer}
|
||||
|
||||
@@ -271,6 +271,7 @@ export function RectificationBoardPeek({
|
||||
<button
|
||||
className="rectification-board-peek"
|
||||
type="button"
|
||||
aria-label={peek.detail ? `${peek.title},${peek.detail}` : peek.title}
|
||||
aria-expanded={expanded}
|
||||
aria-controls={boardId}
|
||||
onClick={onOpen}
|
||||
|
||||
@@ -392,11 +392,17 @@ test("the natal house table is a live board beside the chat, not a message", ()
|
||||
|
||||
test("compact board overlays chat as a bottom sheet above the composer", () => {
|
||||
assert.doesNotMatch(styles, /\.rectification-workspace\.is-compact\.is-board-open \{[\s\S]*grid-template-rows: minmax\(8rem, 1fr\) minmax\(13rem, 46%\)/);
|
||||
assert.match(styles, /\.rectification-workspace \{[\s\S]*isolation: isolate/);
|
||||
assert.match(styles, /\.rectification-workspace \{[\s\S]*z-index: 0/);
|
||||
assert.match(styles, /\.rectification-workspace\.is-compact \.rectification-board-overlay \{[\s\S]*position: absolute/);
|
||||
assert.match(styles, /\.rectification-workspace\.is-compact \.rectification-board-overlay \{[\s\S]*inset: 0/);
|
||||
assert.match(styles, /\.rectification-workspace\.is-compact \.rectification-board-overlay \{[\s\S]*z-index: 20/);
|
||||
assert.match(styles, /\.rectification-workspace\.is-compact \.rectification-board-overlay \{[^}]*z-index: 50/);
|
||||
assert.doesNotMatch(styles, /\.rectification-workspace\.is-compact \.rectification-board-overlay \{[^}]*z-index: 20/);
|
||||
assert.match(styles, /\.rectification-workspace\.is-compact\.is-board-open \.rectification-workspace__chat \.composer-wrap \{[\s\S]*visibility: hidden/);
|
||||
assert.match(styles, /\.rectification-workspace\.is-compact\.is-board-open \.rectification-workspace__chat \.composer-wrap \{[\s\S]*backdrop-filter: none/);
|
||||
assert.match(styles, /\.rectification-workspace__chat \.composer-wrap \{[\s\S]*z-index: 2/);
|
||||
assert.match(styles, /\.rectification-board\.is-sheet \{[\s\S]*height: 92%/);
|
||||
assert.match(styles, /\.rectification-board\.is-sheet \{[^}]*height: 72%/);
|
||||
assert.doesNotMatch(styles, /\.rectification-board\.is-sheet \{[^}]*height: 92%/);
|
||||
assert.match(styles, /\.rectification-board\.is-sheet \{[\s\S]*background: var\(--color-canvas\)/);
|
||||
assert.match(styles, /\.rectification-board\.is-sheet \{[\s\S]*border-block-start: 1px solid var\(--color-border\)/);
|
||||
assert.match(styles, /@keyframes rectification-sheet-enter \{[\s\S]*translateY\(18%\)/);
|
||||
@@ -404,6 +410,14 @@ test("compact board overlays chat as a bottom sheet above the composer", () => {
|
||||
assert.match(styles, /\.rectification-board__close \{[\s\S]*width: 44px/);
|
||||
assert.match(styles, /\.rectification-board__close \{[\s\S]*min-height: 44px/);
|
||||
assert.match(board, /<X aria-hidden="true"/);
|
||||
assert.match(chat, /compactBoard && !boardOpen \? \(/);
|
||||
assert.match(chat, /createPortal\(/);
|
||||
assert.match(chat, /\[data-rectification-header-slot\]/);
|
||||
assert.match(page, /className="chat-header-rectification"/);
|
||||
assert.match(page, /data-rectification-header-slot=""/);
|
||||
assert.match(styles, /\.chat-header-rectification \.rectification-board-peek \{[\s\S]*width: auto/);
|
||||
assert.doesNotMatch(chat, /rectification-workspace__board-trigger/);
|
||||
assert.doesNotMatch(chat.slice(chat.indexOf("className=\"composer-wrap\""), chat.indexOf("<form className=\"composer\"")), /RectificationBoardPeek/);
|
||||
assert.match(chat, /className=\{`rectification-workspace\$\{compactBoard \? " is-compact" : ""\}\$\{boardOpen \? " is-board-open" : ""\}`\}/);
|
||||
});
|
||||
|
||||
|
||||
@@ -13,6 +13,15 @@ test("aligns the session transcript and composer to one readable column", () =>
|
||||
assert.match(globalStyles, /\.conversation:not\(\.is-empty\):not\(\.is-rectification\) \+ \.composer-wrap \.composer-footer[^\{]*\{[^}]*width:\s*min\(var\(--session-column-width\),\s*100%\)/);
|
||||
});
|
||||
|
||||
test("keeps onboarding transcript and intake card on the same session column", () => {
|
||||
const pageSource = readProjectFile("src/app/page.tsx");
|
||||
assert.match(pageSource, /\$\{!profileComplete \? " is-onboarding" : ""\}/);
|
||||
assert.match(globalStyles, /\.conversation\.is-onboarding \.welcome \{[\s\S]*width:\s*min\(calc\(var\(--session-column-width\) \+ \(var\(--session-column-gutter\) \* 2\)\), 100%\)/);
|
||||
assert.match(globalStyles, /\.conversation\.is-onboarding\.is-empty \.welcome \{[\s\S]*width:\s*min\(var\(--session-column-width\), 100%\)/);
|
||||
assert.match(globalStyles, /\.conversation\.is-onboarding \.onboarding-card,\s*\n\.conversation\.is-onboarding \.onboarding-card\.birth-time-transition-card \{[\s\S]*width:\s*100%;[\s\S]*max-width:\s*none/);
|
||||
assert.match(globalStyles, /\.conversation\.is-onboarding-form \.welcome \{[\s\S]*padding:\s*var\(--space-6\) var\(--session-column-gutter\) var\(--space-8\)/);
|
||||
});
|
||||
|
||||
test("keeps message motion restrained and honors reduced-motion preferences", () => {
|
||||
assert.match(messageRowSource, /gsap\.matchMedia\(\)/);
|
||||
assert.match(messageRowSource, /prefers-reduced-motion:\s*no-preference/);
|
||||
|
||||
Reference in New Issue
Block a user