fix(chat): follow active response at bottom
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -1032,7 +1032,7 @@ export default function Home() {
|
||||
const [birthTimeAssessmentPhase, setBirthTimeAssessmentPhase] = useState<BirthTimeAssessmentPhase | null>(null);
|
||||
const [startGreeting, setStartGreeting] = useState("");
|
||||
const [presetMessageLength, setPresetMessageLength] = useState(0);
|
||||
const conversationEnd = useRef<HTMLDivElement>(null);
|
||||
const conversation = useRef<HTMLDivElement>(null);
|
||||
const accountTrigger = useRef<HTMLButtonElement>(null);
|
||||
const accountDialog = useRef<HTMLElement>(null);
|
||||
const dialogReturnTarget = useRef<HTMLButtonElement | null>(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() {
|
||||
</header>
|
||||
|
||||
{!rectificationSurfaceOpen && (
|
||||
<div className={`conversation ${activeSession?.messages.length ? "" : "is-empty"}`}>
|
||||
<div ref={conversation} className={`conversation ${activeSession?.messages.length ? "" : "is-empty"}`}>
|
||||
{!activeSession?.messages.length ? (
|
||||
<div className="welcome">
|
||||
{!profileComplete ? (
|
||||
@@ -3299,7 +3301,6 @@ export default function Home() {
|
||||
{onboardingError && <p className="starter-note">个性化问题暂时不可用,已显示安全的默认问题。</p>}
|
||||
</div>
|
||||
))}
|
||||
<div ref={conversationEnd} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="message-list" aria-busy={isLoading}>
|
||||
@@ -3308,7 +3309,6 @@ export default function Home() {
|
||||
<ChatMessageRow key={message.renderKey} message={message} />
|
||||
))}
|
||||
{activeError && <p className="error-message" role="alert">{activeError}</p>}
|
||||
<div ref={conversationEnd} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -83,7 +83,7 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) {
|
||||
const [acceptingTime, setAcceptingTime] = useState<string | null>(null);
|
||||
const [suggestions, setSuggestions] = useState<string[]>([]);
|
||||
const composer = useRef<HTMLTextAreaElement>(null);
|
||||
const conversationEnd = useRef<HTMLDivElement>(null);
|
||||
const conversation = useRef<HTMLElement>(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 (
|
||||
<>
|
||||
<section className="conversation" aria-label="生时校正对话" aria-busy={busy}>
|
||||
<section ref={conversation} className="conversation" aria-label="生时校正对话" aria-busy={busy}>
|
||||
<div className="message-list" aria-live="polite">
|
||||
{messages.map((message) => <ChatMessageRow key={message.renderKey} message={message} />)}
|
||||
{candidateResult?.selectionAllowed && candidateResult.candidates.length > 0 && (
|
||||
@@ -344,7 +346,6 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) {
|
||||
</p>
|
||||
)}
|
||||
{error && <p className="error-message" role="alert">{error}</p>}
|
||||
<div ref={conversationEnd} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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<HTMLElement>\(null\)/);
|
||||
assert.match(chat, /<section ref=\{conversation\} className="conversation"/);
|
||||
assert.match(chat, /const container = conversation\.current/);
|
||||
assert.match(chat, /top: container\.scrollHeight/);
|
||||
assert.match(chat, /\}, \[busy, error, messages, savedTime\]\);/);
|
||||
assert.doesNotMatch(chat, /conversationEnd|scrollIntoView/);
|
||||
});
|
||||
|
||||
test("candidate state streams before done and renders reusable multi-column choices", () => {
|
||||
assert.match(route, /send\(\{ type: "candidates", result: candidateResult \}\)[\s\S]*send\(\{ type: "done", emitted: true \}\)/);
|
||||
assert.match(chat, /fetch\(`\/api\/rectification\/agent\?sessionId=/);
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user