Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
93 changes: 75 additions & 18 deletions components/AnalysisProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
<svg className="gr" viewBox="-14 -22 168 176">
<rect className="gr-limb gr-al" x="2" y="68" width="20" height="10" rx="5" />
<rect className="gr-limb gr-ar" x="118" y="68" width="20" height="10" rx="5" />
<rect className="gr-limb gr-fl" x="42" y="108" width="22" height="14" rx="7" />
<rect className="gr-limb gr-fr" x="76" y="108" width="22" height="14" rx="7" />
<g className="gr-top">
<line className="gr-stem" x1="70" y1="26" x2="70" y2="12" />
<circle className="gr-bulb" cx="70" cy="9" r="5.5" />
<circle className="gr-prop gr-gear" cx="108" cy="12" r="7.5" />
<path className="gr-ear" d="M26 38 L10 16 L40 28Z" />
<path className="gr-ear" d="M114 38 L130 16 L100 28Z" />
<rect className="gr-head" x="16" y="24" width="108" height="88" rx="26" />
<rect className="gr-screen" x="27" y="35" width="86" height="64" rx="16" />
<g className="gr-eyes"><circle className="gr-eye" cx="53" cy="62" r="7.5" /><circle className="gr-eye" cx="87" cy="62" r="7.5" /></g>
<path className="gr-mouth" d="M57 86 Q70 75 83 86" />
<ellipse className="gr-yawn" cx="70" cy="85" rx="8" ry="9" />
<path className="gr-prop gr-sweat" d="M112 40 q-6 9 0 12 q6 -3 0 -12z" />
</g>
<g className="gr-prop gr-mag"><circle cx="118" cy="86" r="9" /><line x1="124.5" y1="92.5" x2="133" y2="101" /></g>
<rect className="gr-prop gr-crate" x="-12" y="62" width="14" height="14" rx="3" />
<rect className="gr-prop gr-blk" x="36" y="-2" width="14" height="14" rx="3" />
<rect className="gr-prop gr-blk gr-blk2" x="92" y="-2" width="14" height="14" rx="3" />
{[1, 2, 3].map(n => <rect key={n} className={`gr-prop gr-jug gr-jug${n}`} x="63" y="-12" width="10" height="10" rx="2" />)}
{[[98, 20], [108, 10], [118, 0]].map(([x, y], n) => <text key={n} className={`gr-prop gr-z gr-z${n + 1}`} x={x} y={y}>z</text>)}
<text className="gr-prop gr-q" x="6" y="12">?</text>
<text className="gr-prop gr-bang" x="8" y="12">!</text>
<text className="gr-prop gr-note" x="112" y="6">♪</text>
</svg>
);

// Twelve particles; the stage decides what they do.
const FX = Array.from({ length: 12 }, (_, i) => (
<b key={i} style={{ left: `${6 + ((i * 37) % 88)}%`, top: `${12 + ((i * 53) % 70)}%`, animationDelay: `-${((i * 0.61) % 6.5).toFixed(2)}s` }} />
));

const AnalysisProgress: React.FC<AnalysisProgressProps> = ({ stage, message, startTime, percentage }) => {
const [, tick] = useState(0);
useEffect(() => {
Expand All @@ -45,23 +94,27 @@ const AnalysisProgress: React.FC<AnalysisProgressProps> = ({ 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
// (#311).
const announced = (message || '').replace(ELAPSED_SUFFIX, '').trim() || info.description;

return (
<section className="progress" data-s={stage} aria-label="Analysis progress">
<section className="progress" data-s={stage} data-slow={isTakingLong || undefined} aria-label="Analysis progress">
<div className="sr-only" role="status" aria-live="polite" aria-atomic="true">{info.title}. {announced}</div>
<div className="progress-bar" aria-hidden="true"><i style={{ width: `${bar}%` }} /></div>
<i key={stage} className="progress-flash" aria-hidden="true" />

<div className="progress-head">
<div>
Expand All @@ -72,26 +125,30 @@ const AnalysisProgress: React.FC<AnalysisProgressProps> = ({ stage, message, sta
{startTime && (
<div className="progress-time">
<span className="progress-cap">Elapsed:</span> <b>{elapsed >= 60 ? `${Math.floor(elapsed / 60)}m ${elapsed % 60}s` : `${elapsed}s`}</b>
{stage !== 'complete' && <small className={isTakingLong ? 'warn' : undefined}>{isTakingLong ? 'Taking longer than usual...' : info.typical}</small>}
{!done && <small className={isTakingLong ? 'warn' : undefined}>{isTakingLong ? 'Taking longer than usual...' : info.typical}</small>}
</div>
)}
</div>

<div className="progress-body">
<div className="progress-mem" aria-hidden="true">
<div className="progress-cells"><i style={{ '--p': (percentage ?? 0) / 100 } as React.CSSProperties} /></div>
<div className="progress-fx">{FX}</div>
<div className="progress-log">{info.log.map(line => <span key={line}>{line}</span>)}</div>
</div>
<div className="progress-side">
<svg className="progress-face" viewBox="0 0 120 120" aria-hidden="true">
<rect x="4" y="4" width="112" height="112" rx="26" />
<circle cx="42" cy="48" r="8" /><circle cx="78" cy="48" r="8" />
<path d="M40 88Q60 66 80 88" />
</svg>
<div className="progress-fw" aria-hidden="true" data-antic={antic}>
{GREMLIN}
<div className="progress-think" key={`${antic}${isTakingLong}`}>{thoughts.map(t => <span key={t}>{t}</span>)}</div>
</div>
<ol className="progress-steps">
{STAGES.slice(0, 5).map((s, i) => (
<li key={s.key} className="progress-step" data-state={fallback && i === 2 ? 'skip' : stage === 'complete' || i < idx ? 'done' : i === idx ? 'active' : 'pending'}>
<li key={s.key} className="progress-step" data-state={fallback && i === 2 ? 'skip' : done || i < idx ? 'done' : i === idx ? 'active' : 'pending'}>
<span className="progress-node" />
<span className="progress-label">{s.label}</span>
<span className="progress-txt">
<span className="progress-label">{s.label}</span>
<span className="progress-cmd">{fallback && i === 2 ? 'skipped · unavailable' : s.cmd}</span>
</span>
</li>
))}
</ol>
Expand Down
4 changes: 3 additions & 1 deletion scripts/purge-css.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ export const purgeOptions = {
/^hero-/,
// Built from template strings, so the extractor never sees them whole:
// confidence-${source} (AnalysisReportCard), related-match-${kind}
// (RelatedDiscussions), step-${n} (CorpusInsightCharts heatmap).
// (RelatedDiscussions), step-${n} (CorpusInsightCharts heatmap), gr-jug${n} / gr-z${n}
// (AnalysisProgress mascot).
/^confidence-/,
/^related-match-/,
/^step-\d+$/,
/^gr-(jug|z)\d$/,
'app-embed',
// React Router classes
'active',
Expand Down
Loading
Loading