From 8991b94a800b8a240ed3997fe92bd5e42764f9f0 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Thu, 18 Jun 2026 17:26:06 +0000 Subject: [PATCH] feat(ui): jump-to-latest pill when scrolled up in chat When the user scrolls away from the bottom of a conversation, a floating "Jump to latest" pill appears (sticky, centered above the composer); clicking it smooth-scrolls to the newest message and re-pins auto-scroll. Resets on chat switch. Adds the chat.actions.jumpToLatest i18n key to all locales. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto --- .../http/react-ui/public/locales/de/chat.json | 3 ++- .../http/react-ui/public/locales/en/chat.json | 3 ++- .../http/react-ui/public/locales/es/chat.json | 3 ++- .../http/react-ui/public/locales/id/chat.json | 3 ++- .../http/react-ui/public/locales/it/chat.json | 3 ++- .../http/react-ui/public/locales/ko/chat.json | 3 ++- .../react-ui/public/locales/zh-CN/chat.json | 3 ++- core/http/react-ui/src/App.css | 22 +++++++++++++++++++ core/http/react-ui/src/pages/Chat.jsx | 16 ++++++++++++++ 9 files changed, 52 insertions(+), 7 deletions(-) diff --git a/core/http/react-ui/public/locales/de/chat.json b/core/http/react-ui/public/locales/de/chat.json index 8b370d1da..439213811 100644 --- a/core/http/react-ui/public/locales/de/chat.json +++ b/core/http/react-ui/public/locales/de/chat.json @@ -71,7 +71,8 @@ }, "actions": { "copy": "Kopieren", - "regenerate": "Neu generieren" + "regenerate": "Neu generieren", + "jumpToLatest": "Jump to latest" }, "streaming": { "transferring": "Modell wird übertragen...", diff --git a/core/http/react-ui/public/locales/en/chat.json b/core/http/react-ui/public/locales/en/chat.json index 64f6e0650..de9d0507d 100644 --- a/core/http/react-ui/public/locales/en/chat.json +++ b/core/http/react-ui/public/locales/en/chat.json @@ -71,7 +71,8 @@ }, "actions": { "copy": "Copy", - "regenerate": "Regenerate" + "regenerate": "Regenerate", + "jumpToLatest": "Jump to latest" }, "streaming": { "transferring": "Transferring model...", diff --git a/core/http/react-ui/public/locales/es/chat.json b/core/http/react-ui/public/locales/es/chat.json index 4df79286a..d5ade75f9 100644 --- a/core/http/react-ui/public/locales/es/chat.json +++ b/core/http/react-ui/public/locales/es/chat.json @@ -71,7 +71,8 @@ }, "actions": { "copy": "Copiar", - "regenerate": "Regenerar" + "regenerate": "Regenerar", + "jumpToLatest": "Jump to latest" }, "streaming": { "transferring": "Transfiriendo modelo...", diff --git a/core/http/react-ui/public/locales/id/chat.json b/core/http/react-ui/public/locales/id/chat.json index 079c84914..c79edaeb4 100644 --- a/core/http/react-ui/public/locales/id/chat.json +++ b/core/http/react-ui/public/locales/id/chat.json @@ -71,7 +71,8 @@ }, "actions": { "copy": "Salin", - "regenerate": "Hasilkan ulang" + "regenerate": "Hasilkan ulang", + "jumpToLatest": "Jump to latest" }, "streaming": { "transferring": "Mentransfer model...", diff --git a/core/http/react-ui/public/locales/it/chat.json b/core/http/react-ui/public/locales/it/chat.json index edb0397d3..393dc7b01 100644 --- a/core/http/react-ui/public/locales/it/chat.json +++ b/core/http/react-ui/public/locales/it/chat.json @@ -71,7 +71,8 @@ }, "actions": { "copy": "Copia", - "regenerate": "Rigenera" + "regenerate": "Rigenera", + "jumpToLatest": "Torna in fondo" }, "streaming": { "transferring": "Trasferimento del modello...", diff --git a/core/http/react-ui/public/locales/ko/chat.json b/core/http/react-ui/public/locales/ko/chat.json index 86fcef3c5..c14404c5f 100644 --- a/core/http/react-ui/public/locales/ko/chat.json +++ b/core/http/react-ui/public/locales/ko/chat.json @@ -71,7 +71,8 @@ }, "actions": { "copy": "복사", - "regenerate": "다시 생성" + "regenerate": "다시 생성", + "jumpToLatest": "Jump to latest" }, "streaming": { "transferring": "모델 전송 중...", diff --git a/core/http/react-ui/public/locales/zh-CN/chat.json b/core/http/react-ui/public/locales/zh-CN/chat.json index c810fc96c..a96b306b6 100644 --- a/core/http/react-ui/public/locales/zh-CN/chat.json +++ b/core/http/react-ui/public/locales/zh-CN/chat.json @@ -71,7 +71,8 @@ }, "actions": { "copy": "复制", - "regenerate": "重新生成" + "regenerate": "重新生成", + "jumpToLatest": "Jump to latest" }, "streaming": { "transferring": "正在传输模型...", diff --git a/core/http/react-ui/src/App.css b/core/http/react-ui/src/App.css index 5b0c3180b..d70755c7b 100644 --- a/core/http/react-ui/src/App.css +++ b/core/http/react-ui/src/App.css @@ -3090,6 +3090,28 @@ select.input { } .chat-messages::-webkit-scrollbar { width: 6px; } .chat-messages::-webkit-scrollbar-track { background: transparent; } + +/* Floating "jump to latest" pill, shown when scrolled away from the bottom. */ +.chat-jump-latest { + position: sticky; + bottom: 12px; + align-self: center; + display: inline-flex; + align-items: center; + gap: var(--spacing-xs); + padding: var(--spacing-xs) var(--spacing-md); + background: var(--color-surface-raised); + color: var(--color-text-primary); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-full); + box-shadow: var(--shadow-md); + font-size: var(--text-sm); + font-weight: var(--font-weight-medium); + cursor: pointer; + z-index: 5; + animation: pageReveal var(--duration-fast) var(--ease-default) both; +} +.chat-jump-latest:hover { border-color: var(--color-primary-border); color: var(--color-primary); } .chat-messages::-webkit-scrollbar-thumb { background: var(--color-border-subtle); border-radius: 3px; diff --git a/core/http/react-ui/src/pages/Chat.jsx b/core/http/react-ui/src/pages/Chat.jsx index 0e3782693..bdcfb40ad 100644 --- a/core/http/react-ui/src/pages/Chat.jsx +++ b/core/http/react-ui/src/pages/Chat.jsx @@ -336,6 +336,7 @@ export default function Chat() { const messagesRef = useRef(null) const textareaRef = useRef(null) const stickToBottomRef = useRef(true) + const [scrolledUp, setScrolledUp] = useState(false) const chatsMenuRef = useRef(null) // Focus mode: once a conversation has at least one message we slim the @@ -601,6 +602,7 @@ export default function Chat() { const onScroll = () => { const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight stickToBottomRef.current = distanceFromBottom < 80 + setScrolledUp(distanceFromBottom > 160) } el.addEventListener('scroll', onScroll, { passive: true }) return () => el.removeEventListener('scroll', onScroll) @@ -616,6 +618,7 @@ export default function Chat() { // user's focus-mode override — each chat starts fresh. useEffect(() => { stickToBottomRef.current = true + setScrolledUp(false) messagesEndRef.current?.scrollIntoView({ behavior: 'auto' }) setFocusOverride(false) }, [activeChat?.id]) @@ -1235,6 +1238,19 @@ export default function Chat() { )}
+ {scrolledUp && ( +