import { memo, useCallback, useState, useEffect, useRef } from 'react'; import { useRecoilValue } from 'recoil'; import { useForm } from 'react-hook-form'; import { Spinner } from '@librechat/client'; import { useParams } from 'react-router-dom'; import { Constants, buildTree } from 'librechat-data-provider'; import type { TMessage } from 'librechat-data-provider'; import type { ChatFormValues } from '~/common'; import { ChatContext, AddedChatContext, useFileMapContext, ChatFormProvider } from '~/Providers'; import { useChatHelpers, useAddedResponse, useSSE } from '~/hooks'; import ConversationStarters from './Input/ConversationStarters'; import { useGetMessagesByConvoId } from '~/data-provider'; import MessagesView from './Messages/MessagesView'; import Presentation from './Presentation'; import ChatForm from './Input/ChatForm'; import CostBar from './CostBar'; import Landing from './Landing'; import Header from './Header'; import Footer from './Footer'; import { cn } from '~/utils'; import store from '~/store'; function LoadingSpinner() { return (
); } function ChatView({ index = 0, modelCosts, }: { index?: number; modelCosts?: { modelCostTable: Record }; }) { const { conversationId } = useParams(); const rootSubmission = useRecoilValue(store.submissionByIndex(index)); const addedSubmission = useRecoilValue(store.submissionByIndex(index + 1)); const centerFormOnLanding = useRecoilValue(store.centerFormOnLanding); const fileMap = useFileMapContext(); const [showCostBar, setShowCostBar] = useState(false); const lastScrollY = useRef(0); const { data: messagesTree = null, isLoading } = useGetMessagesByConvoId(conversationId ?? '', { select: useCallback( (data: TMessage[]) => { const dataTree = buildTree({ messages: data, fileMap }); return dataTree?.length === 0 ? null : (dataTree ?? null); }, [fileMap], ), enabled: !!fileMap, }); const chatHelpers = useChatHelpers(index, conversationId); const addedChatHelpers = useAddedResponse({ rootIndex: index }); useSSE(rootSubmission, chatHelpers, false); useSSE(addedSubmission, addedChatHelpers, true); const checkIfAtBottom = useCallback( (container: HTMLElement) => { const currentScrollY = container.scrollTop; const scrollHeight = container.scrollHeight; const clientHeight = container.clientHeight; const distanceFromBottom = scrollHeight - currentScrollY - clientHeight; const isAtBottom = distanceFromBottom < 10; const isStreaming = chatHelpers.isSubmitting || addedChatHelpers.isSubmitting; setShowCostBar(isAtBottom && !isStreaming); lastScrollY.current = currentScrollY; }, [chatHelpers.isSubmitting, addedChatHelpers.isSubmitting], ); useEffect(() => { const handleScroll = (event: Event) => { const target = event.target as HTMLElement; checkIfAtBottom(target); }; const findAndAttachScrollListener = () => { const messagesContainer = document.querySelector('[class*="scrollbar-gutter-stable"]'); if (messagesContainer) { checkIfAtBottom(messagesContainer as HTMLElement); messagesContainer.addEventListener('scroll', handleScroll, { passive: true }); return () => { messagesContainer.removeEventListener('scroll', handleScroll); }; } setTimeout(findAndAttachScrollListener, 100); }; const cleanup = findAndAttachScrollListener(); return cleanup; }, [messagesTree, checkIfAtBottom]); useEffect(() => { const isStreaming = chatHelpers.isSubmitting || addedChatHelpers.isSubmitting; if (isStreaming) { setShowCostBar(false); } else { const messagesContainer = document.querySelector('[class*="scrollbar-gutter-stable"]'); if (messagesContainer) { checkIfAtBottom(messagesContainer as HTMLElement); } } }, [chatHelpers.isSubmitting, addedChatHelpers.isSubmitting, checkIfAtBottom]); const methods = useForm({ defaultValues: { text: '' }, }); let content: JSX.Element | null | undefined; const isLandingPage = (!messagesTree || messagesTree.length === 0) && (conversationId === Constants.NEW_CONVO || !conversationId); const isNavigating = (!messagesTree || messagesTree.length === 0) && conversationId != null; if (isLoading && conversationId !== Constants.NEW_CONVO) { content = ; } else if ((isLoading || isNavigating) && !isLandingPage) { content = ; } else if (!isLandingPage) { const isStreaming = chatHelpers.isSubmitting || addedChatHelpers.isSubmitting; content = ( ) } /> ); } else { content = ; } return (
{!isLoading &&
} <>
{content}
{isLandingPage ? :
}
{isLandingPage &&
); } export default memo(ChatView);