diff --git a/client/src/components/Chat/Messages/MessageNav.tsx b/client/src/components/Chat/Messages/MessageNav.tsx index 2a4cd0a57e..fd96ab9d5d 100644 --- a/client/src/components/Chat/Messages/MessageNav.tsx +++ b/client/src/components/Chat/Messages/MessageNav.tsx @@ -12,8 +12,11 @@ type MessageEntry = { id: string; isUser: boolean; preview: string; + isEnd?: boolean; }; +const MESSAGES_END_ID = 'messages-end'; + function extractPreviewFromContent(content?: TMessageContentParts[]): string { if (!content) { return ''; @@ -67,6 +70,9 @@ function getMessageEntries(root: ParentNode, messagesById: Map const msg = messagesById.get(id); entries.push(msg ? buildEntry(id, msg) : buildFallbackEntry(node, id)); } + if (entries.length > 0 && root.querySelector('#' + MESSAGES_END_ID)) { + entries.push({ id: MESSAGES_END_ID, isUser: false, preview: '', isEnd: true }); + } return entries; } @@ -109,12 +115,14 @@ const MessageIndicator = memo(function MessageIndicator({ isActive, isCurrent, label, + hoverText, onSelect, }: { entry: MessageEntry; isActive: boolean; isCurrent: boolean; label: string; + hoverText: string; onSelect: (id: string) => void; }) { return ( @@ -123,24 +131,35 @@ const MessageIndicator = memo(function MessageIndicator({ -

{entry.preview}

+

{hoverText}

@@ -238,53 +257,69 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject { - const el = document.getElementById(id); - if (!el) { - return; - } - const container = el.closest('.scrollbar-gutter-stable'); - if (!container) { - el.scrollIntoView({ behavior: 'smooth', block: 'start' }); - return; - } - const token = ++scrollTokenRef.current; - const scrollMargin = scrollMarginRef.current || readScrollMargin(el); - const startScroll = container.scrollTop; - const start = performance.now(); + const resolveEntryEl = useCallback( + (id: string): HTMLElement | null => { + if (id === MESSAGES_END_ID) { + return scrollableRef.current?.querySelector('#' + MESSAGES_END_ID) ?? null; + } + return document.getElementById(id); + }, + [scrollableRef], + ); - const step = (now: number) => { - if (token !== scrollTokenRef.current) { + const scrollToStart = useCallback( + (id: string) => { + const el = resolveEntryEl(id); + if (!el) { return; } - const progress = Math.min(1, (now - start) / SCROLL_DURATION); - const current = document.getElementById(id); - if (!current) { + const container = el.closest('.scrollbar-gutter-stable'); + if (!container) { + el.scrollIntoView({ behavior: 'smooth', block: 'start' }); return; } - const clamped = computeTargetScroll(container, current, scrollMargin); - container.scrollTop = startScroll + (clamped - startScroll) * easeOutCubic(progress); - if (progress < 1) { - requestAnimationFrame(step); + const token = ++scrollTokenRef.current; + const scrollMargin = scrollMarginRef.current || readScrollMargin(el); + const startScroll = container.scrollTop; + const start = performance.now(); + + const step = (now: number) => { + if (token !== scrollTokenRef.current) { + return; + } + const progress = Math.min(1, (now - start) / SCROLL_DURATION); + const current = resolveEntryEl(id); + if (!current) { + return; + } + const clamped = computeTargetScroll(container, current, scrollMargin); + container.scrollTop = startScroll + (clamped - startScroll) * easeOutCubic(progress); + if (progress < 1) { + requestAnimationFrame(step); + } + }; + + requestAnimationFrame(step); + }, + [resolveEntryEl], + ); + + const scrollToImmediate = useCallback( + (id: string) => { + const el = resolveEntryEl(id); + if (!el) { + return; } - }; - - requestAnimationFrame(step); - }, []); - - const scrollToImmediate = useCallback((id: string) => { - const el = document.getElementById(id); - if (!el) { - return; - } - const container = el.closest('.scrollbar-gutter-stable'); - if (!container) { - return; - } - scrollTokenRef.current++; - const scrollMargin = scrollMarginRef.current || readScrollMargin(el); - container.scrollTop = computeTargetScroll(container, el, scrollMargin); - }, []); + const container = el.closest('.scrollbar-gutter-stable'); + if (!container) { + return; + } + scrollTokenRef.current++; + const scrollMargin = scrollMarginRef.current || readScrollMargin(el); + container.scrollTop = computeTargetScroll(container, el, scrollMargin); + }, + [resolveEntryEl], + ); const focusMessage = useCallback((id: string) => { const el = document.getElementById(id); @@ -304,7 +339,9 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject { for (let i = 0; i < entries.length; i++) { - const el = document.getElementById(entries[i].id); + const el = resolveEntryEl(entries[i].id); offsetsTop[i] = el ? el.offsetTop : Number.POSITIVE_INFINITY; offsetsBottom[i] = el ? el.offsetTop + el.offsetHeight : Number.POSITIVE_INFINITY; } @@ -535,10 +572,11 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject scrollTop + JUMP_EPS) { @@ -553,7 +591,6 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject 0 && scrollTop >= containerMaxScrollTop - JUMP_EPS) { scheduleColumnBottomScroll(); return; @@ -618,7 +655,7 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject { const root = scrollableRef.current; @@ -691,7 +728,7 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject(); for (let i = 0; i < entries.length; i++) { const id = entries[i].id; - const el = document.getElementById(id); + const el = resolveEntryEl(id); if (el) { elementByNewId.set(el, id); } @@ -729,7 +766,7 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject { const container = scrollableRef.current; @@ -742,7 +779,7 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject= 0; i--) { - const el = document.getElementById(entries[i].id); + const el = resolveEntryEl(entries[i].id); if (!el) { continue; } @@ -752,7 +789,7 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject { const container = scrollableRef.current; @@ -765,7 +802,7 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject { const onKeyDown = (e: KeyboardEvent) => { @@ -788,7 +825,9 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject document.removeEventListener('keydown', onKeyDown); }, [focusNav]); - if (entries.length < 3) { + const hasEnd = entries.length > 0 && entries[entries.length - 1].isEnd === true; + const messageCount = hasEnd ? entries.length - 1 : entries.length; + if (messageCount < 3) { return null; } @@ -821,19 +860,27 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject - {entries.map((entry) => ( - - ))} + {entries.map((entry) => { + const label = entry.isEnd + ? localize('com_ui_scroll_to_bottom') + : localize( + entry.isUser + ? 'com_ui_message_nav_go_to_user' + : 'com_ui_message_nav_go_to_assistant', + { 0: entry.preview.slice(0, 30) }, + ); + return ( + + ); + })}