fix(core): ai chat panel scrolling dizziness problem (#10458)

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 <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/sJGviKxfE3Ap685cl5bj/624ea4fa-b8dd-4cf2-a9be-6997bdabc97b.mov" />](https://app.graphite.dev/media/video/sJGviKxfE3Ap685cl5bj/624ea4fa-b8dd-4cf2-a9be-6997bdabc97b.mov)
This commit is contained in:
akumatus
2025-02-26 23:59:12 +00:00
parent 2c79d7229f
commit 903d260880
3 changed files with 41 additions and 11 deletions

View File

@@ -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) {
</div> `
: repeat(
filteredItems,
item => (isChatMessage(item) ? item.id : item.sessionId),
(_, index) => index,
(item, index) => {
const isLast = index === filteredItems.length - 1;
return html`<div class="message">

View File

@@ -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');

View File

@@ -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' : ''};
}
</style>
<div class=${classes} @wheel=${this._onWheel}>
<div class=${classes}>
${keyed(
this._doc,
html`<div class="ai-answer-text-editor affine-page-viewport">
@@ -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;
});
}