mirror of
https://github.com/csunny/DB-GPT.git
synced 2025-07-29 14:57:35 +00:00
fix: resolve streaming output scroll interruption in chat interface
- Implement MutationObserver for real-time DOM change detection - Add triple-layer scroll guarantee: height detection + force check + backup timer - Optimize architecture: separate scroll logic between container and content components - Add intelligent user experience: respect manual scrolling with 100px threshold - Fix second message scroll failure and streaming mode timeout issues - Support both new chat mode and dashboard mode scenarios
This commit is contained in:
parent
5d13cf911d
commit
66377e87ba
@ -181,154 +181,186 @@ const Completion = ({ messages, onSubmit, onFormatContent }: Props) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Track user scrolling behavior
|
// Track user scrolling behavior
|
||||||
const lastScrollTimeRef = useRef(0);
|
|
||||||
const isUserScrollingRef = useRef(false);
|
const isUserScrollingRef = useRef(false);
|
||||||
const prevMessageCountRef = useRef(showMessages.length);
|
const prevMessageCountRef = useRef(0);
|
||||||
const lastContentHeightRef = useRef(0);
|
const lastContentHeightRef = useRef(0);
|
||||||
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const isStreamingRef = useRef(false); // Track if we're in streaming mode
|
||||||
const isAutoScrollingRef = useRef(false); // Track if we're auto-scrolling
|
const streamingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
// Initialize refs
|
// Initialize refs only once
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('Completion initializing with showMessages length:', showMessages.length);
|
console.log('Completion initializing refs');
|
||||||
// Set initial scroll time to allow streaming from the beginning
|
// Initialize message count tracking
|
||||||
if (lastScrollTimeRef.current === 0) {
|
|
||||||
lastScrollTimeRef.current = Date.now() - 3000; // Set to 3 seconds ago
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Update message count tracking when showMessages changes
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('Completion updating prevMessageCountRef:', {
|
|
||||||
currentLength: showMessages.length,
|
|
||||||
prevCount: prevMessageCountRef.current,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only update if this is the first time (count is 0)
|
|
||||||
if (prevMessageCountRef.current === 0) {
|
if (prevMessageCountRef.current === 0) {
|
||||||
prevMessageCountRef.current = showMessages.length;
|
prevMessageCountRef.current = 0; // Will be set when first message arrives
|
||||||
}
|
}
|
||||||
}, [showMessages.length]);
|
}, []); // No dependencies - only run once
|
||||||
|
|
||||||
// Handle scroll events to detect user interaction
|
// Handle scroll events to detect user interaction
|
||||||
const handleScrollEvent = useCallback(() => {
|
const handleScrollEvent = useCallback(() => {
|
||||||
// Ignore scroll events caused by auto-scrolling
|
// Completely ignore scroll events during streaming to prevent interference
|
||||||
if (isAutoScrollingRef.current) {
|
if (isStreamingRef.current) {
|
||||||
|
console.log('Ignoring scroll event during streaming');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastScrollTimeRef.current = Date.now();
|
|
||||||
|
|
||||||
if (scrollableRef.current) {
|
if (scrollableRef.current) {
|
||||||
const scrollElement = scrollableRef.current;
|
const scrollElement = scrollableRef.current;
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollElement;
|
const { scrollTop, scrollHeight, clientHeight } = scrollElement;
|
||||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 20;
|
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 5;
|
||||||
|
|
||||||
|
const wasUserScrolling = isUserScrollingRef.current;
|
||||||
isUserScrollingRef.current = !isAtBottom;
|
isUserScrollingRef.current = !isAtBottom;
|
||||||
|
|
||||||
|
if (wasUserScrolling !== isUserScrollingRef.current) {
|
||||||
|
console.log('User scroll state changed:', {
|
||||||
|
isAtBottom,
|
||||||
|
isUserScrolling: isUserScrollingRef.current,
|
||||||
|
scrollTop,
|
||||||
|
scrollHeight,
|
||||||
|
clientHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Auto-scroll to bottom for new messages or content updates
|
// Simple and reliable scroll function
|
||||||
|
const scrollToBottom = useCallback(() => {
|
||||||
|
if (!scrollableRef.current) return;
|
||||||
|
|
||||||
|
const scrollElement = scrollableRef.current;
|
||||||
|
console.log('Scrolling to bottom');
|
||||||
|
|
||||||
|
// Use instant scroll to avoid animation-related event conflicts
|
||||||
|
scrollElement.scrollTo({
|
||||||
|
top: scrollElement.scrollHeight,
|
||||||
|
behavior: 'instant',
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!scrollableRef.current) return;
|
if (!scrollableRef.current) return;
|
||||||
|
|
||||||
const scrollElement = scrollableRef.current;
|
const scrollElement = scrollableRef.current;
|
||||||
const currentMessageCount = showMessages.length;
|
const currentMessageCount = showMessages.length;
|
||||||
const isNewMessage = currentMessageCount > prevMessageCountRef.current;
|
const isNewMessage = currentMessageCount > prevMessageCountRef.current;
|
||||||
const now = Date.now();
|
|
||||||
const userRecentlyScrolled = now - lastScrollTimeRef.current < 2000;
|
|
||||||
|
|
||||||
console.log('Completion scroll check:', {
|
console.log('Completion scroll check:', {
|
||||||
currentMessageCount,
|
currentMessageCount,
|
||||||
prevCount: prevMessageCountRef.current,
|
prevCount: prevMessageCountRef.current,
|
||||||
isNewMessage,
|
isNewMessage,
|
||||||
showMessagesLength: showMessages.length,
|
isStreaming: isStreamingRef.current,
|
||||||
userRecentlyScrolled,
|
|
||||||
isUserScrolling: isUserScrollingRef.current,
|
isUserScrolling: isUserScrollingRef.current,
|
||||||
isAutoScrolling: isAutoScrollingRef.current,
|
lastContentHeight: lastContentHeightRef.current,
|
||||||
|
currentScrollHeight: scrollElement.scrollHeight,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Always handle new messages first - this is the highest priority
|
// Handle new messages - always scroll to bottom and start streaming mode
|
||||||
if (isNewMessage) {
|
if (isNewMessage) {
|
||||||
console.log('Completion: New message detected, forcing scroll to bottom');
|
console.log('Completion: New message detected, forcing restart of streaming mode');
|
||||||
// New message - always scroll to bottom regardless of user position
|
|
||||||
isAutoScrollingRef.current = true;
|
|
||||||
|
|
||||||
// Reset states IMMEDIATELY to ensure streaming can work
|
// Force exit any existing streaming mode
|
||||||
lastScrollTimeRef.current = Date.now() - 3000; // Allow streaming immediately
|
if (streamingTimeoutRef.current) {
|
||||||
|
clearTimeout(streamingTimeoutRef.current);
|
||||||
|
console.log('Cleared existing streaming timeout');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter streaming mode - disable user scroll detection
|
||||||
|
isStreamingRef.current = true;
|
||||||
isUserScrollingRef.current = false;
|
isUserScrollingRef.current = false;
|
||||||
lastContentHeightRef.current = scrollElement.scrollHeight; // Set current height as baseline
|
|
||||||
|
|
||||||
scrollElement.scrollTo({
|
// Scroll to bottom immediately
|
||||||
top: scrollElement.scrollHeight,
|
scrollToBottom();
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset auto scroll flag after animation
|
// Update message count but reset height tracking to let streaming content set it
|
||||||
setTimeout(() => {
|
prevMessageCountRef.current = currentMessageCount;
|
||||||
isAutoScrollingRef.current = false;
|
lastContentHeightRef.current = 0; // Reset to allow streaming to establish new baseline
|
||||||
}, 100);
|
|
||||||
|
|
||||||
prevMessageCountRef.current = currentMessageCount; // Update count immediately
|
// Exit streaming mode after no content updates for 3 seconds (increased timeout)
|
||||||
return; // Exit early for new messages
|
streamingTimeoutRef.current = setTimeout(() => {
|
||||||
|
console.log('Exiting streaming mode after timeout');
|
||||||
|
isStreamingRef.current = false;
|
||||||
|
}, 3000); // Increased from 2000 to 3000
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle streaming content updates (only if user hasn't manually scrolled recently)
|
// Handle streaming content updates - always scroll during streaming
|
||||||
if (!userRecentlyScrolled && !isUserScrollingRef.current && !isAutoScrollingRef.current) {
|
if (isStreamingRef.current) {
|
||||||
// Streaming content - scroll based on content height change
|
|
||||||
const currentHeight = scrollElement.scrollHeight;
|
const currentHeight = scrollElement.scrollHeight;
|
||||||
|
|
||||||
// Initialize lastContentHeightRef if not set
|
console.log('Completion streaming update:', {
|
||||||
|
currentHeight,
|
||||||
|
lastHeight: lastContentHeightRef.current,
|
||||||
|
isFirstCheck: lastContentHeightRef.current === 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize baseline height or check for changes
|
||||||
if (lastContentHeightRef.current === 0) {
|
if (lastContentHeightRef.current === 0) {
|
||||||
|
console.log('Setting initial baseline height:', currentHeight);
|
||||||
lastContentHeightRef.current = currentHeight;
|
lastContentHeightRef.current = currentHeight;
|
||||||
|
// Don't return here - continue to check for immediate height differences
|
||||||
}
|
}
|
||||||
|
|
||||||
const heightDiff = currentHeight - lastContentHeightRef.current;
|
const heightDiff = currentHeight - lastContentHeightRef.current;
|
||||||
|
|
||||||
console.log('Completion streaming check:', {
|
console.log('Completion streaming height check:', {
|
||||||
currentHeight,
|
currentHeight,
|
||||||
lastHeight: lastContentHeightRef.current,
|
lastHeight: lastContentHeightRef.current,
|
||||||
heightDiff,
|
heightDiff,
|
||||||
threshold: 12,
|
willScroll: heightDiff > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Only scroll if content height increased by at least ~0.5 lines (12px) for smoother experience
|
// Any height increase triggers scroll during streaming
|
||||||
if (heightDiff >= 12) {
|
if (heightDiff > 0) {
|
||||||
// Clear any pending scroll timeout
|
console.log('Completion: Content height increased by', heightDiff, 'px, scrolling');
|
||||||
if (scrollTimeoutRef.current) {
|
|
||||||
clearTimeout(scrollTimeoutRef.current);
|
// Scroll immediately
|
||||||
|
scrollToBottom();
|
||||||
|
|
||||||
|
// Update height tracking
|
||||||
|
lastContentHeightRef.current = currentHeight;
|
||||||
|
|
||||||
|
// Reset streaming timeout - keep streaming mode active
|
||||||
|
if (streamingTimeoutRef.current) {
|
||||||
|
clearTimeout(streamingTimeoutRef.current);
|
||||||
}
|
}
|
||||||
|
streamingTimeoutRef.current = setTimeout(() => {
|
||||||
console.log('Completion: Triggering streaming scroll, heightDiff:', heightDiff);
|
console.log('Exiting streaming mode after content timeout');
|
||||||
|
isStreamingRef.current = false;
|
||||||
// Debounce scroll calls to avoid conflicts
|
}, 3000);
|
||||||
scrollTimeoutRef.current = setTimeout(() => {
|
} else if (heightDiff === 0) {
|
||||||
isAutoScrollingRef.current = true;
|
console.log('Completion: No height change during streaming, content may not be updating');
|
||||||
scrollElement.scrollTo({
|
|
||||||
top: scrollElement.scrollHeight,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
setTimeout(() => {
|
|
||||||
isAutoScrollingRef.current = false;
|
|
||||||
}, 200); // Longer timeout for streaming scroll
|
|
||||||
lastContentHeightRef.current = currentHeight;
|
|
||||||
}, 30); // 30ms debounce for smooth experience
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('Completion streaming blocked:', {
|
// Not streaming - check if user wants auto-scroll
|
||||||
userRecentlyScrolled,
|
if (!isUserScrollingRef.current) {
|
||||||
isUserScrolling: isUserScrollingRef.current,
|
const currentHeight = scrollElement.scrollHeight;
|
||||||
isAutoScrolling: isAutoScrollingRef.current,
|
const heightDiff = currentHeight - lastContentHeightRef.current;
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [showMessages, scene]);
|
|
||||||
|
|
||||||
// Add scroll event listener
|
if (heightDiff > 0) {
|
||||||
|
console.log('Completion: Non-streaming height increase, scrolling');
|
||||||
|
scrollToBottom();
|
||||||
|
lastContentHeightRef.current = currentHeight;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Completion: User has scrolled away, not auto-scrolling');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [showMessages, scene, scrollToBottom]);
|
||||||
|
|
||||||
|
// Add scroll event listener and cleanup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scrollElement = scrollableRef.current;
|
const scrollElement = scrollableRef.current;
|
||||||
if (scrollElement) {
|
if (scrollElement) {
|
||||||
scrollElement.addEventListener('scroll', handleScrollEvent);
|
scrollElement.addEventListener('scroll', handleScrollEvent);
|
||||||
return () => {
|
return () => {
|
||||||
scrollElement.removeEventListener('scroll', handleScrollEvent);
|
scrollElement.removeEventListener('scroll', handleScrollEvent);
|
||||||
|
// Cleanup streaming timeout
|
||||||
|
if (streamingTimeoutRef.current) {
|
||||||
|
clearTimeout(streamingTimeoutRef.current);
|
||||||
|
streamingTimeoutRef.current = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [handleScrollEvent]);
|
}, [handleScrollEvent]);
|
||||||
|
@ -20,242 +20,358 @@ const ChatContentContainer = ({}, ref: React.ForwardedRef<any>) => {
|
|||||||
return scrollRef.current;
|
return scrollRef.current;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initial UI state setup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (scrollRef.current) {
|
if (scrollRef.current) {
|
||||||
scrollRef.current.addEventListener('scroll', handleScroll);
|
|
||||||
|
|
||||||
// Check initially if content is scrollable
|
// Check initially if content is scrollable
|
||||||
const isScrollable = scrollRef.current.scrollHeight > scrollRef.current.clientHeight;
|
const isScrollable = scrollRef.current.scrollHeight > scrollRef.current.clientHeight;
|
||||||
setShowScrollButtons(isScrollable);
|
setShowScrollButtons(isScrollable);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
scrollRef.current && scrollRef.current.removeEventListener('scroll', handleScroll);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const scrollToBottomSmooth = useCallback((force = false, isStreaming = false) => {
|
// Track message count and user scrolling behavior
|
||||||
if (!scrollRef.current) return;
|
const prevMessageCountRef = useRef(0); // Always start from 0 to detect first message correctly
|
||||||
|
|
||||||
const container = scrollRef.current;
|
|
||||||
|
|
||||||
if (force) {
|
|
||||||
// Force scroll for new messages - always scroll regardless of position
|
|
||||||
container.scrollTo({
|
|
||||||
top: container.scrollHeight,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For streaming updates, check if user is near bottom
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
||||||
const buffer = Math.max(100, clientHeight * 0.2);
|
|
||||||
const isNearBottom = scrollTop + clientHeight >= scrollHeight - buffer;
|
|
||||||
|
|
||||||
if (!isNearBottom) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use smooth scroll for streaming updates
|
|
||||||
container.scrollTo({
|
|
||||||
top: container.scrollHeight,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Track message count to detect new messages
|
|
||||||
const prevMessageCountRef = useRef(history.length);
|
|
||||||
const lastScrollTimeRef = useRef(0);
|
|
||||||
const isUserScrollingRef = useRef(false);
|
const isUserScrollingRef = useRef(false);
|
||||||
const lastContentHeightRef = useRef(0);
|
const lastContentHeightRef = useRef(0);
|
||||||
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const isStreamingRef = useRef(false); // Track if we're in streaming mode
|
||||||
const isAutoScrollingRef = useRef(false); // Track if we're auto-scrolling
|
const streamingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const mutationObserverRef = useRef<MutationObserver | null>(null);
|
||||||
|
const backupIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
// Initialize refs
|
// Initialize refs with current history length
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('ChatContentContainer initializing with history length:', history.length);
|
console.log('ChatContentContainer initializing with history length:', history.length);
|
||||||
// Set initial scroll time to allow streaming from the beginning
|
// Set initial message count to current history length (to avoid triggering new message on first render)
|
||||||
if (lastScrollTimeRef.current === 0) {
|
prevMessageCountRef.current = history.length;
|
||||||
lastScrollTimeRef.current = Date.now() - 3000; // Set to 3 seconds ago
|
}, []); // No dependencies - only run once
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Update message count tracking when history changes
|
// Combined scroll event handler for both streaming logic and UI state
|
||||||
useEffect(() => {
|
|
||||||
console.log('ChatContentContainer updating prevMessageCountRef:', {
|
|
||||||
currentLength: history.length,
|
|
||||||
prevCount: prevMessageCountRef.current,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only update if this is the first time (count is 0)
|
|
||||||
if (prevMessageCountRef.current === 0) {
|
|
||||||
prevMessageCountRef.current = history.length;
|
|
||||||
}
|
|
||||||
}, [history.length]);
|
|
||||||
|
|
||||||
// Handle scroll events to detect user interaction
|
|
||||||
const handleScrollEvent = useCallback(() => {
|
const handleScrollEvent = useCallback(() => {
|
||||||
// Ignore scroll events caused by auto-scrolling
|
|
||||||
if (isAutoScrollingRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastScrollTimeRef.current = Date.now();
|
|
||||||
|
|
||||||
if (scrollRef.current) {
|
|
||||||
const scrollElement = scrollRef.current;
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollElement;
|
|
||||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 20;
|
|
||||||
isUserScrollingRef.current = !isAtBottom;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!scrollRef.current) return;
|
if (!scrollRef.current) return;
|
||||||
|
|
||||||
const scrollElement = scrollRef.current;
|
const scrollElement = scrollRef.current;
|
||||||
const currentMessageCount = history.length;
|
const { scrollTop, scrollHeight, clientHeight } = scrollElement;
|
||||||
const isNewMessage = currentMessageCount > prevMessageCountRef.current;
|
|
||||||
const now = Date.now();
|
|
||||||
const userRecentlyScrolled = now - lastScrollTimeRef.current < 2000;
|
|
||||||
|
|
||||||
console.log('ChatContentContainer scroll check:', {
|
|
||||||
currentMessageCount,
|
|
||||||
prevCount: prevMessageCountRef.current,
|
|
||||||
isNewMessage,
|
|
||||||
historyLength: history.length,
|
|
||||||
userRecentlyScrolled,
|
|
||||||
isUserScrolling: isUserScrollingRef.current,
|
|
||||||
isAutoScrolling: isAutoScrollingRef.current,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Always handle new messages first - this is the highest priority
|
|
||||||
if (isNewMessage) {
|
|
||||||
console.log('ChatContentContainer: New message detected, forcing scroll to bottom');
|
|
||||||
// New message - always scroll to bottom regardless of user position
|
|
||||||
isAutoScrollingRef.current = true;
|
|
||||||
|
|
||||||
// Reset states IMMEDIATELY to ensure streaming can work
|
|
||||||
lastScrollTimeRef.current = Date.now() - 3000; // Allow streaming immediately
|
|
||||||
isUserScrollingRef.current = false;
|
|
||||||
lastContentHeightRef.current = scrollElement.scrollHeight; // Set current height as baseline
|
|
||||||
|
|
||||||
scrollElement.scrollTo({
|
|
||||||
top: scrollElement.scrollHeight,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset auto scroll flag after animation
|
|
||||||
setTimeout(() => {
|
|
||||||
isAutoScrollingRef.current = false;
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
prevMessageCountRef.current = currentMessageCount; // Update count immediately
|
|
||||||
return; // Exit early for new messages
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle streaming content updates (only if user hasn't manually scrolled recently)
|
|
||||||
if (!userRecentlyScrolled && !isUserScrollingRef.current && !isAutoScrollingRef.current) {
|
|
||||||
// Streaming content - scroll based on content height change
|
|
||||||
const currentHeight = scrollElement.scrollHeight;
|
|
||||||
|
|
||||||
// Initialize lastContentHeightRef if not set
|
|
||||||
if (lastContentHeightRef.current === 0) {
|
|
||||||
lastContentHeightRef.current = currentHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
const heightDiff = currentHeight - lastContentHeightRef.current;
|
|
||||||
|
|
||||||
console.log('ChatContentContainer streaming check:', {
|
|
||||||
currentHeight,
|
|
||||||
lastHeight: lastContentHeightRef.current,
|
|
||||||
heightDiff,
|
|
||||||
threshold: 12,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only scroll if content height increased by at least ~0.5 lines (12px) for smoother experience
|
|
||||||
if (heightDiff >= 12) {
|
|
||||||
// Clear any pending scroll timeout
|
|
||||||
if (scrollTimeoutRef.current) {
|
|
||||||
clearTimeout(scrollTimeoutRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('ChatContentContainer: Triggering streaming scroll, heightDiff:', heightDiff);
|
|
||||||
|
|
||||||
// Debounce scroll calls to avoid conflicts
|
|
||||||
scrollTimeoutRef.current = setTimeout(() => {
|
|
||||||
isAutoScrollingRef.current = true;
|
|
||||||
scrollElement.scrollTo({
|
|
||||||
top: scrollElement.scrollHeight,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
setTimeout(() => {
|
|
||||||
isAutoScrollingRef.current = false;
|
|
||||||
}, 200); // Longer timeout for streaming scroll
|
|
||||||
lastContentHeightRef.current = currentHeight;
|
|
||||||
}, 30); // 30ms debounce for smooth experience
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('ChatContentContainer streaming blocked:', {
|
|
||||||
userRecentlyScrolled,
|
|
||||||
isUserScrolling: isUserScrollingRef.current,
|
|
||||||
isAutoScrolling: isAutoScrollingRef.current,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [history]);
|
|
||||||
|
|
||||||
// Add scroll event listener
|
|
||||||
useEffect(() => {
|
|
||||||
const scrollElement = scrollRef.current;
|
|
||||||
if (scrollElement) {
|
|
||||||
scrollElement.addEventListener('scroll', handleScrollEvent);
|
|
||||||
return () => {
|
|
||||||
scrollElement.removeEventListener('scroll', handleScrollEvent);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [handleScrollEvent]);
|
|
||||||
|
|
||||||
// Enhanced scroll handler to track user scrolling behavior
|
|
||||||
const handleScroll = useCallback(() => {
|
|
||||||
if (!scrollRef.current) return;
|
|
||||||
|
|
||||||
const container = scrollRef.current;
|
|
||||||
const scrollTop = container.scrollTop;
|
|
||||||
const scrollHeight = container.scrollHeight;
|
|
||||||
const clientHeight = container.clientHeight;
|
|
||||||
const buffer = 20;
|
const buffer = 20;
|
||||||
|
|
||||||
// Record user scroll time
|
// UI state updates (for scroll buttons and header visibility)
|
||||||
lastScrollTimeRef.current = Date.now();
|
|
||||||
|
|
||||||
// Determine if user is actively scrolling up
|
|
||||||
const atBottom = scrollTop + clientHeight >= scrollHeight - buffer;
|
|
||||||
isUserScrollingRef.current = !atBottom;
|
|
||||||
|
|
||||||
// Update wasAtBottomRef
|
|
||||||
const atBottomPrecise = scrollTop + clientHeight >= scrollHeight - 5;
|
const atBottomPrecise = scrollTop + clientHeight >= scrollHeight - 5;
|
||||||
wasAtBottomRef.current = atBottomPrecise;
|
wasAtBottomRef.current = atBottomPrecise;
|
||||||
|
|
||||||
// Check if we're at the top
|
|
||||||
setIsAtTop(scrollTop <= buffer);
|
setIsAtTop(scrollTop <= buffer);
|
||||||
|
|
||||||
// Check if we're at the bottom
|
|
||||||
setIsAtBottom(scrollTop + clientHeight >= scrollHeight - buffer);
|
setIsAtBottom(scrollTop + clientHeight >= scrollHeight - buffer);
|
||||||
|
|
||||||
// Header visibility
|
|
||||||
if (scrollTop >= 42 + 32) {
|
if (scrollTop >= 42 + 32) {
|
||||||
setIsScrollToTop(true);
|
setIsScrollToTop(true);
|
||||||
} else {
|
} else {
|
||||||
setIsScrollToTop(false);
|
setIsScrollToTop(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show scroll buttons when content is scrollable
|
|
||||||
const isScrollable = scrollHeight > clientHeight;
|
const isScrollable = scrollHeight > clientHeight;
|
||||||
setShowScrollButtons(isScrollable);
|
setShowScrollButtons(isScrollable);
|
||||||
|
|
||||||
|
// Streaming logic - only update user scroll state when not in streaming mode
|
||||||
|
if (!isStreamingRef.current) {
|
||||||
|
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 5;
|
||||||
|
const wasUserScrolling = isUserScrollingRef.current;
|
||||||
|
isUserScrollingRef.current = !isAtBottom;
|
||||||
|
|
||||||
|
if (wasUserScrolling !== isUserScrollingRef.current) {
|
||||||
|
console.log('ChatContentContainer: User scroll state changed:', {
|
||||||
|
isAtBottom,
|
||||||
|
isUserScrolling: isUserScrollingRef.current,
|
||||||
|
scrollTop,
|
||||||
|
scrollHeight,
|
||||||
|
clientHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('ChatContentContainer: Ignoring scroll event during streaming');
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Simple and reliable scroll function
|
||||||
|
const scrollToBottomInstant = useCallback(() => {
|
||||||
|
if (!scrollRef.current) return;
|
||||||
|
|
||||||
|
const scrollElement = scrollRef.current;
|
||||||
|
console.log('ChatContentContainer: Scrolling to bottom');
|
||||||
|
|
||||||
|
// Use instant scroll to avoid animation-related event conflicts
|
||||||
|
scrollElement.scrollTo({
|
||||||
|
top: scrollElement.scrollHeight,
|
||||||
|
behavior: 'instant',
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Check for height changes and handle streaming scroll
|
||||||
|
const checkContentHeight = useCallback(() => {
|
||||||
|
if (!scrollRef.current || !isStreamingRef.current) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const scrollElement = scrollRef.current;
|
||||||
|
const currentHeight = scrollElement.scrollHeight;
|
||||||
|
|
||||||
|
console.log('ChatContentContainer streaming height check:', {
|
||||||
|
currentHeight,
|
||||||
|
lastHeight: lastContentHeightRef.current,
|
||||||
|
heightDiff: currentHeight - lastContentHeightRef.current,
|
||||||
|
isStreaming: isStreamingRef.current,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize baseline height or check for changes
|
||||||
|
if (lastContentHeightRef.current === 0) {
|
||||||
|
console.log('ChatContentContainer: Setting initial baseline height:', currentHeight);
|
||||||
|
lastContentHeightRef.current = currentHeight;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const heightDiff = currentHeight - lastContentHeightRef.current;
|
||||||
|
|
||||||
|
// Lower threshold: any height increase triggers scroll during streaming
|
||||||
|
if (heightDiff > 0) {
|
||||||
|
console.log('ChatContentContainer: Content height increased by', heightDiff, 'px, scrolling');
|
||||||
|
|
||||||
|
// Check if user has manually scrolled up significantly during streaming
|
||||||
|
const { scrollTop, clientHeight } = scrollElement;
|
||||||
|
const distanceFromBottom = currentHeight - (scrollTop + clientHeight);
|
||||||
|
|
||||||
|
// If user scrolled up more than 100px, don't force scroll but show a notification
|
||||||
|
if (distanceFromBottom > 100) {
|
||||||
|
console.log('ChatContentContainer: User scrolled away during streaming, not forcing scroll');
|
||||||
|
// Could add a "scroll to bottom" button here
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll immediately
|
||||||
|
scrollToBottomInstant();
|
||||||
|
|
||||||
|
// Update height tracking
|
||||||
|
lastContentHeightRef.current = currentHeight;
|
||||||
|
|
||||||
|
// Reset streaming timeout - keep streaming mode active
|
||||||
|
if (streamingTimeoutRef.current) {
|
||||||
|
clearTimeout(streamingTimeoutRef.current);
|
||||||
|
streamingTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
streamingTimeoutRef.current = setTimeout(() => {
|
||||||
|
console.log('ChatContentContainer: Exiting streaming mode after content timeout');
|
||||||
|
isStreamingRef.current = false;
|
||||||
|
|
||||||
|
// Clean up MutationObserver when exiting streaming mode
|
||||||
|
if (mutationObserverRef.current) {
|
||||||
|
mutationObserverRef.current.disconnect();
|
||||||
|
mutationObserverRef.current = null;
|
||||||
|
console.log('ChatContentContainer: MutationObserver stopped on timeout');
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ChatContentContainer: Error in checkContentHeight:', error);
|
||||||
|
}
|
||||||
|
}, [scrollToBottomInstant]);
|
||||||
|
|
||||||
|
// Force scroll check regardless of height changes
|
||||||
|
const forceScrollCheck = useCallback(() => {
|
||||||
|
if (!scrollRef.current || !isStreamingRef.current) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const scrollElement = scrollRef.current;
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = scrollElement;
|
||||||
|
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 5;
|
||||||
|
|
||||||
|
console.log('ChatContentContainer force scroll check:', {
|
||||||
|
scrollTop,
|
||||||
|
scrollHeight,
|
||||||
|
clientHeight,
|
||||||
|
isAtBottom,
|
||||||
|
distanceFromBottom: scrollHeight - (scrollTop + clientHeight),
|
||||||
|
});
|
||||||
|
|
||||||
|
// If not at bottom during streaming, scroll to bottom
|
||||||
|
if (!isAtBottom) {
|
||||||
|
const distanceFromBottom = scrollHeight - (scrollTop + clientHeight);
|
||||||
|
|
||||||
|
// Only respect user scrolling if they scrolled significantly up
|
||||||
|
if (distanceFromBottom <= 100) {
|
||||||
|
console.log('ChatContentContainer: Forcing scroll to bottom during streaming');
|
||||||
|
scrollToBottomInstant();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update height tracking
|
||||||
|
lastContentHeightRef.current = scrollElement.scrollHeight;
|
||||||
|
|
||||||
|
// Reset streaming timeout
|
||||||
|
if (streamingTimeoutRef.current) {
|
||||||
|
clearTimeout(streamingTimeoutRef.current);
|
||||||
|
streamingTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
streamingTimeoutRef.current = setTimeout(() => {
|
||||||
|
console.log('ChatContentContainer: Exiting streaming mode after content timeout');
|
||||||
|
isStreamingRef.current = false;
|
||||||
|
|
||||||
|
// Clean up MutationObserver when exiting streaming mode
|
||||||
|
if (mutationObserverRef.current) {
|
||||||
|
mutationObserverRef.current.disconnect();
|
||||||
|
mutationObserverRef.current = null;
|
||||||
|
console.log('ChatContentContainer: MutationObserver stopped on timeout');
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ChatContentContainer: Error in forceScrollCheck:', error);
|
||||||
|
}
|
||||||
|
}, [scrollToBottomInstant]);
|
||||||
|
|
||||||
|
// Clean up streaming mode
|
||||||
|
const cleanupStreamingMode = useCallback(() => {
|
||||||
|
console.log('ChatContentContainer: Cleaning up streaming mode');
|
||||||
|
|
||||||
|
isStreamingRef.current = false;
|
||||||
|
|
||||||
|
if (streamingTimeoutRef.current) {
|
||||||
|
clearTimeout(streamingTimeoutRef.current);
|
||||||
|
streamingTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backupIntervalRef.current) {
|
||||||
|
clearInterval(backupIntervalRef.current);
|
||||||
|
backupIntervalRef.current = null;
|
||||||
|
console.log('ChatContentContainer: Backup interval cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mutationObserverRef.current) {
|
||||||
|
try {
|
||||||
|
mutationObserverRef.current.disconnect();
|
||||||
|
mutationObserverRef.current = null;
|
||||||
|
console.log('ChatContentContainer: MutationObserver cleaned up');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ChatContentContainer: Error disconnecting MutationObserver:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Start streaming mode for new messages
|
||||||
|
const startStreamingMode = useCallback(() => {
|
||||||
|
console.log('ChatContentContainer: Starting streaming mode');
|
||||||
|
|
||||||
|
// Clean up any existing streaming mode first
|
||||||
|
cleanupStreamingMode();
|
||||||
|
|
||||||
|
// Enter streaming mode - disable user scroll detection
|
||||||
|
isStreamingRef.current = true;
|
||||||
|
isUserScrollingRef.current = false;
|
||||||
|
|
||||||
|
// Scroll to bottom immediately
|
||||||
|
scrollToBottomInstant();
|
||||||
|
|
||||||
|
// Reset height tracking
|
||||||
|
lastContentHeightRef.current = 0;
|
||||||
|
|
||||||
|
// Set up MutationObserver to watch for DOM changes
|
||||||
|
if (scrollRef.current) {
|
||||||
|
try {
|
||||||
|
mutationObserverRef.current = new MutationObserver(mutations => {
|
||||||
|
// Only process if we're still in streaming mode
|
||||||
|
if (!isStreamingRef.current) return;
|
||||||
|
|
||||||
|
console.log('ChatContentContainer: MutationObserver detected changes:', mutations.length);
|
||||||
|
|
||||||
|
// Always trigger scroll check for any mutation during streaming
|
||||||
|
// Use both height-based and force-based checks
|
||||||
|
setTimeout(() => {
|
||||||
|
checkContentHeight();
|
||||||
|
forceScrollCheck();
|
||||||
|
}, 5); // Very short delay to let DOM settle
|
||||||
|
});
|
||||||
|
|
||||||
|
// Monitor all possible DOM changes that could affect content
|
||||||
|
mutationObserverRef.current.observe(scrollRef.current, {
|
||||||
|
childList: true, // Child elements added/removed
|
||||||
|
subtree: true, // Monitor entire subtree
|
||||||
|
characterData: true, // Text content changes
|
||||||
|
attributes: true, // All attribute changes
|
||||||
|
attributeOldValue: true, // Track attribute value changes
|
||||||
|
characterDataOldValue: true, // Track text changes
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('ChatContentContainer: Enhanced MutationObserver started');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ChatContentContainer: Error creating MutationObserver:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also set up a backup interval to ensure scrolling continues
|
||||||
|
backupIntervalRef.current = setInterval(() => {
|
||||||
|
if (!isStreamingRef.current) {
|
||||||
|
if (backupIntervalRef.current) {
|
||||||
|
clearInterval(backupIntervalRef.current);
|
||||||
|
backupIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('ChatContentContainer: Backup scroll check');
|
||||||
|
forceScrollCheck();
|
||||||
|
}, 500); // Check every 500ms as backup
|
||||||
|
|
||||||
|
// Exit streaming mode after no content updates for 10 seconds
|
||||||
|
streamingTimeoutRef.current = setTimeout(() => {
|
||||||
|
console.log('ChatContentContainer: Exiting streaming mode after timeout');
|
||||||
|
cleanupStreamingMode();
|
||||||
|
}, 10000);
|
||||||
|
}, [scrollToBottomInstant, checkContentHeight, forceScrollCheck, cleanupStreamingMode]);
|
||||||
|
|
||||||
|
// Monitor history changes for new messages
|
||||||
|
useEffect(() => {
|
||||||
|
if (!scrollRef.current) return;
|
||||||
|
|
||||||
|
const currentMessageCount = history.length;
|
||||||
|
const isNewMessage = currentMessageCount > prevMessageCountRef.current;
|
||||||
|
|
||||||
|
console.log('ChatContentContainer message count check:', {
|
||||||
|
currentMessageCount,
|
||||||
|
prevCount: prevMessageCountRef.current,
|
||||||
|
isNewMessage,
|
||||||
|
isStreaming: isStreamingRef.current,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle new messages - always scroll to bottom and start streaming mode
|
||||||
|
if (isNewMessage) {
|
||||||
|
console.log('ChatContentContainer: New message detected, starting streaming mode');
|
||||||
|
|
||||||
|
prevMessageCountRef.current = currentMessageCount;
|
||||||
|
startStreamingMode();
|
||||||
|
} else if (!isStreamingRef.current) {
|
||||||
|
// Handle content updates when not in streaming mode (e.g., static content changes)
|
||||||
|
const scrollElement = scrollRef.current;
|
||||||
|
if (scrollElement && !isUserScrollingRef.current) {
|
||||||
|
const currentHeight = scrollElement.scrollHeight;
|
||||||
|
const heightDiff = currentHeight - lastContentHeightRef.current;
|
||||||
|
|
||||||
|
if (heightDiff > 0) {
|
||||||
|
console.log('ChatContentContainer: Non-streaming content change, scrolling');
|
||||||
|
scrollToBottomInstant();
|
||||||
|
lastContentHeightRef.current = currentHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [history.length, startStreamingMode, scrollToBottomInstant]);
|
||||||
|
|
||||||
|
// Add scroll event listener and cleanup
|
||||||
|
useEffect(() => {
|
||||||
|
const scrollElement = scrollRef.current;
|
||||||
|
if (scrollElement) {
|
||||||
|
scrollElement.addEventListener('scroll', handleScrollEvent);
|
||||||
|
return () => {
|
||||||
|
scrollElement.removeEventListener('scroll', handleScrollEvent);
|
||||||
|
// Use the centralized cleanup function
|
||||||
|
cleanupStreamingMode();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [handleScrollEvent, cleanupStreamingMode]);
|
||||||
|
|
||||||
const scrollToTop = () => {
|
const scrollToTop = () => {
|
||||||
if (scrollRef.current) {
|
if (scrollRef.current) {
|
||||||
scrollRef.current.scrollTo({
|
scrollRef.current.scrollTo({
|
||||||
|
@ -10,11 +10,10 @@ import { useAsyncEffect } from 'ahooks';
|
|||||||
import { Modal } from 'antd';
|
import { Modal } from 'antd';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useContext, useMemo, useState } from 'react';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
const ChatCompletion: React.FC = () => {
|
const ChatCompletion: React.FC = () => {
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const chatId = searchParams?.get('id') ?? '';
|
const chatId = searchParams?.get('id') ?? '';
|
||||||
|
|
||||||
@ -79,160 +78,8 @@ const ChatCompletion: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [chatId, currentDialogInfo]);
|
}, [chatId, currentDialogInfo]);
|
||||||
|
|
||||||
// Track message count and user scrolling behavior
|
|
||||||
const prevMessageCountRef = useRef(history.length);
|
|
||||||
const lastScrollTimeRef = useRef(0);
|
|
||||||
const isUserScrollingRef = useRef(false);
|
|
||||||
const lastContentHeightRef = useRef(0);
|
|
||||||
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
||||||
const isAutoScrollingRef = useRef(false); // Track if we're auto-scrolling
|
|
||||||
|
|
||||||
// Initialize refs
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('ChatCompletion initializing with history length:', history.length);
|
|
||||||
// Set initial scroll time to allow streaming from the beginning
|
|
||||||
if (lastScrollTimeRef.current === 0) {
|
|
||||||
lastScrollTimeRef.current = Date.now() - 3000; // Set to 3 seconds ago
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Update message count tracking when history changes
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('ChatCompletion updating prevMessageCountRef:', {
|
|
||||||
currentLength: history.length,
|
|
||||||
prevCount: prevMessageCountRef.current,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only update if this is the first time (count is 0)
|
|
||||||
if (prevMessageCountRef.current === 0) {
|
|
||||||
prevMessageCountRef.current = history.length;
|
|
||||||
}
|
|
||||||
}, [history.length]);
|
|
||||||
|
|
||||||
// Handle scroll events to detect user interaction
|
|
||||||
const handleScrollEvent = useCallback(() => {
|
|
||||||
// Ignore scroll events caused by auto-scrolling
|
|
||||||
if (isAutoScrollingRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastScrollTimeRef.current = Date.now();
|
|
||||||
|
|
||||||
if (scrollRef.current) {
|
|
||||||
const scrollElement = scrollRef.current;
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollElement;
|
|
||||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 20;
|
|
||||||
isUserScrollingRef.current = !isAtBottom;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!scrollRef.current) return;
|
|
||||||
|
|
||||||
const scrollElement = scrollRef.current;
|
|
||||||
const currentMessageCount = history.length;
|
|
||||||
const isNewMessage = currentMessageCount > prevMessageCountRef.current;
|
|
||||||
const now = Date.now();
|
|
||||||
const userRecentlyScrolled = now - lastScrollTimeRef.current < 2000;
|
|
||||||
|
|
||||||
console.log('ChatCompletion scroll check:', {
|
|
||||||
currentMessageCount,
|
|
||||||
prevCount: prevMessageCountRef.current,
|
|
||||||
isNewMessage,
|
|
||||||
historyLength: history.length,
|
|
||||||
userRecentlyScrolled,
|
|
||||||
isUserScrolling: isUserScrollingRef.current,
|
|
||||||
isAutoScrolling: isAutoScrollingRef.current,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Always handle new messages first - this is the highest priority
|
|
||||||
if (isNewMessage) {
|
|
||||||
console.log('ChatCompletion: New message detected, forcing scroll to bottom');
|
|
||||||
// New message - always scroll to bottom regardless of user position
|
|
||||||
isAutoScrollingRef.current = true;
|
|
||||||
|
|
||||||
// Reset states IMMEDIATELY to ensure streaming can work
|
|
||||||
lastScrollTimeRef.current = Date.now() - 3000; // Allow streaming immediately
|
|
||||||
isUserScrollingRef.current = false;
|
|
||||||
lastContentHeightRef.current = scrollElement.scrollHeight; // Set current height as baseline
|
|
||||||
|
|
||||||
scrollElement.scrollTo({
|
|
||||||
top: scrollElement.scrollHeight,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset auto scroll flag after animation
|
|
||||||
setTimeout(() => {
|
|
||||||
isAutoScrollingRef.current = false;
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
prevMessageCountRef.current = currentMessageCount; // Update count immediately
|
|
||||||
return; // Exit early for new messages
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle streaming content updates (only if user hasn't manually scrolled recently)
|
|
||||||
if (!userRecentlyScrolled && !isUserScrollingRef.current && !isAutoScrollingRef.current) {
|
|
||||||
// Streaming content - scroll based on content height change
|
|
||||||
const currentHeight = scrollElement.scrollHeight;
|
|
||||||
|
|
||||||
// Initialize lastContentHeightRef if not set
|
|
||||||
if (lastContentHeightRef.current === 0) {
|
|
||||||
lastContentHeightRef.current = currentHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
const heightDiff = currentHeight - lastContentHeightRef.current;
|
|
||||||
|
|
||||||
console.log('ChatCompletion streaming check:', {
|
|
||||||
currentHeight,
|
|
||||||
lastHeight: lastContentHeightRef.current,
|
|
||||||
heightDiff,
|
|
||||||
threshold: 12,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only scroll if content height increased by at least ~0.5 lines (12px) for smoother experience
|
|
||||||
if (heightDiff >= 12) {
|
|
||||||
// Clear any pending scroll timeout
|
|
||||||
if (scrollTimeoutRef.current) {
|
|
||||||
clearTimeout(scrollTimeoutRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('ChatCompletion: Triggering streaming scroll, heightDiff:', heightDiff);
|
|
||||||
|
|
||||||
// Debounce scroll calls to avoid conflicts
|
|
||||||
scrollTimeoutRef.current = setTimeout(() => {
|
|
||||||
isAutoScrollingRef.current = true;
|
|
||||||
scrollElement.scrollTo({
|
|
||||||
top: scrollElement.scrollHeight,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
setTimeout(() => {
|
|
||||||
isAutoScrollingRef.current = false;
|
|
||||||
}, 200); // Longer timeout for streaming scroll
|
|
||||||
lastContentHeightRef.current = currentHeight;
|
|
||||||
}, 30); // 30ms debounce for smooth experience
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('ChatCompletion streaming blocked:', {
|
|
||||||
userRecentlyScrolled,
|
|
||||||
isUserScrolling: isUserScrollingRef.current,
|
|
||||||
isAutoScrolling: isAutoScrollingRef.current,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [history]);
|
|
||||||
|
|
||||||
// Add scroll event listener
|
|
||||||
useEffect(() => {
|
|
||||||
const scrollElement = scrollRef.current;
|
|
||||||
if (scrollElement) {
|
|
||||||
scrollElement.addEventListener('scroll', handleScrollEvent);
|
|
||||||
return () => {
|
|
||||||
scrollElement.removeEventListener('scroll', handleScrollEvent);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [handleScrollEvent]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col w-5/6 mx-auto' ref={scrollRef}>
|
<div className='flex flex-col w-5/6 mx-auto'>
|
||||||
{!!showMessages.length &&
|
{!!showMessages.length &&
|
||||||
showMessages.map((content, index) => {
|
showMessages.map((content, index) => {
|
||||||
return (
|
return (
|
||||||
|
Loading…
Reference in New Issue
Block a user