import { useEffect, useMemo, useRef, useState } from 'react' import { createFileRoute, useParams } from '@tanstack/react-router' import { UIEventHandler } from 'react' import debounce from 'lodash.debounce' import { cn } from '@/lib/utils' import { ArrowDown } from 'lucide-react' import HeaderPage from '@/containers/HeaderPage' import { useThreads } from '@/hooks/useThreads' import ChatInput from '@/containers/ChatInput' import { useShallow } from 'zustand/react/shallow' import { ThreadContent } from '@/containers/ThreadContent' import { StreamingContent } from '@/containers/StreamingContent' import { useMessages } from '@/hooks/useMessages' import { fetchMessages } from '@/services/messages' import { useAppState } from '@/hooks/useAppState' import DropdownAssistant from '@/containers/DropdownAssistant' import { useAssistant } from '@/hooks/useAssistant' import { useAppearance } from '@/hooks/useAppearance' import { useTranslation } from '@/i18n/react-i18next-compat' // as route.threadsDetail export const Route = createFileRoute('/threads/$threadId')({ component: ThreadDetail, }) function ThreadDetail() { const { t } = useTranslation() const { threadId } = useParams({ from: Route.id }) const [isUserScrolling, setIsUserScrolling] = useState(false) const [isAtBottom, setIsAtBottom] = useState(true) const [hasScrollbar, setHasScrollbar] = useState(false) const lastScrollTopRef = useRef(0) const { currentThreadId, setCurrentThreadId } = useThreads() const { setCurrentAssistant, assistants } = useAssistant() const { setMessages } = useMessages() const { streamingContent } = useAppState() const { appMainViewBgColor, chatWidth } = useAppearance() const { messages } = useMessages( useShallow((state) => ({ messages: state.messages[threadId], })) ) // Subscribe directly to the thread data to ensure updates when model changes const thread = useThreads(useShallow((state) => state.threads[threadId])) const scrollContainerRef = useRef(null) const isFirstRender = useRef(true) const messagesCount = useMemo(() => messages?.length ?? 0, [messages]) // Function to check scroll position and scrollbar presence const checkScrollState = () => { const scrollContainer = scrollContainerRef.current if (!scrollContainer) return const { scrollTop, scrollHeight, clientHeight } = scrollContainer const isBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10 const hasScroll = scrollHeight > clientHeight setIsAtBottom(isBottom) setHasScrollbar(hasScroll) } useEffect(() => { if (currentThreadId !== threadId) { setCurrentThreadId(threadId) const assistant = assistants.find( (assistant) => assistant.id === thread?.assistants?.[0]?.id ) if (assistant) setCurrentAssistant(assistant) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [threadId, currentThreadId, assistants]) useEffect(() => { fetchMessages(threadId).then((fetchedMessages) => { if (fetchedMessages) { // Update the messages in the store setMessages(threadId, fetchedMessages) } }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [threadId]) useEffect(() => { return () => { // Clear the current thread ID when the component unmounts setCurrentThreadId(undefined) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // Auto-scroll to bottom when component mounts or thread content changes useEffect(() => { const scrollContainer = scrollContainerRef.current if (!scrollContainer) return // Always scroll to bottom on first render or when thread changes if (isFirstRender.current) { isFirstRender.current = false scrollToBottom() setIsAtBottom(true) setIsUserScrolling(false) checkScrollState() return } }, []) // Reset scroll state when thread changes useEffect(() => { isFirstRender.current = true scrollToBottom() setIsAtBottom(true) setIsUserScrolling(false) checkScrollState() }, [threadId]) // Single useEffect for all auto-scrolling logic useEffect(() => { // Only auto-scroll when the user is not actively scrolling // AND either at the bottom OR there's streaming content if (!isUserScrolling && (streamingContent || isAtBottom) && messagesCount) { // Use non-smooth scrolling for auto-scroll to prevent jank scrollToBottom(false) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [streamingContent, isUserScrolling, messagesCount]) useEffect(() => { if (streamingContent) { const interval = setInterval(checkScrollState, 100) return () => clearInterval(interval) } }, [streamingContent]) const scrollToBottom = (smooth = false) => { if (scrollContainerRef.current) { scrollContainerRef.current.scrollTo({ top: scrollContainerRef.current.scrollHeight, ...(smooth ? { behavior: 'smooth' } : {}), }) } } const handleScroll: UIEventHandler = (e) => { const target = e.target as HTMLDivElement const { scrollTop, scrollHeight, clientHeight } = target // Use a small tolerance to better detect when we're at the bottom const isBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10 const hasScroll = scrollHeight > clientHeight // Detect if this is a user-initiated scroll if (Math.abs(scrollTop - lastScrollTopRef.current) > 10) { setIsUserScrolling(!isBottom) } setIsAtBottom(isBottom) setHasScrollbar(hasScroll) lastScrollTopRef.current = scrollTop } // Separate handler for DOM events const handleDOMScroll = (e: Event) => { const target = e.target as HTMLDivElement const { scrollTop, scrollHeight, clientHeight } = target // Use a small tolerance to better detect when we're at the bottom const isBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10 const hasScroll = scrollHeight > clientHeight // Detect if this is a user-initiated scroll if (Math.abs(scrollTop - lastScrollTopRef.current) > 10) { setIsUserScrolling(!isBottom) } setIsAtBottom(isBottom) setHasScrollbar(hasScroll) lastScrollTopRef.current = scrollTop } // Use a shorter debounce time for more responsive scrolling const debouncedScroll = debounce(handleDOMScroll) useEffect(() => { const chatHistoryElement = scrollContainerRef.current if (chatHistoryElement) { chatHistoryElement.addEventListener('scroll', debouncedScroll) return () => chatHistoryElement.removeEventListener('scroll', debouncedScroll) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const threadModel = useMemo(() => thread?.model, [thread]) if (!messages || !threadModel) return null return (
{messages && messages.map((item, index) => { // Only pass isLastMessage to the last message in the array const isLastMessage = index === messages.length - 1 return (
) })}
{ scrollToBottom(true) setIsUserScrolling(false) }} >

{t('scrollToBottom')}

) }