diff --git a/CLAUDE.md b/CLAUDE.md
index 232b437..f8e42b5 100755
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -148,7 +148,7 @@ prerendered markup — never another route's — or hydration mismatches.
### Analyzer live view
-While a run is active `pages/Analyzer.tsx` hides the uploader and wraps the page in `.analyzer-live` (viewport height minus header, minus the sticky ad bar when it is filled), so the ad row and `components/AnalysisProgress.tsx` (a container-query card: grid, face and steps resize to the space left) fit one screen. The leaderboard is placed manually (`DisplayAdSafe fixed`: 320x100, 468x60 or 728x90 by width, no `data-ad-format`), so whatever fills it cannot resize the page, and the card keeps at least 24rem. The ad row and the results list stay mounted so no ad is re-requested. A run that produced a report holds the finished card for 1.6 s, then a summary strip (stop code, likely cause from `getReportFacts`) sits above the results until "Analyze another file". The animation moves only transform, translate, scale and opacity inside the card.
+While a run is active `pages/Analyzer.tsx` hides the uploader and wraps the page in `.analyzer-live` (viewport height minus header, minus the sticky ad bar when it is filled), so the ad row and `components/AnalysisProgress.tsx` (a container-query card: grid, face and steps resize to the space left) fit one screen. The leaderboard is placed manually (`DisplayAdSafe fixed`: 320x100, 468x60 or 728x90 by width, no `data-ad-format`), so whatever fills it cannot resize the page, and the card keeps at least 24rem. The ad row and the results list stay mounted so no ad is re-requested. A run that produced a report holds the finished card for 1.6 s, then a summary strip (stop code, likely cause from `getReportFacts`) sits above the results until "Analyze another file". The animation moves only transform, translate, scale and opacity inside the card. The mascot is an original gremlin (not OpenAI's Codex BSOD pet, whose artwork belongs to OpenAI): each stage has its own pose, `data-antic` switches it to a different antic every 8 s of the run so a long wait stays interesting, and a thought bubble, a typing log and stage particles sit around it, all in `styles.css`.
### Key Files
diff --git a/components/AnalysisProgress.tsx b/components/AnalysisProgress.tsx
index 6fa4652..a5a05aa 100644
--- a/components/AnalysisProgress.tsx
+++ b/components/AnalysisProgress.tsx
@@ -14,20 +14,69 @@ const FACTS = [
'IRQL_NOT_LESS_OR_EQUAL usually means a driver touched memory it should not have.',
];
-// The first five rows are the steps beside the grid; maxSeconds is a stage's own
-// budget for the "taking longer" note.
+// The first five rows are the steps beside the grid (cmd is what the active step shows);
+// maxSeconds is a stage's own budget for the "taking longer" note, think is what the
+// mascot thinks, log is the typing log in the corner of the grid.
const STAGES = [
- { key: 'uploading', title: 'Uploading Dump File', description: 'Securely transferring your crash dump to the analysis server...', label: 'Upload', typical: 'typically 5–15 seconds', maxSeconds: 15 },
- { key: 'queued', title: 'Queued for Analysis', description: 'Your file is in the queue and will be processed shortly...', label: 'Queue', typical: 'usually under a minute', maxSeconds: 60 },
- { key: 'processing', title: 'WinDBG Analysis in Progress', description: 'Running professional debugging commands on your crash dump...', label: 'WinDBG', typical: 'usually 30–90 seconds', maxSeconds: 90 },
- { key: 'downloading', title: 'Downloading Results', description: 'Retrieving the detailed analysis from WinDBG...', label: 'Fetch', typical: 'a few seconds', maxSeconds: 10 },
- { key: 'analyzing', title: 'AI Analysis', description: 'Our AI is interpreting the crash data for you...', label: 'AI', typical: 'usually 10–30 seconds', maxSeconds: 30 },
- { key: 'complete', title: 'Analysis Complete', description: 'Your crash analysis is ready!', label: '', typical: '', maxSeconds: 0 },
+ { key: 'uploading', title: 'Uploading Dump File', description: 'Securely transferring your crash dump to the analysis server...', label: 'Upload', cmd: 'sending dump...', typical: 'typically 5–15 seconds', maxSeconds: 15,
+ think: ['heave ho!', 'shipping your dump...', 'steady... steady...'], log: ['sending dump.dmp', 'to the analysis server', 'keep this tab open'] },
+ { key: 'queued', title: 'Queued for Analysis', description: 'Your file is in the queue and will be processed shortly...', label: 'Queue', cmd: 'waiting for a debugger', typical: 'usually under a minute', maxSeconds: 60,
+ think: ['any minute now...', 'tap tap tap...', 'is it my turn?'], log: ['waiting for a free debugger', 'you are in the queue', 'this page updates itself'] },
+ { key: 'processing', title: 'WinDBG Analysis in Progress', description: 'Running professional debugging commands on your crash dump...', label: 'WinDBG', cmd: 'kd> !analyze -v', typical: 'usually 30–90 seconds', maxSeconds: 90,
+ think: ["let's see what broke", 'kd> hmm...', 'who touched my kernel?'], log: ['kd> !analyze -v', 'kd> lm', 'kd> k'] },
+ { key: 'downloading', title: 'Downloading Results', description: 'Retrieving the detailed analysis from WinDBG...', label: 'Fetch', cmd: 'fetching output', typical: 'a few seconds', maxSeconds: 10,
+ think: ['catch it!', 'nom nom bytes', 'one more block...'], log: ['fetching analysis output', 'receiving the results', 'nearly ready'] },
+ { key: 'analyzing', title: 'AI Analysis', description: 'Our AI is interpreting the crash data for you...', label: 'AI', cmd: 'explaining cause', typical: 'usually 10–30 seconds', maxSeconds: 30,
+ think: ['thinking...', 'hmm... hmm...', 'connecting the dots'], log: ['reading the stack and modules', 'weighing likely causes', 'drafting fixes'] },
+ { key: 'complete', title: 'Analysis Complete', description: 'Your crash analysis is ready!', label: '', cmd: '', typical: '', maxSeconds: 0,
+ think: ['got it!', "report's ready", 'high five!'], log: ['analysis complete', 'report ready', 'scroll for the details'] },
];
+// Every 8 s of the run the mascot does something else (0 = the pose of the stage); each antic has a line.
+const ANTICS = [0, 1, 0, 2, 3, 0, 4, 5, 0, 6, 7, 0, 8];
+const ANTIC_SAYS = ['', 'hi there!', 'just looking around...', '*yaaawn*', 'look, no hands!', 'la la la', 'ooh, what is that?', 'wheee!', 'still got it'];
+const SLOW = ['still working...', 'this one is a big one', 'hang in there!'];
+
// Drops a trailing elapsed counter: `— 40s elapsed` or `(40s)`.
const ELAPSED_SUFFIX = /\s*(?:—\s*\d+s elapsed|\(\d+s\))\s*$/;
+// An original blue-screen gremlin; styles.css gives each stage and antic its own pose.
+const GREMLIN = (
+
+);
+
+// Twelve particles; the stage decides what they do.
+const FX = Array.from({ length: 12 }, (_, i) => (
+
+));
+
const AnalysisProgress: React.FC = ({ stage, message, startTime, percentage }) => {
const [, tick] = useState(0);
useEffect(() => {
@@ -45,13 +94,16 @@ const AnalysisProgress: React.FC = ({ stage, message, sta
const idx = STAGES.findIndex(s => s.key === stage);
const info = STAGES[idx] || STAGES[2];
+ const done = stage === 'complete';
const isTakingLong = info.maxSeconds > 0 && (Date.now() - stageStart.current) / 1000 > info.maxSeconds * 2;
// WinDBG fell back to local analysis: the message becomes a notice and its step is skipped.
const fallback = /unavailable/.test(message || '');
const description = fallback ? info.description
: stage === 'uploading' && percentage !== undefined ? `Uploading... ${percentage}%` : message || info.description;
- const bar = stage === 'complete' ? 100 : ((idx + (stage === 'uploading' ? (percentage ?? 0) * 0.009 : 0.4)) / 5) * 100;
+ const bar = done ? 100 : ((idx + (stage === 'uploading' ? (percentage ?? 0) * 0.009 : 0.4)) / 5) * 100;
const fact = Math.floor(elapsed / 8) % FACTS.length;
+ const antic = done ? 0 : ANTICS[Math.floor(elapsed / 8) % ANTICS.length];
+ const thoughts = isTakingLong ? SLOW : antic ? [ANTIC_SAYS[antic]] : info.think;
// Assistive technology hears the stage and its message, without the
// elapsed counter the queued messages end in, which changes on every poll
@@ -59,9 +111,10 @@ const AnalysisProgress: React.FC = ({ stage, message, sta
const announced = (message || '').replace(ELAPSED_SUFFIX, '').trim() || info.description;
return (
-
+