Skip to content

Commit b46dc47

Browse files
committed
refactor(frontend): Code review fixes for playground components
- Fix NodeJS.Timeout type (use ReturnType<typeof setInterval>) - Add AbortController for request cancellation - Fix useEffect/useCallback dependencies - Add proper aria-labels for accessibility - Add APIError class for typed error handling - Improve mock validation with language detection - Add proper JSDoc comments (not excessive) - Export ProgressData type - Better number formatting (toLocaleString) - Consistent button type='button' attributes All TypeScript checks pass.
1 parent b7891b5 commit b46dc47

8 files changed

Lines changed: 405 additions & 223 deletions

File tree

frontend/src/components/playground/IndexingProgress.tsx

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
/**
22
* IndexingProgress
3-
* Shows real-time progress during repo indexing
3+
*
4+
* Displays real-time progress during repository indexing.
5+
* Shows progress bar, file stats, and current file being processed.
46
*/
57

68
import { cn } from '@/lib/utils';
79
import { Progress } from '@/components/ui/progress';
810

9-
interface ProgressData {
11+
export interface ProgressData {
1012
percent: number;
1113
filesProcessed: number;
1214
filesTotal: number;
@@ -20,32 +22,43 @@ interface IndexingProgressProps {
2022
onCancel?: () => void;
2123
}
2224

23-
// Animated dots for "processing" text
2425
function AnimatedDots() {
2526
return (
26-
<span className="inline-flex">
27-
<span className="animate-[bounce_1s_ease-in-out_infinite]">.</span>
28-
<span className="animate-[bounce_1s_ease-in-out_0.2s_infinite]">.</span>
29-
<span className="animate-[bounce_1s_ease-in-out_0.4s_infinite]">.</span>
27+
<span className="inline-flex" aria-hidden="true">
28+
<span className="animate-bounce" style={{ animationDelay: '0ms' }}>.</span>
29+
<span className="animate-bounce" style={{ animationDelay: '150ms' }}>.</span>
30+
<span className="animate-bounce" style={{ animationDelay: '300ms' }}>.</span>
3031
</span>
3132
);
3233
}
3334

35+
/**
36+
* Estimate remaining time based on current progress.
37+
* Returns null if not enough data to estimate.
38+
*/
39+
function estimateRemainingSeconds(percent: number, filesProcessed: number): number | null {
40+
if (percent <= 0 || filesProcessed <= 0) return null;
41+
42+
// Rough estimate: assume ~0.15s per file on average
43+
const remainingFiles = Math.ceil((filesProcessed / percent) * (100 - percent));
44+
return Math.max(1, Math.ceil(remainingFiles * 0.15));
45+
}
46+
3447
export function IndexingProgress({ progress, repoName, onCancel }: IndexingProgressProps) {
3548
const { percent, filesProcessed, filesTotal, currentFile, functionsFound } = progress;
36-
37-
// Estimate remaining time (rough calculation)
38-
const estimatedRemaining = percent > 0
39-
? Math.ceil(((100 - percent) / percent) * (filesProcessed * 0.1))
40-
: null;
49+
const estimatedRemaining = estimateRemainingSeconds(percent, filesProcessed);
4150

4251
return (
43-
<div className="rounded-xl bg-zinc-900/80 border border-zinc-800 overflow-hidden">
52+
<div
53+
className="rounded-xl bg-zinc-900/80 border border-zinc-800 overflow-hidden"
54+
role="status"
55+
aria-label={`Indexing ${repoName || 'repository'}: ${percent}% complete`}
56+
>
4457
{/* Header */}
4558
<div className="px-5 py-4 border-b border-zinc-800">
4659
<div className="flex items-center justify-between">
4760
<div className="flex items-center gap-2">
48-
<span className="text-lg"></span>
61+
<span className="text-lg" aria-hidden="true"></span>
4962
<span className="text-white font-medium">
5063
Indexing {repoName || 'repository'}
5164
<AnimatedDots />
@@ -62,10 +75,11 @@ export function IndexingProgress({ progress, repoName, onCancel }: IndexingProgr
6275
<Progress
6376
value={percent}
6477
className="h-2 bg-zinc-800"
78+
aria-label={`${percent}% complete`}
6579
/>
6680
</div>
6781

68-
{/* Stats */}
82+
{/* Stats grid */}
6983
<div className="px-5 py-3 bg-zinc-900/50 border-t border-zinc-800">
7084
<div className="grid grid-cols-3 gap-4 text-sm">
7185
<div>
@@ -77,13 +91,13 @@ export function IndexingProgress({ progress, repoName, onCancel }: IndexingProgr
7791
<div>
7892
<div className="text-zinc-500">Functions</div>
7993
<div className="text-white font-medium">
80-
{functionsFound}
94+
{functionsFound.toLocaleString()}
8195
</div>
8296
</div>
8397
<div>
8498
<div className="text-zinc-500">Remaining</div>
8599
<div className="text-white font-medium">
86-
{estimatedRemaining !== null ? `~${estimatedRemaining}s` : '...'}
100+
{estimatedRemaining !== null ? `~${estimatedRemaining}s` : ''}
87101
</div>
88102
</div>
89103
</div>
@@ -93,8 +107,8 @@ export function IndexingProgress({ progress, repoName, onCancel }: IndexingProgr
93107
{currentFile && (
94108
<div className="px-5 py-3 border-t border-zinc-800">
95109
<div className="flex items-center gap-2 text-sm">
96-
<span className="text-zinc-500">📄</span>
97-
<span className="text-zinc-400 font-mono truncate">
110+
<span className="text-zinc-500" aria-hidden="true">📄</span>
111+
<span className="text-zinc-400 font-mono truncate" title={currentFile}>
98112
{currentFile}
99113
</span>
100114
</div>
@@ -105,6 +119,7 @@ export function IndexingProgress({ progress, repoName, onCancel }: IndexingProgr
105119
{onCancel && (
106120
<div className="px-5 py-3 border-t border-zinc-800">
107121
<button
122+
type="button"
108123
onClick={onCancel}
109124
className={cn(
110125
'w-full py-2 px-4 rounded-lg text-sm',

frontend/src/components/playground/RepoModeSelector.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/**
22
* RepoModeSelector
3-
* Tab toggle between Demo repos and User's own repo
3+
*
4+
* Tab toggle between Demo repos and User's custom repo.
5+
* Used at the top of the Playground to switch input modes.
46
*/
57

68
import { cn } from '@/lib/utils';
@@ -13,10 +15,22 @@ interface RepoModeSelectorProps {
1315
disabled?: boolean;
1416
}
1517

16-
export function RepoModeSelector({ mode, onModeChange, disabled }: RepoModeSelectorProps) {
18+
export function RepoModeSelector({
19+
mode,
20+
onModeChange,
21+
disabled = false
22+
}: RepoModeSelectorProps) {
1723
return (
18-
<div className="inline-flex items-center rounded-lg bg-zinc-900 p-1 border border-zinc-800">
24+
<div
25+
className="inline-flex items-center rounded-lg bg-zinc-900 p-1 border border-zinc-800"
26+
role="tablist"
27+
aria-label="Repository source"
28+
>
1929
<button
30+
type="button"
31+
role="tab"
32+
aria-selected={mode === 'demo'}
33+
aria-controls="demo-panel"
2034
onClick={() => onModeChange('demo')}
2135
disabled={disabled}
2236
className={cn(
@@ -30,6 +44,10 @@ export function RepoModeSelector({ mode, onModeChange, disabled }: RepoModeSelec
3044
Demo Repos
3145
</button>
3246
<button
47+
type="button"
48+
role="tab"
49+
aria-selected={mode === 'custom'}
50+
aria-controls="custom-panel"
3351
onClick={() => onModeChange('custom')}
3452
disabled={disabled}
3553
className={cn(

frontend/src/components/playground/RepoUrlInput.tsx

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
/**
22
* RepoUrlInput
3-
* URL input field with GitHub icon and clear button
3+
*
4+
* URL input field for GitHub repository URLs.
5+
* Features debounced validation, GitHub icon, and clear button.
46
*/
57

6-
import { useState, useEffect, useCallback } from 'react';
8+
import { useState, useEffect } from 'react';
79
import { cn } from '@/lib/utils';
810

911
interface RepoUrlInputProps {
@@ -14,45 +16,72 @@ interface RepoUrlInputProps {
1416
placeholder?: string;
1517
}
1618

17-
// GitHub icon
18-
const GitHubIcon = () => (
19-
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
20-
<path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" />
21-
</svg>
22-
);
19+
// Icons as named components for better readability
20+
function GitHubIcon() {
21+
return (
22+
<svg
23+
className="w-5 h-5"
24+
fill="currentColor"
25+
viewBox="0 0 24 24"
26+
aria-hidden="true"
27+
>
28+
<path
29+
fillRule="evenodd"
30+
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
31+
clipRule="evenodd"
32+
/>
33+
</svg>
34+
);
35+
}
2336

24-
// Clear icon
25-
const ClearIcon = () => (
26-
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
27-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
28-
</svg>
29-
);
37+
function ClearIcon() {
38+
return (
39+
<svg
40+
className="w-4 h-4"
41+
fill="none"
42+
stroke="currentColor"
43+
viewBox="0 0 24 24"
44+
aria-hidden="true"
45+
>
46+
<path
47+
strokeLinecap="round"
48+
strokeLinejoin="round"
49+
strokeWidth={2}
50+
d="M6 18L18 6M6 6l12 12"
51+
/>
52+
</svg>
53+
);
54+
}
55+
56+
const DEBOUNCE_MS = 500;
3057

3158
export function RepoUrlInput({
3259
value,
3360
onChange,
3461
onValidate,
35-
disabled,
62+
disabled = false,
3663
placeholder = "https://github.com/owner/repo"
3764
}: RepoUrlInputProps) {
3865
const [localValue, setLocalValue] = useState(value);
3966

40-
// Sync with external value
67+
// Sync with external value changes
4168
useEffect(() => {
4269
setLocalValue(value);
4370
}, [value]);
4471

45-
// Debounced validation
72+
// Debounced validation trigger
4673
useEffect(() => {
74+
if (!localValue.trim() || localValue === value) {
75+
return;
76+
}
77+
4778
const timer = setTimeout(() => {
48-
if (localValue.trim() && localValue !== value) {
49-
onChange(localValue);
50-
onValidate(localValue);
51-
}
52-
}, 500);
79+
onChange(localValue);
80+
onValidate(localValue);
81+
}, DEBOUNCE_MS);
5382

5483
return () => clearTimeout(timer);
55-
}, [localValue]);
84+
}, [localValue, value, onChange, onValidate]);
5685

5786
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
5887
setLocalValue(e.target.value);
@@ -65,6 +94,7 @@ export function RepoUrlInput({
6594

6695
const handleKeyDown = (e: React.KeyboardEvent) => {
6796
if (e.key === 'Enter' && localValue.trim()) {
97+
e.preventDefault();
6898
onChange(localValue);
6999
onValidate(localValue);
70100
}
@@ -73,7 +103,7 @@ export function RepoUrlInput({
73103
return (
74104
<div className="relative">
75105
{/* GitHub icon */}
76-
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500">
106+
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500 pointer-events-none">
77107
<GitHubIcon />
78108
</div>
79109

@@ -85,6 +115,7 @@ export function RepoUrlInput({
85115
onKeyDown={handleKeyDown}
86116
disabled={disabled}
87117
placeholder={placeholder}
118+
aria-label="GitHub repository URL"
88119
className={cn(
89120
'w-full pl-12 pr-12 py-4 text-base rounded-xl',
90121
'bg-zinc-900/50 border border-zinc-800',
@@ -98,8 +129,10 @@ export function RepoUrlInput({
98129
{/* Clear button */}
99130
{localValue && !disabled && (
100131
<button
132+
type="button"
101133
onClick={handleClear}
102-
className="absolute right-4 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300 transition-colors"
134+
aria-label="Clear input"
135+
className="absolute right-4 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300 transition-colors p-1"
103136
>
104137
<ClearIcon />
105138
</button>

0 commit comments

Comments
 (0)