diff --git a/src/frontend/web/von_interface/static/js/chatTab.js b/src/frontend/web/von_interface/static/js/chatTab.js index 2da77749..47c3c699 100644 --- a/src/frontend/web/von_interface/static/js/chatTab.js +++ b/src/frontend/web/von_interface/static/js/chatTab.js @@ -30957,6 +30957,17 @@ function ensureScrollToEndButton(scrollableField = null) { return null; } + // A loading/empty transcript may still inherit page overflow from the + // surrounding composer and navigation. Do not create a latest-message + // affordance until there is an actual transcript item to navigate to. + const hasTranscriptContent = targetField.querySelector( + ':scope > .message-container, :scope > .chat-message' + ); + if (!hasTranscriptContent) { + document.getElementById(CHAT_SCROLL_TO_END_BUTTON_ID)?.remove(); + return null; + } + const wrapper = targetField.closest('.content-wrapper') || targetField.parentElement; let controlHost = wrapper?.querySelector('.chat-composer') || wrapper; if (!isCompactComposer()) { diff --git a/src/frontend/web/von_interface/static/js/test/chatTab.test.js b/src/frontend/web/von_interface/static/js/test/chatTab.test.js index 553d8fa0..f325143b 100644 --- a/src/frontend/web/von_interface/static/js/test/chatTab.test.js +++ b/src/frontend/web/von_interface/static/js/test/chatTab.test.js @@ -12904,7 +12904,7 @@ describe('scroll to latest message affordance', () => {
-
+
Existing message
`; @@ -12933,6 +12933,43 @@ describe('scroll to latest message affordance', () => { expect(button.getAttribute('aria-hidden')).toBe('true'); }); + test.each([ + ['truly empty', ''], + ['loading', '
Loading conversations…
'] + ])('does not render the latest control for an %s conversation', (_label, content) => { + const scrollableField = document.getElementById('scrollableField'); + scrollableField.innerHTML = content; + + __testOnly_updateScrollToEndButtonVisibility(scrollableField); + + expect(document.getElementById('chatScrollToEndButton')).toBeNull(); + }); + + test('renders the control after a streamed message arrives in an empty conversation', () => { + const scrollableField = document.getElementById('scrollableField'); + scrollableField.innerHTML = ''; + __testOnly_updateScrollToEndButtonVisibility(scrollableField); + expect(document.getElementById('chatScrollToEndButton')).toBeNull(); + + const message = document.createElement('div'); + message.className = 'chat-message assistant-message'; + scrollableField.appendChild(message); + __testOnly_updateScrollToEndButtonVisibility(scrollableField); + + expect(document.getElementById('chatScrollToEndButton')).not.toBeNull(); + }); + + test('removes a stale control when pagination or conversation replacement leaves no messages', () => { + const scrollableField = document.getElementById('scrollableField'); + __testOnly_updateScrollToEndButtonVisibility(scrollableField); + expect(document.getElementById('chatScrollToEndButton')).not.toBeNull(); + + scrollableField.innerHTML = '
Loading older messages…
'; + __testOnly_updateScrollToEndButtonVisibility(scrollableField); + + expect(document.getElementById('chatScrollToEndButton')).toBeNull(); + }); + test('clicking floating control scrolls to the latest message', () => { jest.useFakeTimers();