remix-nlux
Version:
Remix IDE NLUX integration. Remix IDE is the leading IDE for building and deploying smart contracts on Ethereum. NLUX is a JavaScript and React library for building conversational AI experiences.
121 lines (104 loc) • 4.75 kB
text/typescript
import {throttle} from '../../utils/throttle';
import {AutoScrollController} from './type';
import {createConversationScrollHandler} from './utils/conversationScrollHandler';
export const createAutoScrollController = (
newConversationContainer: HTMLElement,
autoScroll: boolean,
): AutoScrollController => {
let shouldScrollWhenGenerating: boolean = autoScroll;
let conversationContainer: HTMLElement | undefined = newConversationContainer;
let scrollingStickToConversationEnd: boolean = true;
let activeChatSegment: {uid: string; container: HTMLElement} | undefined = undefined;
const scrollHandler = throttle(createConversationScrollHandler((
{
scrolledToBottom, scrollDirection,
}) => {
if (scrollingStickToConversationEnd) {
// When the user is already at the bottom of the conversation, we stick to the bottom
// and only unstick when the user scrolls up.
if (scrollDirection === 'up') {
scrollingStickToConversationEnd = false;
}
} else {
// When the user is not at the bottom of the conversation, we only stick to the bottom
// when the user scrolls down and reaches the bottom.
if (scrollDirection === 'down' && scrolledToBottom) {
scrollingStickToConversationEnd = true;
}
}
}), 50);
const initConversationContainer = (newConversationContainer: HTMLElement) => {
newConversationContainer.addEventListener('scroll', scrollHandler);
};
const resetConversationContainer = (oldConversationContainer?: HTMLElement) => {
oldConversationContainer?.removeEventListener('scroll', scrollHandler);
};
const handleDoneWithSegment = (segmentId: string) => {
if (activeChatSegment?.uid === segmentId) {
resizeObserver?.disconnect();
mutationObserver?.disconnect();
activeChatSegment = undefined;
resizeObserver = undefined;
mutationObserver = undefined;
}
};
let resizeObserver: ResizeObserver | undefined;
let mutationObserver: MutationObserver | undefined;
const scrollToBottom = () => {
// Using a very large number to make sure the scroll position is set to the bottom.
// The alternative is to use scrollHeight, but that would result in an extra reflow - that can
// be avoided by using a very large number.
// Downside: if the scrollHeight is larger than the max value (50000 pixels), this will not work.
conversationContainer?.scrollTo({
top: 50000,
behavior: 'instant',
});
};
const handleActiveChatSegmentResized = () => {
if (conversationContainer && shouldScrollWhenGenerating && scrollingStickToConversationEnd) {
scrollToBottom();
}
};
const handleActiveChatSegmentDomChanged = () => {
handleActiveChatSegmentResized();
};
initConversationContainer(conversationContainer);
return {
updateConversationContainer: (newConversationContainer: HTMLElement) => {
resetConversationContainer(conversationContainer);
initConversationContainer(newConversationContainer);
conversationContainer = newConversationContainer;
},
updateProps: ({autoScroll}) => {
shouldScrollWhenGenerating = autoScroll;
},
handleNewChatSegmentAdded: (segmentId, segmentContainer) => {
if (activeChatSegment) {
resizeObserver?.disconnect();
mutationObserver?.disconnect();
}
activeChatSegment = {uid: segmentId, container: segmentContainer};
resizeObserver = new ResizeObserver(handleActiveChatSegmentResized);
resizeObserver.observe(segmentContainer);
mutationObserver = new MutationObserver(handleActiveChatSegmentDomChanged);
mutationObserver.observe(segmentContainer, {
childList: true,
subtree: true,
characterData: true,
});
if (shouldScrollWhenGenerating) {
scrollToBottom();
}
},
handleChatSegmentRemoved: (segmentId) => handleDoneWithSegment(segmentId),
handleChatSegmentComplete: (segmentId) => handleDoneWithSegment(segmentId),
destroy: () => {
if (activeChatSegment) {
handleDoneWithSegment(activeChatSegment.uid);
activeChatSegment = undefined;
}
resetConversationContainer(conversationContainer);
conversationContainer = undefined;
},
};
};