"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 { 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< number | null >(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]); 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()} />
)}
); }