From da1f7b527f09b6d163b9ed5691123a917ad818b7 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Tue, 18 Feb 2025 11:41:04 +0700 Subject: [PATCH 1/2] Fix scrolling as new AI Assistant response comes in --- .../app/components/ai-assistant/message/index.gts | 13 +++++++++++-- packages/host/app/components/matrix/room.gts | 9 +++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/host/app/components/ai-assistant/message/index.gts b/packages/host/app/components/ai-assistant/message/index.gts index 505fcf4be1..bbdf54fba8 100644 --- a/packages/host/app/components/ai-assistant/message/index.gts +++ b/packages/host/app/components/ai-assistant/message/index.gts @@ -109,7 +109,10 @@ interface ScrollPositionSignature { Args: { Named: { setScrollPosition: (args: { isBottom: boolean }) => void; - registerConversationScroller: (isScrollable: () => boolean) => void; + registerConversationScroller: ( + isScrollable: () => boolean, + scrollToBottom: () => void, + ) => void; }; }; } @@ -131,6 +134,9 @@ class ScrollPosition extends Modifier { this.hasRegistered = true; registerConversationScroller( () => element.scrollHeight > element.clientHeight, + () => { + element.scrollTop = element.scrollHeight - element.clientHeight; + }, ); } @@ -407,7 +413,10 @@ interface AiAssistantConversationSignature { Element: HTMLDivElement; Args: { setScrollPosition: (args: { isBottom: boolean }) => void; - registerConversationScroller: (isScrollable: () => boolean) => void; + registerConversationScroller: ( + isScrollable: () => boolean, + scrollToBottom: () => void, + ) => void; }; Blocks: { default: []; diff --git a/packages/host/app/components/matrix/room.gts b/packages/host/app/components/matrix/room.gts index c78bd2546e..a9059ec0e3 100644 --- a/packages/host/app/components/matrix/room.gts +++ b/packages/host/app/components/matrix/room.gts @@ -228,6 +228,7 @@ export default class Room extends Component { @tracked private currentMonacoContainer: number | undefined; private getConversationScrollability: (() => boolean) | undefined; + private scrollConversationToBottom: (() => void) | undefined; private roomScrollState: WeakMap< RoomData, { @@ -352,7 +353,7 @@ export default class Room extends Component { }: { index: number; element: HTMLElement; - scrollTo: () => void; + scrollTo: (arg?: any) => void; }) => { this.messageElements.set(element, index); this.messageScrollers.set(index, scrollTo); @@ -372,7 +373,7 @@ export default class Room extends Component { !this.hasUnreadMessages && index === this.messages.length - 1 ) { - scrollTo(); + scrollTo({ block: 'end' }); } else if ( // otherwise if we are permitted to auto-scroll and if there are unread // messages in the room, then scroll to the first unread message in the room. @@ -380,13 +381,17 @@ export default class Room extends Component { index === this.lastReadMessageIndex + 1 ) { scrollTo(); + } else { + this.scrollConversationToBottom?.(); } }; private registerConversationScroller = ( isConversationScrollable: () => boolean, + scrollToBottom: () => void, ) => { this.getConversationScrollability = isConversationScrollable; + this.scrollConversationToBottom = scrollToBottom; }; private setScrollPosition = ({ isBottom }: { isBottom: boolean }) => { From 4e54dc4acf5569aab1d4fa993a615fe5b3ba30d9 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Tue, 18 Feb 2025 15:55:39 +0700 Subject: [PATCH 2/2] Fix host tests --- .../components/ai-assistant/message/index.gts | 16 ++++++ packages/host/app/components/matrix/room.gts | 4 +- .../components/ai-assistant-panel-test.gts | 49 +++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/packages/host/app/components/ai-assistant/message/index.gts b/packages/host/app/components/ai-assistant/message/index.gts index bbdf54fba8..6d443f898c 100644 --- a/packages/host/app/components/ai-assistant/message/index.gts +++ b/packages/host/app/components/ai-assistant/message/index.gts @@ -89,6 +89,7 @@ interface MessageScrollerSignature { class MessageScroller extends Modifier { private hasRegistered = false; + private observer?: MutationObserver; modify( element: HTMLElement, _positional: [], @@ -102,6 +103,21 @@ class MessageScroller extends Modifier { scrollTo: element.scrollIntoView.bind(element), }); } + + this.observer?.disconnect(); + + this.observer = new MutationObserver(() => { + registerScroller({ + index, + element, + scrollTo: element.scrollIntoView.bind(element), + }); + }); + this.observer.observe(element, { childList: true, subtree: true }); + + registerDestructor(this, () => { + this.observer?.disconnect(); + }); } } diff --git a/packages/host/app/components/matrix/room.gts b/packages/host/app/components/matrix/room.gts index a9059ec0e3..3ccde8d910 100644 --- a/packages/host/app/components/matrix/room.gts +++ b/packages/host/app/components/matrix/room.gts @@ -373,7 +373,7 @@ export default class Room extends Component { !this.hasUnreadMessages && index === this.messages.length - 1 ) { - scrollTo({ block: 'end' }); + scrollTo(); } else if ( // otherwise if we are permitted to auto-scroll and if there are unread // messages in the room, then scroll to the first unread message in the room. @@ -381,7 +381,7 @@ export default class Room extends Component { index === this.lastReadMessageIndex + 1 ) { scrollTo(); - } else { + } else if (this.isScrolledToBottom) { this.scrollConversationToBottom?.(); } }; diff --git a/packages/host/tests/integration/components/ai-assistant-panel-test.gts b/packages/host/tests/integration/components/ai-assistant-panel-test.gts index 8d2ab8a763..3da2d411c7 100644 --- a/packages/host/tests/integration/components/ai-assistant-panel-test.gts +++ b/packages/host/tests/integration/components/ai-assistant-panel-test.gts @@ -1609,6 +1609,55 @@ module('Integration | ai-assistant-panel', function (hooks) { ); }); + test('scrolling stays at the bottom if a message is streaming in', async function (assert) { + await setCardInOperatorModeState(`${testRealmURL}Person/fadhlan`); + await renderComponent( + class TestDriver extends GlimmerComponent { + + }, + ); + await waitFor('[data-test-person="Fadhlan"]'); + let roomId = createAndJoinRoom('@testuser:staging', 'test room 1'); + fillRoomWithReadMessages(roomId); + await settled(); + await click('[data-test-open-ai-assistant]'); + await waitFor('[data-test-message-idx="39"]'); + assert.ok( + isAiAssistantScrolledToBottom(), + 'AI assistant is scrolled to bottom', + ); + + let eventId = simulateRemoteMessage(roomId, '@aibot:localhost', { + body: `thinking...`, + msgtype: 'm.text', + formatted_body: `thinking...`, + format: 'org.matrix.custom.html', + isStreamingFinished: false, + }); + assert.ok( + isAiAssistantScrolledToBottom(), + 'AI assistant is scrolled to bottom', + ); + simulateRemoteMessage(roomId, '@aibot:localhost', { + body: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`, + msgtype: 'm.text', + formatted_body: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`, + format: 'org.matrix.custom.html', + isStreamingFinished: true, + ['m.relates_to']: { + rel_type: 'm.replace', + event_id: eventId, + }, + }); + assert.ok( + isAiAssistantScrolledToBottom(), + 'AI assistant is scrolled to bottom', + ); + }); + test('sends read receipts only for bot messages', async function (assert) { let roomId = await renderAiAssistantPanel();