Working example of streaming, intersection observer, other UI updates

This commit is contained in:
sabaimran
2024-07-04 00:30:01 +05:30
parent 78d1a29bc1
commit d5ba916978
8 changed files with 534 additions and 76 deletions

View File

@@ -20,3 +20,11 @@ div.agentIndicator a {
div.agentIndicator {
padding: 10px;
}
div.trainOfThought {
border: 1px var(--border-color) solid;
border-radius: 16px;
padding: 16px;
margin: 12px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.03);
}

View File

@@ -3,9 +3,9 @@
import styles from './chatHistory.module.css';
import { useRef, useEffect, useState } from 'react';
import ChatMessage, { ChatHistoryData, SingleChatMessage } from '../chatMessage/chatMessage';
import ChatMessage, { ChatHistoryData, SingleChatMessage, StreamMessage, TrainOfThought } from '../chatMessage/chatMessage';
import ReferencePanel, { hasValidReferences} from '../referencePanel/referencePanel';
import ReferencePanel, { hasValidReferences } from '../referencePanel/referencePanel';
import { ScrollArea } from "@/components/ui/scroll-area"
@@ -29,43 +29,149 @@ interface ChatHistory {
interface ChatHistoryProps {
conversationId: string;
setTitle: (title: string) => void;
incomingMessages?: StreamMessage[];
pendingMessage?: string;
}
function constructTrainOfThought(trainOfThought: string[], lastMessage: boolean, key: string) {
const lastIndex = trainOfThought.length - 1;
return (
<div className={`${styles.trainOfThought}`} key={key}>
{trainOfThought.map((train, index) => (
<TrainOfThought message={train} primary={index === lastIndex && lastMessage} />
))}
</div>
)
}
export default function ChatHistory(props: ChatHistoryProps) {
const [data, setData] = useState<ChatHistoryData | null>(null);
const [isLoading, setLoading] = useState(true)
const [isLoading, setLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(0);
const [hasMoreMessages, setHasMoreMessages] = useState(true);
const ref = useRef<HTMLDivElement>(null);
const chatHistoryRef = useRef(null);
const chatHistoryRef = useRef<HTMLDivElement | null>(null);
const sentinelRef = useRef<HTMLDivElement | null>(null);
const [showReferencePanel, setShowReferencePanel] = useState(true);
const [referencePanelData, setReferencePanelData] = useState<SingleChatMessage | null>(null);
const [incompleteIncomingMessageIndex, setIncompleteIncomingMessageIndex] = useState<number | null>(null);
useEffect(() => {
// useEffect(() => {
fetch(`/api/chat/history?client=web&conversation_id=${props.conversationId}&n=10`)
// // TODO add intersection observer to load more messages incrementally using parameter n=. Right now, it loads all messages at once.
// fetch(`/api/chat/history?client=web&conversation_id=${props.conversationId}`)
// .then(response => response.json())
// .then((chatData: ChatResponse) => {
// setLoading(false);
// // Render chat options, if any
// if (chatData) {
// setData(chatData.response);
// props.setTitle(chatData.response.slug);
// }
// })
// .catch(err => {
// console.error(err);
// return;
// });
// }, [props.conversationId]);
useEffect(() => {
console.log("hasMoreMessages", hasMoreMessages);
const observer = new IntersectionObserver(entries => {
console.log("entries intersection observer", entries);
if (entries[0].isIntersecting && hasMoreMessages) {
console.log("call fetchMoreMessages");
fetchMoreMessages(currentPage);
console.log("currentPage", currentPage);
setCurrentPage((prev) => prev + 1);
}
}, { threshold: 1.0 });
if (sentinelRef.current) {
console.log("observe sentinel");
observer.observe(sentinelRef.current);
}
return () => observer.disconnect();
}, [sentinelRef.current, hasMoreMessages, currentPage, props.conversationId]);
const fetchMoreMessages = (currentPage: number) => {
if (!hasMoreMessages) return;
console.log("fetchMoreMessages", currentPage);
const nextPage = currentPage + 1;
fetch(`/api/chat/history?client=web&conversation_id=${props.conversationId}&n=${10*nextPage}`)
.then(response => response.json())
.then((chatData: ChatResponse) => {
setLoading(false);
// Render chat options, if any
if (chatData) {
console.log(chatData);
if (chatData && chatData.response && chatData.response.chat.length > 0) {
console.log(chatData);
if (chatData.response.chat.length === data?.chat.length) {
setHasMoreMessages(false);
return;
}
scrollToBottom();
setData(chatData.response);
props.setTitle(chatData.response.slug);
setLoading(false);
} else {
console.log("No more messages");
setHasMoreMessages(false);
}
})
.catch(err => {
console.error(err);
return;
});
}, [props.conversationId]);
};
useEffect(() => {
if (props.incomingMessages) {
const lastMessage = props.incomingMessages[props.incomingMessages.length - 1];
if (lastMessage && !lastMessage.completed) {
setIncompleteIncomingMessageIndex(props.incomingMessages.length - 1);
}
}
console.log("isUserAtBottom", isUserAtBottom());
if (isUserAtBottom()) {
scrollToBottom();
}
}, [props.incomingMessages]);
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;
}
useEffect(() => {
const observer = new MutationObserver((mutationsList, observer) => {
// If the addedNodes property has one or more nodes
for(let mutation of mutationsList) {
for (let mutation of mutationsList) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// Call your function here
renderMathInElement(document.body, {
@@ -88,9 +194,9 @@ export default function ChatHistory(props: ChatHistoryProps) {
return () => observer.disconnect();
}, []);
if (isLoading) {
return <Loading />;
}
// if (isLoading) {
// return <Loading />;
// }
function constructAgentLink() {
if (!data || !data.agent || !data.agent.slug) return `/agents`;
@@ -111,9 +217,10 @@ export default function ChatHistory(props: ChatHistoryProps) {
<ScrollArea className={`h-[80vh]`}>
<div ref={ref}>
<div className={styles.chatHistory} ref={chatHistoryRef}>
<div ref={sentinelRef} style={{ height: '1px' }}></div>
{(data && data.chat) && data.chat.map((chatMessage, index) => (
<ChatMessage
key={index}
key={`${index}fullHistory`}
chatMessage={chatMessage}
setReferencePanelData={setReferencePanelData}
setShowReferencePanel={setShowReferencePanel}
@@ -121,9 +228,77 @@ export default function ChatHistory(props: ChatHistoryProps) {
borderLeftColor='orange-500'
/>
))}
{
props.incomingMessages && props.incomingMessages.map((message, index) => {
return (
<>
<ChatMessage
key={`${index}outgoing`}
chatMessage={
{
message: message.rawQuery,
context: [],
onlineContext: {},
created: message.timestamp,
by: "you",
intent: {},
automationId: '',
}
}
setReferencePanelData={() => { }}
setShowReferencePanel={() => { }}
customClassName='fullHistory'
borderLeftColor='orange-500' />
{
message.trainOfThought && constructTrainOfThought(message.trainOfThought, index === incompleteIncomingMessageIndex, `${index}trainOfThought`)
}
<ChatMessage
key={`${index}incoming`}
chatMessage={
{
message: message.rawResponse,
context: message.context,
onlineContext: message.onlineContext,
created: message.timestamp,
by: "khoj",
intent: {},
automationId: '',
}
}
setReferencePanelData={setReferencePanelData}
setShowReferencePanel={setShowReferencePanel}
customClassName='fullHistory'
borderLeftColor='orange-500'
/>
</>
)
})
}
{
props.pendingMessage &&
<ChatMessage
key={"pendingMessage"}
chatMessage={
{
message: props.pendingMessage,
context: [],
onlineContext: {},
created: new Date().toISOString(),
by: "you",
intent: {},
automationId: '',
}
}
setReferencePanelData={() => { }}
setShowReferencePanel={() => { }}
customClassName='fullHistory'
borderLeftColor='orange-500'
/>
}
{
(hasValidReferences(referencePanelData) && showReferencePanel) &&
<ReferencePanel referencePanelData={referencePanelData} setShowReferencePanel={setShowReferencePanel} />
<ReferencePanel referencePanelData={referencePanelData} setShowReferencePanel={setShowReferencePanel} />
}
<div className={`${styles.agentIndicator}`}>
<a className='no-underline mx-2 flex' href={constructAgentLink()} target="_blank" rel="noreferrer">