|
|
@@ -163,7 +163,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|
|
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
|
|
const stickyFollowRef = useRef<boolean>(false)
|
|
|
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
|
|
|
- const [isAtBottom, setIsAtBottom] = useState(false)
|
|
|
+ const isAtBottomRef = useRef(false)
|
|
|
const lastTtsRef = useRef<string>("")
|
|
|
const [wasStreaming, setWasStreaming] = useState<boolean>(false)
|
|
|
const [checkpointWarning, setCheckpointWarning] = useState<
|
|
|
@@ -520,6 +520,23 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|
|
}
|
|
|
// Reset user response flag for new task
|
|
|
userRespondedRef.current = false
|
|
|
+
|
|
|
+ // Ensure new task starts anchored to the bottom. Virtuoso's
|
|
|
+ // initialTopMostItemIndex fires at mount but the message data may
|
|
|
+ // arrive asynchronously, so we also engage sticky follow and
|
|
|
+ // explicitly scroll after a frame to handle the race.
|
|
|
+ let rafId: number | undefined
|
|
|
+ if (task?.ts) {
|
|
|
+ stickyFollowRef.current = true
|
|
|
+ rafId = requestAnimationFrame(() => {
|
|
|
+ virtuosoRef.current?.scrollTo({ top: Number.MAX_SAFE_INTEGER, behavior: "auto" })
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return () => {
|
|
|
+ if (rafId !== undefined) {
|
|
|
+ cancelAnimationFrame(rafId)
|
|
|
+ }
|
|
|
+ }
|
|
|
}, [task?.ts])
|
|
|
|
|
|
const taskTs = task?.ts
|
|
|
@@ -1393,7 +1410,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|
|
|
|
|
const handleRowHeightChange = useCallback(
|
|
|
(isTaller: boolean) => {
|
|
|
- if (isAtBottom) {
|
|
|
+ if (isAtBottomRef.current) {
|
|
|
if (isTaller) {
|
|
|
scrollToBottomSmooth()
|
|
|
} else {
|
|
|
@@ -1401,7 +1418,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
- [scrollToBottomSmooth, scrollToBottomAuto, isAtBottom],
|
|
|
+ [scrollToBottomSmooth, scrollToBottomAuto],
|
|
|
)
|
|
|
|
|
|
// Disable sticky follow when user scrolls up inside the chat container
|
|
|
@@ -1413,23 +1430,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|
|
}, [])
|
|
|
useEvent("wheel", handleWheel, window, { passive: true })
|
|
|
|
|
|
- // Also disable sticky follow when the chat container is scrolled away from bottom
|
|
|
- useEffect(() => {
|
|
|
- const el = scrollContainerRef.current
|
|
|
- if (!el) return
|
|
|
- const onScroll = () => {
|
|
|
- // Consider near-bottom within a small threshold consistent with Virtuoso settings
|
|
|
- const nearBottom = Math.abs(el.scrollHeight - el.scrollTop - el.clientHeight) < 10
|
|
|
- if (!nearBottom) {
|
|
|
- stickyFollowRef.current = false
|
|
|
- }
|
|
|
- // Keep UI button state in sync with scroll position
|
|
|
- setShowScrollToBottom(!nearBottom)
|
|
|
- }
|
|
|
- el.addEventListener("scroll", onScroll, { passive: true })
|
|
|
- return () => el.removeEventListener("scroll", onScroll)
|
|
|
- }, [])
|
|
|
-
|
|
|
// Effect to clear checkpoint warning when messages appear or task changes
|
|
|
useEffect(() => {
|
|
|
if (isHidden || !task) {
|
|
|
@@ -1767,9 +1767,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|
|
itemContent={itemContent}
|
|
|
followOutput={(isAtBottom: boolean) => isAtBottom || stickyFollowRef.current}
|
|
|
atBottomStateChange={(isAtBottom: boolean) => {
|
|
|
- setIsAtBottom(isAtBottom)
|
|
|
- // Only show the scroll-to-bottom button if not at bottom
|
|
|
+ isAtBottomRef.current = isAtBottom
|
|
|
setShowScrollToBottom(!isAtBottom)
|
|
|
+ // Clear sticky follow when user scrolls away from bottom
|
|
|
+ if (!isAtBottom) {
|
|
|
+ stickyFollowRef.current = false
|
|
|
+ }
|
|
|
}}
|
|
|
atBottomThreshold={10}
|
|
|
initialTopMostItemIndex={groupedMessages.length - 1}
|
|
|
@@ -1898,7 +1901,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|
|
onSelectImages={selectImages}
|
|
|
shouldDisableImages={shouldDisableImages}
|
|
|
onHeightChange={() => {
|
|
|
- if (isAtBottom) {
|
|
|
+ if (isAtBottomRef.current) {
|
|
|
scrollToBottomAuto()
|
|
|
}
|
|
|
}}
|