@@ -104,6 +104,8 @@ export const TopicRefiner: FC<Omit<TopicRefinerProps, 'isLlmProcessing'>> = ({
104104 const [ submitProgress , setSubmitProgress ] = useState ( 0 ) ;
105105 const [ submitStatus , setSubmitStatus ] = useState < string > ( '' ) ;
106106 const [ showMemoryLimitModal , setShowMemoryLimitModal ] = useState ( false ) ;
107+ const [ showApiKeyErrorModal , setShowApiKeyErrorModal ] = useState ( false ) ;
108+ const [ apiKeyError , setApiKeyError ] = useState < string > ( '' ) ;
107109
108110 // Function to fetch unique repository count for all finalized topics
109111 const fetchUniqueReposCount = async ( topics : string [ ] ) => {
@@ -294,6 +296,31 @@ export const TopicRefiner: FC<Omit<TopicRefinerProps, 'isLlmProcessing'>> = ({
294296 }
295297 } , [ llmSuggestions , selectedModel , searchTerm , isGettingSuggestions ] ) ;
296298
299+ // Effect to focus API key input when error modal opens
300+ useEffect ( ( ) => {
301+ if ( showApiKeyErrorModal ) {
302+ const timer = setTimeout ( ( ) => {
303+ const apiKeyInput = document . getElementById ( 'newApiKey' ) as HTMLInputElement ;
304+ if ( apiKeyInput ) {
305+ apiKeyInput . focus ( ) ;
306+ apiKeyInput . select ( ) ; // Select the text so user can easily replace it
307+ }
308+ } , 100 ) ;
309+ return ( ) => clearTimeout ( timer ) ;
310+ }
311+ } , [ showApiKeyErrorModal ] ) ;
312+
313+ // Effect to ensure loading state is reset if it gets stuck
314+ useEffect ( ( ) => {
315+ if ( isGettingSuggestions ) {
316+ const timer = setTimeout ( ( ) => {
317+ console . log ( 'Loading state timeout - resetting isGettingSuggestions' ) ;
318+ setIsGettingSuggestions ( false ) ;
319+ } , 30000 ) ; // 30 second timeout
320+ return ( ) => clearTimeout ( timer ) ;
321+ }
322+ } , [ isGettingSuggestions ] ) ;
323+
297324 const handleGetSuggestions = async ( ) => {
298325 if ( ! apiKey ) {
299326 alert ( 'Please enter an API key' ) ;
@@ -311,27 +338,58 @@ export const TopicRefiner: FC<Omit<TopicRefinerProps, 'isLlmProcessing'>> = ({
311338 // Clear previous suggestions for this model
312339 setSuggestionsByModel ( prev => prev . filter ( s => s . model !== currentModel ) ) ;
313340
314- // Get suggestions from the API
315- await onRequestSuggestions ( selectedModel , customPrompt , apiKey , selectedTopics ) ;
341+ // Set a timeout to ensure loading state is reset if something goes wrong
342+ const timeoutId = setTimeout ( ( ) => {
343+ if ( isGettingSuggestions ) {
344+ setIsGettingSuggestions ( false ) ;
345+ }
346+ } , 30000 ) ; // 30 second timeout
347+
348+ try {
349+ // Get suggestions from the API
350+ await onRequestSuggestions ( selectedModel , customPrompt , apiKey , selectedTopics ) ;
316351
317- // Wait for suggestions to be processed by the effect
318- let attempts = 0 ;
319- const maxAttempts = 50 ;
320- const checkInterval = 100 ;
352+ // Wait for suggestions to be processed by the effect
353+ let attempts = 0 ;
354+ const maxAttempts = 50 ;
355+ const checkInterval = 100 ;
321356
322- while ( attempts < maxAttempts && isGettingSuggestions ) {
323- await new Promise ( resolve => setTimeout ( resolve , checkInterval ) ) ;
324- attempts ++ ;
325- }
357+ while ( attempts < maxAttempts && isGettingSuggestions ) {
358+ await new Promise ( resolve => setTimeout ( resolve , checkInterval ) ) ;
359+ attempts ++ ;
360+ }
326361
327- if ( isGettingSuggestions ) {
328- // If we're still getting suggestions after timeout, something went wrong
329- throw new Error ( 'Timeout waiting for suggestions to be processed' ) ;
362+ if ( isGettingSuggestions ) {
363+ // If we're still getting suggestions after timeout, something went wrong
364+ throw new Error ( 'Timeout waiting for suggestions to be processed' ) ;
365+ }
366+ } finally {
367+ clearTimeout ( timeoutId ) ; // Clear the timeout
330368 }
331369 } catch ( error ) {
332370 console . error ( 'Error getting suggestions:' , error ) ;
333- alert ( 'Failed to get AI suggestions. Please try again.' ) ;
371+ console . log ( 'Error type:' , typeof error ) ;
372+ console . log ( 'Error message:' , error instanceof Error ? error . message : String ( error ) ) ;
373+
374+ // Always reset loading state first
334375 setIsGettingSuggestions ( false ) ;
376+
377+ // Check if the error is related to API key
378+ const errorMessage = error instanceof Error ? error . message : String ( error ) ;
379+ const isApiKeyError = errorMessage . toLowerCase ( ) . includes ( 'api key' ) ||
380+ errorMessage . toLowerCase ( ) . includes ( 'authentication' ) ||
381+ errorMessage . toLowerCase ( ) . includes ( 'invalid' ) ||
382+ errorMessage . toLowerCase ( ) . includes ( 'unauthorized' ) ||
383+ errorMessage . toLowerCase ( ) . includes ( '401' ) ||
384+ errorMessage . toLowerCase ( ) . includes ( '403' ) ;
385+
386+ if ( isApiKeyError ) {
387+ setApiKeyError ( errorMessage ) ;
388+ setShowApiKeyErrorModal ( true ) ;
389+ // Don't clear the API key here - let the user see what they entered and modify it
390+ } else {
391+ alert ( 'Failed to get AI suggestions. Please try again.' ) ;
392+ }
335393 }
336394 } ;
337395
@@ -1285,6 +1343,120 @@ export const TopicRefiner: FC<Omit<TopicRefinerProps, 'isLlmProcessing'>> = ({
12851343 </ div >
12861344 </ div >
12871345 ) }
1346+
1347+ { /* API Key Error Modal */ }
1348+ { showApiKeyErrorModal && (
1349+ < div className = "modal show d-block" tabIndex = { - 1 } role = "dialog" style = { { backgroundColor : 'rgba(0,0,0,0.5)' } } >
1350+ < div className = "modal-dialog modal-dialog-centered" >
1351+ < div className = "modal-content" >
1352+ < div className = "modal-header" >
1353+ < h5 className = "modal-title d-flex align-items-center" >
1354+ < i className = "fas fa-key text-danger me-2" > </ i >
1355+ Invalid API Key
1356+ </ h5 >
1357+ < button
1358+ type = "button"
1359+ className = "btn-close"
1360+ onClick = { ( ) => {
1361+ setShowApiKeyErrorModal ( false ) ;
1362+ setApiKeyError ( '' ) ;
1363+ setIsGettingSuggestions ( false ) ; // Ensure loading state is reset
1364+ } }
1365+ aria-label = "Close"
1366+ > </ button >
1367+ </ div >
1368+ < div className = "modal-body" >
1369+ < div className = "alert alert-danger mb-3" >
1370+ < p className = "mb-2" >
1371+ < strong > API Key Error:</ strong > Your API key appears to be invalid or has expired.
1372+ </ p >
1373+ < p className = "mb-0" >
1374+ Please enter a valid API key to continue using AI features.
1375+ </ p >
1376+ </ div >
1377+ < div className = "mb-3" >
1378+ < label htmlFor = "newApiKey" className = "form-label fw-bold" > New API Key</ label >
1379+ < div className = "input-group" >
1380+ < input
1381+ type = "password"
1382+ id = "newApiKey"
1383+ className = "form-control"
1384+ value = { apiKey }
1385+ onChange = { ( e ) => setApiKey ( e . target . value ) }
1386+ onKeyDown = { ( e ) => {
1387+ if ( e . key === 'Enter' && apiKey . trim ( ) && ! isGettingSuggestions ) {
1388+ e . preventDefault ( ) ;
1389+ setShowApiKeyErrorModal ( false ) ;
1390+ setApiKeyError ( '' ) ;
1391+ handleGetSuggestions ( ) ;
1392+ }
1393+ } }
1394+ placeholder = "Enter your new API key"
1395+ />
1396+ < button
1397+ className = "btn btn-outline-secondary"
1398+ type = "button"
1399+ onClick = { ( ) => {
1400+ const input = document . getElementById ( 'newApiKey' ) as HTMLInputElement
1401+ input . type = input . type === 'password' ? 'text' : 'password'
1402+ } }
1403+ >
1404+ Show/Hide
1405+ </ button >
1406+ </ div >
1407+ < small className = "text-muted" >
1408+ Your API key will be used only for this session and won't be stored.
1409+ </ small >
1410+ </ div >
1411+ { apiKeyError && (
1412+ < div className = "alert alert-info" >
1413+ < small >
1414+ < strong > Error Details:</ strong > { apiKeyError }
1415+ </ small >
1416+ </ div >
1417+ ) }
1418+ </ div >
1419+ < div className = "modal-footer" >
1420+ < button
1421+ type = "button"
1422+ className = "btn btn-secondary"
1423+ onClick = { ( ) => {
1424+ setShowApiKeyErrorModal ( false ) ;
1425+ setApiKeyError ( '' ) ;
1426+ setIsGettingSuggestions ( false ) ; // Ensure loading state is reset
1427+ } }
1428+ >
1429+ Cancel
1430+ </ button >
1431+ < button
1432+ type = "button"
1433+ className = "btn btn-primary"
1434+ onClick = { async ( ) => {
1435+ if ( apiKey . trim ( ) ) {
1436+ setShowApiKeyErrorModal ( false ) ;
1437+ setApiKeyError ( '' ) ;
1438+ // Re-try the suggestion request with the new API key
1439+ await handleGetSuggestions ( ) ;
1440+ } else {
1441+ alert ( 'Please enter a valid API key' ) ;
1442+ }
1443+ } }
1444+ disabled = { ! apiKey . trim ( ) || isGettingSuggestions }
1445+ >
1446+ { isGettingSuggestions ? (
1447+ < >
1448+ < Loader2 size = { 16 } className = "animate-spin me-2" />
1449+ Retrying...
1450+ </ >
1451+ ) : (
1452+ 'Update API Key & Retry'
1453+ ) }
1454+ </ button >
1455+ </ div >
1456+ </ div >
1457+ </ div >
1458+ </ div >
1459+ ) }
12881460 </ main >
12891461 ) ;
12901462} ;
0 commit comments