mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-18 13:49:09 -04:00
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 <mudler@localai.io>
This commit is contained in:
@@ -71,7 +71,8 @@
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Kopieren",
|
||||
"regenerate": "Neu generieren"
|
||||
"regenerate": "Neu generieren",
|
||||
"jumpToLatest": "Jump to latest"
|
||||
},
|
||||
"streaming": {
|
||||
"transferring": "Modell wird übertragen...",
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Copy",
|
||||
"regenerate": "Regenerate"
|
||||
"regenerate": "Regenerate",
|
||||
"jumpToLatest": "Jump to latest"
|
||||
},
|
||||
"streaming": {
|
||||
"transferring": "Transferring model...",
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Copiar",
|
||||
"regenerate": "Regenerar"
|
||||
"regenerate": "Regenerar",
|
||||
"jumpToLatest": "Jump to latest"
|
||||
},
|
||||
"streaming": {
|
||||
"transferring": "Transfiriendo modelo...",
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Salin",
|
||||
"regenerate": "Hasilkan ulang"
|
||||
"regenerate": "Hasilkan ulang",
|
||||
"jumpToLatest": "Jump to latest"
|
||||
},
|
||||
"streaming": {
|
||||
"transferring": "Mentransfer model...",
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Copia",
|
||||
"regenerate": "Rigenera"
|
||||
"regenerate": "Rigenera",
|
||||
"jumpToLatest": "Torna in fondo"
|
||||
},
|
||||
"streaming": {
|
||||
"transferring": "Trasferimento del modello...",
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
},
|
||||
"actions": {
|
||||
"copy": "복사",
|
||||
"regenerate": "다시 생성"
|
||||
"regenerate": "다시 생성",
|
||||
"jumpToLatest": "Jump to latest"
|
||||
},
|
||||
"streaming": {
|
||||
"transferring": "모델 전송 중...",
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
},
|
||||
"actions": {
|
||||
"copy": "复制",
|
||||
"regenerate": "重新生成"
|
||||
"regenerate": "重新生成",
|
||||
"jumpToLatest": "Jump to latest"
|
||||
},
|
||||
"streaming": {
|
||||
"transferring": "正在传输模型...",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
{scrolledUp && (
|
||||
<button
|
||||
type="button"
|
||||
className="chat-jump-latest"
|
||||
onClick={() => {
|
||||
stickToBottomRef.current = true
|
||||
setScrolledUp(false)
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}}
|
||||
>
|
||||
<i className="fas fa-arrow-down" aria-hidden="true" /> {t('actions.jumpToLatest')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Token info bar */}
|
||||
|
||||
Reference in New Issue
Block a user