From 903d26088080382e1ae4ed613b08de2fe992aed7 Mon Sep 17 00:00:00 2001 From: akumatus Date: Wed, 26 Feb 2025 23:59:12 +0000 Subject: [PATCH] fix(core): ai chat panel scrolling dizziness problem (#10458) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix issue [AF-2281](https://linear.app/affine-design/issue/AF-2281). ### What Changed? - During the re-rendering process of the rich-text editor, the container height is always expanded. - If the user manually scrolls the chat panel, immediately stop automatically scrolling [录屏2025-02-27 07.30.08.mov (uploaded via Graphite) ](https://app.graphite.dev/media/video/sJGviKxfE3Ap685cl5bj/624ea4fa-b8dd-4cf2-a9be-6997bdabc97b.mov) --- .../ai/chat-panel/chat-panel-messages.ts | 6 ++++- .../src/blocksuite/ai/chat-panel/index.ts | 26 +++++++++++++++++-- .../blocksuite/ai/components/text-renderer.ts | 20 ++++++++------ 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-messages.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-messages.ts index 32ace3aab8..c54872b038 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-messages.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-messages.ts @@ -152,6 +152,10 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) { @query('.chat-panel-messages') accessor messagesContainer: HTMLDivElement | null = null; + getScrollContainer(): HTMLDivElement | null { + return this.messagesContainer; + } + private _renderAIOnboarding() { return this.isLoading || !this.host?.doc.get(FeatureFlagService).getFlag('enable_ai_onboarding') @@ -251,7 +255,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) { ` : repeat( filteredItems, - item => (isChatMessage(item) ? item.id : item.sessionId), + (_, index) => index, (item, index) => { const isLast = index === filteredItems.length - 1; return html`
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts index fef11066c9..96778a3302 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts @@ -129,6 +129,8 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) { // request counter to track the latest request private _updateHistoryCounter = 0; + private _wheelTriggered = false; + private readonly _updateHistory = async () => { const { doc } = this; this.isLoading = true; @@ -261,10 +263,12 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) { private _chatContextId: string | null | undefined = null; private readonly _scrollToEnd = () => { - this._chatMessages.value?.scrollToEnd(); + if (!this._wheelTriggered) { + this._chatMessages.value?.scrollToEnd(); + } }; - private readonly _throttledScrollToEnd = throttle(this._scrollToEnd, 1000); + private readonly _throttledScrollToEnd = throttle(this._scrollToEnd, 600); private readonly _cleanupHistories = async () => { const notification = this.host.std.getOptional(NotificationProvider); @@ -324,6 +328,11 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) { }); } + if (this.chatContextValue.status === 'loading') { + // reset the wheel triggered flag when the status is loading + this._wheelTriggered = false; + } + if ( _changedProperties.has('chatContextValue') && (this.chatContextValue.status === 'loading' || @@ -341,6 +350,19 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) { } } + protected override firstUpdated(): void { + const chatMessages = this._chatMessages.value; + if (chatMessages) { + chatMessages.updateComplete + .then(() => { + chatMessages.getScrollContainer()?.addEventListener('wheel', () => { + this._wheelTriggered = true; + }); + }) + .catch(console.error); + } + } + override connectedCallback() { super.connectedCallback(); if (!this.doc) throw new Error('doc is required'); diff --git a/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts b/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts index e632a60595..094d7a3132 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts @@ -185,6 +185,8 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) { private _answers: string[] = []; + private _maxContainerHeight = 0; + private readonly _clearTimer = () => { if (this._timer) { clearInterval(this._timer); @@ -256,13 +258,6 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) { } }; - private _onWheel(e: MouseEvent) { - e.stopPropagation(); - if (this.state === 'generating') { - e.preventDefault(); - } - } - override connectedCallback() { super.connectedCallback(); this._answers.push(this.answer); @@ -301,7 +296,7 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) { max-height: ${maxHeight ? Math.max(maxHeight, 200) + 'px' : ''}; } -
+
${keyed( this._doc, html`
@@ -328,6 +323,15 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) { super.updated(changedProperties); requestAnimationFrame(() => { if (!this._container) return; + // Track max height during generation + if (this.state === 'generating') { + this._maxContainerHeight = Math.max( + this._maxContainerHeight, + this._container.scrollHeight + ); + // Apply min-height to prevent shrinking + this._container.style.minHeight = `${this._maxContainerHeight}px`; + } this._container.scrollTop = this._container.scrollHeight; }); }