import { useState, useRef, useEffect, useCallback, useLayoutEffect } from "react"; import { Send, Square, Plus } from "lucide-react"; import { useLLM } from "../hooks/useLLM"; import { MessageBubble } from "./MessageBubble"; import { BrandMark } from "./BrandMark"; import { MODEL_CONFIG } from "../model-config"; const TEXTAREA_MIN_HEIGHT = "7.5rem"; export function ChatApp() { const { messages, isGenerating, tps, send, stop, status, clearChat } = useLLM(); const [input, setInput] = useState(""); const scrollRef = useRef(null); const textareaRef = useRef(null); const isReady = status.state === "ready"; const hasMessages = messages.length > 0; const hasCompletedRef = useRef(false); useEffect(() => { if (hasMessages && !isGenerating) hasCompletedRef.current = true; if (!hasMessages) hasCompletedRef.current = false; }, [hasMessages, isGenerating]); const showNewChat = isReady && hasMessages && !isGenerating && hasCompletedRef.current; const prevMsgCountRef = useRef(0); const lastUserRef = useRef(null); const bottomSpacerRef = useRef(null); const userHasScrolledRef = useRef(false); const getContainerPadTop = useCallback(() => { const container = scrollRef.current; if (!container) return 0; return parseFloat(getComputedStyle(container).paddingTop) || 0; }, []); const recalcSpacer = useCallback(() => { const container = scrollRef.current; const userElement = lastUserRef.current; const spacer = bottomSpacerRef.current; if (!container || !userElement || !spacer) return; const userOffsetInContent = userElement.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop; const padTop = getContainerPadTop(); const padBottom = parseFloat(getComputedStyle(container).paddingBottom) || 0; const usableHeight = container.clientHeight - padTop - padBottom; const contentBelowUser = spacer.getBoundingClientRect().top - userElement.getBoundingClientRect().top; spacer.style.height = `${Math.max(0, usableHeight - contentBelowUser)}px`; if (!userHasScrolledRef.current) { const desiredScrollTop = userOffsetInContent - padTop; if (Math.abs(container.scrollTop - desiredScrollTop) > 0.5) { container.scrollTo({ top: desiredScrollTop, behavior: "smooth" }); } } }, [getContainerPadTop]); useLayoutEffect(() => { recalcSpacer(); const isNewMessage = messages.length > prevMsgCountRef.current; prevMsgCountRef.current = messages.length; if (isNewMessage) { userHasScrolledRef.current = false; const container = scrollRef.current; const userElement = lastUserRef.current; if (!container || !userElement) return; const scrollTarget = container.scrollTop + (userElement.getBoundingClientRect().top - container.getBoundingClientRect().top) - getContainerPadTop(); container.scrollTo({ top: scrollTarget, behavior: "smooth" }); } }, [messages, isGenerating, recalcSpacer, getContainerPadTop]); useEffect(() => { window.addEventListener("resize", recalcSpacer); return () => window.removeEventListener("resize", recalcSpacer); }, [recalcSpacer]); useEffect(() => { const container = scrollRef.current; if (!container) return; const markScrolled = () => { if (isGenerating) { userHasScrolledRef.current = true; } }; container.addEventListener("wheel", markScrolled, { passive: true }); container.addEventListener("touchmove", markScrolled, { passive: true }); return () => { container.removeEventListener("wheel", markScrolled); container.removeEventListener("touchmove", markScrolled); }; }, [isGenerating]); useLayoutEffect(() => { const container = scrollRef.current; if (!container) return; let lastHeight = container.clientHeight; const observer = new ResizeObserver(() => { const h = container.clientHeight; if (h !== lastHeight) { lastHeight = h; recalcSpacer(); } }); observer.observe(container); return () => observer.disconnect(); }, [recalcSpacer]); const handleSubmit = useCallback( (event?: React.FormEvent) => { event?.preventDefault(); const text = input.trim(); if (!text || !isReady || isGenerating) return; setInput(""); if (textareaRef.current) { textareaRef.current.style.height = TEXTAREA_MIN_HEIGHT; } send(text); }, [input, isReady, isGenerating, send], ); const handleInputKeyDown = useCallback( (event: React.KeyboardEvent) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); handleSubmit(); } }, [handleSubmit], ); const lastUserIndex = messages.findLastIndex((message) => message.role === "user"); const renderInputArea = (showDisclaimer: boolean) => (