mirror of https://github.com/kortix-ai/suna.git
improve streaming
This commit is contained in:
parent
5c8a82ffc4
commit
6dbc63baee
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { ComposioUrlDetector } from './composio-url-detector';
|
||||||
|
|
||||||
|
interface StreamingTextProps {
|
||||||
|
content: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StreamingText: React.FC<StreamingTextProps> = ({
|
||||||
|
content,
|
||||||
|
className = "text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere"
|
||||||
|
}) => {
|
||||||
|
if (!content) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-hidden">
|
||||||
|
<ComposioUrlDetector content={content} className={className} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -17,6 +17,7 @@ import { AgentAvatar, AgentName } from './agent-avatar';
|
||||||
import { parseXmlToolCalls, isNewXmlFormat } from '@/components/thread/tool-views/xml-parser';
|
import { parseXmlToolCalls, isNewXmlFormat } from '@/components/thread/tool-views/xml-parser';
|
||||||
import { ShowToolStream } from './ShowToolStream';
|
import { ShowToolStream } from './ShowToolStream';
|
||||||
import { ComposioUrlDetector } from './composio-url-detector';
|
import { ComposioUrlDetector } from './composio-url-detector';
|
||||||
|
import { StreamingText } from './StreamingText';
|
||||||
import { HIDE_STREAMING_XML_TAGS } from '@/components/thread/utils';
|
import { HIDE_STREAMING_XML_TAGS } from '@/components/thread/utils';
|
||||||
|
|
||||||
|
|
||||||
|
@ -910,12 +911,10 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{textBeforeTag && (
|
<StreamingText
|
||||||
<ComposioUrlDetector content={textBeforeTag} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere" />
|
content={textBeforeTag}
|
||||||
)}
|
className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere"
|
||||||
{showCursor && (
|
/>
|
||||||
<span className="inline-block h-4 w-0.5 bg-primary ml-0.5 -mb-1 animate-pulse" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{detectedTag && (
|
{detectedTag && (
|
||||||
<ShowToolStream
|
<ShowToolStream
|
||||||
|
@ -926,8 +925,6 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
||||||
startTime={Date.now()}
|
startTime={Date.now()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
@ -970,15 +967,16 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
||||||
{debugMode && streamingText ? (
|
{debugMode && streamingText ? (
|
||||||
<pre className="text-xs font-mono whitespace-pre-wrap overflow-x-auto p-2 border border-border rounded-md bg-muted/30">
|
<pre className="text-xs font-mono whitespace-pre-wrap overflow-x-auto p-2 border border-border rounded-md bg-muted/30">
|
||||||
{streamingText}
|
{streamingText}
|
||||||
|
{showCursor && (
|
||||||
|
<span className="inline-block h-4 w-0.5 bg-primary ml-0.5 -mb-1 animate-pulse duration-1000" />
|
||||||
|
)}
|
||||||
</pre>
|
</pre>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{textBeforeTag && (
|
<StreamingText
|
||||||
<ComposioUrlDetector content={textBeforeTag} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere" />
|
content={textBeforeTag}
|
||||||
)}
|
className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere"
|
||||||
{showCursor && (
|
/>
|
||||||
<span className="inline-block h-4 w-0.5 bg-primary ml-0.5 -mb-1 animate-pulse" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{detectedTag && (
|
{detectedTag && (
|
||||||
<ShowToolStream
|
<ShowToolStream
|
||||||
|
@ -1070,9 +1068,9 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
||||||
{/* Streaming indicator content */}
|
{/* Streaming indicator content */}
|
||||||
<div className="max-w-[90%] px-4 py-3 text-sm">
|
<div className="max-w-[90%] px-4 py-3 text-sm">
|
||||||
<div className="flex items-center gap-1.5 py-1">
|
<div className="flex items-center gap-1.5 py-1">
|
||||||
<div className="h-1.5 w-1.5 rounded-full bg-primary/50 animate-pulse" />
|
<div className="h-1.5 w-1.5 rounded-full bg-primary/50 animate-pulse duration-1000" />
|
||||||
<div className="h-1.5 w-1.5 rounded-full bg-primary/50 animate-pulse delay-150" />
|
<div className="h-1.5 w-1.5 rounded-full bg-primary/50 animate-pulse duration-1000 delay-150" />
|
||||||
<div className="h-1.5 w-1.5 rounded-full bg-primary/50 animate-pulse delay-300" />
|
<div className="h-1.5 w-1.5 rounded-full bg-primary/50 animate-pulse duration-1000 delay-300" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
streamAgent,
|
streamAgent,
|
||||||
|
@ -90,6 +90,34 @@ export function useAgentStream(
|
||||||
const [textContent, setTextContent] = useState<
|
const [textContent, setTextContent] = useState<
|
||||||
{ content: string; sequence?: number }[]
|
{ content: string; sequence?: number }[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
|
// Add throttled state updates for smoother streaming
|
||||||
|
const throttleRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const pendingContentRef = useRef<{ content: string; sequence?: number }[]>([]);
|
||||||
|
|
||||||
|
// Throttled content update function for smoother streaming
|
||||||
|
const flushPendingContent = useCallback(() => {
|
||||||
|
if (pendingContentRef.current.length > 0) {
|
||||||
|
const newContent = [...pendingContentRef.current];
|
||||||
|
pendingContentRef.current = [];
|
||||||
|
|
||||||
|
React.startTransition(() => {
|
||||||
|
setTextContent((prev) => [...prev, ...newContent]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const addContentThrottled = useCallback((content: { content: string; sequence?: number }) => {
|
||||||
|
pendingContentRef.current.push(content);
|
||||||
|
|
||||||
|
// Clear existing throttle
|
||||||
|
if (throttleRef.current) {
|
||||||
|
clearTimeout(throttleRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new throttle for smooth updates (16ms ≈ 60fps)
|
||||||
|
throttleRef.current = setTimeout(flushPendingContent, 16);
|
||||||
|
}, [flushPendingContent]);
|
||||||
const [toolCall, setToolCall] = useState<ParsedContent | null>(null);
|
const [toolCall, setToolCall] = useState<ParsedContent | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [agentRunId, setAgentRunId] = useState<string | null>(null);
|
const [agentRunId, setAgentRunId] = useState<string | null>(null);
|
||||||
|
@ -101,9 +129,16 @@ export function useAgentStream(
|
||||||
const setMessagesRef = useRef(setMessages); // Ref to hold the setMessages function
|
const setMessagesRef = useRef(setMessages); // Ref to hold the setMessages function
|
||||||
|
|
||||||
const orderedTextContent = useMemo(() => {
|
const orderedTextContent = useMemo(() => {
|
||||||
return textContent
|
// Use a more efficient approach for streaming performance
|
||||||
.sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0))
|
if (textContent.length === 0) return '';
|
||||||
.reduce((acc, curr) => acc + curr.content, '');
|
|
||||||
|
// Sort once and concatenate efficiently
|
||||||
|
const sorted = textContent.slice().sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0));
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < sorted.length; i++) {
|
||||||
|
result += sorted[i].content;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}, [textContent]);
|
}, [textContent]);
|
||||||
|
|
||||||
// Refs to capture current state for persistence
|
// Refs to capture current state for persistence
|
||||||
|
@ -341,14 +376,16 @@ export function useAgentStream(
|
||||||
parsedMetadata.stream_status === 'chunk' &&
|
parsedMetadata.stream_status === 'chunk' &&
|
||||||
parsedContent.content
|
parsedContent.content
|
||||||
) {
|
) {
|
||||||
setTextContent((prev) => {
|
// Use throttled approach for smoother streaming
|
||||||
return prev.concat({
|
addContentThrottled({
|
||||||
sequence: message.sequence,
|
sequence: message.sequence,
|
||||||
content: parsedContent.content,
|
content: parsedContent.content,
|
||||||
});
|
|
||||||
});
|
});
|
||||||
callbacks.onAssistantChunk?.({ content: parsedContent.content });
|
callbacks.onAssistantChunk?.({ content: parsedContent.content });
|
||||||
} else if (parsedMetadata.stream_status === 'complete') {
|
} else if (parsedMetadata.stream_status === 'complete') {
|
||||||
|
// Flush any pending content before completing
|
||||||
|
flushPendingContent();
|
||||||
|
|
||||||
setTextContent([]);
|
setTextContent([]);
|
||||||
setToolCall(null);
|
setToolCall(null);
|
||||||
if (message.message_id) callbacks.onMessage(message);
|
if (message.message_id) callbacks.onMessage(message);
|
||||||
|
@ -554,6 +591,15 @@ export function useAgentStream(
|
||||||
return () => {
|
return () => {
|
||||||
isMountedRef.current = false;
|
isMountedRef.current = false;
|
||||||
|
|
||||||
|
// Clean up throttle timeout
|
||||||
|
if (throttleRef.current) {
|
||||||
|
clearTimeout(throttleRef.current);
|
||||||
|
throttleRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush any remaining pending content
|
||||||
|
flushPendingContent();
|
||||||
|
|
||||||
// Don't automatically cleanup streams on navigation
|
// Don't automatically cleanup streams on navigation
|
||||||
// Only set mounted flag to false to prevent new operations
|
// Only set mounted flag to false to prevent new operations
|
||||||
// Streams will be cleaned up when they naturally complete or on explicit stop
|
// Streams will be cleaned up when they naturally complete or on explicit stop
|
||||||
|
|
Loading…
Reference in New Issue