/ components / LoadingScreen.tsx
LoadingScreen.tsx
1 import { useEffect, useRef, useState } from 'react'; 2 import { Brain, GitPullRequest, FileCode, Loader, Globe } from 'lucide-react'; 3 4 interface Props { 5 message: string; 6 streamingText?: string; 7 activeToolCall?: string | null; 8 } 9 10 const phases = [ 11 { icon: GitPullRequest, text: 'Fetching PR data…' }, 12 { icon: FileCode, text: 'Building context…' }, 13 { icon: Brain, text: 'Analyzing code…' }, 14 { icon: Loader, text: 'Generating review…' }, 15 ]; 16 17 export function LoadingScreen({ message, streamingText, activeToolCall }: Props) { 18 const preRef = useRef<HTMLPreElement>(null); 19 const [phaseIndex, setPhaseIndex] = useState(0); 20 21 useEffect(() => { 22 if (preRef.current) { 23 preRef.current.scrollTop = preRef.current.scrollHeight; 24 } 25 }, [streamingText]); 26 27 useEffect(() => { 28 const interval = setInterval(() => { 29 setPhaseIndex((i) => (i < phases.length - 1 ? i + 1 : i)); 30 }, 3000); 31 return () => clearInterval(interval); 32 }, []); 33 34 // Once we have streaming text, jump to last phase 35 useEffect(() => { 36 if (streamingText) setPhaseIndex(phases.length - 1); 37 }, [streamingText]); 38 39 const phase = phases[phaseIndex]; 40 const PhaseIcon = phase.icon; 41 42 return ( 43 <div className="flex min-h-screen items-center justify-center p-8 relative"> 44 {/* Radial gradient background */} 45 <div 46 className="absolute inset-0 pointer-events-none" 47 style={{ 48 background: 'radial-gradient(ellipse at center, var(--accent-glow) 0%, transparent 60%)', 49 }} 50 /> 51 52 <div className="flex flex-col items-center gap-6 text-center w-full max-w-2xl relative z-10"> 53 {/* Concentric rings with brain icon */} 54 <div className="relative flex items-center justify-center w-28 h-28"> 55 {/* Rings */} 56 <div className="loading-ring absolute inset-0 rounded-full border-2 border-primary/40" /> 57 <div className="loading-ring-delayed absolute inset-2 rounded-full border-2 border-primary/30" /> 58 <div className="loading-ring-delayed-2 absolute inset-4 rounded-full border-2 border-primary/20" /> 59 {/* Center icon */} 60 <Brain className="h-10 w-10 text-primary" /> 61 </div> 62 63 {/* Phase indicator */} 64 <div className="flex items-center gap-2 text-muted-foreground"> 65 <PhaseIcon className="h-4 w-4 animate-pulse" /> 66 <span className="text-sm">{phase.text}</span> 67 </div> 68 69 {/* Phase dots */} 70 <div className="flex gap-2"> 71 {phases.map((_, i) => ( 72 <div 73 key={i} 74 className={`h-1.5 rounded-full transition-all duration-500 ${ 75 i <= phaseIndex ? 'w-6 bg-primary' : 'w-1.5 bg-muted-foreground/30' 76 }`} 77 /> 78 ))} 79 </div> 80 81 {activeToolCall && ( 82 <div className="flex items-center gap-2 rounded-full border border-primary/30 bg-primary/10 px-3 py-1 text-xs text-primary animate-pulse"> 83 <Globe className="h-3 w-3" /> 84 <span>{activeToolCall}</span> 85 </div> 86 )} 87 88 <p className="text-xs text-muted-foreground/60">{message}</p> 89 90 {streamingText && ( 91 <pre 92 ref={preRef} 93 className="w-full text-left text-xs text-muted-foreground/70 font-mono rounded-lg p-4 overflow-y-auto max-h-48 whitespace-pre-wrap break-all border border-primary/20 bg-primary/5" 94 style={{ boxShadow: '0 0 20px var(--accent-glow)' }} 95 > 96 {streamingText} 97 </pre> 98 )} 99 </div> 100 </div> 101 ); 102 }