import { useState, useEffect, useRef, useMemo } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkMath from 'remark-math'; import rehypeKatex from 'rehype-katex'; import rehypeRaw from 'rehype-raw'; import { getChatMarkdownComponents } from '../utils/markdownComponents.jsx'; const SimpleChat = ({ messages, currentChunkIndex, onSend, isLoading }) => { const [input, setInput] = useState(''); const containerRef = useRef(null); const anchorRef = useRef(null); // <- will be a tiny zero-height anchor BEFORE the bubble const textareaRef = useRef(null); const handleSubmit = (e) => { e.preventDefault(); if (!input.trim() || isLoading ) return; onSend(input.trim()); setInput(''); }; const handleKeyDown = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSubmit(e); } }; // Auto-resize textarea useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = 'auto'; textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 120) + 'px'; } }, [input]); // Since messages are now filtered to current chunk only, use the last message for anchoring const { anchorIndex, firstInChunkIndex } = useMemo(() => { const lastIndex = messages.length > 0 ? messages.length - 1 : -1; const firstIndex = messages.length > 0 ? 0 : -1; return { anchorIndex: lastIndex, firstInChunkIndex: firstIndex }; }, [messages]); // Scroll by scrolling the ZERO-HEIGHT anchor into view AFTER layout commits. const scrollAfterLayout = () => { requestAnimationFrame(() => { requestAnimationFrame(() => { if (anchorRef.current && containerRef.current) { // Calculate position relative to container instead of using scrollIntoView const anchorTop = anchorRef.current.offsetTop; containerRef.current.scrollTo({ top: anchorTop, behavior: 'smooth' }); } else if (containerRef.current) { // fallback: go to top containerRef.current.scrollTo({ top: 0, behavior: 'smooth' }); } }); }); }; // When chunk changes, try to pin. useEffect(() => { if (anchorIndex !== -1) { scrollAfterLayout(); } else if (containerRef.current) { requestAnimationFrame(() => containerRef.current.scrollTo({ top: 0, behavior: 'smooth' })); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentChunkIndex, anchorIndex]); // New messages: pin the new anchor after layout useEffect(() => { if (anchorIndex !== -1) scrollAfterLayout(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [messages.length, anchorIndex]); return (