/ 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  }