11/**
2- * HeroPlayground
2+ * HeroPlayground (v2 - WebSocket Enhanced)
33 *
44 * Combined demo + custom repo experience for the landing page hero.
5- * Handles mode switching, URL validation, indexing, and search.
5+ * Now with real-time WebSocket progress updates and streaming file list.
6+ *
7+ * Features:
8+ * - Mode switching (demo/custom)
9+ * - URL validation
10+ * - Real-time indexing progress via WebSocket
11+ * - Streaming file list (the "holy shit" moment)
12+ * - Celebration screen on completion
613 */
714
8- import { useState , useCallback } from 'react' ;
15+ import { useState , useCallback , useEffect } from 'react' ;
916import { useNavigate } from 'react-router-dom' ;
17+ import { motion , AnimatePresence } from 'framer-motion' ;
1018import { Button } from '@/components/ui/button' ;
1119import {
1220 RepoModeSelector ,
1321 RepoUrlInput ,
1422 ValidationStatus ,
1523 IndexingProgress ,
16- type RepoMode
24+ IndexingComplete ,
25+ type RepoMode ,
26+ type IndexingPhase ,
1727} from '@/components/playground' ;
1828import { useAnonymousSession } from '@/hooks/useAnonymousSession' ;
29+ import { useIndexingWebSocket } from '@/hooks/useIndexingWebSocket' ;
1930import { cn } from '@/lib/utils' ;
2031
2132// Demo repos config
@@ -62,18 +73,46 @@ export function HeroPlayground({
6273 const [ selectedDemo , setSelectedDemo ] = useState ( DEMO_REPOS [ 0 ] . id ) ;
6374 const [ customUrl , setCustomUrl ] = useState ( '' ) ;
6475 const [ query , setQuery ] = useState ( '' ) ;
76+ const [ showCelebration , setShowCelebration ] = useState ( false ) ;
6577
66- // Anonymous session hook for custom repos
67- const { state, validateUrl, startIndexing, reset, session } = useAnonymousSession ( ) ;
78+ // Anonymous session hook for validation and job creation
79+ const { state, validateUrl, startIndexing, reset : resetSession , session } = useAnonymousSession ( ) ;
80+
81+ // Extract jobId for WebSocket connection
82+ const jobId = state . status === 'indexing' ? state . jobId : null ;
83+ const repoName = customUrl . split ( '/' ) . pop ( ) ?. replace ( '.git' , '' ) || 'repository' ;
84+
85+ // WebSocket hook for real-time progress
86+ const wsState = useIndexingWebSocket ( jobId , {
87+ maxRecentFiles : 12 ,
88+ onCompleted : ( repoId , stats ) => {
89+ console . log ( '[HeroPlayground] Indexing completed:' , repoId , stats ) ;
90+ setShowCelebration ( true ) ;
91+ } ,
92+ onError : ( error , recoverable ) => {
93+ console . error ( '[HeroPlayground] Indexing error:' , error , recoverable ) ;
94+ } ,
95+ } ) ;
96+
97+ // Map WebSocket phase to IndexingProgress phase
98+ const getIndexingPhase = ( ) : IndexingPhase => {
99+ if ( wsState . phase === 'cloning' ) return 'cloning' ;
100+ if ( wsState . phase === 'indexing' ) return 'indexing' ;
101+ if ( wsState . phase === 'completed' ) return 'completed' ;
102+ if ( wsState . phase === 'error' ) return 'error' ;
103+ return 'connecting' ;
104+ } ;
68105
69106 // Handle mode change
70107 const handleModeChange = useCallback ( ( newMode : RepoMode ) => {
71108 setMode ( newMode ) ;
72109 if ( newMode === 'demo' ) {
73- reset ( ) ;
110+ resetSession ( ) ;
111+ wsState . reset ( ) ;
74112 setCustomUrl ( '' ) ;
113+ setShowCelebration ( false ) ;
75114 }
76- } , [ reset ] ) ;
115+ } , [ resetSession , wsState ] ) ;
77116
78117 // Handle search submit
79118 const handleSearch = useCallback ( ( e ?: React . FormEvent ) => {
@@ -84,8 +123,24 @@ export function HeroPlayground({
84123 onSearch ( query , selectedDemo , false ) ;
85124 } else if ( state . status === 'ready' ) {
86125 onSearch ( query , state . repoId , true ) ;
126+ } else if ( wsState . isCompleted && wsState . repoId ) {
127+ // Search using repo from completed WebSocket state
128+ onSearch ( query , wsState . repoId , true ) ;
87129 }
88- } , [ query , mode , selectedDemo , state , loading , onSearch ] ) ;
130+ } , [ query , mode , selectedDemo , state , wsState , loading , onSearch ] ) ;
131+
132+ // Start searching after celebration
133+ const handleStartSearching = useCallback ( ( ) => {
134+ setShowCelebration ( false ) ;
135+ } , [ ] ) ;
136+
137+ // Index another repo
138+ const handleIndexAnother = useCallback ( ( ) => {
139+ resetSession ( ) ;
140+ wsState . reset ( ) ;
141+ setCustomUrl ( '' ) ;
142+ setShowCelebration ( false ) ;
143+ } , [ resetSession , wsState ] ) ;
89144
90145 // Get validation state for ValidationStatus component
91146 const getValidationState = ( ) => {
@@ -96,44 +151,44 @@ export function HeroPlayground({
96151 return { type : 'idle' as const } ;
97152 } ;
98153
154+ // Determine visibility states
155+ const showDemoSelector = mode === 'demo' ;
156+ const showUrlInput = mode === 'custom' && ! [ 'indexing' , 'ready' ] . includes ( state . status ) && ! showCelebration && ! wsState . isCompleted ;
157+ const showValidation = mode === 'custom' && [ 'validating' , 'valid' , 'invalid' ] . includes ( state . status ) && ! showCelebration ;
158+ const showIndexing = mode === 'custom' && state . status === 'indexing' && ! showCelebration && ! wsState . isCompleted ;
159+ const showReady = ( mode === 'custom' && state . status === 'ready' ) || ( wsState . isCompleted && ! showCelebration ) ;
160+ const isSearchDisabled = mode === 'custom' && state . status !== 'ready' && ! wsState . isCompleted ;
161+
99162 // Can search?
100163 const canSearch = mode === 'demo'
101164 ? remaining > 0 && query . trim ( ) . length > 0
102- : state . status === 'ready' && remaining > 0 && query . trim ( ) . length > 0 ;
103-
104- // Determine what to show based on state
105- const showDemoSelector = mode === 'demo' ;
106- const showUrlInput = mode === 'custom' && ! [ 'indexing' , 'ready' ] . includes ( state . status ) ;
107- const showValidation = mode === 'custom' && [ 'validating' , 'valid' , 'invalid' ] . includes ( state . status ) ;
108- const showIndexing = mode === 'custom' && state . status === 'indexing' ;
109- // Always show search - in custom mode it's disabled until repo is indexed
110- const showSearch = true ;
111- const isSearchDisabled = mode === 'custom' && state . status !== 'ready' ;
165+ : ( state . status === 'ready' || wsState . isCompleted ) && remaining > 0 && query . trim ( ) . length > 0 ;
112166
113167 // Get contextual placeholder text
114168 const getPlaceholder = ( ) => {
115169 if ( mode === 'demo' ) {
116170 return "Search for authentication, error handling..." ;
117171 }
118- // Custom mode placeholders based on state
119- switch ( state . status ) {
120- case 'idle' :
121- return "Enter a GitHub URL above to start..." ;
122- case 'validating' :
123- return "Validating repository..." ;
124- case 'valid' :
125- return "Click 'Index Repository' to continue..." ;
126- case 'invalid' :
127- return "Fix the URL above to continue..." ;
128- case 'indexing' :
129- return "Indexing in progress..." ;
130- case 'ready' :
131- return `Search in ${ state . repoName } ...` ;
132- default :
133- return "Enter a GitHub URL to search..." ;
134- }
172+ if ( state . status === 'idle' ) return "Enter a GitHub URL above to start..." ;
173+ if ( state . status === 'validating' ) return "Validating repository..." ;
174+ if ( state . status === 'valid' ) return "Click 'Index Repository' to continue..." ;
175+ if ( state . status === 'invalid' ) return "Fix the URL above to continue..." ;
176+ if ( state . status === 'indexing' ) return "Indexing in progress..." ;
177+ if ( state . status === 'ready' || wsState . isCompleted ) return `Search in ${ repoName } ...` ;
178+ return "Enter a GitHub URL to search..." ;
135179 } ;
136180
181+ // Compute ready state info
182+ const readyInfo = wsState . isCompleted && wsState . completedStats ? {
183+ repoName,
184+ fileCount : wsState . completedStats . files_processed ,
185+ functionsFound : wsState . completedStats . functions_indexed ,
186+ } : state . status === 'ready' ? {
187+ repoName : state . repoName ,
188+ fileCount : state . fileCount ,
189+ functionsFound : state . functionsFound ,
190+ } : null ;
191+
137192 return (
138193 < div className = "w-full max-w-2xl mx-auto" >
139194 { /* Mode Selector */ }
@@ -174,65 +229,114 @@ export function HeroPlayground({
174229 ) }
175230
176231 { /* Custom URL Input */ }
177- { showUrlInput && (
178- < div className = "mb-4" >
179- < RepoUrlInput
180- value = { customUrl }
181- onChange = { setCustomUrl }
182- onValidate = { validateUrl }
183- placeholder = "https://github.com/owner/repo"
184- disabled = { state . status === 'validating' }
185- />
186- </ div >
187- ) }
232+ < AnimatePresence mode = "wait" >
233+ { showUrlInput && (
234+ < motion . div
235+ key = "url-input"
236+ initial = { { opacity : 0 , y : - 10 } }
237+ animate = { { opacity : 1 , y : 0 } }
238+ exit = { { opacity : 0 , y : 10 } }
239+ className = "mb-4"
240+ >
241+ < RepoUrlInput
242+ value = { customUrl }
243+ onChange = { setCustomUrl }
244+ onValidate = { validateUrl }
245+ placeholder = "https://github.com/owner/repo"
246+ disabled = { state . status === 'validating' }
247+ />
248+ </ motion . div >
249+ ) }
188250
189- { /* Validation Status */ }
190- { showValidation && (
191- < div className = "mb-4" >
192- < ValidationStatus
193- state = { getValidationState ( ) }
194- onStartIndexing = { state . status === 'valid' ? startIndexing : undefined }
195- />
196- </ div >
197- ) }
251+ { /* Validation Status */ }
252+ { showValidation && (
253+ < motion . div
254+ key = "validation"
255+ initial = { { opacity : 0 , y : - 10 } }
256+ animate = { { opacity : 1 , y : 0 } }
257+ exit = { { opacity : 0 , y : 10 } }
258+ className = "mb-4"
259+ >
260+ < ValidationStatus
261+ state = { getValidationState ( ) }
262+ onStartIndexing = { state . status === 'valid' ? startIndexing : undefined }
263+ />
264+ </ motion . div >
265+ ) }
198266
199- { /* Indexing Progress */ }
200- { showIndexing && (
201- < div className = "mb-4" >
202- < IndexingProgress
203- progress = { state . progress }
204- repoName = { customUrl . split ( '/' ) . pop ( ) ?. replace ( '.git' , '' ) }
205- onCancel = { reset }
206- />
207- </ div >
208- ) }
267+ { /* Indexing Progress with WebSocket streaming */ }
268+ { showIndexing && (
269+ < motion . div
270+ key = "indexing"
271+ initial = { { opacity : 0 , scale : 0.95 } }
272+ animate = { { opacity : 1 , scale : 1 } }
273+ exit = { { opacity : 0 , scale : 0.95 } }
274+ className = "mb-4"
275+ >
276+ < IndexingProgress
277+ progress = { wsState . progress }
278+ phase = { getIndexingPhase ( ) }
279+ repoName = { repoName }
280+ recentFiles = { wsState . recentFiles }
281+ onCancel = { ( ) => {
282+ resetSession ( ) ;
283+ wsState . reset ( ) ;
284+ } }
285+ />
286+ </ motion . div >
287+ ) }
209288
210- { /* Ready State Banner */ }
211- { mode === 'custom' && state . status === 'ready' && (
212- < div className = "mb-4 px-4 py-3 rounded-lg bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-between" >
213- < div className = "flex items-center gap-2" >
214- < span className = "text-emerald-400" > ✓</ span >
215- < span className = "text-emerald-300 text-sm" >
216- < strong > { state . repoName } </ strong > indexed · { state . fileCount } files · { state . functionsFound } functions
217- </ span >
218- </ div >
219- < button
220- type = "button"
221- onClick = { reset }
222- className = "text-xs text-zinc-500 hover:text-zinc-300"
289+ { /* Celebration Screen */ }
290+ { showCelebration && wsState . completedStats && (
291+ < motion . div
292+ key = "celebration"
293+ initial = { { opacity : 0 , scale : 0.9 } }
294+ animate = { { opacity : 1 , scale : 1 } }
295+ exit = { { opacity : 0 , scale : 0.9 } }
296+ className = "mb-4"
223297 >
224- Index different repo
225- </ button >
226- </ div >
227- ) }
298+ < IndexingComplete
299+ repoName = { repoName }
300+ stats = { wsState . completedStats }
301+ onStartSearching = { handleStartSearching }
302+ onIndexAnother = { handleIndexAnother }
303+ />
304+ </ motion . div >
305+ ) }
306+
307+ { /* Ready State Banner */ }
308+ { showReady && ! showCelebration && readyInfo && (
309+ < motion . div
310+ key = "ready"
311+ initial = { { opacity : 0 , y : - 10 } }
312+ animate = { { opacity : 1 , y : 0 } }
313+ exit = { { opacity : 0 , y : 10 } }
314+ className = "mb-4 px-4 py-3 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-between"
315+ >
316+ < div className = "flex items-center gap-2" >
317+ < span className = "text-emerald-400" > ✓</ span >
318+ < span className = "text-emerald-300 text-sm" >
319+ < strong > { readyInfo . repoName } </ strong > indexed · { readyInfo . fileCount } files · { readyInfo . functionsFound . toLocaleString ( ) } functions
320+ </ span >
321+ </ div >
322+ < button
323+ type = "button"
324+ onClick = { handleIndexAnother }
325+ className = "text-xs text-zinc-500 hover:text-zinc-300 transition-colors"
326+ >
327+ Index different repo
328+ </ button >
329+ </ motion . div >
330+ ) }
331+ </ AnimatePresence >
228332
229333 { /* Search Box */ }
230- { showSearch && (
334+ { ! showCelebration && (
231335 < >
232336 < div className = "relative mb-4" >
233337 < div className = "absolute inset-0 bg-gradient-to-r from-indigo-500/20 to-cyan-500/20 rounded-2xl blur-xl opacity-50" />
234338 < div className = { cn (
235- "relative bg-zinc-900/80 rounded-2xl border border-zinc-800 p-3" ,
339+ "relative bg-zinc-900/80 rounded-2xl border border-zinc-800 p-3 transition-opacity duration-300 " ,
236340 isSearchDisabled && "opacity-60"
237341 ) } >
238342 < form onSubmit = { handleSearch } className = "flex items-center gap-3" >
@@ -282,19 +386,23 @@ export function HeroPlayground({
282386 ) }
283387
284388 { /* Error State */ }
285- { state . status === 'error' && (
286- < div className = "mt-4 px-4 py-3 rounded-lg bg-red-500/10 border border-red-500/20" >
287- < p className = "text-red-300 text-sm" > { state . message } </ p >
288- { state . canRetry && (
289- < button
290- type = "button"
291- onClick = { reset }
292- className = "mt-2 text-xs text-red-400 hover:text-red-300"
293- >
294- Try again
295- </ button >
296- ) }
297- </ div >
389+ { ( state . status === 'error' || wsState . hasError ) && (
390+ < motion . div
391+ initial = { { opacity : 0 , y : 10 } }
392+ animate = { { opacity : 1 , y : 0 } }
393+ className = "mt-4 px-4 py-3 rounded-xl bg-red-500/10 border border-red-500/20"
394+ >
395+ < p className = "text-red-300 text-sm" >
396+ { state . status === 'error' ? state . message : wsState . error }
397+ </ p >
398+ < button
399+ type = "button"
400+ onClick = { handleIndexAnother }
401+ className = "mt-2 text-xs text-red-400 hover:text-red-300 transition-colors"
402+ >
403+ Try again
404+ </ button >
405+ </ motion . div >
298406 ) }
299407
300408 { /* Upgrade CTA (when limit reached) */ }
@@ -312,3 +420,5 @@ export function HeroPlayground({
312420 </ div >
313421 ) ;
314422}
423+
424+ export default HeroPlayground ;
0 commit comments