import { useState, useRef, useEffect, useCallback } from "react"; import { useAtom, useAtomValue } from "jotai"; import { chatMessagesAtom, chatStreamCountAtom } from "../atoms/chatAtoms"; import { IpcClient } from "@/ipc/ipc_client"; import { selectedAppIdAtom } from "@/atoms/appAtoms"; 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"; interface ChatPanelProps { chatId?: number; isPreviewOpen: boolean; onTogglePreview: () => void; } export function ChatPanel({ chatId, isPreviewOpen, onTogglePreview, }: ChatPanelProps) { const appId = useAtomValue(selectedAppIdAtom); const [messages, setMessages] = useAtom(chatMessagesAtom); const [appName, setAppName] = useState("Chat"); const [isVersionPaneOpen, setIsVersionPaneOpen] = useState(false); const [error, setError] = useState(null); const streamCount = useAtomValue(chatStreamCountAtom); // Reference to store the processed prompt so we don't submit it twice const processedPromptRef = useRef(null); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); // Scroll-related properties const [isUserScrolling, setIsUserScrolling] = useState(false); const userScrollTimeoutRef = useRef(null); const lastScrollTopRef = useRef(0); const scrollToBottom = (behavior: ScrollBehavior = "smooth") => { messagesEndRef.current?.scrollIntoView({ behavior }); }; const handleScroll = () => { if (!messagesContainerRef.current) return; const container = messagesContainerRef.current; const currentScrollTop = container.scrollTop; if (currentScrollTop < lastScrollTopRef.current) { setIsUserScrolling(true); if (userScrollTimeoutRef.current) { window.clearTimeout(userScrollTimeoutRef.current); } userScrollTimeoutRef.current = window.setTimeout(() => { setIsUserScrolling(false); }, 1000); } lastScrollTopRef.current = currentScrollTop; }; useEffect(() => { console.log("streamCount", streamCount); scrollToBottom(); }, [streamCount]); 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); } }; }, []); useEffect(() => { const fetchAppName = async () => { if (!appId) return; try { const app = await IpcClient.getInstance().getApp(appId); if (app?.name) { setAppName(app.name); } } catch (error) { console.error("Failed to fetch app name:", error); } }; fetchAppName(); }, [appId]); const fetchChatMessages = useCallback(async () => { if (!chatId) { setMessages([]); return; } const chat = await IpcClient.getInstance().getChat(chatId); setMessages(chat.messages); }, [chatId, setMessages]); useEffect(() => { fetchChatMessages(); }, [fetchChatMessages]); // Auto-scroll effect when messages change useEffect(() => { if ( !isUserScrolling && messagesContainerRef.current && messages.length > 0 ) { const { scrollTop, clientHeight, scrollHeight } = messagesContainerRef.current; const threshold = 280; const isNearBottom = scrollHeight - (scrollTop + clientHeight) <= threshold; if (isNearBottom) { requestAnimationFrame(() => { scrollToBottom("instant"); }); } } }, [messages, isUserScrolling]); return (
setIsVersionPaneOpen(!isVersionPaneOpen)} />
{!isVersionPaneOpen && (
setError(null)} />
)} setIsVersionPaneOpen(false)} />
); }