feat(chat): smooth streaming output
This commit is contained in:
@@ -24,6 +24,9 @@ import { useSpeechRecognition, useSpeechSynthesis } from "./GlobalChatbox.voice"
|
||||
import { useAgentChatSession } from "./hooks/useAgentChatSession";
|
||||
import { useAgentToolActions } from "./hooks/useAgentToolActions";
|
||||
|
||||
const STREAMING_BOTTOM_RESERVE_PX = 180;
|
||||
const STREAMING_SCROLL_RESTORE_AT_PX = STREAMING_BOTTOM_RESERVE_PX - 36;
|
||||
|
||||
export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
||||
const [width, setWidth] = useState(520);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
@@ -35,6 +38,9 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
||||
useState<AgentApprovalMode>("request");
|
||||
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
const workspaceScrollRef = useRef<HTMLDivElement>(null);
|
||||
const isNearBottomRef = useRef(true);
|
||||
const streamingScrollFrameRef = useRef<number | null>(null);
|
||||
const composerRef = useRef<AgentComposerHandle | null>(null);
|
||||
const hasResetForOpenRef = useRef(false);
|
||||
const theme = useTheme();
|
||||
@@ -123,9 +129,53 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
||||
bottomRef.current?.scrollIntoView({ behavior });
|
||||
}, []);
|
||||
|
||||
const cancelStreamingScroll = useCallback(() => {
|
||||
if (streamingScrollFrameRef.current === null) return;
|
||||
window.cancelAnimationFrame(streamingScrollFrameRef.current);
|
||||
streamingScrollFrameRef.current = null;
|
||||
}, []);
|
||||
|
||||
const scheduleStreamingScrollToBottom = useCallback(() => {
|
||||
if (streamingScrollFrameRef.current !== null) return;
|
||||
streamingScrollFrameRef.current = window.requestAnimationFrame(() => {
|
||||
streamingScrollFrameRef.current = null;
|
||||
const container = workspaceScrollRef.current;
|
||||
if (!container || !isNearBottomRef.current) return;
|
||||
|
||||
const distanceToBottom =
|
||||
container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||
if (distanceToBottom < STREAMING_SCROLL_RESTORE_AT_PX) return;
|
||||
|
||||
container.scrollTop = container.scrollHeight - container.clientHeight;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleWorkspaceScrollStateChange = useCallback((isNearBottom: boolean) => {
|
||||
isNearBottomRef.current = isNearBottom;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom(isStreaming ? "auto" : "smooth");
|
||||
}, [isStreaming, messages, scrollToBottom]);
|
||||
if (isStreaming) {
|
||||
if (!isNearBottomRef.current) return;
|
||||
scheduleStreamingScrollToBottom();
|
||||
return;
|
||||
}
|
||||
cancelStreamingScroll();
|
||||
scrollToBottom("smooth");
|
||||
}, [
|
||||
cancelStreamingScroll,
|
||||
isStreaming,
|
||||
messages,
|
||||
scheduleStreamingScrollToBottom,
|
||||
scrollToBottom,
|
||||
]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
cancelStreamingScroll();
|
||||
},
|
||||
[cancelStreamingScroll],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
@@ -140,10 +190,12 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
||||
composerRef.current?.clear();
|
||||
setIsHistoryOpen(false);
|
||||
composerRef.current?.focus();
|
||||
isNearBottomRef.current = true;
|
||||
cancelStreamingScroll();
|
||||
scrollToBottom("auto");
|
||||
}, 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [createSession, isHydrating, open, scrollToBottom]);
|
||||
}, [cancelStreamingScroll, createSession, isHydrating, open, scrollToBottom]);
|
||||
|
||||
const handleSend = useCallback(async (prompt: string) => {
|
||||
if (isStreaming || isCheckingAuth) return;
|
||||
@@ -181,9 +233,11 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
||||
composerRef.current?.clear();
|
||||
window.setTimeout(() => {
|
||||
composerRef.current?.focus();
|
||||
isNearBottomRef.current = true;
|
||||
cancelStreamingScroll();
|
||||
scrollToBottom("auto");
|
||||
}, 0);
|
||||
}, [createSession, handleStopSpeech, scrollToBottom, stopListening]);
|
||||
}, [cancelStreamingScroll, createSession, handleStopSpeech, scrollToBottom, stopListening]);
|
||||
|
||||
const handleHistoryToggle = useCallback(() => {
|
||||
setIsHistoryOpen((prev) => !prev);
|
||||
@@ -377,7 +431,9 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
||||
messages={messages}
|
||||
isStreaming={isStreaming}
|
||||
isLoadingSession={Boolean(loadingSessionId)}
|
||||
scrollContainerRef={workspaceScrollRef}
|
||||
bottomRef={bottomRef}
|
||||
onScrollStateChange={handleWorkspaceScrollStateChange}
|
||||
speakingMessageId={speakingMessageId}
|
||||
speechState={speechState}
|
||||
onSpeak={handleSpeak}
|
||||
|
||||
Reference in New Issue
Block a user