feat(05-03): Implement API fallback handling for research display
- Enhanced exaSearch service with Exa API + fallback tier system * Tier 1: Exa API (primary) * Tier 2: Synthetic results with suggested web sources * Improved error handling with graceful degradation - Updated backend exerciseResearch route to return provider info * Returns 'provider' field identifying which API was used * Returns 'status' field (success/degraded) for UI feedback * Better error messages for debugging - Enhanced ResearchDisplay component with fallback feedback * New ResearchProviderBadge shows which provider was used * Visual indicators for fallback results (Suggested badge) * Support for multiple provider types (exa, fallback, gemini, etc.) * Improved error handling and recovery flows - Updated ExerciseResearchPanel with better error handling * Proper response parsing from backend * Forwards provider and status info to display component * Improved accessibility with tooltip hints - Added comprehensive Research Display styling * Responsive layout for mobile and desktop * Visual hierarchy for summaries and sources * Provider badge styling with color-coding * Fallback state indicators for user awareness
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
const { generateWithFallback } = require('../utils/gemini-fallback');
|
||||
|
||||
const DEFAULT_EXA_API_URL = 'https://api.exa.ai/search';
|
||||
|
||||
const buildSummary = (results) => {
|
||||
@@ -22,57 +20,52 @@ const buildSummary = (results) => {
|
||||
return snippets.slice(0, 3).join(' ');
|
||||
};
|
||||
|
||||
const generateFallbackSummary = async (query, numResults) => {
|
||||
try {
|
||||
console.log('🔄 Fallback: Generating AI summary for query:', query);
|
||||
const prompt = `Provide a concise summary of exercise form and technique for: "${query}". Include key points about proper form, common mistakes, and benefits. Keep it to 2-3 sentences.`;
|
||||
|
||||
const response = await generateWithFallback(prompt, {
|
||||
temperature: 0.7,
|
||||
maxTokens: 256
|
||||
});
|
||||
/**
|
||||
* Create synthetic results for fallback scenarios
|
||||
* Generates plausible web search results when primary API is unavailable
|
||||
*/
|
||||
const createFallbackResults = (query, numResults = 5) => {
|
||||
const sources = [
|
||||
{ domain: 'wikipedia.org', title: `${query} - Wikipedia` },
|
||||
{ domain: 'youtube.com', title: `${query} Tutorial | How to Perform Correctly` },
|
||||
{ domain: 'fitnessforum.com', title: `Best Practices for ${query} Form and Technique` },
|
||||
{ domain: 'acefitness.org', title: `Exercise Guide: ${query}` },
|
||||
{ domain: 'stronglifts.com', title: `${query} Guide: Everything You Need to Know` },
|
||||
{ domain: 'bodybuilding.com', title: `${query} Exercise - Benefits and Variations` },
|
||||
{ domain: 'nhs.uk', title: `${query}: Health Benefits and Safety` },
|
||||
{ domain: 'healthline.com', title: `${query}: Technique, Benefits & Common Mistakes` }
|
||||
];
|
||||
|
||||
if (response.success && response.data) {
|
||||
console.log(`✅ AI fallback summary generated (provider: ${response.provider})`);
|
||||
// Extract text from the appropriate response format
|
||||
let summaryText = '';
|
||||
if (response.data.response) {
|
||||
summaryText = response.data.response;
|
||||
} else if (response.data.choices?.[0]?.message?.content) {
|
||||
summaryText = response.data.choices[0].message.content;
|
||||
}
|
||||
|
||||
return {
|
||||
summary: summaryText,
|
||||
results: [], // No source links from AI fallback
|
||||
provider: response.provider
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('AI fallback summary generation failed:', err.message);
|
||||
}
|
||||
|
||||
// Last resort: simple template
|
||||
return {
|
||||
summary: `Exercise information for ${query}. Consult a fitness professional for proper form and safety guidelines.`,
|
||||
results: [],
|
||||
provider: 'template'
|
||||
};
|
||||
return sources.slice(0, numResults).map((source, index) => ({
|
||||
id: `fallback-${index}`,
|
||||
title: source.title,
|
||||
url: `https://${source.domain}/search?q=${encodeURIComponent(query)}`,
|
||||
snippet: `Learn about proper ${query} technique, benefits, and safety precautions.`,
|
||||
publishedDate: new Date().toISOString(),
|
||||
score: 0.8 - (index * 0.05),
|
||||
isFallback: true,
|
||||
provider: 'fallback'
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Main research search function with Exa API + fallback support
|
||||
* Tier 1: Exa API (primary)
|
||||
* Tier 2: Fallback to synthetic results with suggested sources
|
||||
*/
|
||||
const searchExerciseResearch = async ({ query, numResults = 5 }) => {
|
||||
if (!query || typeof query !== 'string') {
|
||||
throw new Error('Query must be a non-empty string');
|
||||
}
|
||||
|
||||
const apiKey = process.env.EXA_API_KEY;
|
||||
|
||||
// Tier 1: Try Exa API if configured
|
||||
const apiUrl = process.env.EXA_API_URL || DEFAULT_EXA_API_URL;
|
||||
|
||||
// Tier 1: Try Exa API (primary)
|
||||
if (apiKey) {
|
||||
try {
|
||||
console.log('📍 Tier 1: Attempting Exa search API...');
|
||||
const apiUrl = process.env.EXA_API_URL || DEFAULT_EXA_API_URL;
|
||||
|
||||
console.log(`📍 [Research] Attempting Exa API for: "${query}"`);
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -84,10 +77,13 @@ const searchExerciseResearch = async ({ query, numResults = 5 }) => {
|
||||
numResults,
|
||||
type: 'neural',
|
||||
useAutoprompt: true
|
||||
})
|
||||
}),
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
console.warn(`⚠️ [Research] Exa API error: ${response.status}`);
|
||||
throw new Error(`Exa search failed: ${response.status}`);
|
||||
}
|
||||
|
||||
@@ -101,27 +97,38 @@ const searchExerciseResearch = async ({ query, numResults = 5 }) => {
|
||||
: result.snippet,
|
||||
highlight: result.highlight,
|
||||
publishedDate: result.publishedDate,
|
||||
score: result.score
|
||||
score: result.score,
|
||||
provider: 'exa'
|
||||
}));
|
||||
|
||||
console.log('✅ Exa API success');
|
||||
console.log(`✅ [Research] Exa API success - ${results.length} results`);
|
||||
|
||||
return {
|
||||
summary: buildSummary(results),
|
||||
results,
|
||||
provider: 'exa'
|
||||
provider: 'exa',
|
||||
status: 'success'
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Exa API failed: ${err.message}, falling back to AI...`);
|
||||
console.warn(`⚠️ [Research] Exa API failed: ${err.message}`);
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ EXA_API_KEY not configured, using AI fallback...');
|
||||
console.warn('⚠️ [Research] EXA_API_KEY not configured, using fallback');
|
||||
}
|
||||
|
||||
// Tier 2: Fallback to AI-generated summary
|
||||
return generateFallbackSummary(query, numResults);
|
||||
// Tier 2: Fallback to synthetic results with suggested sources
|
||||
console.log(`📍 [Research] Using fallback results for: "${query}"`);
|
||||
const fallbackResults = createFallbackResults(query, numResults);
|
||||
|
||||
return {
|
||||
summary: `Research sources for "${query}". Click links below to learn more about this exercise.`,
|
||||
results: fallbackResults,
|
||||
provider: 'fallback',
|
||||
status: 'degraded'
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
searchExerciseResearch,
|
||||
generateFallbackSummary
|
||||
createFallbackResults
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user