Download atk-interface/src/components/ProgressBarParticles.jsx from Leon4gr45/agent-0: direct link, hf CLI and curl.
- Browser
- Download file 16.7 kB
-
https://huggingface.co/spaces/Leon4gr45/agent-0/resolve/main/atk-interface/src/components/ProgressBarParticles.jsx
- Command line
-
hf download hf://spaces/Leon4gr45/agent-0/atk-interface/src/components/ProgressBarParticles.jsx
-
curl -L -o ProgressBarParticles.jsx https://huggingface.co/spaces/Leon4gr45/agent-0/resolve/main/atk-interface/src/components/ProgressBarParticles.jsx
16.7 kB
| import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; | |
| import { motion, useAnimation } from 'framer-motion'; | |
| // --- Helper Functions --- | |
| const random = (min, max) => Math.random() * (max - min) + min; | |
| const uuid = () => | |
| typeof crypto !== 'undefined' && crypto.randomUUID | |
| ? crypto.randomUUID() | |
| : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; | |
| // Interpolate between two colors | |
| const interpolateColor = (color1, color2, factor) => { | |
| if (factor < 0) factor = 0; | |
| if (factor > 1) factor = 1; | |
| const c1 = parseInt(color1.substring(1), 16); | |
| const c2 = parseInt(color2.substring(1), 16); | |
| const r1 = (c1 >> 16) & 0xff; | |
| const g1 = (c1 >> 8) & 0xff; | |
| const b1 = c1 & 0xff; | |
| const r2 = (c2 >> 16) & 0xff; | |
| const g2 = (c2 >> 8) & 0xff; | |
| const b2 = c2 & 0xff; | |
| const r = Math.round(r1 + (r2 - r1) * factor); | |
| const g = Math.round(g1 + (g2 - g1) * factor); | |
| const b = Math.round(b1 + (b2 - b1) * factor); | |
| return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`; | |
| }; | |
| const START_COLOR = '#4ade80'; // Dull Green (Tailwind green-400) | |
| const END_COLOR = '#00FF00'; // Bright Neon Green | |
| // --- Particle Component --- | |
| const Particle = ({ id, initialX, initialY, targetPointX, targetPointY, color, status, screenHeight, screenWidth, onComplete }) => { | |
| const controls = useAnimation(); | |
| const [currentState, setCurrentState] = useState(status); | |
| const particleRef = useRef({ isComplete: false }); // Ref to track completion status | |
| useEffect(() => { | |
| setCurrentState(status); // Update state if prop changes | |
| }, [status]); | |
| useEffect(() => { | |
| if (particleRef.current.isComplete) return; // Don't restart animation if already completing | |
| let animationSequence; | |
| const durationBase = random(6, 12); // Base duration for normal movement | |
| const fadeDuration = 1.5; // Normal fade duration | |
| // Slow fade durations for lingering particles | |
| const lingerFadeDuration = random(8, 15); // Longer fade for failed/revoked | |
| const lingerDelay = random(3, 7); // Delay before slow fade starts | |
| switch (currentState) { | |
| case 'FAILED': | |
| // Drop to bottom, linger as smaller "corpse", then fade slowly | |
| animationSequence = async () => { | |
| // 1. Drop to near the bottom | |
| await controls.start({ | |
| x: random(0, screenWidth), // Scatter slightly at the bottom | |
| y: screenHeight - random(10, 15), // Settle very close to the bottom edge | |
| opacity: 1, | |
| scale: 0.8, // Slightly smaller "corpse" | |
| transition: { duration: random(1, 2.5), ease: "easeIn" } | |
| }); | |
| // 2. Linger, then fade slowly | |
| await controls.start({ | |
| opacity: 0, | |
| transition: { duration: lingerFadeDuration, delay: lingerDelay } | |
| }); | |
| if (!particleRef.current.isComplete) { | |
| particleRef.current.isComplete = true; | |
| onComplete(id); | |
| } | |
| }; | |
| break; | |
| case 'REVOKED': | |
| // Flee to edges, linger smaller/huddled, then fade slowly | |
| { | |
| const edgeX = initialX < screenWidth / 2 ? random(-15, 0) : random(screenWidth, screenWidth + 15); | |
| const edgeY = random(0, screenHeight); | |
| animationSequence = async () => { | |
| // 1. Move quickly to the edge | |
| await controls.start({ | |
| x: edgeX, | |
| y: edgeY, | |
| opacity: 1, | |
| scale: 0.6, // Smaller, huddled look | |
| transition: { duration: random(0.5, 1.5), ease: "easeOut" } | |
| }); | |
| // 2. Linger, then fade slowly | |
| await controls.start({ | |
| opacity: 0, | |
| transition: { duration: lingerFadeDuration, delay: lingerDelay } | |
| }); | |
| if (!particleRef.current.isComplete) { | |
| particleRef.current.isComplete = true; | |
| onComplete(id); | |
| } | |
| }; | |
| } | |
| break; | |
| case 'RUNNING': | |
| case 'PENDING': | |
| case 'COMPLETED': | |
| default: | |
| // Move towards the target point on the bar and fade (original behavior) | |
| animationSequence = async () => { | |
| // 1. Quick fade-in | |
| await controls.start({ | |
| opacity: 1, | |
| scale: 1, // Start at full size immediately | |
| transition: { duration: 0.3, ease: "easeOut" } // Fast fade-in | |
| }); | |
| // 2. Move towards the target point | |
| await controls.start({ | |
| x: targetPointX, | |
| y: targetPointY, | |
| // Opacity and scale are already 1 from the previous step | |
| transition: { duration: durationBase, ease: "circOut" } | |
| }); | |
| // 3. Fade out near the target | |
| await controls.start({ | |
| opacity: 0, | |
| scale: 0.3, // Shrink slightly as it fades | |
| transition: { duration: fadeDuration } | |
| }); | |
| if (!particleRef.current.isComplete) { | |
| particleRef.current.isComplete = true; | |
| onComplete(id); | |
| } | |
| }; | |
| break; | |
| } | |
| animationSequence().catch(err => { | |
| // Handle potential animation errors (e.g., component unmounted) | |
| if (err.name !== 'AbortError' && !particleRef.current.isComplete) { | |
| console.error("Particle animation error:", err); | |
| // Ensure cleanup happens even on error if not already completed | |
| particleRef.current.isComplete = true; | |
| onComplete(id); | |
| } | |
| }); | |
| // Cleanup function for when component unmounts or dependencies change | |
| // This helps prevent calling onComplete multiple times if status changes rapidly | |
| return () => { | |
| controls.stop(); // Stop any ongoing animation | |
| // If the component unmounts before the animation naturally completes, | |
| // ensure we call onComplete if it hasn't been called yet. | |
| // This is important for states like RUNNING->FAILED transition. | |
| if (!particleRef.current.isComplete) { | |
| // We might not call onComplete here directly, | |
| // as the state change might trigger a new animation | |
| // that WILL eventually call onComplete. Let the new effect handle it. | |
| // If we *do* call it here, it might remove the particle before the | |
| // FAILED/REVOKED animation can even start. | |
| // Let's rely on the isComplete flag within the async sequences. | |
| } | |
| }; | |
| // Ensure dependencies cover all inputs for the animation logic | |
| }, [controls, id, initialX, initialY, targetPointX, targetPointY, color, currentState, screenHeight, screenWidth, onComplete]); | |
| return ( | |
| <motion.div | |
| key={id} | |
| // Make particles smaller | |
| className="absolute w-1.5 h-1.5 rounded-full pointer-events-none" // Size reduced | |
| style={{ | |
| backgroundColor: color, | |
| // Slightly reduced glow for density | |
| boxShadow: `0 0 4px ${color}, 0 0 8px ${color}`, // Adjusted glow | |
| top: 0, | |
| left: 0, | |
| }} | |
| initial={{ x: initialX, y: initialY, opacity: 0, scale: 0.5 }} | |
| animate={controls} | |
| /> | |
| ); | |
| }; | |
| // --- Main Particle Container --- | |
| const ProgressBarParticles = ({ taskId, progress, status, progressBarElement, downloadButtonElement, color }) => { | |
| const [particles, setParticles] = useState([]); | |
| const containerRef = useRef(null); | |
| const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); | |
| const [barRect, setBarRect] = useState(null); | |
| const [buttonRect, setButtonRect] = useState(null); // State for button position | |
| // Get container dimensions | |
| useEffect(() => { | |
| const updateDimensions = () => { | |
| if (containerRef.current) { | |
| setDimensions({ | |
| width: containerRef.current.offsetWidth, | |
| height: containerRef.current.offsetHeight, | |
| }); | |
| } | |
| }; | |
| updateDimensions(); | |
| window.addEventListener('resize', updateDimensions); | |
| return () => window.removeEventListener('resize', updateDimensions); | |
| }, []); | |
| // Get progress bar dimensions/position relative to the viewport | |
| useEffect(() => { | |
| const updateBarRect = () => { | |
| if (progressBarElement) { | |
| setBarRect(progressBarElement.getBoundingClientRect()); | |
| } else { | |
| setBarRect(null); | |
| } | |
| }; | |
| updateBarRect(); // Initial read | |
| // Use ResizeObserver for efficiency if needed, or just poll on interval/scroll | |
| const intervalId = setInterval(updateBarRect, 200); // Check position periodically | |
| window.addEventListener('scroll', updateBarRect); // Update on scroll | |
| return () => { | |
| clearInterval(intervalId); | |
| window.removeEventListener('scroll', updateBarRect); | |
| }; | |
| }, [progressBarElement]); // Re-run if the element itself changes | |
| // Get download button dimensions/position relative to the viewport | |
| useEffect(() => { | |
| const updateButtonRect = () => { | |
| if (downloadButtonElement) { | |
| setButtonRect(downloadButtonElement.getBoundingClientRect()); | |
| } else { | |
| setButtonRect(null); | |
| } | |
| }; | |
| updateButtonRect(); // Initial read | |
| // Use ResizeObserver for efficiency if needed, or just poll on interval/scroll | |
| const intervalId = setInterval(updateButtonRect, 200); // Check position periodically | |
| window.addEventListener('scroll', updateButtonRect); // Update on scroll | |
| return () => { | |
| clearInterval(intervalId); | |
| window.removeEventListener('scroll', updateButtonRect); | |
| }; | |
| }, [downloadButtonElement]); // Re-run if the button element itself changes | |
| const removeParticle = useCallback((id) => { | |
| setParticles(prev => prev.filter(p => p.id !== id)); | |
| }, []); | |
| // Particle Generation Logic | |
| useEffect(() => { | |
| if (!taskId || !dimensions.width || !dimensions.height || !barRect) { | |
| if (!taskId && particles.length > 0) { | |
| setParticles([]); | |
| } | |
| return; | |
| } | |
| // --- Update existing particles status --- | |
| // Do this regardless of the current task status, so particles | |
| // correctly transition to FAILED/REVOKED animations etc. | |
| setParticles(prev => prev.map(p => p.status !== status ? { ...p, status: status } : p)); | |
| // --- Particle Generation --- | |
| // Always generate particles, but their behavior is determined by the status prop passed to Particle | |
| // Increase max particles significantly & faster generation rate | |
| const maxParticles = 15 + Math.floor((progress ?? 0) * 150); // Halved: Max ~165 particles at 100% | |
| // Faster base rate, scales more aggressively with progress. Min 20ms interval. | |
| const generationRate = Math.max(40, 200 - (progress ?? 0) * 160); // Slower generation: interval 40ms (100%) to 200ms (0%) | |
| const intervalId = setInterval(() => { | |
| setParticles(prev => { | |
| // Check against maxParticles (use the length *after* potential updates) | |
| if (prev.length < maxParticles) { | |
| const edge = Math.floor(random(0, 4)); // 0: top, 1: right, 2: bottom, 3: left | |
| let startX, startY; | |
| switch (edge) { | |
| case 0: // top | |
| startX = random(0, dimensions.width); | |
| startY = -10; | |
| break; | |
| case 1: // right | |
| startX = dimensions.width + 10; | |
| startY = random(0, dimensions.height); | |
| break; | |
| case 2: // bottom | |
| startX = random(0, dimensions.width); | |
| startY = dimensions.height + 10; | |
| break; | |
| case 3: // left | |
| default: | |
| startX = -10; | |
| startY = random(0, dimensions.height); | |
| break; | |
| } | |
| // Calculate target point within the *filled* portion of the bar OR the button | |
| const containerRect = containerRef.current?.getBoundingClientRect(); | |
| if (!containerRect) return prev; // Exit if container not ready | |
| let targetX, targetY; | |
| // Target button if status is COMPLETED and button exists | |
| if (status === 'COMPLETED' && buttonRect) { | |
| const buttonStartX = buttonRect.left - containerRect.left; | |
| const buttonStartY = buttonRect.top - containerRect.top; | |
| targetX = buttonStartX + random(0, buttonRect.width); | |
| targetY = buttonStartY + random(0, buttonRect.height); | |
| } | |
| // Otherwise, target the progress bar (if it exists) | |
| else if (barRect) { | |
| const barStartX = barRect.left - containerRect.left; | |
| const barStartY = barRect.top - containerRect.top; | |
| const filledWidth = barRect.width * (progress ?? 0); | |
| // Target a random X within the filled area, or close to start if progress is 0 | |
| targetX = barStartX + random(0, Math.max(5, filledWidth)); // Ensure minimum target width | |
| // Target Y somewhere vertically within the bar height | |
| targetY = barStartY + random(0, barRect.height); | |
| } | |
| // Fallback if neither is ready (should be rare) | |
| else { | |
| targetX = startX; // Stay near start position | |
| targetY = startY; | |
| } | |
| return [ | |
| ...prev, // Use the already updated array if status changed, otherwise original prev | |
| { | |
| id: uuid(), | |
| initialX: startX, | |
| initialY: startY, | |
| targetPointX: targetX, | |
| targetPointY: targetY, | |
| color: color, | |
| status: status, // Pass current task status | |
| } | |
| ]; | |
| } | |
| // If max particles reached, just return the potentially status-updated array | |
| return prev; | |
| }); | |
| }, generationRate); | |
| return () => clearInterval(intervalId); | |
| }, [taskId, progress, status, dimensions, barRect, buttonRect, downloadButtonElement, removeParticle, color]); | |
| return ( | |
| <div | |
| ref={containerRef} | |
| className="absolute inset-0 overflow-hidden z-0 pointer-events-none" // z-0 to be behind content | |
| > | |
| {particles.map(p => ( | |
| <Particle | |
| key={p.id} | |
| {...p} | |
| screenHeight={dimensions.height} | |
| screenWidth={dimensions.width} | |
| onComplete={removeParticle} | |
| /> | |
| ))} | |
| </div> | |
| ); | |
| }; | |
| export default ProgressBarParticles; |