/ components / Spinner / useStalledAnimation.ts
useStalledAnimation.ts
 1  import { useRef } from 'react'
 2  
 3  // Hook to handle the transition to red when tokens stop flowing.
 4  // Driven by the parent's animation clock time instead of independent intervals,
 5  // so it slows down when the terminal is blurred.
 6  export function useStalledAnimation(
 7    time: number,
 8    currentResponseLength: number,
 9    hasActiveTools = false,
10    reducedMotion = false,
11  ): {
12    isStalled: boolean
13    stalledIntensity: number
14  } {
15    const lastTokenTime = useRef(time)
16    const lastResponseLength = useRef(currentResponseLength)
17    const mountTime = useRef(time)
18    const stalledIntensityRef = useRef(0)
19    const lastSmoothTime = useRef(time)
20  
21    // Reset timer when new tokens arrive (check actual length change)
22    if (currentResponseLength > lastResponseLength.current) {
23      lastTokenTime.current = time
24      lastResponseLength.current = currentResponseLength
25      stalledIntensityRef.current = 0
26      lastSmoothTime.current = time
27    }
28  
29    // Derive time since last token from animation clock
30    let timeSinceLastToken: number
31    if (hasActiveTools) {
32      timeSinceLastToken = 0
33      lastTokenTime.current = time
34    } else if (currentResponseLength > 0) {
35      timeSinceLastToken = time - lastTokenTime.current
36    } else {
37      timeSinceLastToken = time - mountTime.current
38    }
39  
40    // Calculate stalled intensity based on time since last token
41    // Start showing red after 3 seconds of no new tokens (only when no tools are active)
42    const isStalled = timeSinceLastToken > 3000 && !hasActiveTools
43    const intensity = isStalled
44      ? Math.min((timeSinceLastToken - 3000) / 2000, 1) // Fade over 2 seconds
45      : 0
46  
47    // Smooth intensity transition driven by animation frame ticks
48    if (!reducedMotion && (intensity > 0 || stalledIntensityRef.current > 0)) {
49      const dt = time - lastSmoothTime.current
50      if (dt >= 50) {
51        const steps = Math.floor(dt / 50)
52        let current = stalledIntensityRef.current
53        for (let i = 0; i < steps; i++) {
54          const diff = intensity - current
55          if (Math.abs(diff) < 0.01) {
56            current = intensity
57            break
58          }
59          current += diff * 0.1
60        }
61        stalledIntensityRef.current = current
62        lastSmoothTime.current = time
63      }
64    } else {
65      stalledIntensityRef.current = intensity
66      lastSmoothTime.current = time
67    }
68  
69    // When reducedMotion is enabled, use instant intensity change
70    const effectiveIntensity = reducedMotion
71      ? intensity
72      : stalledIntensityRef.current
73  
74    return { isStalled, stalledIntensity: effectiveIntensity }
75  }