import { useState, useRef, useEffect, useCallback } from "react"; import { useAtomValue, useSetAtom } from "jotai"; import { chatMessagesByIdAtom, chatStreamCountByIdAtom, isStreamingByIdAtom, } from "../atoms/chatAtoms"; import { IpcClient } from "@/ipc/ipc_client"; import { ChatHeader } from "./chat/ChatHeader"; import { MessagesList } from "./chat/MessagesList"; import { ChatInput } from "./chat/ChatInput"; import { VersionPane } from "./chat/VersionPane"; import { ChatError } from "./chat/ChatError"; import { Button } from "@/components/ui/button"; import { ArrowDown } from "lucide-react"; interface ChatPanelProps { chatId?: number; isPreviewOpen: boolean; onTogglePreview: () => void; } export function ChatPanel({ chatId, isPreviewOpen, onTogglePreview, }: ChatPanelProps) { const messagesById = useAtomValue(chatMessagesByIdAtom); const setMessagesById = useSetAtom(chatMessagesByIdAtom); const [isVersionPaneOpen, setIsVersionPaneOpen] = useState(false); const [error, setError] = useState(null); const streamCountById = useAtomValue(chatStreamCountByIdAtom); const isStreamingById = useAtomValue(isStreamingByIdAtom); // Reference to store the processed prompt so we don't submit it twice const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); // Scroll-related properties const [isUserScrolling, setIsUserScrolling] = useState(false); const [showScrollButton, setShowScrollButton] = useState(false); const userScrollTimeoutRef = useRef(null); const lastScrollTopRef = useRef(0); const scrollToBottom = (behavior: ScrollBehavior = "smooth") => { messagesEndRef.current?.scrollIntoView({ behavior }); }; const handleScrollButtonClick = () => { if (!messagesContainerRef.current) return; scrollToBottom("smooth"); }; const getDistanceFromBottom = () => { if (!messagesContainerRef.current) return 0; const container = messagesContainerRef.current; return ( container.scrollHeight - (container.scrollTop + container.clientHeight) ); }; const isNearBottom = (threshold: number = 100) => { return getDistanceFromBottom() <= threshold; }; const scrollAwayThreshold = 150; // pixels from bottom to consider "scrolled away" const handleScroll = useCallback(() => { if (!messagesContainerRef.current) return; const container = messagesContainerRef.current; const distanceFromBottom = container.scrollHeight - (container.scrollTop + container.clientHeight); // User has scrolled away from bottom if (distanceFromBottom > scrollAwayThreshold) { setIsUserScrolling(true); setShowScrollButton(true); if (userScrollTimeoutRef.current) { window.clearTimeout(userScrollTimeoutRef.current); } userScrollTimeoutRef.current = window.setTimeout(() => { setIsUserScrolling(false); }, 2000); // Increased timeout to 2 seconds } else { // User is near bottom setIsUserScrolling(false); setShowScrollButton(false); } lastScrollTopRef.current = container.scrollTop; }, []); useEffect(() => { const streamCount = chatId ? (streamCountById.get(chatId) ?? 0) : 0; console.log("streamCount - scrolling to bottom", streamCount); scrollToBottom(); }, [ chatId, chatId ? (streamCountById.get(chatId) ?? 0) : 0, chatId ? (isStreamingById.get(chatId) ?? false) : false, ]); useEffect(() => { const container = messagesContainerRef.current; if (container) { container.addEventListener("scroll", handleScroll, { passive: true }); } return () => { if (container) { container.removeEventListener("scroll", handleScroll); } if (userScrollTimeoutRef.current) { window.clearTimeout(userScrollTimeoutRef.current); } }; }, [handleScroll]); const fetchChatMessages = useCallback(async () => { if (!chatId) { // no-op when no chat return; } const chat = await IpcClient.getInstance().getChat(chatId); setMessagesById((prev) => { const next = new Map(prev); next.set(chatId, chat.messages); return next; }); }, [chatId, setMessagesById]); useEffect(() => { fetchChatMessages(); }, [fetchChatMessages]); const messages = chatId ? (messagesById.get(chatId) ?? []) : []; const isStreaming = chatId ? (isStreamingById.get(chatId) ?? false) : false; // Auto-scroll effect when messages change during streaming useEffect(() => { if ( !isUserScrolling && isStreaming && messagesContainerRef.current && messages.length > 0 ) { // Only auto-scroll if user is close to bottom if (isNearBottom(280)) { requestAnimationFrame(() => { scrollToBottom("instant"); }); } } }, [messages, isUserScrolling, isStreaming]); return (
setIsVersionPaneOpen(!isVersionPaneOpen)} />
{!isVersionPaneOpen && (
{/* Scroll to bottom button */} {showScrollButton && (
)}
setError(null)} />
)} setIsVersionPaneOpen(false)} />
); }