mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
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:
@@ -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">
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user