@plteam/chat-ui
Version:
CUI Kit is a free and open-source library for creating AI assistant chat interfaces, built with React, Material UI, and TypeScript
122 lines (121 loc) • 4.98 kB
JavaScript
import * as React from 'react';
import { ChatViewConstants } from '../ChatViewConstants';
import { useTheme } from '@mui/material/styles';
import { useThreadContext } from '../thread/ThreadContext';
import { when } from '../../utils/observers/when';
import { useObserverValue } from '../hooks/useObserverValue';
class FollowingClass {
_model;
getPosition;
scrollTo;
marginTop;
_observer;
_isFollowing = false;
_elementPosition = 'top';
_removeListeners;
_yDown = 0;
constructor(_model, getPosition, scrollTo, marginTop) {
this._model = _model;
this.getPosition = getPosition;
this.scrollTo = scrollTo;
this.marginTop = marginTop;
}
init = () => {
when(this._model.typing, () => !!this._model.typing.value, () => {
const lastMessage = document.getElementById(ChatViewConstants.MESSAGE_BOX_ID)
?.querySelector(`[${ChatViewConstants.MESSAGE_DATA_SCROLL_ANCHOR}="true"]`);
if (lastMessage) {
// Call disconnect listener
when(this._model.typing, () => this._model.typing.value === false, () => {
this.disconnect();
});
this._observer = new ResizeObserver(() => {
this._scroll(lastMessage);
});
this._observer.observe(lastMessage);
const { scrollHeight, offsetHeight } = this.getPosition();
if (scrollHeight <= offsetHeight)
this.setFollowing(true);
}
});
return this;
};
setFollowing = (value) => {
if (value) {
const { scrollHeight, offsetHeight } = this.getPosition();
this._elementPosition = scrollHeight > offsetHeight ? 'bottom' : 'top';
this._initEventListeners();
}
this._isFollowing = value;
};
offFollowing = () => {
this._isFollowing = false;
};
handleTouchStart = (event) => {
this._yDown = event.touches[0].clientY;
};
handleTouchMove = (event) => {
if (!this._yDown) {
return;
}
const yUp = event.touches[0].clientY;
const yDiff = Math.abs(this._yDown - yUp);
if (yDiff > 20)
this.offFollowing();
};
_initEventListeners = () => {
this._removeListeners?.();
document.addEventListener('wheel', this.offFollowing);
document.addEventListener('touchstart', this.handleTouchStart, false);
document.addEventListener('touchmove', this.handleTouchMove, false);
this._removeListeners = () => {
document.removeEventListener('wheel', this.offFollowing);
document.removeEventListener('touchstart', this.handleTouchStart);
document.removeEventListener('touchmove', this.handleTouchMove);
};
};
disconnect = () => {
this._observer?.disconnect();
this._removeListeners?.();
};
_scroll = (element) => {
const { scrollTop } = this.getPosition();
if (this._isFollowing) {
const bounds = element.getBoundingClientRect();
const position = this._elementPosition;
const topPadding = 8;
// Тут нужно минусовать высоту скролл бара, т.к. иначе верхушка сообщения будет оставаться за ним
const minus = position === 'top' ? this.marginTop + topPadding : 0;
this.scrollTo(bounds[position] + scrollTop - minus);
}
};
}
export const useMessageFollowing = (showButton, getPosition, scrollTo) => {
const [followingModel, setFollowingModel] = React.useState();
const { apiRef } = useThreadContext();
const messages = useObserverValue(apiRef.current?.getListener('allMessages')) ?? [];
const lastMessageModel = React.useMemo(() => {
if (!messages.length)
return undefined;
const model = messages[messages.length - 1];
return model?.isAssistant ? model : undefined;
}, [messages.length]);
const theme = useTheme();
React.useEffect(() => {
const dialogueContaier = document.getElementById(ChatViewConstants.DIALOGUE_ROOT_ID);
const marginTop = dialogueContaier?.getBoundingClientRect().top ?? 0;
const newModel = lastMessageModel
// TODO: hardcode
? (new FollowingClass(lastMessageModel, getPosition, scrollTo, marginTop)).init()
: undefined;
setFollowingModel(newModel);
return () => {
followingModel?.disconnect();
};
}, [lastMessageModel, theme]);
React.useEffect(() => {
if (followingModel && !showButton) {
followingModel.setFollowing(true);
}
}, [showButton]);
};