diff --git a/extension/demo/chat-poc/app.js b/extension/demo/chat-poc/app.js index 2eb038a..a76e134 100644 --- a/extension/demo/chat-poc/app.js +++ b/extension/demo/chat-poc/app.js @@ -30,6 +30,12 @@ * See README.md and the Web Agent API spec for more details. */ +// ============================================================================= +// Constants +// ============================================================================= + +const BOTTOM_STICKY_THRESHOLD = 8; // Pixels from bottom to auto-stick + // ============================================================================= // State // ============================================================================= @@ -39,6 +45,7 @@ let messages = []; let useTools = true; let isProcessing = false; let availableTools = []; +let stickToBottom = true; // ============================================================================= // DOM Elements @@ -66,6 +73,8 @@ const toolsModalClose = document.getElementById('tools-modal-close'); const toolsModalContent = document.getElementById('tools-modal-content'); const toolsModalCount = document.getElementById('tools-modal-count'); +const jumpToLatestBtn = document.getElementById('jump-to-latest'); + // ============================================================================= // Status Checking // ============================================================================= @@ -336,16 +345,53 @@ function removeThinking() { if (el) el.remove(); } -function scrollToBottom() { +function scrollToBottom(force = false) { + if (!force && !stickToBottom) return; chatContainer.scrollTop = chatContainer.scrollHeight; } +function setStickToBottom(nowStuck) { + if (nowStuck === stickToBottom) return; + stickToBottom = nowStuck; + jumpToLatestBtn.classList.toggle('visible', !nowStuck); +} + +chatContainer.addEventListener('scroll', () => { + const distance = chatContainer.scrollHeight - chatContainer.scrollTop - chatContainer.clientHeight; + setStickToBottom(distance < BOTTOM_STICKY_THRESHOLD); +}, { passive: true }); + +function hasScrollableContent() { + return chatContainer.scrollHeight > chatContainer.clientHeight; +} + +// Catch user intent BEFORE the scroll event lands — otherwise a token arriving +// in the same tick can call scrollToBottom() and clobber the user's wheel before +// our scroll listener ever sees the new position. +chatContainer.addEventListener('wheel', (e) => { + if (e.deltaY < 0 && hasScrollableContent()) setStickToBottom(false); +}, { passive: true }); + +chatContainer.addEventListener('keydown', (e) => { + if ((e.key === 'ArrowUp' || e.key === 'PageUp' || e.key === 'Home') && hasScrollableContent()) { + setStickToBottom(false); + } +}); + +jumpToLatestBtn.addEventListener('click', () => { + scrollToBottom(true); + setStickToBottom(true); +}); + function clearChat() { messages.length = 0; messagesEl.innerHTML = ''; messagesEl.appendChild(emptyState); emptyState.style.display = 'flex'; - + + stickToBottom = true; + jumpToLatestBtn.classList.remove('visible'); + // Destroy and reset session if (session) { session.destroy(); diff --git a/extension/demo/chat-poc/index.html b/extension/demo/chat-poc/index.html index 5ce17b3..aba2ab1 100644 --- a/extension/demo/chat-poc/index.html +++ b/extension/demo/chat-poc/index.html @@ -95,10 +95,52 @@ .status-dot.error { background: var(--color-error); } /* Chat container */ + .chat-area { + flex: 1; + position: relative; + display: flex; + flex-direction: column; + min-height: 0; + } + .chat-container { flex: 1; overflow-y: auto; padding: var(--space-4); + min-height: 0; + } + + .jump-to-latest { + position: absolute; + bottom: var(--space-3); + left: 50%; + transform: translateX(-50%) translateY(8px); + background: var(--color-accent-primary); + color: white; + border: none; + border-radius: var(--radius-full); + padding: 6px 14px; + font-size: var(--text-xs); + font-weight: var(--weight-medium); + cursor: pointer; + display: inline-flex; + align-items: center; + gap: var(--space-1); + box-shadow: var(--shadow-sm); + opacity: 0; + pointer-events: none; + transition: opacity var(--transition-fast), transform var(--transition-fast), background var(--transition-fast); + z-index: 1; + } + + .jump-to-latest:hover { + background: var(--color-accent-primary-hover); + } + + .jump-to-latest.visible { + opacity: 1; + pointer-events: auto; + transform: translateX(-50%) translateY(0); } .messages { @@ -540,17 +582,25 @@ -