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:
2026-03-02 23:45:07 +01:00
parent 2a0496b915
commit f580fa81a6
5 changed files with 444 additions and 167 deletions
+10 -4
View File
@@ -45,7 +45,8 @@ const createExerciseResearchRouter = ({ pool, exaSearch }) => {
? Math.min(requestedResults, 10)
: 5;
const { summary, results } = await exaSearch({ query, numResults });
// Fetch research with fallback support
const { summary, results, provider, status } = await exaSearch({ query, numResults });
let researchRecord = null;
try {
@@ -53,7 +54,7 @@ const createExerciseResearchRouter = ({ pool, exaSearch }) => {
`INSERT INTO research_results (exercise_id, query, summary, results, provider)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, created_at`,
[exerciseId, query, summary, JSON.stringify(results), 'exa']
[exerciseId, query, summary, JSON.stringify(results), provider || 'exa']
);
researchRecord = insertResult.rows[0] || null;
} catch (err) {
@@ -65,11 +66,16 @@ const createExerciseResearchRouter = ({ pool, exaSearch }) => {
query,
summary,
results,
stored: researchRecord
stored: researchRecord,
provider: provider || 'exa',
status: status || 'success'
});
} catch (err) {
console.error('Error running exercise research:', err);
res.status(500).json({ error: 'Failed to fetch research' });
res.status(500).json({
error: 'Failed to fetch research',
message: err.message
});
}
});
+58 -51
View File
@@ -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
};