import React, { useState, useEffect, useCallback } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Lightbulb, AlertTriangle, AlertCircle, Info, ChevronRight, ChevronDown, ChevronUp, Shield, RefreshCw, CheckCircle2, Dumbbell, Apple, Moon, Heart, Droplets, TestTube, Stethoscope, Sparkles, XCircle, } from 'lucide-react'; import { API_BASE_URL } from '../config/api'; import './RecommendationsPanel.css'; // Types interface Action { type: string; text: string; } interface Source { name: string; url?: string; } interface Provenance { memory?: boolean; graph?: boolean; metrics?: boolean; profile?: boolean; } interface Recommendation { id: string; title: string; severity: 'URGENT' | 'WARNING' | 'INFO'; why: string; actions: Action[]; followup: Action[]; sources: Source[]; provenance?: Provenance; } interface RecommendationsResponse { updated_at: string; disclaimer: string; items: Recommendation[]; total_count: number; urgent_count: number; warning_count: number; } /** Stored API returns priority as "high"|"medium"|"low" and description instead of why */ interface StoredItem { id: string; title: string; description?: string; priority: string; actions?: string[] | { type?: string; text: string }[]; evidence?: string[]; provenance?: Provenance; } interface RecommendationsPanelProps { authToken?: string; apiBaseUrl?: string; maxInitialDisplay?: number; refreshTrigger?: number; // Increment this to trigger a refresh (from WebSocket events) showGenerateButton?: boolean; // Show generate button for generating new recommendations onRecommendationsGenerated?: () => void; // Callback after generating recommendations variant?: 'dashboard' | 'page'; // Layout variant } // Action type to icon mapping const actionIcons: Record = { EXERCISE: , DIET: , HABIT: , SLEEP: , STRESS: , HYDRATION: , TEST: , DOCTOR: , GENERAL: , }; // Severity icons const severityIcons: Record = { URGENT: , WARNING: , INFO: , }; function mapStoredToRecommendation(stored: StoredItem): Recommendation { const severityMap = { high: 'URGENT' as const, medium: 'WARNING' as const, low: 'INFO' as const }; const severity = severityMap[stored.priority as keyof typeof severityMap] ?? 'INFO'; const rawActions = stored.actions ?? []; const actions: Action[] = rawActions.map((a) => { if (typeof a === 'string') return { type: 'GENERAL' as const, text: a }; const text = a?.text != null ? String(a.text) : ''; return { type: ((a?.type as Action['type']) ?? 'GENERAL'), text }; }); return { id: stored.id, title: stored.title ?? '', severity, why: stored.description ?? '', actions, followup: [], sources: [], provenance: stored.provenance, }; } // Provenance tooltip component - shows data sources const ProvenanceTooltip: React.FC<{ provenance?: Provenance }> = ({ provenance }) => { if (!provenance) return null; const sources = []; if (provenance.memory) sources.push({ label: 'Memory', icon: '🧠', desc: 'Your preferences & facts' }); if (provenance.graph) sources.push({ label: 'Graph', icon: 'πŸ•ΈοΈ', desc: 'Health relationships' }); if (provenance.metrics) sources.push({ label: 'Metrics', icon: 'πŸ“Š', desc: 'Lab values & observations' }); if (provenance.profile) sources.push({ label: 'Profile', icon: 'πŸ‘€', desc: 'Your health profile' }); if (sources.length === 0) return null; return (
✨ Sources
Recommendation Sources {sources.map((src, i) => (
{src.icon} {src.label} {src.desc}
))}
); }; const RecommendationsPanel: React.FC = ({ authToken, apiBaseUrl = API_BASE_URL, maxInitialDisplay = 3, refreshTrigger = 0, showGenerateButton = true, onRecommendationsGenerated, variant = 'dashboard', }) => { const [recommendations, setRecommendations] = useState(null); const [loading, setLoading] = useState(true); const [generating, setGenerating] = useState(false); const [error, setError] = useState(null); const [expanded, setExpanded] = useState(false); const [expandedCards, setExpandedCards] = useState>(new Set()); const fetchRecommendations = useCallback(async () => { if (!authToken) { setError('Authentication required'); setLoading(false); return; } setLoading(true); setError(null); const headers = { Authorization: `Bearer ${authToken}`, 'Content-Type': 'application/json', }; try { // Prefer stored (Grok-generated) when available; otherwise use rule-based const [storedRes, ruleRes] = await Promise.all([ fetch(`${apiBaseUrl}/api/recommendations/stored`, { headers }), fetch(`${apiBaseUrl}/api/recommendations`, { headers }), ]); if (storedRes.ok) { const storedData = await storedRes.json(); if (storedData.items?.length > 0) { const mapped: RecommendationsResponse = { ...storedData, items: storedData.items.map((item: StoredItem) => mapStoredToRecommendation(item)), }; setRecommendations(mapped); return; } } if (!ruleRes.ok) { throw new Error(`Failed to fetch recommendations: ${ruleRes.status}`); } const ruleData: RecommendationsResponse = await ruleRes.json(); setRecommendations(ruleData); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load recommendations'); } finally { setLoading(false); } }, [authToken, apiBaseUrl]); useEffect(() => { fetchRecommendations(); }, [fetchRecommendations]); // Refresh when refreshTrigger changes (triggered by WebSocket events from parent) useEffect(() => { if (refreshTrigger > 0) { fetchRecommendations(); } }, [refreshTrigger, fetchRecommendations]); // Generate new recommendations const handleGenerateRecommendations = async () => { if (!authToken) return; setGenerating(true); setError(null); try { const response = await fetch(`${apiBaseUrl}/api/recommendations/generate`, { method: 'POST', headers: { Authorization: `Bearer ${authToken}`, 'Content-Type': 'application/json', }, }); if (response.ok) { await fetchRecommendations(); onRecommendationsGenerated?.(); } else { let message = `Generate failed (${response.status})`; try { const body = await response.json(); if (body?.error && typeof body.error === 'string') { message = body.error; } } catch { // ignore parse error } setError(message); } } catch (err) { const message = err instanceof Error ? err.message : 'Failed to generate recommendations'; setError(message); console.error('Error generating recommendations:', err); } finally { setGenerating(false); } }; const toggleCardExpanded = (id: string) => { setExpandedCards(prev => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }; const displayedItems = recommendations?.items.slice( 0, expanded ? undefined : maxInitialDisplay ) || []; const hasMore = (recommendations?.items.length || 0) > maxInitialDisplay; if (loading) { return (

Loading personalized recommendations...

); } if (error) { return (

{error}

); } if (!recommendations || recommendations.items.length === 0) { return (

Recommendations

{showGenerateButton && ( )}

No recommendations yet

Generate personalized recommendations based on your health profile and reports.

{showGenerateButton && ( )}

{recommendations?.disclaimer || 'This is wellness guidance, not medical advice.'}

); } return (

Recommendations

{showGenerateButton && ( )}
{recommendations.urgent_count > 0 && ( {recommendations.urgent_count} Urgent )} {recommendations.warning_count > 0 && ( {recommendations.warning_count} Attention )} {recommendations.total_count - recommendations.urgent_count - recommendations.warning_count > 0 && ( {recommendations.total_count - recommendations.urgent_count - recommendations.warning_count} Tips )}
{displayedItems.map((item, index) => (

{item.title ?? ''}

{severityIcons[item.severity ?? 'INFO']} {item.severity ?? 'INFO'}

{item.why}

Suggested Actions

{(item.actions ?? []).slice(0, expandedCards.has(item.id) ? undefined : 3).map((action, idx) => ( {actionIcons[action.type] || } {action.text ?? ''} ))} {(item.actions ?? []).length > 3 && !expandedCards.has(item.id) && ( )}
{(item.followup ?? []).length > 0 && (

Follow-up

{(item.followup ?? []).map((follow, idx) => (
{actionIcons[follow.type] || } {follow.text ?? ''}
))}
)} {(item.sources ?? []).length > 0 && expandedCards.has(item.id) && (
{(item.sources ?? []).map((source, idx) => ( source.url ? ( πŸ“Ž {source.name ?? 'Source'} ) : ( πŸ“Ž {source.name ?? 'Source'} ) ))}
)} {((item.actions ?? []).length > 3 || (item.sources ?? []).length > 0) && ( )}
))}
{hasMore && ( )}

{recommendations.disclaimer}

); }; export default RecommendationsPanel;