Skip to content

Commit d2f4527

Browse files
committed
fix(frontend): NaN errors and AnimatePresence warnings (#154)
Bug fixes: 1. NaN/division by zero - Added safe value guards in IndexingProgress - safePercent, safeFilesProcessed, safeFilesTotal, safeFunctionsFound - Shows 'Starting...' when filesTotal is 0 - Stats show '—' instead of NaN/0/0 2. AnimatePresence warnings - Moved error state inside AnimatePresence - Error state now has proper key and exit animation - Visibility conditions properly exclude simultaneous renders - Changed error styling from red to amber (friendlier) 3. Session state - Error states now properly clear other states - showUrlInput excludes error state - showValidation excludes error state - showIndexing excludes wsHasError Closes #154
1 parent 6bf8439 commit d2f4527

2 files changed

Lines changed: 66 additions & 34 deletions

File tree

frontend/src/components/playground/HeroPlayground.tsx

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,13 @@ export function HeroPlayground({
169169
}, [state]);
170170

171171
// Determine visibility states
172+
const hasError = state.status === 'error' || wsHasError;
172173
const showDemoSelector = mode === 'demo';
173-
const showUrlInput = mode === 'custom' && !['indexing', 'ready'].includes(state.status) && !showCelebration && !wsIsCompleted;
174-
const showValidation = mode === 'custom' && ['validating', 'valid', 'invalid'].includes(state.status) && !showCelebration;
175-
const showIndexing = mode === 'custom' && state.status === 'indexing' && !showCelebration && !wsIsCompleted;
174+
const showUrlInput = mode === 'custom' && !['indexing', 'ready', 'error'].includes(state.status) && !showCelebration && !wsIsCompleted && !wsHasError;
175+
const showValidation = mode === 'custom' && ['validating', 'valid', 'invalid'].includes(state.status) && !showCelebration && !hasError;
176+
const showIndexing = mode === 'custom' && state.status === 'indexing' && !showCelebration && !wsIsCompleted && !wsHasError;
176177
const showReady = (mode === 'custom' && state.status === 'ready') || (wsIsCompleted && !showCelebration);
178+
const showError = mode === 'custom' && hasError && !showCelebration;
177179
const isSearchDisabled = mode === 'custom' && state.status !== 'ready' && !wsIsCompleted;
178180

179181
// Can search?
@@ -350,6 +352,34 @@ export function HeroPlayground({
350352
</button>
351353
</motion.div>
352354
)}
355+
356+
{/* Error State - Inside AnimatePresence to prevent simultaneous renders */}
357+
{showError && (
358+
<motion.div
359+
key="error"
360+
initial={{ opacity: 0, y: 10 }}
361+
animate={{ opacity: 1, y: 0 }}
362+
exit={{ opacity: 0, y: -10 }}
363+
className="mb-4 px-4 py-3 rounded-xl bg-amber-500/10 border border-amber-500/20"
364+
>
365+
<div className="flex items-start gap-3">
366+
<span className="text-amber-400 text-lg">⚠️</span>
367+
<div className="flex-1">
368+
<p className="text-amber-200 text-sm font-medium mb-1">Something went wrong</p>
369+
<p className="text-zinc-400 text-sm">
370+
{state.status === 'error' ? state.message : wsError || 'An unexpected error occurred'}
371+
</p>
372+
<button
373+
type="button"
374+
onClick={handleIndexAnother}
375+
className="mt-3 text-sm text-amber-400 hover:text-amber-300 transition-colors font-medium"
376+
>
377+
← Try again with a different repository
378+
</button>
379+
</div>
380+
</div>
381+
</motion.div>
382+
)}
353383
</AnimatePresence>
354384

355385
{/* Search Box */}
@@ -407,26 +437,6 @@ export function HeroPlayground({
407437
</>
408438
)}
409439

410-
{/* Error State */}
411-
{(state.status === 'error' || wsHasError) && (
412-
<motion.div
413-
initial={{ opacity: 0, y: 10 }}
414-
animate={{ opacity: 1, y: 0 }}
415-
className="mt-4 px-4 py-3 rounded-xl bg-red-500/10 border border-red-500/20"
416-
>
417-
<p className="text-red-300 text-sm">
418-
{state.status === 'error' ? state.message : wsError}
419-
</p>
420-
<button
421-
type="button"
422-
onClick={handleIndexAnother}
423-
className="mt-2 text-xs text-red-400 hover:text-red-300 transition-colors"
424-
>
425-
Try again
426-
</button>
427-
</motion.div>
428-
)}
429-
430440
{/* Upgrade CTA (when limit reached) */}
431441
{remaining <= 0 && (
432442
<div className="mt-6 p-4 rounded-xl bg-gradient-to-r from-indigo-600/20 to-purple-600/20 border border-indigo-500/30 text-center">

frontend/src/components/playground/IndexingProgress.tsx

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -267,10 +267,19 @@ export function IndexingProgress({
267267
}: IndexingProgressProps) {
268268
const { percent, filesProcessed, filesTotal, currentFile, functionsFound } = progress;
269269

270-
// Estimate remaining time
270+
// Safe values to prevent NaN/division by zero
271+
const safePercent = Number.isFinite(percent) ? Math.max(0, Math.min(100, percent)) : 0;
272+
const safeFilesProcessed = Number.isFinite(filesProcessed) ? Math.max(0, filesProcessed) : 0;
273+
const safeFilesTotal = Number.isFinite(filesTotal) ? Math.max(0, filesTotal) : 0;
274+
const safeFunctionsFound = Number.isFinite(functionsFound) ? Math.max(0, functionsFound) : 0;
275+
276+
// Is this the initial "starting" state?
277+
const isStarting = safeFilesTotal === 0 || (safePercent === 0 && safeFilesProcessed === 0);
278+
279+
// Estimate remaining time (only when we have real data)
271280
const estimatedRemaining = (() => {
272-
if (percent <= 0 || filesProcessed <= 0) return null;
273-
const remainingFiles = Math.ceil((filesProcessed / percent) * (100 - percent));
281+
if (isStarting || safePercent <= 0 || safeFilesProcessed <= 0) return null;
282+
const remainingFiles = Math.ceil((safeFilesProcessed / safePercent) * (100 - safePercent));
274283
return Math.max(1, Math.ceil(remainingFiles * 0.15));
275284
})();
276285

@@ -287,7 +296,7 @@ export function IndexingProgress({
287296
initial={{ opacity: 0, y: 20 }}
288297
animate={{ opacity: 1, y: 0 }}
289298
role="status"
290-
aria-label={`Indexing ${repoName || 'repository'}: ${percent}% complete`}
299+
aria-label={`Indexing ${repoName || 'repository'}: ${safePercent}% complete`}
291300
>
292301
{/* Header */}
293302
<div className="px-5 py-4 border-b border-zinc-800/50 bg-gradient-to-r from-zinc-900 to-zinc-900/50">
@@ -308,12 +317,12 @@ export function IndexingProgress({
308317
</div>
309318
<motion.span
310319
className="text-3xl font-bold text-indigo-400"
311-
key={percent}
320+
key={safePercent}
312321
initial={{ scale: 1.3 }}
313322
animate={{ scale: 1 }}
314323
transition={{ type: 'spring', stiffness: 300 }}
315324
>
316-
{percent}%
325+
{safePercent}%
317326
</motion.span>
318327
</div>
319328

@@ -323,16 +332,29 @@ export function IndexingProgress({
323332

324333
{/* Progress bar */}
325334
<div className="px-5 py-4">
326-
<GlowingProgress value={percent} />
335+
<GlowingProgress value={safePercent} />
327336
</div>
328337

329338
{/* Stats grid */}
330339
<div className="px-5 py-4 bg-zinc-900/50 border-t border-b border-zinc-800/50">
331340
<div className="grid grid-cols-4 gap-4">
332-
<StatCard label="Files" value={`${filesProcessed}/${filesTotal}`} />
333-
<StatCard label="Functions" value={functionsFound} highlight />
334-
<StatCard label="Remaining" value={estimatedRemaining ? `~${estimatedRemaining}s` : '—'} />
335-
<StatCard label="Speed" value={filesProcessed > 0 ? `${Math.round(functionsFound / filesProcessed)}/file` : '—'} />
341+
<StatCard
342+
label="Files"
343+
value={isStarting ? 'Starting...' : `${safeFilesProcessed}/${safeFilesTotal}`}
344+
/>
345+
<StatCard
346+
label="Functions"
347+
value={isStarting ? '—' : safeFunctionsFound}
348+
highlight={!isStarting && safeFunctionsFound > 0}
349+
/>
350+
<StatCard
351+
label="Remaining"
352+
value={estimatedRemaining ? `~${estimatedRemaining}s` : '—'}
353+
/>
354+
<StatCard
355+
label="Speed"
356+
value={safeFilesProcessed > 0 ? `${Math.round(safeFunctionsFound / safeFilesProcessed)}/file` : '—'}
357+
/>
336358
</div>
337359
</div>
338360

0 commit comments

Comments
 (0)