import { useEffect, useRef, useState } from "react"; import Markdown from "react-markdown"; import ChatInputForm from "./ChatInputForm"; import Navbar from "./Navbar"; import SharedCard from "./memories/SharedCard"; import { User } from "@supermemory/shared/types"; import { CoreMessage } from "ai"; import { AnimatePresence, motion } from "framer-motion"; import { ChevronDown, ChevronUp } from "lucide-react"; import { useChatStream } from "~/lib/hooks/use-chat-stream"; import { Memory } from "~/lib/types/memory"; interface ChatProps { user: User; chatMessages: CoreMessage[]; initialThreadUuid?: string; } function Chat({ user, chatMessages, initialThreadUuid }: ChatProps) { const { threadUuid, chatMessages: chatMessagesStreamed, input, setInput, sendMessage, isLoading, } = useChatStream(chatMessages, initialThreadUuid); const [expandedMessageIndexes, setExpandedMessageIndexes] = useState([]); const [shouldAutoScroll, setShouldAutoScroll] = useState(true); const messagesEndRef = useRef(null); const scrollContainerRef = useRef(null); const [streamingText, setStreamingText] = useState(""); const hasAnnotations = chatMessagesStreamed.some( (message) => message.role === "assistant" && message.annotations?.length, ); const toggleExpand = (index: number) => { setExpandedMessageIndexes((prev) => prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index], ); }; const scrollToBottom = () => { if (messagesEndRef.current && shouldAutoScroll) { messagesEndRef.current.scrollIntoView({ behavior: "smooth" }); } }; useEffect(() => { scrollToBottom(); }, [chatMessagesStreamed]); useEffect(() => { const lastMessage = chatMessagesStreamed[chatMessagesStreamed.length - 1]; if (lastMessage?.role === "assistant" && isLoading) { setStreamingText(lastMessage.content as string); } else { setStreamingText(""); } }, [chatMessagesStreamed, isLoading]); const handleScroll = () => { if (!scrollContainerRef.current) return; const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current; const isAtBottom = Math.abs(scrollHeight - clientHeight - scrollTop) < 10; setShouldAutoScroll(isAtBottom); }; const renderAttachments = (attachments: any[]) => (
{attachments.map((attachment) => ( {attachment.url.endsWith(".png") || attachment.url.endsWith(".jpg") || attachment.url.endsWith(".jpeg") ? ( {attachment.name} ) : ( )} ))}
); const renderMessageContent = (content: string | any, isLatestAndLoading: boolean) => ( {isLatestAndLoading ? streamingText : typeof content === "string" ? content.replace(/[\s\S]*?<\/context>/g, "") : content} ); const groupAnnotationsByHost = (annotations: Memory[]) => { return annotations.reduce( (acc, curr) => { let host = ""; try { const url = new URL(curr.url || ""); host = url.host; } catch { host = "unknown"; } if (!acc[host]) acc[host] = []; acc[host].push(curr); return acc; }, {} as Record, ); }; const renderAnnotations = (messageAnnotations: Memory[], index: number, isMobile = false) => { const isExpanded = expandedMessageIndexes.includes(index); const groupedAnnotations = groupAnnotationsByHost(messageAnnotations); return (
{!isExpanded ? ( toggleExpand(index)} className={`text-${isMobile ? "xs" : "sm"} text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1`} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} > {messageAnnotations.length} relevant{" "} {messageAnnotations.length === 1 ? "item" : "items"} ) : (
Related Context
{isMobile ? messageAnnotations.map((annotation, i) => ( )) : Object.entries(groupedAnnotations).map(([host, items], i) => (
{items.length > 1 && (
{host} ({items.length} items)
)}
{items.map((annotation, j) => ( ))}
))}
toggleExpand(index)} className={`flex items-center gap-${isMobile ? "1" : "2"} text-${isMobile ? "xs" : "sm"} text-muted-foreground hover:text-foreground transition-colors`} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} > Show less
)}
); }; return (
{chatMessagesStreamed.map((message, index) => { const isLatestAndLoading = index === chatMessagesStreamed.length - 1 && isLoading && message.role === "assistant"; if (message.role === "user") { return ( {message.experimental_attachments && renderAttachments(message.experimental_attachments)} {message.content} ); } if (message.role === "assistant") { const messageAnnotations = message.annotations?.[0] ? (message.annotations[0] as unknown as Memory[]) : undefined; return ( <> {messageAnnotations && messageAnnotations.length > 0 && renderAnnotations(messageAnnotations, index, true)}
{renderMessageContent(message.content, isLatestAndLoading)}
{messageAnnotations && messageAnnotations.length > 0 && renderAnnotations(messageAnnotations, index)}
); } return null; })} {isLoading && !chatMessagesStreamed[chatMessagesStreamed.length - 1]?.content && (
{[3 / 4, 1 / 2, 2 / 3, 1 / 3].map((width, i) => (
))}
{hasAnnotations && (
{[...Array(2)].map((_, i) => (
))}
)} )}
); } export default Chat;