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