Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions jac-gpt-fullstack/hooks/useChat.cl.jac
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
42 changes: 39 additions & 3 deletions jac-gpt-fullstack/pages/JacChatbot.cl.jac
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
Expand All @@ -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(<div key={msg.id} style={{"animation": "fadeIn 0.3s ease"}}>
msgRef = None;
if msg.id == lastUserMsgId {
msgRef = chat.messagesEndRef;
}
messageItems.push(<div key={msg.id} ref={msgRef} style={{"animation": "fadeIn 0.3s ease"}}>
<ChatMessage
message={msg.content}
isUser={msg.isUser}
Expand Down Expand Up @@ -128,7 +164,7 @@ def:pub JacChatbot() -> any {
mainContent = None;
if hasMessages {
# Active conversation - messages area
mainContent = <ScrollArea ref={scrollAreaRef} style={{"flex": "1", "paddingLeft": "16px", "paddingRight": "16px"}} type="scroll">
mainContent = <ScrollArea ref={scrollAreaRef} viewportRef={viewportRef} style={{"flex": "1", "paddingLeft": "16px", "paddingRight": "16px"}} type="scroll">
<div style={{
"maxWidth": "768px",
"margin": "0 auto",
Expand All @@ -144,8 +180,8 @@ def:pub JacChatbot() -> any {
}}>
{messageItems}
{loadingContent}
<div style={{"height": String(spacerHeight) + "px"}} />
</div>
<div ref={chat.messagesEndRef} />
</div>
</ScrollArea>;
} else {
Expand Down