@@ -2,29 +2,9 @@ import { memo, useEffect, useRef, useState } from 'react'
22import CopilotMarkdownRenderer from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer'
33
44/**
5- * Minimum delay between characters (fast catch-up mode)
5+ * Character animation delay in milliseconds
66 */
7- const MIN_DELAY = 1
8-
9- /**
10- * Maximum delay between characters (when waiting for content)
11- */
12- const MAX_DELAY = 12
13-
14- /**
15- * Default delay when streaming normally
16- */
17- const DEFAULT_DELAY = 4
18-
19- /**
20- * How far behind (in characters) before we speed up
21- */
22- const CATCH_UP_THRESHOLD = 20
23-
24- /**
25- * How close to content before we slow down
26- */
27- const SLOW_DOWN_THRESHOLD = 5
7+ const CHARACTER_DELAY = 3
288
299/**
3010 * StreamingIndicator shows animated dots during message streaming
@@ -54,50 +34,21 @@ interface SmoothStreamingTextProps {
5434 isStreaming : boolean
5535}
5636
57- /**
58- * Calculates adaptive delay based on how far behind animation is from actual content
59- *
60- * @param displayedLength - Current displayed content length
61- * @param totalLength - Total available content length
62- * @returns Delay in milliseconds
63- */
64- function calculateAdaptiveDelay ( displayedLength : number , totalLength : number ) : number {
65- const charsRemaining = totalLength - displayedLength
66-
67- if ( charsRemaining > CATCH_UP_THRESHOLD ) {
68- // Far behind - speed up to catch up
69- // Scale from MIN_DELAY to DEFAULT_DELAY based on how far behind
70- const catchUpFactor = Math . min ( 1 , ( charsRemaining - CATCH_UP_THRESHOLD ) / 50 )
71- return MIN_DELAY + ( DEFAULT_DELAY - MIN_DELAY ) * ( 1 - catchUpFactor )
72- }
73-
74- if ( charsRemaining <= SLOW_DOWN_THRESHOLD ) {
75- // Close to content edge - slow down to feel natural
76- // The closer we are, the slower we go (up to MAX_DELAY)
77- const slowFactor = 1 - charsRemaining / SLOW_DOWN_THRESHOLD
78- return DEFAULT_DELAY + ( MAX_DELAY - DEFAULT_DELAY ) * slowFactor
79- }
80-
81- // Normal streaming speed
82- return DEFAULT_DELAY
83- }
84-
8537/**
8638 * SmoothStreamingText component displays text with character-by-character animation
87- * Creates a smooth streaming effect for AI responses with adaptive speed
88- *
89- * Uses adaptive pacing: speeds up when catching up, slows down near content edge
39+ * Creates a smooth streaming effect for AI responses
9040 *
9141 * @param props - Component props
9242 * @returns Streaming text with smooth animation
9343 */
9444export const SmoothStreamingText = memo (
9545 ( { content, isStreaming } : SmoothStreamingTextProps ) => {
96- const [ displayedContent , setDisplayedContent ] = useState ( '' )
46+ // Initialize with full content when not streaming to avoid flash on page load
47+ const [ displayedContent , setDisplayedContent ] = useState ( ( ) => ( isStreaming ? '' : content ) )
9748 const contentRef = useRef ( content )
98- const rafRef = useRef < number | null > ( null )
99- const indexRef = useRef ( 0 )
100- const lastFrameTimeRef = useRef < number > ( 0 )
49+ const timeoutRef = useRef < NodeJS . Timeout | null > ( null )
50+ // Initialize index based on streaming state
51+ const indexRef = useRef ( isStreaming ? 0 : content . length )
10152 const isAnimatingRef = useRef ( false )
10253
10354 useEffect ( ( ) => {
@@ -110,51 +61,42 @@ export const SmoothStreamingText = memo(
11061 }
11162
11263 if ( isStreaming ) {
113- if ( indexRef . current < content . length && ! isAnimatingRef . current ) {
114- isAnimatingRef . current = true
115- lastFrameTimeRef . current = performance . now ( )
116-
117- const animateText = ( timestamp : number ) => {
64+ if ( indexRef . current < content . length ) {
65+ const animateText = ( ) => {
11866 const currentContent = contentRef . current
11967 const currentIndex = indexRef . current
120- const elapsed = timestamp - lastFrameTimeRef . current
12168
122- // Calculate adaptive delay based on how far behind we are
123- const delay = calculateAdaptiveDelay ( currentIndex , currentContent . length )
124-
125- if ( elapsed >= delay ) {
126- if ( currentIndex < currentContent . length ) {
127- const newDisplayed = currentContent . slice ( 0 , currentIndex + 1 )
128- setDisplayedContent ( newDisplayed )
129- indexRef . current = currentIndex + 1
130- lastFrameTimeRef . current = timestamp
131- }
132- }
133-
134- if ( indexRef . current < currentContent . length ) {
135- rafRef . current = requestAnimationFrame ( animateText )
69+ if ( currentIndex < currentContent . length ) {
70+ const newDisplayed = currentContent . slice ( 0 , currentIndex + 1 )
71+ setDisplayedContent ( newDisplayed )
72+ indexRef . current = currentIndex + 1
73+ timeoutRef . current = setTimeout ( animateText , CHARACTER_DELAY )
13674 } else {
13775 isAnimatingRef . current = false
13876 }
13977 }
14078
141- rafRef . current = requestAnimationFrame ( animateText )
142- } else if ( indexRef . current < content . length && isAnimatingRef . current ) {
143- // Animation already running, it will pick up new content automatically
79+ if ( ! isAnimatingRef . current ) {
80+ if ( timeoutRef . current ) {
81+ clearTimeout ( timeoutRef . current )
82+ }
83+ isAnimatingRef . current = true
84+ animateText ( )
85+ }
14486 }
14587 } else {
14688 // Streaming ended - show full content immediately
147- if ( rafRef . current ) {
148- cancelAnimationFrame ( rafRef . current )
89+ if ( timeoutRef . current ) {
90+ clearTimeout ( timeoutRef . current )
14991 }
15092 setDisplayedContent ( content )
15193 indexRef . current = content . length
15294 isAnimatingRef . current = false
15395 }
15496
15597 return ( ) => {
156- if ( rafRef . current ) {
157- cancelAnimationFrame ( rafRef . current )
98+ if ( timeoutRef . current ) {
99+ clearTimeout ( timeoutRef . current )
158100 }
159101 isAnimatingRef . current = false
160102 }
0 commit comments