From def9651b55b69c9c0ee5f369d1811d08652c78e3 Mon Sep 17 00:00:00 2001 From: Eva Ho Date: Wed, 12 Nov 2025 22:05:52 -0500 Subject: [PATCH] adding space at the end to push the content up when new message --- app/ui/app/src/components/Chat.tsx | 11 +- app/ui/app/src/components/MessageList.tsx | 5 + .../components/ai-elements/conversation.tsx | 237 +++++++++++++++++- 3 files changed, 244 insertions(+), 9 deletions(-) diff --git a/app/ui/app/src/components/Chat.tsx b/app/ui/app/src/components/Chat.tsx index e415b2051..02d152067 100644 --- a/app/ui/app/src/components/Chat.tsx +++ b/app/ui/app/src/components/Chat.tsx @@ -5,10 +5,10 @@ import { DisplayUpgrade } from "./DisplayUpgrade"; import { DisplayStale } from "./DisplayStale"; import { DisplayLogin } from "./DisplayLogin"; import { - Conversation, ConversationContent, ConversationScrollButton, -} from "./ai-elements"; + ConversationWithSpacer, +} from "./ai-elements/conversation"; import { useChat, useSendMessage, @@ -192,10 +192,11 @@ export default function Chat({ chatId }: { chatId: string }) { ) : (
- - +
{selectedModel && shouldShowStaleDisplay && ( diff --git a/app/ui/app/src/components/MessageList.tsx b/app/ui/app/src/components/MessageList.tsx index 5953634a6..cce801c4a 100644 --- a/app/ui/app/src/components/MessageList.tsx +++ b/app/ui/app/src/components/MessageList.tsx @@ -3,6 +3,7 @@ import React from "react"; import Message from "./Message"; import Downloading from "./Downloading"; import { ErrorMessage } from "./ErrorMessage"; +import { ConversationSpacer } from "./ai-elements/conversation"; export default function MessageList({ messages, @@ -94,6 +95,7 @@ export default function MessageList({
)} + + {/* Dynamic spacer */} +
); } diff --git a/app/ui/app/src/components/ai-elements/conversation.tsx b/app/ui/app/src/components/ai-elements/conversation.tsx index f1a5e3707..80ec1f07d 100644 --- a/app/ui/app/src/components/ai-elements/conversation.tsx +++ b/app/ui/app/src/components/ai-elements/conversation.tsx @@ -8,12 +8,15 @@ import { useRef, createContext, useContext, + useState, } from "react"; import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom"; -// Create a context to share the "allow scroll" state +// Create a context to share the "allow scroll" state and spacer state const ConversationControlContext = createContext<{ allowScroll: () => void; + spacerHeight: number; + setSpacerHeight: (height: number) => void; } | null>(null); export type ConversationProps = ComponentProps & { @@ -27,6 +30,7 @@ export const Conversation = ({ ...props }: ConversationProps) => { const shouldStopScrollRef = useRef(true); + const [spacerHeight, setSpacerHeight] = useState(0); const allowScroll = useCallback(() => { shouldStopScrollRef.current = false; @@ -36,7 +40,9 @@ export const Conversation = ({ }, []); return ( - + { + return ( + + + <>{children} + + ); +}; + +// This component manages the spacer state but doesn't render the spacer itself +const SpacerController = ({ + isStreaming, + messageCount, +}: { + isStreaming: boolean; + messageCount: number; +}) => { + const context = useContext(ConversationControlContext); + const { scrollToBottom } = useStickToBottomContext(); + const previousMessageCountRef = useRef(messageCount); + const scrollContainerRef = useRef(null); + const [isActiveInteraction, setIsActiveInteraction] = useState(false); + + // Get reference to scroll container + useEffect(() => { + const container = document.querySelector('[role="log"]') as HTMLElement; + scrollContainerRef.current = container; + }, []); + + // Calculate spacer height based on actual DOM elements + const calculateSpacerHeight = useCallback(() => { + const container = scrollContainerRef.current; + if (!container) { + console.log("❌ No container"); + return 0; + } + + const containerHeight = container.clientHeight; + + // Find all message elements + const messageElements = container.querySelectorAll( + "[data-message-index]", + ) as NodeListOf; + console.log("📝 Found", messageElements.length, "message elements"); + + if (messageElements.length === 0) return 0; + + // Log all messages and their roles + messageElements.forEach((el, i) => { + const role = el.getAttribute("data-message-role"); + console.log(` Message ${i}: role="${role}"`); + }); + + // Find the last user message + let lastUserMessageElement: HTMLElement | null = null; + let lastUserMessageIndex = -1; + + for (let i = messageElements.length - 1; i >= 0; i--) { + const el = messageElements[i]; + const role = el.getAttribute("data-message-role"); + if (role === "user") { + lastUserMessageElement = el; + lastUserMessageIndex = i; + console.log("✅ Found user message at index:", i); + break; + } + } + + if (!lastUserMessageElement) { + console.log("❌ No user message found!"); + return 0; + } + + // Calculate height of content after the last user message + let contentHeightAfter = 0; + for (let i = lastUserMessageIndex + 1; i < messageElements.length; i++) { + contentHeightAfter += messageElements[i].offsetHeight; + } + + const userMessageHeight = lastUserMessageElement.offsetHeight; + + // Goal: Position user message at the top with some padding + // We want the user message to start at around 10% from the top of viewport + const targetTopPosition = containerHeight * 0.05; // 10% from top + + // Calculate spacer: we need enough space so that when scrolled to bottom: + // spacerHeight = containerHeight - targetTopPosition - userMessageHeight - contentAfter + const calculatedHeight = + containerHeight - + targetTopPosition - + userMessageHeight - + contentHeightAfter; + + const baseHeight = Math.max(0, calculatedHeight); + + console.log( + "📊 Container:", + containerHeight, + "User msg:", + userMessageHeight, + "Content after:", + contentHeightAfter, + "Target top pos:", + targetTopPosition, + "→ Final spacer:", + baseHeight, + ); + + return baseHeight; + }, []); + + // When a new message is submitted, set initial spacer height and scroll + useEffect(() => { + if (messageCount > previousMessageCountRef.current) { + console.log("🎯 NEW MESSAGE - Setting spacer"); + // Allow scrolling by temporarily disabling stopScroll + context?.allowScroll(); + setIsActiveInteraction(true); + + const container = scrollContainerRef.current; + + if (container) { + // Wait for new message to render in DOM + requestAnimationFrame(() => { + requestAnimationFrame(() => { + const spacerHeight = calculateSpacerHeight(); + console.log("📏 Calculated spacer:", spacerHeight); + context?.setSpacerHeight(spacerHeight); + + // Wait for spacer to be added to DOM, then scroll + requestAnimationFrame(() => { + requestAnimationFrame(() => { + // Use the library's scrollToBottom method + console.log("📜 Scrolling to bottom"); + scrollToBottom("instant"); + console.log("📜 Final scrollTop:", container.scrollTop); + }); + }); + }); + }); + } + } + previousMessageCountRef.current = messageCount; + }, [messageCount, context, calculateSpacerHeight, scrollToBottom]); + + // Update active interaction state + useEffect(() => { + if (isStreaming) { + setIsActiveInteraction(true); + } + // Don't automatically set to false when streaming stops + // Let the ResizeObserver handle clearing the spacer naturally + }, [isStreaming]); + + // Use ResizeObserver to recalculate spacer as content changes + useEffect(() => { + const container = scrollContainerRef.current; + if (!container || !isActiveInteraction) return; + + const resizeObserver = new ResizeObserver(() => { + const newHeight = calculateSpacerHeight(); + context?.setSpacerHeight(newHeight); + + // Clear active interaction when spacer reaches 0 + if (newHeight === 0) { + setIsActiveInteraction(false); + } + }); + + // Observe all message elements + const messageElements = container.querySelectorAll("[data-message-index]"); + messageElements.forEach((element) => { + resizeObserver.observe(element); + }); + + return () => { + resizeObserver.disconnect(); + }; + }, [isActiveInteraction, calculateSpacerHeight, context]); + + // Remove the effect that clears spacer when not streaming + // This was causing the spacer to disappear prematurely + + return null; +}; + const ConversationContentInternal = ({ isStreaming, shouldStopScrollRef, @@ -98,12 +301,36 @@ export type ConversationContentProps = ComponentProps< export const ConversationContent = ({ className, + children, ...props }: ConversationContentProps) => { return ( + {children} + + ); +}; + +// Spacer component that can be placed anywhere in your content +export const ConversationSpacer = () => { + const context = useContext(ConversationControlContext); + const spacerHeight = context?.spacerHeight ?? 0; + + console.log("🎨 Spacer render - height:", spacerHeight); + + if (spacerHeight === 0) return null; + + return ( +