Skip to content

Commit f055ebb

Browse files
authored
Fix scrolling as new AI Assistant response comes in (#2159)
1 parent 07cd5c0 commit f055ebb

File tree

3 files changed

+82
-3
lines changed

3 files changed

+82
-3
lines changed

Diff for: packages/host/app/components/ai-assistant/message/index.gts

+27-2
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ interface MessageScrollerSignature {
8989

9090
class MessageScroller extends Modifier<MessageScrollerSignature> {
9191
private hasRegistered = false;
92+
private observer?: MutationObserver;
9293
modify(
9394
element: HTMLElement,
9495
_positional: [],
@@ -102,14 +103,32 @@ class MessageScroller extends Modifier<MessageScrollerSignature> {
102103
scrollTo: element.scrollIntoView.bind(element),
103104
});
104105
}
106+
107+
this.observer?.disconnect();
108+
109+
this.observer = new MutationObserver(() => {
110+
registerScroller({
111+
index,
112+
element,
113+
scrollTo: element.scrollIntoView.bind(element),
114+
});
115+
});
116+
this.observer.observe(element, { childList: true, subtree: true });
117+
118+
registerDestructor(this, () => {
119+
this.observer?.disconnect();
120+
});
105121
}
106122
}
107123

108124
interface ScrollPositionSignature {
109125
Args: {
110126
Named: {
111127
setScrollPosition: (args: { isBottom: boolean }) => void;
112-
registerConversationScroller: (isScrollable: () => boolean) => void;
128+
registerConversationScroller: (
129+
isScrollable: () => boolean,
130+
scrollToBottom: () => void,
131+
) => void;
113132
};
114133
};
115134
}
@@ -131,6 +150,9 @@ class ScrollPosition extends Modifier<ScrollPositionSignature> {
131150
this.hasRegistered = true;
132151
registerConversationScroller(
133152
() => element.scrollHeight > element.clientHeight,
153+
() => {
154+
element.scrollTop = element.scrollHeight - element.clientHeight;
155+
},
134156
);
135157
}
136158

@@ -407,7 +429,10 @@ interface AiAssistantConversationSignature {
407429
Element: HTMLDivElement;
408430
Args: {
409431
setScrollPosition: (args: { isBottom: boolean }) => void;
410-
registerConversationScroller: (isScrollable: () => boolean) => void;
432+
registerConversationScroller: (
433+
isScrollable: () => boolean,
434+
scrollToBottom: () => void,
435+
) => void;
411436
};
412437
Blocks: {
413438
default: [];

Diff for: packages/host/app/components/matrix/room.gts

+6-1
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ export default class Room extends Component<Signature> {
228228

229229
@tracked private currentMonacoContainer: number | undefined;
230230
private getConversationScrollability: (() => boolean) | undefined;
231+
private scrollConversationToBottom: (() => void) | undefined;
231232
private roomScrollState: WeakMap<
232233
RoomData,
233234
{
@@ -352,7 +353,7 @@ export default class Room extends Component<Signature> {
352353
}: {
353354
index: number;
354355
element: HTMLElement;
355-
scrollTo: () => void;
356+
scrollTo: (arg?: any) => void;
356357
}) => {
357358
this.messageElements.set(element, index);
358359
this.messageScrollers.set(index, scrollTo);
@@ -380,13 +381,17 @@ export default class Room extends Component<Signature> {
380381
index === this.lastReadMessageIndex + 1
381382
) {
382383
scrollTo();
384+
} else if (this.isScrolledToBottom) {
385+
this.scrollConversationToBottom?.();
383386
}
384387
};
385388

386389
private registerConversationScroller = (
387390
isConversationScrollable: () => boolean,
391+
scrollToBottom: () => void,
388392
) => {
389393
this.getConversationScrollability = isConversationScrollable;
394+
this.scrollConversationToBottom = scrollToBottom;
390395
};
391396

392397
private setScrollPosition = ({ isBottom }: { isBottom: boolean }) => {

Diff for: packages/host/tests/integration/components/ai-assistant-panel-test.gts

+49
Original file line numberDiff line numberDiff line change
@@ -1609,6 +1609,55 @@ module('Integration | ai-assistant-panel', function (hooks) {
16091609
);
16101610
});
16111611

1612+
test('scrolling stays at the bottom if a message is streaming in', async function (assert) {
1613+
await setCardInOperatorModeState(`${testRealmURL}Person/fadhlan`);
1614+
await renderComponent(
1615+
class TestDriver extends GlimmerComponent {
1616+
<template>
1617+
<OperatorMode @onClose={{noop}} />
1618+
<CardPrerender />
1619+
</template>
1620+
},
1621+
);
1622+
await waitFor('[data-test-person="Fadhlan"]');
1623+
let roomId = createAndJoinRoom('@testuser:staging', 'test room 1');
1624+
fillRoomWithReadMessages(roomId);
1625+
await settled();
1626+
await click('[data-test-open-ai-assistant]');
1627+
await waitFor('[data-test-message-idx="39"]');
1628+
assert.ok(
1629+
isAiAssistantScrolledToBottom(),
1630+
'AI assistant is scrolled to bottom',
1631+
);
1632+
1633+
let eventId = simulateRemoteMessage(roomId, '@aibot:localhost', {
1634+
body: `thinking...`,
1635+
msgtype: 'm.text',
1636+
formatted_body: `thinking...`,
1637+
format: 'org.matrix.custom.html',
1638+
isStreamingFinished: false,
1639+
});
1640+
assert.ok(
1641+
isAiAssistantScrolledToBottom(),
1642+
'AI assistant is scrolled to bottom',
1643+
);
1644+
simulateRemoteMessage(roomId, '@aibot:localhost', {
1645+
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.`,
1646+
msgtype: 'm.text',
1647+
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.`,
1648+
format: 'org.matrix.custom.html',
1649+
isStreamingFinished: true,
1650+
['m.relates_to']: {
1651+
rel_type: 'm.replace',
1652+
event_id: eventId,
1653+
},
1654+
});
1655+
assert.ok(
1656+
isAiAssistantScrolledToBottom(),
1657+
'AI assistant is scrolled to bottom',
1658+
);
1659+
});
1660+
16121661
test('sends read receipts only for bot messages', async function (assert) {
16131662
let roomId = await renderAiAssistantPanel();
16141663

0 commit comments

Comments
 (0)