/ components / LoadingScreen.tsx
LoadingScreen.tsx
1 import { useEffect, useRef, useState } from 'react'; 2 3 interface Props { 4 message: string; 5 streamingText?: string; 6 activeToolCall?: string | null; 7 } 8 9 // Phase copy reads as natural prose, not log messages. The wait is 10 // the first page of the monograph — these are chapter titles, not 11 // status updates. 12 const phases = [ 13 'Reading the pull request', 14 'Building the context around it', 15 'Looking at the changes one by one', 16 'Composing the walkthrough', 17 ]; 18 19 export function LoadingScreen({ message, streamingText, activeToolCall }: Props) { 20 const preRef = useRef<HTMLPreElement>(null); 21 const [phaseIndex, setPhaseIndex] = useState(0); 22 23 useEffect(() => { 24 if (preRef.current) { 25 preRef.current.scrollTop = preRef.current.scrollHeight; 26 } 27 }, [streamingText]); 28 29 useEffect(() => { 30 const interval = setInterval(() => { 31 setPhaseIndex((i) => (i < phases.length - 1 ? i + 1 : i)); 32 }, 4500); 33 return () => clearInterval(interval); 34 }, []); 35 36 // Once streaming text starts arriving we know we're in the final 37 // phase — don't pretend otherwise. 38 useEffect(() => { 39 if (streamingText) setPhaseIndex(phases.length - 1); 40 }, [streamingText]); 41 42 const progress = ((phaseIndex + 1) / phases.length) * 100; 43 44 return ( 45 <div className="flex min-h-screen items-start justify-center px-8 pt-[18vh] pb-12"> 46 <div className="w-full max-w-2xl flex flex-col gap-10"> 47 {/* Chapter chip — same vocabulary as the rest of the 48 monograph. Tells the reader what stage of the wait 49 they're in without dramatizing it. */} 50 <div className="slide-chapter"> 51 <span>Section 00</span> 52 <span aria-hidden="true">·</span> 53 <span>{phaseIndex + 1} of {phases.length}</span> 54 </div> 55 56 {/* Serif phase title — the dominant element on the page, 57 same treatment as a real slide title. The wait IS a 58 slide; it just happens to be the first one. */} 59 <h1 className="slide-title">{phases[phaseIndex]}</h1> 60 61 {/* Hairline progress rule. A single 1px line that fills 62 from left to right as phases advance. No glow, no 63 shimmer, no animated stripes — just a quiet rule that 64 tells you where you are. */} 65 <div className="relative h-px w-full bg-border" role="progressbar" aria-valuenow={progress}> 66 <div 67 className="absolute left-0 top-0 h-px bg-[var(--ring)] loading-progress-fill" 68 style={{ width: `${progress}%` }} 69 /> 70 </div> 71 72 {/* Active tool call — small inline mono line, no pill, no 73 border, no glow. Reads like a margin note. */} 74 {activeToolCall && ( 75 <p className="slide-meta"> 76 <span className="text-muted-foreground/60">·</span> {activeToolCall} 77 </p> 78 )} 79 80 {/* Free-text status message from the caller. Quiet meta 81 register, not a headline. */} 82 {message && <p className="slide-meta opacity-70">{message}</p>} 83 84 {/* Streaming text from the model. A calm mono panel 85 on a faint warm-paper tint, no glow, no cyan border. 86 Reads like the AI's marginalia. */} 87 {streamingText && ( 88 <pre 89 ref={preRef} 90 className="loading-stream w-full text-left text-xs font-mono text-muted-foreground rounded-md px-4 py-3 max-h-56 overflow-y-auto whitespace-pre-wrap" 91 > 92 {streamingText} 93 </pre> 94 )} 95 </div> 96 </div> 97 ); 98 }