diff --git a/application/single_app/static/js/chat/chat-conversations.js b/application/single_app/static/js/chat/chat-conversations.js index 4380ffa0..77dd90e6 100644 --- a/application/single_app/static/js/chat/chat-conversations.js +++ b/application/single_app/static/js/chat/chat-conversations.js @@ -111,6 +111,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 @@ -1670,6 +1685,8 @@ export async function selectConversation(conversationId) { } if (isCollaborativeConversation && window.chatCollaboration?.activateConversation) { + await loadMessages(conversationId); + scrollConversationViewToBottom(); await window.chatCollaboration.activateConversation(conversationId, metadata); } else { window.chatCollaboration?.deactivateConversation?.(); @@ -1677,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-global.js b/application/single_app/static/js/chat/chat-global.js index 0cb42f72..7ea874a4 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 1e58be83..021d3864 100644 --- a/application/single_app/static/js/chat/chat-messages.js +++ b/application/single_app/static/js/chat/chat-messages.js @@ -5117,7 +5117,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 --- @@ -5209,7 +5216,10 @@ export function appendMessage( // Validate image URL before creating img tag if (messageContent && messageContent !== 'null' && messageContent.trim() !== '') { - messageContentHtml = `${isUserUpload ? 'Uploaded' : 'Generated'} Image`; + // 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
`; } @@ -5399,6 +5409,28 @@ export function appendMessage( chatbox.appendChild(messageDiv); hydrateChatWorkspaceAttachmentProgress(messageDiv); + // Attach safe image element and error handler for generated/uploaded images + if (sender === "image") { + 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); + } + } + // Highlight code blocks in the messages messageDiv.querySelectorAll('pre code[class^="language-"]').forEach((block) => { const match = block.className.match(/language-([a-zA-Z0-9]+)/); @@ -5517,7 +5549,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 } @@ -5568,6 +5609,12 @@ export function sendMessage() { updateSendButtonVisibility(); // Keep focus on input userInput.focus(); + + // 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() { @@ -6216,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(); diff --git a/application/single_app/templates/chats.html b/application/single_app/templates/chats.html index 6e3274ce..460a49b1 100644 --- a/application/single_app/templates/chats.html +++ b/application/single_app/templates/chats.html @@ -297,8 +297,20 @@
-
- +
+
+ +
+
diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md index 2cea1494..f9fd1427 100644 --- a/docs/explanation/release_notes.md +++ b/docs/explanation/release_notes.md @@ -4,6 +4,16 @@ For feature-focused and fix-focused drill-downs by version, see [Features by Ver ### **(v0.250.005)** +#### New Features + +* **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)** + #### Bug Fixes * **Admin Settings Save 500 Fix**