diff --git a/jac-gpt-fullstack/hooks/useChat.cl.jac b/jac-gpt-fullstack/hooks/useChat.cl.jac index d5bdb3183..6fd669eaf 100644 --- a/jac-gpt-fullstack/hooks/useChat.cl.jac +++ b/jac-gpt-fullstack/hooks/useChat.cl.jac @@ -52,13 +52,18 @@ def:pub useChat() -> any { } }, [messageCount]); - # Scroll to bottom only when new messages are added (not during streaming updates) + # Scroll latest user message to top when new messages are added (not during streaming updates) useEffect(lambda -> None { currentCount = messages.length; # Only scroll if message count increased (new message added) if currentCount > prevMessageCountRef.current { if messagesEndRef.current { - messagesEndRef.current.scrollIntoView({"behavior": "smooth"}); + # Delay scroll slightly to ensure DOM has settled after rapid state updates + setTimeout(lambda -> None { + if messagesEndRef.current { + messagesEndRef.current.scrollIntoView({"behavior": "smooth", "block": "start"}); + } + }, 50); } } prevMessageCountRef.current = currentCount; diff --git a/jac-gpt-fullstack/pages/JacChatbot.cl.jac b/jac-gpt-fullstack/pages/JacChatbot.cl.jac index 40232ef04..62e81293a 100644 --- a/jac-gpt-fullstack/pages/JacChatbot.cl.jac +++ b/jac-gpt-fullstack/pages/JacChatbot.cl.jac @@ -26,6 +26,8 @@ def:pub JacChatbot() -> any { chat = useChat(); isAuthenticated = jacIsLoggedIn(); scrollAreaRef = useRef(None); + viewportRef = useRef(None); + has spacerHeight: int = 0; # Add CSS animations on mount useEffect(lambda -> None { @@ -52,6 +54,28 @@ def:pub JacChatbot() -> any { } }, []); + # Dynamically calculate spacer height so the latest user message can scroll to top + useEffect(lambda -> None { + viewport = viewportRef.current; + msgEl = chat.messagesEndRef.current; + if not viewport or not msgEl { + if spacerHeight != 0 { + spacerHeight = 0; + } + return; + } + vpHeight = viewport.clientHeight; + # Get message's absolute position within the scroll content + msgTop = msgEl.getBoundingClientRect().top - viewport.getBoundingClientRect().top + viewport.scrollTop; + # Content height without the current spacer + contentHeight = viewport.scrollHeight - spacerHeight; + # Only enough spacer to allow scrollIntoView to bring the message to the top + needed = Math.max(0, Math.ceil(msgTop + vpHeight - contentHeight)); + if needed != spacerHeight { + spacerHeight = needed; + } + }, [chat.messages, spacerHeight]); + def toggleSidebar() -> None { sidebarOpen = not sidebarOpen; } @@ -60,10 +84,22 @@ def:pub JacChatbot() -> any { docPanelOpen = not docPanelOpen; } + # Find last user message id for scroll targeting + lastUserMsgId = None; + for msg in chat.messages { + if msg.isUser { + lastUserMsgId = msg.id; + } + } + # Build message items messageItems = []; for msg in chat.messages { - messageItems.push(
+ msgRef = None; + if msg.id == lastUserMsgId { + msgRef = chat.messagesEndRef; + } + messageItems.push(
any { mainContent = None; if hasMessages { # Active conversation - messages area - mainContent = + mainContent =
any { }}> {messageItems} {loadingContent} +
-
; } else {