1212 * - Celebration screen on completion
1313 */
1414
15- import { useState , useCallback , useEffect } from 'react' ;
15+ import { useState , useCallback , useMemo , useRef } from 'react' ;
1616import { useNavigate } from 'react-router-dom' ;
1717import { motion , AnimatePresence } from 'framer-motion' ;
1818import { Button } from '@/components/ui/button' ;
@@ -82,37 +82,54 @@ export function HeroPlayground({
8282 const jobId = state . status === 'indexing' ? state . jobId : null ;
8383 const repoName = customUrl . split ( '/' ) . pop ( ) ?. replace ( '.git' , '' ) || 'repository' ;
8484
85- // WebSocket hook for real-time progress
86- const wsState = useIndexingWebSocket ( jobId , {
85+ // Stable callbacks for WebSocket hook (MUST be memoized to prevent infinite loops!)
86+ const handleWsCompleted = useCallback ( ( ) => {
87+ setShowCelebration ( true ) ;
88+ } , [ ] ) ;
89+
90+ const handleWsError = useCallback ( ( error : string , recoverable : boolean ) => {
91+ console . error ( '[HeroPlayground] Indexing error:' , error , recoverable ) ;
92+ } , [ ] ) ;
93+
94+ // Memoize options to prevent recreation every render
95+ const wsOptions = useMemo ( ( ) => ( {
8796 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- } ) ;
97+ onCompleted : handleWsCompleted ,
98+ onError : handleWsError ,
99+ } ) , [ handleWsCompleted , handleWsError ] ) ;
100+
101+ // WebSocket hook for real-time progress
102+ const {
103+ phase : wsPhase ,
104+ progress : wsProgress ,
105+ recentFiles,
106+ completedStats,
107+ repoId : wsRepoId ,
108+ error : wsError ,
109+ isCompleted : wsIsCompleted ,
110+ hasError : wsHasError ,
111+ reset : wsReset ,
112+ } = useIndexingWebSocket ( jobId , wsOptions ) ;
96113
97114 // 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' ;
115+ const getIndexingPhase = useCallback ( ( ) : IndexingPhase => {
116+ if ( wsPhase === 'cloning' ) return 'cloning' ;
117+ if ( wsPhase === 'indexing' ) return 'indexing' ;
118+ if ( wsPhase === 'completed' ) return 'completed' ;
119+ if ( wsPhase === 'error' ) return 'error' ;
103120 return 'connecting' ;
104- } ;
121+ } , [ wsPhase ] ) ;
105122
106123 // Handle mode change
107124 const handleModeChange = useCallback ( ( newMode : RepoMode ) => {
108125 setMode ( newMode ) ;
109126 if ( newMode === 'demo' ) {
110127 resetSession ( ) ;
111- wsState . reset ( ) ;
128+ wsReset ( ) ;
112129 setCustomUrl ( '' ) ;
113130 setShowCelebration ( false ) ;
114131 }
115- } , [ resetSession , wsState ] ) ;
132+ } , [ resetSession , wsReset ] ) ;
116133
117134 // Handle search submit
118135 const handleSearch = useCallback ( ( e ?: React . FormEvent ) => {
@@ -123,11 +140,11 @@ export function HeroPlayground({
123140 onSearch ( query , selectedDemo , false ) ;
124141 } else if ( state . status === 'ready' ) {
125142 onSearch ( query , state . repoId , true ) ;
126- } else if ( wsState . isCompleted && wsState . repoId ) {
143+ } else if ( wsIsCompleted && wsRepoId ) {
127144 // Search using repo from completed WebSocket state
128- onSearch ( query , wsState . repoId , true ) ;
145+ onSearch ( query , wsRepoId , true ) ;
129146 }
130- } , [ query , mode , selectedDemo , state , wsState , loading , onSearch ] ) ;
147+ } , [ query , mode , selectedDemo , state , wsIsCompleted , wsRepoId , loading , onSearch ] ) ;
131148
132149 // Start searching after celebration
133150 const handleStartSearching = useCallback ( ( ) => {
@@ -137,35 +154,35 @@ export function HeroPlayground({
137154 // Index another repo
138155 const handleIndexAnother = useCallback ( ( ) => {
139156 resetSession ( ) ;
140- wsState . reset ( ) ;
157+ wsReset ( ) ;
141158 setCustomUrl ( '' ) ;
142159 setShowCelebration ( false ) ;
143- } , [ resetSession , wsState ] ) ;
160+ } , [ resetSession , wsReset ] ) ;
144161
145162 // Get validation state for ValidationStatus component
146- const getValidationState = ( ) => {
163+ const getValidationState = useCallback ( ( ) => {
147164 if ( state . status === 'idle' ) return { type : 'idle' as const } ;
148165 if ( state . status === 'validating' ) return { type : 'validating' as const } ;
149166 if ( state . status === 'valid' ) return { type : 'valid' as const , validation : state . validation } ;
150167 if ( state . status === 'invalid' ) return { type : 'invalid' as const , error : state . error , reason : state . reason } ;
151168 return { type : 'idle' as const } ;
152- } ;
169+ } , [ state ] ) ;
153170
154171 // Determine visibility states
155172 const showDemoSelector = mode === 'demo' ;
156- const showUrlInput = mode === 'custom' && ! [ 'indexing' , 'ready' ] . includes ( state . status ) && ! showCelebration && ! wsState . isCompleted ;
173+ const showUrlInput = mode === 'custom' && ! [ 'indexing' , 'ready' ] . includes ( state . status ) && ! showCelebration && ! wsIsCompleted ;
157174 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 ;
175+ const showIndexing = mode === 'custom' && state . status === 'indexing' && ! showCelebration && ! wsIsCompleted ;
176+ const showReady = ( mode === 'custom' && state . status === 'ready' ) || ( wsIsCompleted && ! showCelebration ) ;
177+ const isSearchDisabled = mode === 'custom' && state . status !== 'ready' && ! wsIsCompleted ;
161178
162179 // Can search?
163180 const canSearch = mode === 'demo'
164181 ? remaining > 0 && query . trim ( ) . length > 0
165- : ( state . status === 'ready' || wsState . isCompleted ) && remaining > 0 && query . trim ( ) . length > 0 ;
182+ : ( state . status === 'ready' || wsIsCompleted ) && remaining > 0 && query . trim ( ) . length > 0 ;
166183
167184 // Get contextual placeholder text
168- const getPlaceholder = ( ) => {
185+ const getPlaceholder = useCallback ( ( ) => {
169186 if ( mode === 'demo' ) {
170187 return "Search for authentication, error handling..." ;
171188 }
@@ -174,20 +191,28 @@ export function HeroPlayground({
174191 if ( state . status === 'valid' ) return "Click 'Index Repository' to continue..." ;
175192 if ( state . status === 'invalid' ) return "Fix the URL above to continue..." ;
176193 if ( state . status === 'indexing' ) return "Indexing in progress..." ;
177- if ( state . status === 'ready' || wsState . isCompleted ) return `Search in ${ repoName } ...` ;
194+ if ( state . status === 'ready' || wsIsCompleted ) return `Search in ${ repoName } ...` ;
178195 return "Enter a GitHub URL to search..." ;
179- } ;
196+ } , [ mode , state . status , wsIsCompleted , repoName ] ) ;
180197
181198 // 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 ;
199+ const readyInfo = useMemo ( ( ) => {
200+ if ( wsIsCompleted && completedStats ) {
201+ return {
202+ repoName,
203+ fileCount : completedStats . files_processed ,
204+ functionsFound : completedStats . functions_indexed ,
205+ } ;
206+ }
207+ if ( state . status === 'ready' ) {
208+ return {
209+ repoName : state . repoName ,
210+ fileCount : state . fileCount ,
211+ functionsFound : state . functionsFound ,
212+ } ;
213+ }
214+ return null ;
215+ } , [ wsIsCompleted , completedStats , state , repoName ] ) ;
191216
192217 return (
193218 < div className = "w-full max-w-2xl mx-auto" >
@@ -274,20 +299,17 @@ export function HeroPlayground({
274299 className = "mb-4"
275300 >
276301 < IndexingProgress
277- progress = { wsState . progress }
302+ progress = { wsProgress }
278303 phase = { getIndexingPhase ( ) }
279304 repoName = { repoName }
280- recentFiles = { wsState . recentFiles }
281- onCancel = { ( ) => {
282- resetSession ( ) ;
283- wsState . reset ( ) ;
284- } }
305+ recentFiles = { recentFiles }
306+ onCancel = { handleIndexAnother }
285307 />
286308 </ motion . div >
287309 ) }
288310
289311 { /* Celebration Screen */ }
290- { showCelebration && wsState . completedStats && (
312+ { showCelebration && completedStats && (
291313 < motion . div
292314 key = "celebration"
293315 initial = { { opacity : 0 , scale : 0.9 } }
@@ -297,7 +319,7 @@ export function HeroPlayground({
297319 >
298320 < IndexingComplete
299321 repoName = { repoName }
300- stats = { wsState . completedStats }
322+ stats = { completedStats }
301323 onStartSearching = { handleStartSearching }
302324 onIndexAnother = { handleIndexAnother }
303325 />
@@ -386,14 +408,14 @@ export function HeroPlayground({
386408 ) }
387409
388410 { /* Error State */ }
389- { ( state . status === 'error' || wsState . hasError ) && (
411+ { ( state . status === 'error' || wsHasError ) && (
390412 < motion . div
391413 initial = { { opacity : 0 , y : 10 } }
392414 animate = { { opacity : 1 , y : 0 } }
393415 className = "mt-4 px-4 py-3 rounded-xl bg-red-500/10 border border-red-500/20"
394416 >
395417 < p className = "text-red-300 text-sm" >
396- { state . status === 'error' ? state . message : wsState . error }
418+ { state . status === 'error' ? state . message : wsError }
397419 </ p >
398420 < button
399421 type = "button"
0 commit comments