diff --git a/app/ui/app/src/components/Chat.tsx b/app/ui/app/src/components/Chat.tsx index 85b51f55b..e415b2051 100644 --- a/app/ui/app/src/components/Chat.tsx +++ b/app/ui/app/src/components/Chat.tsx @@ -100,41 +100,7 @@ export default function Chat({ chatId }: { chatId: string }) { const sendMessageMutation = useSendMessage(chatId); const latestMessageRef = useRef(null); - const prevMessageCountRef = useRef(messages.length); - // Scroll to latest message when messages change - useEffect(() => { - // Only scroll if a new message was actually added (not just re-render) - if (messages.length > prevMessageCountRef.current) { - if (latestMessageRef.current) { - // Find the scrollable parent container - let scrollContainer = latestMessageRef.current.parentElement; - while (scrollContainer) { - const overflowY = window.getComputedStyle(scrollContainer).overflowY; - if (overflowY === "auto" || overflowY === "scroll") { - break; - } - scrollContainer = scrollContainer.parentElement; - } - - if (scrollContainer) { - const containerRect = scrollContainer.getBoundingClientRect(); - const targetRect = latestMessageRef.current.getBoundingClientRect(); - const scrollAmount = - targetRect.top - containerRect.top + scrollContainer.scrollTop; - - scrollContainer.scrollTo({ - top: scrollAmount, - behavior: "smooth", - }); - } - } - } - - prevMessageCountRef.current = messages.length; - }, [messages.length, latestMessageRef]); - - // Simplified submit handler - ChatForm handles all the attachment logic const handleChatFormSubmit = ( message: string, options: { @@ -229,8 +195,9 @@ export default function Chat({ chatId }: { chatId: string }) { - + ; +// Create a context to share the "allow scroll" state +const ConversationControlContext = createContext<{ + allowScroll: () => void; +} | null>(null); -export const Conversation = ({ className, ...props }: ConversationProps) => ( - -); +export type ConversationProps = ComponentProps & { + isStreaming?: boolean; +}; + +export const Conversation = ({ + className, + isStreaming = false, + children, + ...props +}: ConversationProps) => { + const shouldStopScrollRef = useRef(true); + + const allowScroll = useCallback(() => { + shouldStopScrollRef.current = false; + setTimeout(() => { + shouldStopScrollRef.current = true; + }, 100); + }, []); + + return ( + + + + <>{children} + + + + ); +}; + +const ConversationContentInternal = ({ + isStreaming, + shouldStopScrollRef, + children, +}: { + isStreaming: boolean; + shouldStopScrollRef: React.MutableRefObject; + children: React.ReactNode; +}) => { + const { stopScroll } = useStickToBottomContext(); + const stopScrollRef = useRef(stopScroll); + + useEffect(() => { + stopScrollRef.current = stopScroll; + }, [stopScroll]); + + useEffect(() => { + if (!isStreaming) return; + + const interval = setInterval(() => { + if (shouldStopScrollRef.current) { + stopScrollRef.current(); + } + }, 16); + + if (shouldStopScrollRef.current) { + stopScrollRef.current(); + } + + return () => clearInterval(interval); + }, [isStreaming, shouldStopScrollRef]); + + return <>{children}; +}; export type ConversationContentProps = ComponentProps< typeof StickToBottom.Content ->; +> & { + isStreaming?: boolean; +}; export const ConversationContent = ({ className, ...props -}: ConversationContentProps) => ( - -); - -export type ConversationEmptyStateProps = ComponentProps<"div"> & { - title?: string; - description?: string; - icon?: React.ReactNode; +}: ConversationContentProps) => { + return ( + + ); }; -export const ConversationEmptyState = ({ - className, - title = "No messages yet", - description = "Start a conversation to see messages here", - icon, - children, - ...props -}: ConversationEmptyStateProps) => ( -
- {children ?? ( - <> - {icon &&
{icon}
} -
-

{title}

- {description && ( -

{description}

- )} -
- - )} -
-); - export type ConversationScrollButtonProps = ComponentProps<"button">; export const ConversationScrollButton = ({ @@ -73,10 +115,13 @@ export const ConversationScrollButton = ({ ...props }: ConversationScrollButtonProps) => { const { isAtBottom, scrollToBottom } = useStickToBottomContext(); + const context = useContext(ConversationControlContext); const handleScrollToBottom = useCallback(() => { + console.log("scrollToBottom"); + context?.allowScroll(); scrollToBottom(); - }, [scrollToBottom]); + }, [scrollToBottom, context]); return ( !isAtBottom && ( @@ -84,7 +129,7 @@ export const ConversationScrollButton = ({ className={clsx( "absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full z-50", "bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700", - "p-3 shadow-lg hover:shadow-xl transition-all", + "p-1 shadow-lg hover:shadow-xl transition-all", "text-neutral-700 dark:text-neutral-200 hover:scale-105", "hover:cursor-pointer", className, diff --git a/app/ui/app/src/components/ai-elements/index.ts b/app/ui/app/src/components/ai-elements/index.ts index f55210518..51d3b885d 100644 --- a/app/ui/app/src/components/ai-elements/index.ts +++ b/app/ui/app/src/components/ai-elements/index.ts @@ -1,10 +1,8 @@ export { Conversation, ConversationContent, - ConversationEmptyState, ConversationScrollButton, type ConversationProps, type ConversationContentProps, - type ConversationEmptyStateProps, type ConversationScrollButtonProps, } from "./conversation";