Skip to content

Commit f329d92

Browse files
authored
Merge pull request #53 from data-exp-lab/fix-api
Fix API Handling
2 parents c6e6470 + 7024fb0 commit f329d92

File tree

2 files changed

+237
-30
lines changed

2 files changed

+237
-30
lines changed

src/components/TopicRefiner.tsx

Lines changed: 186 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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
};

src/views/TopicHistogram.tsx

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -383,12 +383,31 @@ const TopicHistogram: FC = () => {
383383
}
384384
} catch (error) {
385385
console.error('Error fetching explanation:', error);
386-
notify({
387-
message: "Failed to get topic explanation. Please try again.",
388-
type: "error"
389-
});
390-
setTopicExplanation("Sorry, I couldn't generate an explanation for this topic at the moment.");
391-
// Don't clear the selected topic on error, so user can try again
386+
387+
// Check if the error is related to API key
388+
const errorMessage = error instanceof Error ? error.message : String(error);
389+
const isApiKeyError = errorMessage.toLowerCase().includes('api key') ||
390+
errorMessage.toLowerCase().includes('authentication') ||
391+
errorMessage.toLowerCase().includes('invalid') ||
392+
errorMessage.toLowerCase().includes('unauthorized') ||
393+
errorMessage.toLowerCase().includes('401') ||
394+
errorMessage.toLowerCase().includes('403');
395+
396+
if (isApiKeyError) {
397+
notify({
398+
message: "Invalid API key. Please update your API key in the AI Settings.",
399+
type: "error"
400+
});
401+
setApiKey(''); // Clear the API key
402+
setSelectedTopicForExplanation(null);
403+
setPendingExplanationTopic(topic); // Store the topic for later
404+
} else {
405+
notify({
406+
message: "Failed to get topic explanation. Please try again.",
407+
type: "error"
408+
});
409+
setTopicExplanation("Sorry, I couldn't generate an explanation for this topic at the moment.");
410+
}
392411
} finally {
393412
setIsLoadingExplanation(false);
394413
}
@@ -419,14 +438,15 @@ const TopicHistogram: FC = () => {
419438
})
420439
});
421440

422-
if (!response.ok) {
423-
throw new Error(`HTTP error! status: ${response.status}`);
424-
}
425-
426441
const data = await response.json();
427442
// console.log('Received AI suggestions:', data);
428443

429-
if (data.success && Array.isArray(data.result)) {
444+
if (!data.success) {
445+
// Backend returned an error
446+
throw new Error(data.error || data.message || 'Unknown error from backend');
447+
}
448+
449+
if (Array.isArray(data.result)) {
430450
// Update state synchronously
431451
setLlmSuggestionsState(data.result);
432452

@@ -467,11 +487,26 @@ const TopicHistogram: FC = () => {
467487
}
468488
} catch (error) {
469489
console.error('Error getting AI suggestions:', error);
470-
notify({
471-
message: "Failed to get AI suggestions. Please try again.",
472-
type: "error"
473-
});
474-
setLlmSuggestionsState([]);
490+
491+
// Check if the error is related to API key
492+
const errorMessage = error instanceof Error ? error.message : String(error);
493+
const isApiKeyError = errorMessage.toLowerCase().includes('api key') ||
494+
errorMessage.toLowerCase().includes('authentication') ||
495+
errorMessage.toLowerCase().includes('invalid') ||
496+
errorMessage.toLowerCase().includes('unauthorized') ||
497+
errorMessage.toLowerCase().includes('401') ||
498+
errorMessage.toLowerCase().includes('403');
499+
500+
if (isApiKeyError) {
501+
// Re-throw the error so it can be handled by the TopicRefiner component
502+
throw new Error(`API Key Error: ${errorMessage}`);
503+
} else {
504+
notify({
505+
message: "Failed to get AI suggestions. Please try again.",
506+
type: "error"
507+
});
508+
setLlmSuggestionsState([]);
509+
}
475510
}
476511
};
477512

0 commit comments

Comments
 (0)