f580fa81a6
- 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
88 lines
2.6 KiB
JavaScript
88 lines
2.6 KiB
JavaScript
const express = require('express');
|
|
|
|
const normalizeQuery = (exerciseName, body) => {
|
|
if (body && typeof body.query === 'string' && body.query.trim()) {
|
|
return body.query.trim();
|
|
}
|
|
|
|
if (body && typeof body.name === 'string' && body.name.trim()) {
|
|
return body.name.trim();
|
|
}
|
|
|
|
return `${exerciseName} exercise`;
|
|
};
|
|
|
|
const createExerciseResearchRouter = ({ pool, exaSearch }) => {
|
|
if (!pool || typeof pool.query !== 'function') {
|
|
throw new Error('Pool with query function is required');
|
|
}
|
|
if (!exaSearch || typeof exaSearch !== 'function') {
|
|
throw new Error('exaSearch function is required');
|
|
}
|
|
|
|
const router = express.Router();
|
|
|
|
router.post('/:id/research', async (req, res) => {
|
|
try {
|
|
const exerciseId = Number.parseInt(req.params.id, 10);
|
|
if (!Number.isInteger(exerciseId)) {
|
|
return res.status(400).json({ error: 'Exercise id must be an integer' });
|
|
}
|
|
|
|
const exerciseResult = await pool.query(
|
|
'SELECT id, name, description, muscle_groups, difficulty, equipment_needed FROM exercises WHERE id = $1',
|
|
[exerciseId]
|
|
);
|
|
|
|
if (!exerciseResult.rows.length) {
|
|
return res.status(404).json({ error: 'Exercise not found' });
|
|
}
|
|
|
|
const exercise = exerciseResult.rows[0];
|
|
const query = normalizeQuery(exercise.name, req.body);
|
|
const requestedResults = req.body?.num_results;
|
|
const numResults = Number.isInteger(requestedResults) && requestedResults > 0
|
|
? Math.min(requestedResults, 10)
|
|
: 5;
|
|
|
|
// Fetch research with fallback support
|
|
const { summary, results, provider, status } = await exaSearch({ query, numResults });
|
|
|
|
let researchRecord = null;
|
|
try {
|
|
const insertResult = await pool.query(
|
|
`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), provider || 'exa']
|
|
);
|
|
researchRecord = insertResult.rows[0] || null;
|
|
} catch (err) {
|
|
console.warn('Failed to store research results:', err.message);
|
|
}
|
|
|
|
res.json({
|
|
exercise,
|
|
query,
|
|
summary,
|
|
results,
|
|
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',
|
|
message: err.message
|
|
});
|
|
}
|
|
});
|
|
|
|
return router;
|
|
};
|
|
|
|
module.exports = {
|
|
createExerciseResearchRouter
|
|
};
|