Skip to content

Commit 3359365

Browse files
authored
Merge pull request #135 from DevanshuNEU/feature/114-anonymous-indexing-ui
feat(frontend): Anonymous repo indexing UI components (#114)
2 parents dc4579c + b46dc47 commit 3359365

8 files changed

Lines changed: 1201 additions & 0 deletions

File tree

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* IndexingProgress
3+
*
4+
* Displays real-time progress during repository indexing.
5+
* Shows progress bar, file stats, and current file being processed.
6+
*/
7+
8+
import { cn } from '@/lib/utils';
9+
import { Progress } from '@/components/ui/progress';
10+
11+
export interface ProgressData {
12+
percent: number;
13+
filesProcessed: number;
14+
filesTotal: number;
15+
currentFile?: string;
16+
functionsFound: number;
17+
}
18+
19+
interface IndexingProgressProps {
20+
progress: ProgressData;
21+
repoName?: string;
22+
onCancel?: () => void;
23+
}
24+
25+
function AnimatedDots() {
26+
return (
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>
31+
</span>
32+
);
33+
}
34+
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+
47+
export function IndexingProgress({ progress, repoName, onCancel }: IndexingProgressProps) {
48+
const { percent, filesProcessed, filesTotal, currentFile, functionsFound } = progress;
49+
const estimatedRemaining = estimateRemainingSeconds(percent, filesProcessed);
50+
51+
return (
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+
>
57+
{/* Header */}
58+
<div className="px-5 py-4 border-b border-zinc-800">
59+
<div className="flex items-center justify-between">
60+
<div className="flex items-center gap-2">
61+
<span className="text-lg" aria-hidden="true"></span>
62+
<span className="text-white font-medium">
63+
Indexing {repoName || 'repository'}
64+
<AnimatedDots />
65+
</span>
66+
</div>
67+
<span className="text-2xl font-bold text-indigo-400">
68+
{percent}%
69+
</span>
70+
</div>
71+
</div>
72+
73+
{/* Progress bar */}
74+
<div className="px-5 py-4">
75+
<Progress
76+
value={percent}
77+
className="h-2 bg-zinc-800"
78+
aria-label={`${percent}% complete`}
79+
/>
80+
</div>
81+
82+
{/* Stats grid */}
83+
<div className="px-5 py-3 bg-zinc-900/50 border-t border-zinc-800">
84+
<div className="grid grid-cols-3 gap-4 text-sm">
85+
<div>
86+
<div className="text-zinc-500">Files</div>
87+
<div className="text-white font-medium">
88+
{filesProcessed} / {filesTotal}
89+
</div>
90+
</div>
91+
<div>
92+
<div className="text-zinc-500">Functions</div>
93+
<div className="text-white font-medium">
94+
{functionsFound.toLocaleString()}
95+
</div>
96+
</div>
97+
<div>
98+
<div className="text-zinc-500">Remaining</div>
99+
<div className="text-white font-medium">
100+
{estimatedRemaining !== null ? `~${estimatedRemaining}s` : '—'}
101+
</div>
102+
</div>
103+
</div>
104+
</div>
105+
106+
{/* Current file */}
107+
{currentFile && (
108+
<div className="px-5 py-3 border-t border-zinc-800">
109+
<div className="flex items-center gap-2 text-sm">
110+
<span className="text-zinc-500" aria-hidden="true">📄</span>
111+
<span className="text-zinc-400 font-mono truncate" title={currentFile}>
112+
{currentFile}
113+
</span>
114+
</div>
115+
</div>
116+
)}
117+
118+
{/* Cancel button */}
119+
{onCancel && (
120+
<div className="px-5 py-3 border-t border-zinc-800">
121+
<button
122+
type="button"
123+
onClick={onCancel}
124+
className={cn(
125+
'w-full py-2 px-4 rounded-lg text-sm',
126+
'bg-zinc-800 hover:bg-zinc-700 text-zinc-400 hover:text-zinc-200',
127+
'transition-colors duration-200'
128+
)}
129+
>
130+
Cancel
131+
</button>
132+
</div>
133+
)}
134+
</div>
135+
);
136+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* RepoModeSelector
3+
*
4+
* Tab toggle between Demo repos and User's custom repo.
5+
* Used at the top of the Playground to switch input modes.
6+
*/
7+
8+
import { cn } from '@/lib/utils';
9+
10+
export type RepoMode = 'demo' | 'custom';
11+
12+
interface RepoModeSelectorProps {
13+
mode: RepoMode;
14+
onModeChange: (mode: RepoMode) => void;
15+
disabled?: boolean;
16+
}
17+
18+
export function RepoModeSelector({
19+
mode,
20+
onModeChange,
21+
disabled = false
22+
}: RepoModeSelectorProps) {
23+
return (
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+
>
29+
<button
30+
type="button"
31+
role="tab"
32+
aria-selected={mode === 'demo'}
33+
aria-controls="demo-panel"
34+
onClick={() => onModeChange('demo')}
35+
disabled={disabled}
36+
className={cn(
37+
'px-4 py-2 text-sm font-medium rounded-md transition-all duration-200',
38+
mode === 'demo'
39+
? 'bg-zinc-800 text-white shadow-sm'
40+
: 'text-zinc-400 hover:text-zinc-200',
41+
disabled && 'opacity-50 cursor-not-allowed'
42+
)}
43+
>
44+
Demo Repos
45+
</button>
46+
<button
47+
type="button"
48+
role="tab"
49+
aria-selected={mode === 'custom'}
50+
aria-controls="custom-panel"
51+
onClick={() => onModeChange('custom')}
52+
disabled={disabled}
53+
className={cn(
54+
'px-4 py-2 text-sm font-medium rounded-md transition-all duration-200',
55+
mode === 'custom'
56+
? 'bg-indigo-600 text-white shadow-sm'
57+
: 'text-zinc-400 hover:text-zinc-200',
58+
disabled && 'opacity-50 cursor-not-allowed'
59+
)}
60+
>
61+
Your Repo
62+
</button>
63+
</div>
64+
);
65+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* RepoUrlInput
3+
*
4+
* URL input field for GitHub repository URLs.
5+
* Features debounced validation, GitHub icon, and clear button.
6+
*/
7+
8+
import { useState, useEffect } from 'react';
9+
import { cn } from '@/lib/utils';
10+
11+
interface RepoUrlInputProps {
12+
value: string;
13+
onChange: (url: string) => void;
14+
onValidate: (url: string) => void;
15+
disabled?: boolean;
16+
placeholder?: string;
17+
}
18+
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+
}
36+
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;
57+
58+
export function RepoUrlInput({
59+
value,
60+
onChange,
61+
onValidate,
62+
disabled = false,
63+
placeholder = "https://github.com/owner/repo"
64+
}: RepoUrlInputProps) {
65+
const [localValue, setLocalValue] = useState(value);
66+
67+
// Sync with external value changes
68+
useEffect(() => {
69+
setLocalValue(value);
70+
}, [value]);
71+
72+
// Debounced validation trigger
73+
useEffect(() => {
74+
if (!localValue.trim() || localValue === value) {
75+
return;
76+
}
77+
78+
const timer = setTimeout(() => {
79+
onChange(localValue);
80+
onValidate(localValue);
81+
}, DEBOUNCE_MS);
82+
83+
return () => clearTimeout(timer);
84+
}, [localValue, value, onChange, onValidate]);
85+
86+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
87+
setLocalValue(e.target.value);
88+
};
89+
90+
const handleClear = () => {
91+
setLocalValue('');
92+
onChange('');
93+
};
94+
95+
const handleKeyDown = (e: React.KeyboardEvent) => {
96+
if (e.key === 'Enter' && localValue.trim()) {
97+
e.preventDefault();
98+
onChange(localValue);
99+
onValidate(localValue);
100+
}
101+
};
102+
103+
return (
104+
<div className="relative">
105+
{/* GitHub icon */}
106+
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500 pointer-events-none">
107+
<GitHubIcon />
108+
</div>
109+
110+
{/* Input */}
111+
<input
112+
type="url"
113+
value={localValue}
114+
onChange={handleChange}
115+
onKeyDown={handleKeyDown}
116+
disabled={disabled}
117+
placeholder={placeholder}
118+
aria-label="GitHub repository URL"
119+
className={cn(
120+
'w-full pl-12 pr-12 py-4 text-base rounded-xl',
121+
'bg-zinc-900/50 border border-zinc-800',
122+
'text-white placeholder-zinc-500',
123+
'focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500',
124+
'transition-all duration-200',
125+
disabled && 'opacity-50 cursor-not-allowed'
126+
)}
127+
/>
128+
129+
{/* Clear button */}
130+
{localValue && !disabled && (
131+
<button
132+
type="button"
133+
onClick={handleClear}
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"
136+
>
137+
<ClearIcon />
138+
</button>
139+
)}
140+
</div>
141+
);
142+
}

0 commit comments

Comments
 (0)