UNPKG

@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
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]); };