From 4f3939c83ec0157c5632a08c102e154d7cc14873 Mon Sep 17 00:00:00 2001 From: Chad Palmer Date: Tue, 23 Jun 2026 22:40:26 +0000 Subject: [PATCH 1/5] fix-508-scroll - Updated js and template so that users aren't forced to the bottom suddenly when answer is done streaming. --- application/single_app/config.py | 2 +- .../single_app/static/js/chat/chat-global.js | 70 +++++++++++++++++-- .../static/js/chat/chat-messages.js | 24 +++++-- application/single_app/templates/chats.html | 16 ++++- docs/explanation/release_notes.md | 10 +++ 5 files changed, 111 insertions(+), 11 deletions(-) diff --git a/application/single_app/config.py b/application/single_app/config.py index 30cb1b95f..b3419b568 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -94,7 +94,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.241.007" +VERSION = "0.241.008" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') diff --git a/application/single_app/static/js/chat/chat-global.js b/application/single_app/static/js/chat/chat-global.js index 0cb42f724..7ea874a46 100644 --- a/application/single_app/static/js/chat/chat-global.js +++ b/application/single_app/static/js/chat/chat-global.js @@ -9,9 +9,71 @@ let groupPrompts = []; let publicPrompts = []; let currentlyEditingId = null; +function getChatScrollContainer() { + return ( + document.getElementById("chat-messages-container") || + document.getElementById("chatbox") || + null + ); +} + +function isChatNearBottom(threshold = 40) { + const container = getChatScrollContainer(); + if (!container) return true; + + const distanceFromBottom = + container.scrollHeight - (container.scrollTop + container.clientHeight); + return distanceFromBottom <= threshold; +} + function scrollChatToBottom() { - const chatbox = document.getElementById("chatbox"); - if (chatbox) { - chatbox.scrollTop = chatbox.scrollHeight; + const container = getChatScrollContainer(); + if (container) { + container.scrollTop = container.scrollHeight; } -} \ No newline at end of file +} + +function showScrollToBottomButton() { + const btn = document.getElementById("scroll-to-bottom-btn"); + if (!btn) return; + btn.classList.remove("d-none"); +} + +function hideScrollToBottomButton() { + const btn = document.getElementById("scroll-to-bottom-btn"); + if (!btn) return; + btn.classList.add("d-none"); +} + +function initializeChatScrollBehavior() { + const container = getChatScrollContainer(); + const btn = document.getElementById("scroll-to-bottom-btn"); + + if (!container) return; + + // Initial state + if (isChatNearBottom()) { + hideScrollToBottomButton(); + } else { + showScrollToBottomButton(); + } + + container.addEventListener("scroll", () => { + if (isChatNearBottom()) { + hideScrollToBottomButton(); + } else { + showScrollToBottomButton(); + } + }); + + if (btn) { + btn.addEventListener("click", () => { + scrollChatToBottom(); + hideScrollToBottomButton(); + }); + } +} + +window.addEventListener("load", () => { + initializeChatScrollBehavior(); +}); \ No newline at end of file diff --git a/application/single_app/static/js/chat/chat-messages.js b/application/single_app/static/js/chat/chat-messages.js index c904639d6..40357ca34 100644 --- a/application/single_app/static/js/chat/chat-messages.js +++ b/application/single_app/static/js/chat/chat-messages.js @@ -1036,7 +1036,14 @@ export function appendMessage( }); } - scrollChatToBottom(); + // For AI messages, only auto-scroll if the user is currently near + // the bottom. This prevents a final jump after a long answer if + // the user has scrolled up to read earlier content. + if (typeof isChatNearBottom === 'function' && typeof scrollChatToBottom === 'function') { + if (isChatNearBottom()) { + scrollChatToBottom(); + } + } return; // <<< EXIT EARLY FOR AI MESSAGES // --- Handle ALL OTHER message types --- @@ -1106,7 +1113,7 @@ export function appendMessage( // Validate image URL before creating img tag if (messageContent && messageContent !== 'null' && messageContent.trim() !== '') { - messageContentHtml = `${isUserUpload ? 'Uploaded' : 'Generated'} Image`; + messageContentHtml = `${isUserUpload ? 'Uploaded' : 'Generated'} Image`; } else { messageContentHtml = `
Failed to ${isUserUpload ? 'load' : 'generate'} image - invalid response from image service
`; } @@ -1378,7 +1385,16 @@ export function appendMessage( } } - scrollChatToBottom(); + // For new user/file/image messages, scroll to bottom once so the + // user sees what they just sent. For history loads, only scroll + // if they are already near the bottom. + if (isNewMessage && typeof scrollChatToBottom === 'function') { + scrollChatToBottom(); + } else if (typeof isChatNearBottom === 'function' && typeof scrollChatToBottom === 'function') { + if (isChatNearBottom()) { + scrollChatToBottom(); + } + } } // End of the large 'else' block for non-AI messages } @@ -1436,7 +1452,7 @@ export function actuallySendMessage(finalMessageToSend) { const tempUserMessageId = `temp_user_${Date.now()}`; // Append user message first with temporary ID - appendMessage("You", finalMessageToSend, null, tempUserMessageId); + appendMessage("You", finalMessageToSend, null, tempUserMessageId, false, [], [], [], null, null, null, true); userInput.value = ""; userInput.style.height = ""; // Update send button visibility after clearing input diff --git a/application/single_app/templates/chats.html b/application/single_app/templates/chats.html index dfcb11385..fd4ac4d3e 100644 --- a/application/single_app/templates/chats.html +++ b/application/single_app/templates/chats.html @@ -267,8 +267,20 @@
-
- +
+
+ +
+
diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md index db17633db..8e93d98f6 100644 --- a/docs/explanation/release_notes.md +++ b/docs/explanation/release_notes.md @@ -4,6 +4,16 @@ This page tracks notable Simple Chat releases and organizes the detailed change For feature-focused and fix-focused drill-downs by version, see [Features by Version](/explanation/features/) and [Fixes by Version](/explanation/fixes/). +### **(v0.241.008)** + +#### User Interface Enhancements + +* **Chat Scroll Behavior and 508 Usability** + * Updated chat message rendering so the viewport no longer jumps to the very bottom of long assistant responses when they finish loading while the user is reading near the top. + * Auto-scroll now only occurs when the user is already near the bottom of the conversation, and a floating "scroll to latest message" button appears when new content arrives below the current view. + * This aligns the chat experience more closely with other AI chat tools and reduces unexpected motion for 508 testers and keyboard users. + * (Ref: `chats.html`, `chat-global.js`, `chat-messages.js`) + ### **(v0.241.007)** ## New Feature From 9fc5d6d88f5716d40f69de98a5bfd4a1716302d1 Mon Sep 17 00:00:00 2001 From: Chad Palmer Date: Wed, 24 Jun 2026 19:06:22 +0000 Subject: [PATCH 2/5] fix-508-scroll - Fixed xss-sink-check issue. --- .../single_app/static/js/chat/chat-messages.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/application/single_app/static/js/chat/chat-messages.js b/application/single_app/static/js/chat/chat-messages.js index 40357ca34..4846841e7 100644 --- a/application/single_app/static/js/chat/chat-messages.js +++ b/application/single_app/static/js/chat/chat-messages.js @@ -1113,7 +1113,9 @@ export function appendMessage( // Validate image URL before creating img tag if (messageContent && messageContent !== 'null' && messageContent.trim() !== '') { - messageContentHtml = `${isUserUpload ? 'Uploaded' : 'Generated'} Image`; + const safeSrc = escapeHtml(messageContent); + const safeAlt = escapeHtml(isUserUpload ? 'Uploaded Image' : 'Generated Image'); + messageContentHtml = `${safeAlt}`; } else { messageContentHtml = `
Failed to ${isUserUpload ? 'load' : 'generate'} image - invalid response from image service
`; } @@ -1263,6 +1265,17 @@ export function appendMessage( // Append and scroll (common actions for non-AI) chatbox.appendChild(messageDiv); + // Attach safe error handler for generated/uploaded images + if (sender === "image") { + const imgEl = messageDiv.querySelector('img.generated-image'); + if (imgEl) { + imgEl.addEventListener('error', () => { + imgEl.src = '/static/images/image-error.png'; + imgEl.alt = 'Failed to load image'; + }); + } + } + // Highlight code blocks in the messages messageDiv.querySelectorAll('pre code[class^="language-"]').forEach((block) => { const match = block.className.match(/language-([a-zA-Z0-9]+)/); From 178f26be539fc7fc066ca302017a025db835b30f Mon Sep 17 00:00:00 2001 From: Chad Palmer Date: Wed, 24 Jun 2026 19:17:54 +0000 Subject: [PATCH 3/5] fix-508-scroll - Fixed another xss-sink-check issue. --- .../static/js/chat/chat-messages.js | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/application/single_app/static/js/chat/chat-messages.js b/application/single_app/static/js/chat/chat-messages.js index 4846841e7..7a6bb7586 100644 --- a/application/single_app/static/js/chat/chat-messages.js +++ b/application/single_app/static/js/chat/chat-messages.js @@ -1113,9 +1113,10 @@ export function appendMessage( // Validate image URL before creating img tag if (messageContent && messageContent !== 'null' && messageContent.trim() !== '') { - const safeSrc = escapeHtml(messageContent); - const safeAlt = escapeHtml(isUserUpload ? 'Uploaded Image' : 'Generated Image'); - messageContentHtml = `${safeAlt}`; + // Use a placeholder container; the actual element will be + // created with DOM APIs after insertion to avoid string-based + // attribute interpolation in src/data-*. + messageContentHtml = ''; } else { messageContentHtml = `
Failed to ${isUserUpload ? 'load' : 'generate'} image - invalid response from image service
`; } @@ -1265,14 +1266,25 @@ export function appendMessage( // Append and scroll (common actions for non-AI) chatbox.appendChild(messageDiv); - // Attach safe error handler for generated/uploaded images + // Attach safe image element and error handler for generated/uploaded images if (sender === "image") { - const imgEl = messageDiv.querySelector('img.generated-image'); - if (imgEl) { + const placeholder = messageDiv.querySelector('.generated-image-placeholder'); + if (placeholder && messageContent && messageContent !== 'null' && messageContent.trim() !== '') { + const imgEl = document.createElement('img'); + imgEl.className = 'generated-image'; + imgEl.style.width = '170px'; + imgEl.style.height = '170px'; + imgEl.style.cursor = 'pointer'; + imgEl.src = messageContent; + imgEl.alt = isUserUpload ? 'Uploaded Image' : 'Generated Image'; + imgEl.dataset.imageSrc = messageContent; + imgEl.addEventListener('error', () => { imgEl.src = '/static/images/image-error.png'; imgEl.alt = 'Failed to load image'; }); + + placeholder.replaceWith(imgEl); } } From f9d7c28de1c346c09defe04267d5290b797a9812 Mon Sep 17 00:00:00 2001 From: Chad Palmer Date: Tue, 30 Jun 2026 21:10:34 +0000 Subject: [PATCH 4/5] fix-508-scroll - Added function to scroll to bottom on message load. --- .../static/js/chat/chat-conversations.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/application/single_app/static/js/chat/chat-conversations.js b/application/single_app/static/js/chat/chat-conversations.js index 560571e92..ade68da60 100644 --- a/application/single_app/static/js/chat/chat-conversations.js +++ b/application/single_app/static/js/chat/chat-conversations.js @@ -85,6 +85,21 @@ async function refreshAgentsAndModelsForActiveConversation() { await refreshAgentsForActiveConversation(); await refreshModelSelection(); } + +function scrollConversationViewToBottom() { + const container = document.getElementById("chat-messages-container") || document.getElementById("chatbox"); + if (!container) { + return; + } + // Use animation frames so layout-dependent heights settle first. + requestAnimationFrame(() => { + container.scrollTop = container.scrollHeight; + requestAnimationFrame(() => { + container.scrollTop = container.scrollHeight; + }); + }); +} + let selectionModeActive = false; // Track if selection mode is active let selectionModeTimer = null; // Timer for auto-hiding checkboxes let showHiddenConversations = false; // Track if hidden conversations should be shown @@ -1336,9 +1351,11 @@ export async function selectConversation(conversationId) { } await loadMessages(conversationId); + scrollConversationViewToBottom(); try { const streamingModule = await import('./chat-streaming.js'); await streamingModule.reattachStreamingConversation(conversationId); + scrollConversationViewToBottom(); } catch (error) { console.warn('Failed to reattach active stream for conversation:', error); } From b30b8427087ee42f52b0a5f4d73d148ce9d54c63 Mon Sep 17 00:00:00 2001 From: Chad Palmer Date: Tue, 30 Jun 2026 22:55:54 +0000 Subject: [PATCH 5/5] fix-508-scroll - Fixed issues that arose after merge. --- .../static/js/chat/chat-conversations.js | 11 +++-------- .../static/js/chat/chat-messages.js | 19 +++++++------------ 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/application/single_app/static/js/chat/chat-conversations.js b/application/single_app/static/js/chat/chat-conversations.js index bf7e79571..77dd90e67 100644 --- a/application/single_app/static/js/chat/chat-conversations.js +++ b/application/single_app/static/js/chat/chat-conversations.js @@ -1684,15 +1684,9 @@ export async function selectConversation(conversationId) { renderConversationHeaderBadges(convoItem); } - await loadMessages(conversationId); - scrollConversationViewToBottom(); - try { - const streamingModule = await import('./chat-streaming.js'); - await streamingModule.reattachStreamingConversation(conversationId); - scrollConversationViewToBottom(); - } catch (error) { - console.warn('Failed to reattach active stream for conversation:', error); if (isCollaborativeConversation && window.chatCollaboration?.activateConversation) { + await loadMessages(conversationId); + scrollConversationViewToBottom(); await window.chatCollaboration.activateConversation(conversationId, metadata); } else { window.chatCollaboration?.deactivateConversation?.(); @@ -1700,6 +1694,7 @@ export async function selectConversation(conversationId) { try { const streamingModule = await import('./chat-streaming.js'); await streamingModule.reattachStreamingConversation(conversationId); + scrollConversationViewToBottom(); } catch (error) { console.warn('Failed to reattach active stream for conversation:', error); } diff --git a/application/single_app/static/js/chat/chat-messages.js b/application/single_app/static/js/chat/chat-messages.js index 6e873957f..021d38646 100644 --- a/application/single_app/static/js/chat/chat-messages.js +++ b/application/single_app/static/js/chat/chat-messages.js @@ -5609,18 +5609,13 @@ export function sendMessage() { updateSendButtonVisibility(); // Keep focus on input userInput.focus(); -} -export function actuallySendMessage(finalMessageToSend) { - // Generate a temporary message ID for the user message - const tempUserMessageId = `temp_user_${Date.now()}`; - - // Append user message first with temporary ID - appendMessage("You", finalMessageToSend, null, tempUserMessageId, false, [], [], [], null, null, null, true); - userInput.value = ""; - userInput.style.height = ""; - // Update send button visibility after clearing input - updateSendButtonVisibility(); + // After sending, ensure the chat view scrolls so the + // user can see their newly submitted message. + if (typeof window.scrollChatToBottom === 'function') { + window.scrollChatToBottom(); + } +} function getCurrentModelSelection() { let modelDeployment = modelSelect?.value; @@ -6268,7 +6263,7 @@ export function actuallySendMessage(finalMessageToSend) { } const pendingCollaborativeContext = window.chatCollaboration?.getPendingMessageContext?.({ invocationTarget }) || null; - appendMessage("You", displayMessageText, null, tempUserMessageId, false, [], [], [], null, null, pendingCollaborativeContext); + appendMessage("You", displayMessageText, null, tempUserMessageId, false, [], [], [], null, null, pendingCollaborativeContext, true); userInput.value = ""; userInput.style.height = ""; updateSendButtonVisibility();