Skip to content

Commit b7891b5

Browse files
committed
feat(frontend): Add anonymous indexing components and hook
- Add useAnonymousSession hook with full state machine - Add playground-api.ts service (mock validation for #134) - Add RepoModeSelector component (Demo/Custom tabs) - Add RepoUrlInput with debounced validation - Add ValidationStatus component (valid/invalid/loading) - Add IndexingProgress with real-time updates Part of #114
1 parent dc4579c commit b7891b5

8 files changed

Lines changed: 1019 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)