diff --git a/frontend/src/components/ExerciseResearchPanel.jsx b/frontend/src/components/ExerciseResearchPanel.jsx index 4fdb640..5a4b585 100644 --- a/frontend/src/components/ExerciseResearchPanel.jsx +++ b/frontend/src/components/ExerciseResearchPanel.jsx @@ -1,46 +1,119 @@ -import { useState } from 'react' +import { useState, useCallback } from 'react' import ResearchDisplay from './ResearchDisplay' const API_URL = '/api' +const PROVIDER_LABELS = { + ollama: { label: 'Local AI', icon: '🏠', className: 'provider-local' }, + gemini: { label: 'Gemini', icon: '✨', className: 'provider-gemini' }, + openrouter: { label: 'OpenRouter', icon: '🔀', className: 'provider-openrouter' }, + opencode: { label: 'OpenCode', icon: '💡', className: 'provider-opencode' }, + exa: { label: 'Exa Search', icon: '🔍', className: 'provider-exa' }, +} + +const ERROR_MESSAGES = { + 'Failed to fetch': 'Network error — check your connection and try again.', + 'Load failed': 'Network error — check your connection and try again.', + '429': 'AI provider is rate-limited. Retrying with fallback…', + '503': 'Service unavailable. Retrying with fallback…', + '500': 'Server error while fetching research.', +} + +function normalizeErrorMessage(err) { + for (const [key, msg] of Object.entries(ERROR_MESSAGES)) { + if (err.message.includes(key)) return msg + } + return err.message || 'An unexpected error occurred.' +} + +function ProviderBadge({ provider }) { + if (!provider) return null + const meta = PROVIDER_LABELS[provider] || { label: provider, icon: '🤖', className: 'provider-unknown' } + return ( + + {meta.icon} {meta.label} + + ) +} + function ExerciseResearchPanel({ exerciseId, exerciseName }) { const [loading, setLoading] = useState(false) const [research, setResearch] = useState(null) const [error, setError] = useState(null) + const [provider, setProvider] = useState(null) + const [retryCount, setRetryCount] = useState(0) - const fetchResearch = async () => { + const fetchResearch = useCallback(async (attempt = 0) => { setLoading(true) setError(null) + try { const res = await fetch(`${API_URL}/exercises/${exerciseId}/research`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }) + if (!res.ok) { - const data = await res.json() - throw new Error(data.error || 'Failed to fetch research') + let errMsg = `Request failed (${res.status})` + try { + const data = await res.json() + errMsg = data.error || errMsg + } catch { + // ignore JSON parse error + } + + // Auto-retry once on 429/503 (provider fallback happening on backend) + if ((res.status === 429 || res.status === 503) && attempt < 1) { + console.warn(`[Research] Status ${res.status}, retrying (attempt ${attempt + 1})…`) + setRetryCount(c => c + 1) + setTimeout(() => fetchResearch(attempt + 1), 2000) + return + } + + throw new Error(errMsg) } + const data = await res.json() setResearch(data) + + // Extract provider from stored record or response metadata + const detectedProvider = + data.provider || + (data.stored && data.stored.provider) || + null + setProvider(detectedProvider) + setRetryCount(0) } catch (err) { - setError(err.message) + console.error('[Research] Fetch failed:', err) + setError(normalizeErrorMessage(err)) } finally { setLoading(false) } - } + }, [exerciseId]) + + const handleFetch = () => fetchResearch(0) return (