function ResearchLoadingSkeleton({ exerciseName }) {
return (
Searching for information on {exerciseName}…
)
}
function ResearchError({ message, onDismiss }) {
return (
⚠
{message}
{onDismiss && (
)}
)
}
function ResearchSourceCard({ result, index }) {
return (
{index + 1}
{result.title}
↗
{result.snippet && (
{result.snippet}
)}
{result.isFallback && (
Suggested
)}
)
}
function ResearchProviderBadge({ provider, status }) {
if (!provider) return null;
const badgeConfig = {
exa: { emoji: '🔍', label: 'Exa Search', color: 'primary' },
fallback: { emoji: '🔗', label: 'Web Sources', color: 'secondary' },
gemini: { emoji: '✨', label: 'AI Summary', color: 'accent' },
openrouter: { emoji: '🤖', label: 'AI Powered', color: 'accent' }
};
const config = badgeConfig[provider] || { emoji: '📊', label: provider, color: 'secondary' };
const isDegraded = status === 'degraded';
return (
{config.emoji}
{config.label}
{isDegraded && (
(Fallback)
)}
);
}
/**
* ResearchDisplay — pure presentational component.
*
* Props:
* loading {boolean} Show loading skeleton
* error {string} Error message to display
* data {object} Research data: { summary, results, provider, status }
* name {string} Exercise name (shown during loading)
* onDismiss {function} Clear error callback
*/
function ResearchDisplay({ loading, error, data, name, onDismiss }) {
if (loading) {
return
}
if (error) {
return
}
if (!data) return null
const hasSummary = Boolean(data.summary)
const hasSources = Array.isArray(data.results) && data.results.length > 0
return (
{hasSources && (
🔗
Sources
{data.results.length}
{data.results.map((result, i) => (
))}
)}
{!hasSummary && !hasSources && (
No research data found for this exercise.
)}
)
}
export default ResearchDisplay