mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 23:07:02 +08: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')
|
@query('.chat-panel-messages')
|
||||||
accessor messagesContainer: HTMLDivElement | null = null;
|
accessor messagesContainer: HTMLDivElement | null = null;
|
||||||
|
|
||||||
|
getScrollContainer(): HTMLDivElement | null {
|
||||||
|
return this.messagesContainer;
|
||||||
|
}
|
||||||
|
|
||||||
private _renderAIOnboarding() {
|
private _renderAIOnboarding() {
|
||||||
return this.isLoading ||
|
return this.isLoading ||
|
||||||
!this.host?.doc.get(FeatureFlagService).getFlag('enable_ai_onboarding')
|
!this.host?.doc.get(FeatureFlagService).getFlag('enable_ai_onboarding')
|
||||||
@@ -251,7 +255,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
</div> `
|
</div> `
|
||||||
: repeat(
|
: repeat(
|
||||||
filteredItems,
|
filteredItems,
|
||||||
item => (isChatMessage(item) ? item.id : item.sessionId),
|
(_, index) => index,
|
||||||
(item, index) => {
|
(item, index) => {
|
||||||
const isLast = index === filteredItems.length - 1;
|
const isLast = index === filteredItems.length - 1;
|
||||||
return html`<div class="message">
|
return html`<div class="message">
|
||||||
|
|||||||
@@ -129,6 +129,8 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
|
|||||||
// request counter to track the latest request
|
// request counter to track the latest request
|
||||||
private _updateHistoryCounter = 0;
|
private _updateHistoryCounter = 0;
|
||||||
|
|
||||||
|
private _wheelTriggered = false;
|
||||||
|
|
||||||
private readonly _updateHistory = async () => {
|
private readonly _updateHistory = async () => {
|
||||||
const { doc } = this;
|
const { doc } = this;
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
@@ -261,10 +263,12 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
|
|||||||
private _chatContextId: string | null | undefined = null;
|
private _chatContextId: string | null | undefined = null;
|
||||||
|
|
||||||
private readonly _scrollToEnd = () => {
|
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 () => {
|
private readonly _cleanupHistories = async () => {
|
||||||
const notification = this.host.std.getOptional(NotificationProvider);
|
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 (
|
if (
|
||||||
_changedProperties.has('chatContextValue') &&
|
_changedProperties.has('chatContextValue') &&
|
||||||
(this.chatContextValue.status === 'loading' ||
|
(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() {
|
override connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
if (!this.doc) throw new Error('doc is required');
|
if (!this.doc) throw new Error('doc is required');
|
||||||
|
|||||||
@@ -185,6 +185,8 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
|
|||||||
|
|
||||||
private _answers: string[] = [];
|
private _answers: string[] = [];
|
||||||
|
|
||||||
|
private _maxContainerHeight = 0;
|
||||||
|
|
||||||
private readonly _clearTimer = () => {
|
private readonly _clearTimer = () => {
|
||||||
if (this._timer) {
|
if (this._timer) {
|
||||||
clearInterval(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() {
|
override connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this._answers.push(this.answer);
|
this._answers.push(this.answer);
|
||||||
@@ -301,7 +296,7 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
|
|||||||
max-height: ${maxHeight ? Math.max(maxHeight, 200) + 'px' : ''};
|
max-height: ${maxHeight ? Math.max(maxHeight, 200) + 'px' : ''};
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class=${classes} @wheel=${this._onWheel}>
|
<div class=${classes}>
|
||||||
${keyed(
|
${keyed(
|
||||||
this._doc,
|
this._doc,
|
||||||
html`<div class="ai-answer-text-editor affine-page-viewport">
|
html`<div class="ai-answer-text-editor affine-page-viewport">
|
||||||
@@ -328,6 +323,15 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
|
|||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (!this._container) return;
|
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;
|
this._container.scrollTop = this._container.scrollHeight;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user