All files / src/components/chat/task-header AutoCondenseMarker.tsx

0% Statements 0/106
0% Branches 0/1
0% Functions 0/1
0% Lines 0/106

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130                                                                                                                                                                                                                                                                   
import { cn } from "@heroui/react"
import React, { useEffect, useMemo, useRef, useState } from "react"
 
export const AutoCondenseMarker: React.FC<{
	threshold: number
	usage: number
	isContextWindowHoverOpen?: boolean
	shouldAnimate?: boolean
}> = ({ threshold, usage, isContextWindowHoverOpen, shouldAnimate = false }) => {
	const [isAnimating, setIsAnimating] = useState(false)
	const [animatedPosition, setAnimatedPosition] = useState(0)
	const [showPercentageAfterAnimation, setShowPercentageAfterAnimation] = useState(false)
	const [isFadingOut, setIsFadingOut] = useState(false)
 
	// Refs to store animation frame and timeout IDs for cleanup
	const animationFrameRef = useRef<number | null>(null)
	const fadeOutTimeoutRef = useRef<NodeJS.Timeout | null>(null)
	const hideTimeoutRef = useRef<NodeJS.Timeout | null>(null)
 
	// Animation effect when shouldAnimate prop changes (initial load)
	useEffect(() => {
		// Cleanup function to cancel any pending animations or timeouts
		const cleanup = () => {
			if (animationFrameRef.current !== null) {
				cancelAnimationFrame(animationFrameRef.current)
				animationFrameRef.current = null
			}
			if (fadeOutTimeoutRef.current !== null) {
				clearTimeout(fadeOutTimeoutRef.current)
				fadeOutTimeoutRef.current = null
			}
			if (hideTimeoutRef.current !== null) {
				clearTimeout(hideTimeoutRef.current)
				hideTimeoutRef.current = null
			}
		}
 
		if (shouldAnimate && threshold > 0) {
			// Clean up any existing animations before starting new one
			cleanup()
 
			setIsAnimating(true)
			const targetPosition = threshold * 100
			const duration = 1200 // ms - slowed down from 800ms
			const startTime = Date.now()
 
			const animate = () => {
				const elapsed = Date.now() - startTime
				const progress = Math.min(elapsed / duration, 1)
				// Ease-out animation curve
				const easeOut = 1 - (1 - progress) ** 3
				const currentPosition = easeOut * targetPosition
				setAnimatedPosition(currentPosition)
 
				if (progress < 1) {
					animationFrameRef.current = requestAnimationFrame(animate)
				} else {
					animationFrameRef.current = null
					setIsAnimating(false)
					setShowPercentageAfterAnimation(true)
					// Start fade out after 1 second
					fadeOutTimeoutRef.current = setTimeout(() => {
						setIsFadingOut(true)
						// Completely hide after fade transition
						hideTimeoutRef.current = setTimeout(() => {
							setShowPercentageAfterAnimation(false)
							setIsFadingOut(false)
							hideTimeoutRef.current = null
							setAnimatedPosition(threshold * 100) // Ensure it ends exactly at threshold
						}, 300) // 300ms fade duration
						fadeOutTimeoutRef.current = null
					}, 1000)
				}
			}
 
			animationFrameRef.current = requestAnimationFrame(animate)
		}
 
		// Cleanup on unmount or when dependencies change
		return cleanup
	}, [shouldAnimate, threshold])
 
	// The marker position is calculated based on the threshold percentage
	// It goes over the progress bar to indicate where the auto-condense will trigger
	// and it should highlight from what the current percentage (usage) is
	// to the threshold percentage
	const marker = useMemo(() => {
		const _threshold = threshold * 100
		// Always use the current threshold for position and label - animation only affects visual movement
		const position = _threshold
		const startingPosition = isAnimating ? animatedPosition : position
 
		return {
			start: startingPosition + "%",
			label: startingPosition.toFixed(0),
			end: usage > startingPosition ? usage - startingPosition + "%" : 0,
		}
	}, [threshold, usage, isAnimating, animatedPosition])
 
	if (!threshold) {
		return null
	}
 
	return (
		<div className="flex-1" id="auto-condense-threshold-marker">
			<div
				className={cn(
					"absolute top-0 bottom-0 h-full cursor-pointer pointer-events-none z-10 bg-button-background shadow-lg w-1",
					{
						"transition-all duration-75": !isAnimating,
					},
				)}
				style={{
					left: marker.start,
					transform: isAnimating ? `translateX(${animatedPosition - threshold * 100}%)` : "translateX(0)",
				}}>
				{(isContextWindowHoverOpen || isAnimating || showPercentageAfterAnimation) && (
					<div
						className={cn("absolute -top-4 -left-1 text-button-background font-mono text-xs", {
							"opacity-0": isFadingOut,
						})}>
						{marker.label}%
					</div>
				)}
			</div>
		</div>
	)
}
AutoCondenseMarker.displayName = "AutoCondenseMarker"