'use client' import styles from './chatHistory.module.css'; import { useRef, useEffect, useState } from 'react'; import ChatMessage, { ChatHistoryData, StreamMessage, TrainOfThought } from '../chatMessage/chatMessage'; import { ScrollArea } from "@/components/ui/scroll-area" import renderMathInElement from 'katex/contrib/auto-render'; import 'katex/dist/katex.min.css'; import { InlineLoading } from '../loading/loading'; import { Lightbulb } from "@phosphor-icons/react"; import ProfileCard from '../profileCard/profileCard'; import { getIconFromIconName } from '@/app/common/iconUtils'; import { AgentData } from '@/app/agents/page'; import React from 'react'; interface ChatResponse { status: string; response: ChatHistoryData; } interface ChatHistory { [key: string]: string } interface ChatHistoryProps { conversationId: string; setTitle: (title: string) => void; incomingMessages?: StreamMessage[]; pendingMessage?: string; publicConversationSlug?: string; setAgent: (agent: AgentData) => void; } function constructTrainOfThought(trainOfThought: string[], lastMessage: boolean, agentColor: string, key: string, completed: boolean = false) { const lastIndex = trainOfThought.length - 1; return (
{ !completed && } {trainOfThought.map((train, index) => ( ))}
) } export default function ChatHistory(props: ChatHistoryProps) { const [data, setData] = useState(null); const [currentPage, setCurrentPage] = useState(0); const [hasMoreMessages, setHasMoreMessages] = useState(true); const ref = useRef(null); const chatHistoryRef = useRef(null); const sentinelRef = useRef(null); const [incompleteIncomingMessageIndex, setIncompleteIncomingMessageIndex] = useState(null); const [fetchingData, setFetchingData] = useState(false); const [isMobileWidth, setIsMobileWidth] = useState(false); useEffect(() => { window.addEventListener('resize', () => { setIsMobileWidth(window.innerWidth < 768); }); setIsMobileWidth(window.innerWidth < 768); }, []); useEffect(() => { // This function ensures that scrolling to bottom happens after the data (chat messages) has been updated and rendered the first time. const scrollToBottomAfterDataLoad = () => { // Assume the data is loading in this scenario. if (!data?.chat.length) { setTimeout(() => { scrollToBottom(); }, 500); } }; if (currentPage < 2) { // Call the function defined above. scrollToBottomAfterDataLoad(); } }, [data, currentPage]); useEffect(() => { if (!hasMoreMessages || fetchingData) return; // TODO: A future optimization would be to add a time to delay to re-enabling the intersection observer. const observer = new IntersectionObserver(entries => { if (entries[0].isIntersecting && hasMoreMessages) { setFetchingData(true); fetchMoreMessages(currentPage); setCurrentPage((prev) => prev + 1); } }, { threshold: 1.0 }); if (sentinelRef.current) { observer.observe(sentinelRef.current); } return () => observer.disconnect(); }, [hasMoreMessages, currentPage, fetchingData]); useEffect(() => { setHasMoreMessages(true); setFetchingData(false); setCurrentPage(0); setData(null); }, [props.conversationId]); useEffect(() => { if (props.incomingMessages) { const lastMessage = props.incomingMessages[props.incomingMessages.length - 1]; if (lastMessage && !lastMessage.completed) { setIncompleteIncomingMessageIndex(props.incomingMessages.length - 1); } } if (isUserAtBottom()) { scrollToBottom(); } }, [props.incomingMessages]); useEffect(() => { const observer = new MutationObserver((mutationsList, observer) => { // If the addedNodes property has one or more nodes for (let mutation of mutationsList) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { // Call your function here renderMathInElement(document.body, { delimiters: [ { left: '$$', right: '$$', display: true }, { left: '\\[', right: '\\]', display: true }, { left: '$', right: '$', display: false }, { left: '\\(', right: '\\)', display: false }, ], }); } } }); if (chatHistoryRef.current) { observer.observe(chatHistoryRef.current, { childList: true }); } // Clean up the observer on component unmount return () => observer.disconnect(); }, []); function fetchMoreMessages(currentPage: number) { if (!hasMoreMessages || fetchingData) return; const nextPage = currentPage + 1; let conversationFetchURL = ''; if (props.conversationId) { conversationFetchURL = `/api/chat/history?client=web&conversation_id=${props.conversationId}&n=${10 * nextPage}`; } else if (props.publicConversationSlug) { conversationFetchURL = `/api/chat/share/history?client=web&public_conversation_slug=${props.publicConversationSlug}&n=${10 * nextPage}`; } else { return; } fetch(conversationFetchURL) .then(response => response.json()) .then((chatData: ChatResponse) => { props.setTitle(chatData.response.slug); if (chatData && chatData.response && chatData.response.chat && chatData.response.chat.length > 0) { if (chatData.response.chat.length === data?.chat.length) { setHasMoreMessages(false); setFetchingData(false); return; } props.setAgent(chatData.response.agent); setData(chatData.response); if (currentPage < 2) { scrollToBottom(); } setFetchingData(false); } else { if (chatData.response.agent && chatData.response.conversation_id) { const chatMetadata = { chat: [], agent: chatData.response.agent, conversation_id: chatData.response.conversation_id, slug: chatData.response.slug, } props.setAgent(chatData.response.agent); setData(chatMetadata); } setHasMoreMessages(false); setFetchingData(false); } }) .catch(err => { console.error(err); window.location.href = "/"; }); }; const scrollToBottom = () => { if (chatHistoryRef.current) { chatHistoryRef.current.scrollIntoView(false); } } const isUserAtBottom = () => { if (!chatHistoryRef.current) return false; // NOTE: This isn't working. It always seems to return true. This is because const { scrollTop, scrollHeight, clientHeight } = chatHistoryRef.current as HTMLDivElement; const threshold = 25; // pixels from the bottom // Considered at the bottom if within threshold pixels from the bottom return scrollTop + clientHeight >= scrollHeight - threshold; } function constructAgentLink() { if (!data || !data.agent || !data.agent.slug) return `/agents`; return `/agents?agent=${data.agent.slug}` } function constructAgentName() { if (!data || !data.agent || !data.agent.name) return `Agent`; return data.agent.name; } function constructAgentPersona() { if (!data || !data.agent || !data.agent.persona) return ``; return data.agent.persona; } if (!props.conversationId && !props.publicConversationSlug) { return null; } return (
{fetchingData && }
{(data && data.chat) && data.chat.map((chatMessage, index) => ( ))} { props.incomingMessages && props.incomingMessages.map((message, index) => { return ( { message.trainOfThought && constructTrainOfThought( message.trainOfThought, index === incompleteIncomingMessageIndex, data?.agent.color || 'orange', `${index}trainOfThought`, message.completed) } ) }) } { props.pendingMessage && } {data &&
} description={constructAgentPersona()} />
}
) }