diff --git a/static/src/components/chat/ChatArea.vue b/static/src/components/chat/ChatArea.vue index 4c5fd13..39a0994 100644 --- a/static/src/components/chat/ChatArea.vue +++ b/static/src/components/chat/ChatArea.vue @@ -790,9 +790,9 @@ const { stopScroll } = useStickToBottom({ resize: { - damping: 0.68, - stiffness: 0.11, - mass: 1.05 + damping: 0.5, + stiffness: 0.28, + mass: 0.75 }, initial: 'instant' }); @@ -809,6 +809,7 @@ let lastUserWheelUpTs = 0; let lastProgrammaticHintTs = 0; let lastProgrammaticHintSource = ''; let suppressUserIntentUntil = 0; +let isHandlingLargeGrowth = false; let scrollListener: ((event: Event) => void) | null = null; let wheelListener: ((event: WheelEvent) => void) | null = null; let traceAttachLogged = false; @@ -924,6 +925,32 @@ function attachBounceListener() { const heightDelta = height - lastObservedHeight; lastObservedTop = top; lastObservedHeight = height; + + // 大幅高度增长检测:当 scrollHeight 突增(内容大幅展开/渲染), + // 且用户没有主动脱离锁定(escapedFromLock=false)时, + // 用 instant 瞬间追底,绕过 spring 动画的延迟。 + // 这解决了思考块展开、Markdown 表格渲染等场景下滚动跟不上内容增长的问题。 + const largeGrowthThreshold = Math.max(200, target.clientHeight * 0.35 || 200); + if ( + !isHandlingLargeGrowth && + heightDelta > largeGrowthThreshold && + !escapedFromLock.value + ) { + isHandlingLargeGrowth = true; + markProgrammaticHint('ChatArea.largeGrowth'); + suppressUserIntentUntil = Date.now() + 900; + scrollToBottom({ animation: 'instant', preserveScrollPosition: false }); + bounceTraceLog( + 'large-growth:instant-scroll', + { heightDelta, threshold: largeGrowthThreshold, ...getScrollMetrics(target) }, + 'large-growth:instant-scroll', + 120 + ); + setTimeout(() => { + isHandlingLargeGrowth = false; + }, 400); + } + const now = Date.now(); const trusted = (event as any).isTrusted === true; const closeToProgrammaticHint = now - lastProgrammaticHintTs <= 140;