From 34395e902c402efcb824cbe597ecf1f42e331128 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Sun, 9 Aug 2026 01:19:39 +0800 Subject: [PATCH] fix(chat): follow active response at bottom --- frontend/src/app/globals.css | 4 ++-- frontend/src/app/page.tsx | 10 +++++----- .../src/components/rectification-agentic-chat.tsx | 13 +++++++------ frontend/tests/consultation-entrypoint.test.ts | 6 ++++++ frontend/tests/rectification-agentic-entry.test.ts | 9 +++++++++ frontend/tests/starter-questions.test.ts | 5 ++++- 6 files changed, 33 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index dac5b2f8..47d6ca55 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -557,7 +557,7 @@ button:disabled { cursor: default; opacity: .45; } @media (hover: hover) { .product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover) { border-color: color-mix(in srgb, var(--color-action) 44%, var(--color-border)); transform: translateY(-1px); } } -.product-entrypoint-card:has(.product-entrypoint-hitarea:disabled) { opacity: .65; } +.product-entrypoint-card:has(.product-entrypoint-hitarea:disabled) { opacity: 1; } .product-entrypoint-hitarea:disabled { cursor: not-allowed; } .daily-starlanguage-heading { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); } .daily-starlanguage-heading > span { color: var(--color-action); font-size: var(--type-overline); font-weight: 600; letter-spacing: 1.5px; } @@ -1167,7 +1167,7 @@ input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled } .product-entrypoint-card:has(.product-entrypoint-hitarea:disabled) { - opacity: .65; + opacity: 1; } .product-entrypoint-hitarea:disabled { diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 84aa845d..de868931 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1032,7 +1032,7 @@ export default function Home() { const [birthTimeAssessmentPhase, setBirthTimeAssessmentPhase] = useState(null); const [startGreeting, setStartGreeting] = useState(""); const [presetMessageLength, setPresetMessageLength] = useState(0); - const conversationEnd = useRef(null); + const conversation = useRef(null); const accountTrigger = useRef(null); const accountDialog = useRef(null); const dialogReturnTarget = useRef(null); @@ -1697,8 +1697,10 @@ export default function Home() { useEffect(() => { if (starterHomeVisible) return; + const container = conversation.current; + if (!container) return; const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; - conversationEnd.current?.scrollIntoView({ behavior: isLoading || reduceMotion ? "auto" : "smooth", block: "end" }); + container.scrollTo({ top: container.scrollHeight, behavior: isLoading || reduceMotion ? "auto" : "smooth" }); }, [activeSessionId, activeSession?.messages.length, activeStreamingText, isLoading, onboardingPending, onboardingStep, presetMessageFinished, profileComplete, starterHomeVisible]); useEffect(() => { @@ -3127,7 +3129,7 @@ export default function Home() { {!rectificationSurfaceOpen && ( -
+
{!activeSession?.messages.length ? (
{!profileComplete ? ( @@ -3299,7 +3301,6 @@ export default function Home() { {onboardingError &&

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

}
))} -
) : (
@@ -3308,7 +3309,6 @@ export default function Home() { ))} {activeError &&

{activeError}

} -
)}
diff --git a/frontend/src/components/rectification-agentic-chat.tsx b/frontend/src/components/rectification-agentic-chat.tsx index 5cd1c7ff..afd052c1 100644 --- a/frontend/src/components/rectification-agentic-chat.tsx +++ b/frontend/src/components/rectification-agentic-chat.tsx @@ -83,7 +83,7 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) { const [acceptingTime, setAcceptingTime] = useState(null); const [suggestions, setSuggestions] = useState([]); const composer = useRef(null); - const conversationEnd = useRef(null); + const conversation = useRef(null); const keyCounter = useRef(0); const openingStarted = useRef(false); @@ -93,12 +93,14 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) { }, [onPendingChange]); useEffect(() => { + const container = conversation.current; + if (!container) return; const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; - conversationEnd.current?.scrollIntoView({ + container.scrollTo({ + top: container.scrollHeight, behavior: busy || reduceMotion ? "auto" : "smooth", - block: "end", }); - }, [busy, error, messages.length, savedTime]); + }, [busy, error, messages, savedTime]); const send = useCallback(async (request: AgenticRectificationRequest, showUserMessage = true) => { const trimmed = request.action === "message" ? request.message.trim() : ""; @@ -301,7 +303,7 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) { return ( <> -
+
{messages.map((message) => )} {candidateResult?.selectionAllowed && candidateResult.candidates.length > 0 && ( @@ -344,7 +346,6 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) {

)} {error &&

{error}

} -
diff --git a/frontend/tests/consultation-entrypoint.test.ts b/frontend/tests/consultation-entrypoint.test.ts index 31625584..4cede283 100644 --- a/frontend/tests/consultation-entrypoint.test.ts +++ b/frontend/tests/consultation-entrypoint.test.ts @@ -270,6 +270,12 @@ test("starter cards use transient pressed feedback instead of sticky hover shadi assert.equal(starterHoverSurfaceRules.length, 0); assert.match(styles, /\.product-entrypoint-card:has\(\.product-entrypoint-hitarea:not\(:disabled\):active\)\s*\{[^}]*background\s*:/); assert.match(styles, /\.starter-theme-card:not\(:disabled\):active\s*\{[^}]*background\s*:/); + + const disabledProductRules = [...styles.matchAll( + /\.product-entrypoint-card:has\(\.product-entrypoint-hitarea:disabled\)\s*\{([^}]*)\}/g, + )]; + assert.equal(disabledProductRules.length, 2); + for (const rule of disabledProductRules) assert.match(rule[1], /opacity:\s*1\s*;/); }); test("starter homepage stays editorial and hides technical chart parameters", () => { diff --git a/frontend/tests/rectification-agentic-entry.test.ts b/frontend/tests/rectification-agentic-entry.test.ts index e5424a7b..f621739c 100644 --- a/frontend/tests/rectification-agentic-entry.test.ts +++ b/frontend/tests/rectification-agentic-entry.test.ts @@ -156,6 +156,15 @@ test("Agentic rectification uses one case-level entitlement and the session-pinn assert.doesNotMatch(route, /loadLanguageModelCatalog|resolveLanguageModelFromCatalog|\bresolveLanguageModel\(|\bdefaultLanguageModel\(/); }); +test("Agentic rectification scrolls the conversation container as streamed messages grow", () => { + assert.match(chat, /const conversation = useRef\(null\)/); + assert.match(chat, /
{ assert.match(route, /send\(\{ type: "candidates", result: candidateResult \}\)[\s\S]*send\(\{ type: "done", emitted: true \}\)/); assert.match(chat, /fetch\(`\/api\/rectification\/agent\?sessionId=/); diff --git a/frontend/tests/starter-questions.test.ts b/frontend/tests/starter-questions.test.ts index 02e02e5d..25d705ca 100644 --- a/frontend/tests/starter-questions.test.ts +++ b/frontend/tests/starter-questions.test.ts @@ -165,7 +165,10 @@ test("keeps the empty starter home at the top instead of auto-scrolling", () => ); assert.match(autoScrollEffect, /if \(starterHomeVisible\) return/); - assert.match(autoScrollEffect, /conversationEnd\.current\?\.scrollIntoView/); + assert.match(autoScrollEffect, /const container = conversation\.current/); + assert.match(autoScrollEffect, /container\.scrollTo\(\{ top: container\.scrollHeight/); + assert.match(pageSource, /ref=\{conversation\} className=\{`conversation/); + assert.doesNotMatch(pageSource, /conversationEnd|scrollIntoView/); }); test("does not submit the composer while an IME composition is active", () => {