darved2305 commited on
Commit
bee3c5c
·
1 Parent(s): 5c1c645

ui fixes, added youtube recommendations in 2D simulation page

Browse files
README.md CHANGED
@@ -1,8 +1,8 @@
1
- # Lumea Health Platform
2
 
3
  A full-stack health companion platform for preventive health management. Upload medical reports, extract health metrics via OCR, track trends, and receive AI-powered health recommendations.
4
 
5
- > ⚠️ **Disclaimer**: This platform is a support tool for personal health tracking. It does not provide medical advice, diagnosis, or treatment. Always consult a licensed healthcare professional for medical decisions.
6
 
7
  ---
8
 
@@ -38,9 +38,9 @@ Lumea is a unified medical companion platform that enables users to:
38
  - **Receive AI recommendations** based on extracted lab values and health patterns powered by OpenRouter (Claude Sonnet 4.5)
39
  - **Compare reports over time** with AI-powered summaries and trend analysis
40
  - **Chat with an AI assistant** grounded in your personal health data
41
- - **🧠 Memory & Knowledge Graph**: Persistent health memory with Mem0 and interconnected insights via Neo4j/Graphiti for temporal reasoning and contradiction detection
42
- - **🎙️ Voice Agent**: Speak naturally to an AI health assistant for hands-free access to personalized health insights
43
- - **🏥 Physics Twin**: Interactive 2D organ simulation with real-time telemetry, condition detection, and evidence-based health recommendations powered by your actual medical data
44
 
45
  ---
46
 
@@ -68,106 +68,106 @@ Lumea is a unified medical companion platform that enables users to:
68
 
69
  ```mermaid
70
  flowchart TB
71
- subgraph Client["Frontend (React)"]
72
- UI[React UI]
73
- WS[WebSocket Client]
74
- end
75
-
76
- subgraph API["Backend (FastAPI)"]
77
- Auth[Auth Routes]
78
- Dashboard[Dashboard Routes]
79
- Reports[Reports Routes]
80
- Profile[Profile Routes]
81
- Recommendations[Recommendations Routes]
82
- AISummary[AI Summary Routes]
83
- Assistant[Assistant Routes]
84
- WSServer[WebSocket Server]
85
- end
86
-
87
- subgraph Services["Backend Services"]
88
- OCR[PDF/OCR Extractor]
89
- Classifier[Document Classifier]
90
- MetricExtractor[Metric Extractor]
91
- MetricsService[Metrics Service]
92
- RecommendationEngine[Recommendation Engine]
93
- AISummaryService[AI Summary Service]
94
- RAG[RAG Service]
95
- LLM[LLM Service]
96
- end
97
-
98
- subgraph External["External Services"]
99
- GrokAPI[Grok API]
100
- Ollama[Ollama / MedGemma]
101
- Gemini[Gemini API]
102
- end
103
-
104
- subgraph Storage["Data Layer"]
105
- DB[(PostgreSQL / Neon)]
106
- ChromaDB[(ChromaDB Vector Store)]
107
- FileStorage[File Storage]
108
- end
109
-
110
- UI --> Auth
111
- UI --> Dashboard
112
- UI --> Reports
113
- UI --> Profile
114
- UI --> Recommendations
115
- UI --> AISummary
116
- UI --> Assistant
117
- WS <--> WSServer
118
-
119
- Reports --> OCR
120
- OCR --> Classifier
121
- Classifier --> MetricExtractor
122
- MetricExtractor --> MetricsService
123
- MetricsService --> RecommendationEngine
124
-
125
- AISummary --> AISummaryService
126
- AISummaryService --> GrokAPI
127
- Assistant --> RAG
128
- RAG --> ChromaDB
129
- RAG --> LLM
130
- LLM --> Ollama
131
- LLM --> Gemini
132
-
133
- Auth --> DB
134
- Dashboard --> DB
135
- Reports --> DB
136
- Reports --> FileStorage
137
- Profile --> DB
138
- Recommendations --> DB
139
- AISummary --> DB
140
  ```
141
 
142
  ### Data Flow: Report Upload to Health Index
143
 
144
  ```mermaid
145
  sequenceDiagram
146
- participant User
147
- participant Frontend
148
- participant Backend
149
- participant OCR
150
- participant Classifier
151
- participant Extractor
152
- participant DB
153
- participant WebSocket
154
-
155
- User->>Frontend: Upload PDF/Image
156
- Frontend->>Backend: POST /api/reports/upload
157
- Backend->>DB: Create report (status: uploaded)
158
- Backend-->>Frontend: {id, status: uploaded}
159
-
160
- Backend->>OCR: Extract text (background)
161
- OCR-->>Backend: Raw text
162
- Backend->>Classifier: Classify document
163
- Classifier-->>Backend: Category + doc_type
164
- Backend->>Extractor: Extract metrics
165
- Extractor-->>Backend: Observations[]
166
- Backend->>DB: Save observations + update report
167
- Backend->>DB: Recompute health index
168
- Backend->>WebSocket: emit report_parsed
169
- WebSocket-->>Frontend: report_parsed event
170
- Frontend->>Frontend: Refresh UI
171
  ```
172
 
173
  ---
@@ -182,6 +182,8 @@ sequenceDiagram
182
  - **Condition Detection**: 10 rule-based health conditions auto-detected with severity mapping
183
  - **Body Impact Overlay**: 2D SVG visualization showing organs affected by detected conditions
184
  - **Smart Recommendations**: Evidence-based health actions with YouTube educational links per condition
 
 
185
  - **Time-Series History**: View per-report organ scores and historical trends
186
  - **Lifestyle Integration**: Incorporates self-reported profile data (sleep hours, stress level)
187
 
@@ -196,7 +198,7 @@ sequenceDiagram
196
  - Tracks conditions, symptoms, medications, supplements, allergies
197
  - Family medical history and genetic test results
198
  - **Once completed, users are never re-asked** – profile status persists in DB
199
- - "Profile Complete" indicator with quick-edit access via Settings page
200
  - **Real-time SMS reminders** via Twilio (or mock mode for testing)
201
  - Background scheduler processes due reminders every 60 seconds
202
  - Default reminders auto-generated: medication, appointment, checkup, hydration
@@ -224,7 +226,7 @@ sequenceDiagram
224
  - Citations from reports and observations
225
  - WebSocket streaming for real-time responses
226
 
227
- ### 🎙️ Voice Agent (AI Health Assistant)
228
  - **Natural voice conversations** with your health data
229
  - **Speech-to-Text**: Browser Web Speech API for hands-free input
230
  - **AI Processing**: Google Gemini with personalized health context
@@ -251,47 +253,47 @@ The **Memory & Knowledge Graph** system provides persistent, intelligent health
251
 
252
  ```mermaid
253
  flowchart TB
254
- subgraph Input["Health Data Sources"]
255
- Profile[Health Profile]
256
- Reports[Medical Reports]
257
- Questionnaire[User Questionnaires]
258
- Assistant[AI Assistant Chats]
259
- end
260
-
261
- subgraph Memory["Mem0 Memory Layer"]
262
- Extraction[Fact Extraction<br/>via Groq LLM]
263
- Storage[ChromaDB<br/>Vector Store]
264
- Retrieval[Semantic Search]
265
- end
266
-
267
- subgraph Graph["Graphiti Knowledge Graph"]
268
- Entities[Entity Nodes<br/>conditions, metrics, meds]
269
- Relationships[Temporal Relationships<br/>affects, caused_by, treats]
270
- Episodes[Health Episodes<br/>time-stamped events]
271
- Insights[AI-Generated Insights<br/>contradictions, patterns]
272
- end
273
-
274
- subgraph DB["Neo4j Graph Database"]
275
- Nodes[Entity Nodes]
276
- Edges[Relationship Edges]
277
- Indexes[Vector Indexes<br/>Ollama embeddings]
278
- end
279
-
280
- Input --> Extraction
281
- Extraction --> Storage
282
- Storage --> Retrieval
283
-
284
- Input --> Entities
285
- Entities --> Relationships
286
- Relationships --> Episodes
287
- Episodes --> Insights
288
-
289
- Entities --> Nodes
290
- Relationships --> Edges
291
- Edges --> Indexes
292
-
293
- Retrieval -.->|Enriches| Recommendations[AI Recommendations]
294
- Insights -.->|Powers| Dashboard[Features Dashboard]
295
  ```
296
 
297
  ### Key Features
@@ -299,9 +301,9 @@ flowchart TB
299
  #### 1. **Persistent Memory (Mem0)**
300
 
301
  - **Automatic Fact Extraction**: Converts natural language health data into structured facts
302
- - "Sleep is about 5 hours per night" → Extracted memory fact
303
- - "Has condition High Blood Pressure" → Indexed and searchable
304
- - "BMI is 25.2 which falls in overweight range" → Computed and stored
305
 
306
  - **User-Scoped Storage**: Each user's memories are isolated and secure
307
  - **Vector-Based Retrieval**: Semantic search powered by local Ollama embeddings (nomic-embed-text)
@@ -310,46 +312,46 @@ flowchart TB
310
  #### 2. **Knowledge Graph (Graphiti + Neo4j)**
311
 
312
  - **Entity Relationship Mapping**:
313
- - Conditions связаны with symptoms, medications, and risk factors
314
- - Lab values connected to reference ranges and health implications
315
- - Lifestyle factors (sleep, diet, exercise) linked to health outcomes
316
 
317
  - **Temporal Reasoning**:
318
- - Track health changes over time (BP trend: 140/90 in Jan → 130/85 in Feb)
319
- - Episode creation for significant health events
320
- - Valid/expired relationships based on temporal context
321
 
322
  - **Contradiction Detection**:
323
- - Identifies conflicting health data (e.g., "no allergies" vs. "allergic to dust")
324
- - Highlights diagnostic uncertainties
325
- - Flags medication interactions
326
 
327
  - **Graph-Powered Insights**:
328
- - **Temporal Patterns**: "BP decreased after starting medication X"
329
- - **Key Relationships**: "High BMI correlates with elevated cholesterol"
330
- - **Contradictions**: "Sleep reported as both 5hrs and 8hrs - needs clarification"
331
 
332
  #### 3. **Interactive Visualization**
333
 
334
  **Features Dashboard** (`/features`) provides:
335
 
336
  - **Memory Dashboard**:
337
- - View all extracted health facts
338
- - Search memories semantically
339
- - Add/delete facts manually
340
- - Real-time sync status
341
 
342
  - **Knowledge Graph Viewer**:
343
- - Interactive node-edge visualization
344
- - Filter by conditions, medications, metrics
345
- - Zoom/pan for detailed exploration
346
- - Click nodes/edges for metadata
347
 
348
  - **AI Insights Panel**:
349
- - Temporal insights (trend analysis)
350
- - Relationship insights (correlation patterns)
351
- - Contradiction insights (data conflicts)
352
- - Each insight includes evidence and confidence score
353
 
354
  ### Technical Implementation
355
 
@@ -424,7 +426,7 @@ flowchart TB
424
 
425
  Lumea includes a comprehensive medicine management system that helps users find affordable generic alternatives to prescribed medicines and locate nearby pharmacies, including government-sponsored Jan Aushadhi Kendras offering subsidized medications.
426
 
427
- > ⚠️ **Medical Disclaimer**: This feature is a support tool for informational purposes only. It does NOT provide medical advice. Always consult your doctor or pharmacist before switching medicines or starting new treatments.
428
 
429
  ### Overview
430
 
@@ -440,89 +442,89 @@ The Medicines feature enables users to:
440
  ### User Flow
441
 
442
  1. **Input Medicine Information**
443
- - User enters brand name (e.g., "Aspirin 500mg") or uploads prescription image
444
- - System extracts text via OCR or Grok AI parsing
445
 
446
  2. **Normalize Medicine Data**
447
- - `MedicineNormalizer` service extracts: salt, strength, form (tablet/capsule/liquid), release type (immediate/sustained)
448
- - Queries `GenericCatalog` table to validate and standardize extraction
449
 
450
  3. **Find Substitutes**
451
- - `SubstituteFinder` queries database for alternatives with 4-tier ranked matching:
452
- - **Tier 1** (1.0 score): Exact match on salt + strength + form + release type
453
- - **Tier 2** (0.8 score): Same salt + strength + form
454
- - **Tier 3** (0.6 score): Same salt + strength
455
- - **Tier 4** (0.4 score): Same salt only
456
- - Results sorted by price (Jan Aushadhi first for lowest cost)
457
 
458
  4. **Locate Pharmacies** (Optional)
459
- - User enters location (latitude/longitude) and search radius
460
- - `PharmacyLocator` queries Google Places API for pharmacies
461
- - Results include Jan Aushadhi Kendras (government pharmacies) and private pharmacies
462
- - Results cached for 1 hour to reduce API calls
463
 
464
  5. **Save & Track**
465
- - User can save medicines to personal list with notes
466
- - Each save creates `UserSavedMedicine` entry for quick reference
467
 
468
  ### Architecture Diagram
469
 
470
  ```mermaid
471
  flowchart TB
472
- subgraph Frontend["Frontend (React)"]
473
- MedicinesPage["Medicines.tsx Page"]
474
- SubstitutePanel["Substitute Finder Panel"]
475
- PharmacyPanel["Pharmacy Locator Panel"]
476
- end
477
-
478
- subgraph API["Backend API (FastAPI)"]
479
- NormalizeRoute["POST /normalize<br/>POST /normalize/batch"]
480
- SubstituteRoute["POST /substitutes<br/>POST /substitutes/from-text"]
481
- PharmacyRoute["GET /pharmacies/nearby<br/>GET /pharmacies/{id}<br/>POST /pharmacies/{id}/click"]
482
- SavedRoute["POST /saved<br/>GET /saved<br/>DELETE /saved/{id}"]
483
- end
484
-
485
- subgraph Services["Backend Services"]
486
- MedicineNormalizer["MedicineNormalizer<br/>- normalize()<br/>- normalize_batch()"]
487
- SubstituteFinder["SubstituteFinder<br/>- find_substitutes()<br/>- save_medicine()"]
488
- PharmacyLocator["PharmacyLocator<br/>- search_nearby()<br/>- get_place_details()<br/>- 1hr cache"]
489
- GrokService["GrokMedicineService<br/>- get_alternatives_for_text()"]
490
- end
491
-
492
- subgraph External["External Services"]
493
- GooglePlaces["Google Places API<br/>(Pharmacy Search)"]
494
- GrokAPI["Grok xAI API<br/>(Medicine Parsing)"]
495
- end
496
-
497
- subgraph Storage["Data Layer"]
498
- DB[(PostgreSQL)]
499
- GenericCatalog["GenericCatalog Table<br/>(600K+ medicines)"]
500
- UserSavedMedicine["UserSavedMedicine Table"]
501
- SubstituteQuery["SubstituteQuery Table<br/>(Analytics)"]
502
- PharmacyClick["PharmacyClick Table<br/>(Analytics)"]
503
- end
504
-
505
- MedicinesPage --> SubstitutePanel
506
- MedicinesPage --> PharmacyPanel
507
- SubstitutePanel --> SubstituteRoute
508
- PharmacyPanel --> PharmacyRoute
509
- PharmacyPanel --> SavedRoute
510
-
511
- SubstituteRoute --> GrokService
512
- SubstituteRoute --> MedicineNormalizer
513
- SubstituteRoute --> SubstituteFinder
514
- PharmacyRoute --> PharmacyLocator
515
-
516
- MedicineNormalizer --> DB
517
- MedicineNormalizer --> GenericCatalog
518
- SubstituteFinder --> DB
519
- SubstituteFinder --> GenericCatalog
520
- SubstituteFinder --> SubstituteQuery
521
- PharmacyLocator --> GooglePlaces
522
- PharmacyLocator --> PharmacyClick
523
-
524
- GrokService --> GrokAPI
525
- GrokAPI -->|Parsed results| SubstituteFinder
526
  ```
527
 
528
  ### API Endpoints
@@ -547,47 +549,47 @@ flowchart TB
547
  Request:
548
  ```json
549
  {
550
- "text": "Aspirin 500mg tablets"
551
  }
552
  ```
553
 
554
  Response:
555
  ```json
556
  {
557
- "original_text": "Aspirin 500mg tablets",
558
- "normalized": {
559
- "brand_name": "Aspirin",
560
- "salt": "Acetylsalicylic Acid",
561
- "strength": "500",
562
- "form": "tablet",
563
- "release_type": "immediate",
564
- "confidence": 0.95
565
- },
566
- "substitutes": [
567
- {
568
- "rank": 1,
569
- "product_name": "Jan Aushadhi Aspirin 500mg",
570
- "salt": "Acetylsalicylic Acid",
571
- "strength": "500",
572
- "form": "tablet",
573
- "mrp": "5.00",
574
- "is_jan_aushadhi": true,
575
- "match_score": 1.0,
576
- "match_reason": "Exact match: Same salt, strength, form, and type"
577
- },
578
- {
579
- "rank": 2,
580
- "product_name": "Ecosprin 500mg",
581
- "salt": "Acetylsalicylic Acid",
582
- "strength": "500",
583
- "form": "tablet",
584
- "mrp": "8.50",
585
- "is_jan_aushadhi": false,
586
- "match_score": 1.0,
587
- "match_reason": "Exact match: Same salt, strength, form, and type"
588
- }
589
- ],
590
- "disclaimer": "Always confirm with your doctor or pharmacist before switching medicines"
591
  }
592
  ```
593
 
@@ -601,32 +603,32 @@ GET /api/medicines/pharmacies/nearby?lat=40.7128&lng=-74.0060&radius_m=1000&type
601
  Response:
602
  ```json
603
  {
604
- "pharmacies": [
605
- {
606
- "place_id": "ChIJN1blbgBQwokRzKgy6E_B_1Q",
607
- "name": "Jan Aushadhi Kendra - Downtown",
608
- "address": "123 Main St, New York, NY 10001",
609
- "latitude": 40.7128,
610
- "longitude": -74.0060,
611
- "rating": 4.7,
612
- "is_open": true,
613
- "is_jan_aushadhi": true,
614
- "phone": "+1-212-555-0123"
615
- },
616
- {
617
- "place_id": "ChIJrc_p_1BQwokRzKgy6E_B_2Q",
618
- "name": "Metro Pharmacy",
619
- "address": "456 Broadway, New York, NY 10002",
620
- "latitude": 40.7150,
621
- "longitude": -74.0030,
622
- "rating": 4.2,
623
- "is_open": true,
624
- "is_jan_aushadhi": false,
625
- "phone": "+1-212-555-0456"
626
- }
627
- ],
628
- "next_page_token": null,
629
- "total_results": 2
630
  }
631
  ```
632
 
@@ -636,36 +638,36 @@ Response:
636
 
637
  Lumea includes a fully integrated **Voice Agent** that provides an ElevenLabs-style conversational health assistant experience. Users can speak naturally to the AI agent, which responds with personalized health guidance based on their profile, conditions, medications, allergies, and recent lab reports.
638
 
639
- > ⚠️ **Medical Disclaimer**: The Voice Agent is an informational support tool. It does NOT provide medical diagnosis, dosage recommendations, or emergency care. Always consult licensed healthcare professionals for medical decisions.
640
 
641
  ### Overview
642
 
643
- The Voice Agent transforms how users interact with their health data through natural voice conversations powered by cutting-edge AI technologies.
644
 
645
  **Key Capabilities**:
646
 
647
- - **🎙️ Voice Interaction**: Speak naturally using browser-based speech recognition (Web Speech API)
648
- - **🧠 Personalized AI Responses**: Powered by Google Gemini with access to your complete health profile
649
- - **🔊 Natural Speech Output**: Human-like voice responses via ElevenLabs TTS (with browser fallback)
650
- - **🛡️ Medical Safety Filters**: Detects emergency keywords, dosage inquiries, and provides appropriate safety responses
651
- - **Real-time Feedback**: Visual orb animations reflect listening/thinking/speaking states
652
- - **📝 Transcript View**: Optional chat-mode panel displays conversation history
653
- - **🎯 Context-Aware**: Accesses your health conditions, medications, allergies, recent reports, and RAG-indexed data
654
 
655
  ### How It Works
656
 
657
- The Voice Agent uses a sophisticated three-stage pipeline to deliver personalized health conversations:
658
 
659
  ```mermaid
660
  graph LR
661
- A[👤 User Speaks] --> B[🎤 Browser STT<br/>Web Speech API]
662
- B --> C[🧠 LLM Processing<br/>Google Gemini]
663
- C --> D[📊 Health Context<br/>Profile + Reports + RAG]
664
- D --> C
665
- C --> E[🔊 Text-to-Speech<br/>ElevenLabs API]
666
- E --> F[🔉 Browser Fallback<br/>Web Speech API]
667
- E --> G[👤 User Hears Response]
668
- F --> G
669
  ```
670
 
671
  #### Stage 1: Speech-to-Text (STT)
@@ -677,75 +679,75 @@ graph LR
677
  #### Stage 2: AI Processing & Context Retrieval
678
  - **LLM Provider**: Google Gemini API (`gemini-flash-latest`)
679
  - **Health Context Integration**:
680
- - User demographics (age, gender, BMI)
681
- - Active medical conditions
682
- - Current medications (name, dose, frequency)
683
- - Known allergies and their severity
684
- - Recent lab reports and observations
685
- - Lifestyle factors (sleep, exercise, smoking, alcohol)
686
  - **RAG Enhancement**: Queries ChromaDB vector store for relevant historical health data
687
  - **Safety Layer**: HIPAA-compliant system prompt with strict medical guidelines:
688
- - Never diagnose conditions
689
- - Never recommend medication dosages
690
- - Always suggest professional consultation
691
- - Detect emergency situations and direct to 911
692
 
693
  #### Stage 3: Text-to-Speech (TTS)
694
  - **Primary**: ElevenLabs TTS API
695
- - Model: `eleven_turbo_v2_5` (optimized for speed, free tier compatible)
696
- - Voice: Rachel (`21m00Tcm4TlvDq8ikWAM`)
697
- - Quality: Studio-grade natural voice synthesis
698
  - **Fallback**: Browser Text-to-Speech API
699
- - Activates automatically if ElevenLabs unavailable (503 errors)
700
- - Uses system voices
701
- - Zero latency, no API costs
702
 
703
  ### Real-World Benefits
704
 
705
  **For Everyday Users**:
706
- - 🏃 **Hands-Free Access**: Check health info while cooking, exercising, or commuting
707
- - 💬 **Natural Conversations**: Ask questions in plain language, no medical jargon required
708
- - 📱 **Accessibility**: Ideal for users with visual impairments or reading difficulties
709
- - **Quick Insights**: Faster than navigating multiple screens and charts
710
 
711
  **For Health-Conscious Individuals**:
712
- - 🔍 **Report Interpretation**: "What do my cholesterol levels mean?"
713
- - 💊 **Medication Context**: "Why am I taking this medication?"
714
- - 📊 **Trend Analysis**: "How has my blood pressure changed over time?"
715
- - 🎯 **Personalized Guidance**: Responses tailored to YOUR specific health profile
716
 
717
  **Safety & Reliability**:
718
- - 🚨 **Emergency Detection**: Recognizes crisis keywords (chest pain, can't breathe, etc.)
719
- - 💊 **Dosage Protection**: Refuses to recommend medication changes
720
- - 🔒 **HIPAA Guidelines**: All responses include disclaimers and professional consultation advice
721
- - 🌐 **Fallback Mechanisms**: Graceful degradation if AI/TTS services unavailable
722
 
723
  ### API Endpoints
724
 
725
  | Method | Endpoint | Description | Auth | Response |
726
  |--------|----------|-------------|------|----------|
727
- | GET | `/api/voice/context` | Get user health context summary with profile completeness status | Yes | `{profile_complete: bool, summary: {...}, has_personalization: bool}` |
728
- | POST | `/api/voice/answer` | Generate personalized AI answer using Gemini + health context | Yes | `{answer_text: string, flags: [string], used_context: {...}}` |
729
- | POST | `/api/voice/tts` | Convert text to speech using ElevenLabs API | Yes | Binary audio stream (MP3) |
730
- | GET | `/api/voice/tts/status` | Check ElevenLabs TTS configuration status | Yes | `{configured: bool, voice_id: string, runtime_check: {...}}` |
731
 
732
  **Context Response Details**:
733
  ```json
734
  {
735
- "profile_complete": true,
736
- "has_personalization": true,
737
- "summary": {
738
- "name": "Darshan Ved",
739
- "age": 49,
740
- "gender": "Male",
741
- "bmi": 21.5,
742
- "conditions": ["Hypertension"],
743
- "medications": ["Lisinopril 10mg"],
744
- "allergies": ["Penicillin"],
745
- "reports_count": 5,
746
- "sleep_hours": 7,
747
- "exercise_frequency": "3-4 times/week"
748
- }
749
  }
750
  ```
751
 
@@ -764,12 +766,12 @@ Add to **root `.env`** (for Docker) and **`backend/.env`** (for local developmen
764
  # ===== AI/LLM Configuration =====
765
  # Google Gemini API (Primary LLM for Voice Agent)
766
  USE_GEMINI_FALLBACK=true
767
- GEMINI_API_KEY=AIzaSy... # Get from https://aistudio.google.com/apikey
768
 
769
  # ===== Text-to-Speech Configuration =====
770
  # ElevenLabs TTS for Voice Agent (Optional - has browser fallback)
771
- ELEVENLABS_API_KEY=sk_... # Get from https://elevenlabs.io/
772
- ELEVENLABS_VOICE_ID=21m00Tcm4TlvDq8ikWAM # Rachel voice (recommended)
773
  ```
774
 
775
  #### Docker Configuration
@@ -778,13 +780,13 @@ Ensure `docker-compose.yml` includes these environment variables in the backend
778
 
779
  ```yaml
780
  services:
781
- backend:
782
- environment:
783
- # ... other env vars ...
784
- - GEMINI_API_KEY=${GEMINI_API_KEY:-}
785
- - USE_GEMINI_FALLBACK=${USE_GEMINI_FALLBACK:-true}
786
- - ELEVENLABS_API_KEY=${ELEVENLABS_API_KEY:-}
787
- - ELEVENLABS_VOICE_ID=${ELEVENLABS_VOICE_ID:-21m00Tcm4TlvDq8ikWAM}
788
  ```
789
 
790
  #### Getting API Keys
@@ -803,13 +805,13 @@ services:
803
  3. Create new API key (starts with `sk_...`)
804
  4. Free tier includes: 10,000 characters/month
805
  5. Recommended voice IDs:
806
- - `21m00Tcm4TlvDq8ikWAM` - Rachel (conversational, female)
807
- - `ErXwobaYiN019PkySvjV` - Antoni (clear, male)
808
- - `EXAVITQu4vr4xnSDxMaL` - Bella (warm, female)
809
 
810
  **Free Tier Model Compatibility**:
811
- - Works: `gemini-flash-latest`, `gemini-1.5-flash-8b`
812
- - Quota exceeded: `gemini-2.0-flash`, `gemini-2.5-flash` (requires paid tier)
813
 
814
  ### Technology Stack
815
 
@@ -847,19 +849,19 @@ services:
847
 
848
  ```bash
849
  curl -X GET http://localhost:8000/api/voice/tts/status \
850
- -H "Authorization: Bearer <your_jwt_token>"
851
  ```
852
 
853
  **Expected Response**:
854
  ```json
855
  {
856
- "configured": true,
857
- "voice_id": "21m00Tcm4TlvDq8ikWAM",
858
- "runtime_check": {
859
- "api_key_loaded": true,
860
- "api_key_prefix": "sk_d78...",
861
- "voice_id": "21m00Tcm4TlvDq8ikWAM"
862
- }
863
  }
864
  ```
865
 
@@ -867,24 +869,24 @@ curl -X GET http://localhost:8000/api/voice/tts/status \
867
 
868
  ```bash
869
  curl -X GET http://localhost:8000/api/voice/context \
870
- -H "Authorization: Bearer <your_jwt_token>"
871
  ```
872
 
873
  **Expected Response**:
874
  ```json
875
  {
876
- "profile_complete": true,
877
- "has_personalization": true,
878
- "summary": {
879
- "name": "Darshan Ved",
880
- "age": 49,
881
- "gender": "Male",
882
- "bmi": 21.5,
883
- "conditions": ["Hypertension"],
884
- "medications": ["Lisinopril"],
885
- "allergies": ["Penicillin"],
886
- "reports_count": 5
887
- }
888
  }
889
  ```
890
 
@@ -892,24 +894,24 @@ curl -X GET http://localhost:8000/api/voice/context \
892
 
893
  ```bash
894
  curl -X POST http://localhost:8000/api/voice/answer \
895
- -H "Authorization: Bearer <your_jwt_token>" \
896
- -H "Content-Type: application/json" \
897
- -d '{
898
- "text": "What do my cholesterol levels mean?"
899
- }'
900
  ```
901
 
902
  **Expected Response**:
903
  ```json
904
  {
905
- "answer_text": "Based on your recent lab reports, your total cholesterol is 195 mg/dL, which falls within the desirable range (below 200 mg/dL). Your LDL cholesterol is 110 mg/dL, also in the optimal range. However, I recommend discussing these results with your healthcare provider for personalized guidance based on your hypertension condition.",
906
- "flags": [],
907
- "used_context": {
908
- "has_profile": true,
909
- "conditions_count": 1,
910
- "medications_count": 1,
911
- "has_rag_context": true
912
- }
913
  }
914
  ```
915
 
@@ -917,10 +919,10 @@ curl -X POST http://localhost:8000/api/voice/answer \
917
 
918
  ```bash
919
  curl -X POST http://localhost:8000/api/voice/tts \
920
- -H "Authorization: Bearer <your_jwt_token>" \
921
- -H "Content-Type: application/json" \
922
- -d '{"text": "Hello! Your health data shows positive trends."}' \
923
- --output test_voice.mp3
924
 
925
  # Play the audio file
926
  # Windows: start test_voice.mp3
@@ -933,34 +935,34 @@ curl -X POST http://localhost:8000/api/voice/tts \
933
  **Emergency Detection Test**:
934
  ```bash
935
  curl -X POST http://localhost:8000/api/voice/answer \
936
- -H "Authorization: Bearer <your_jwt_token>" \
937
- -H "Content-Type: application/json" \
938
- -d '{"text": "I am having severe chest pain"}'
939
  ```
940
 
941
  **Expected Response**:
942
  ```json
943
  {
944
- "answer_text": "I'm detecting words that suggest this might be an emergency situation. Please call emergency services immediately (911) or go to the nearest emergency room...",
945
- "flags": ["emergency"],
946
- "used_context": {}
947
  }
948
  ```
949
 
950
  **Dosage Inquiry Test**:
951
  ```bash
952
  curl -X POST http://localhost:8000/api/voice/answer \
953
- -H "Authorization: Bearer <your_jwt_token>" \
954
- -H "Content-Type: application/json" \
955
- -d '{"text": "Can I double my medication dosage?"}'
956
  ```
957
 
958
  **Expected Response**:
959
  ```json
960
  {
961
- "answer_text": "I cannot provide specific dosage recommendations. Please consult your doctor or pharmacist before making any changes to your medications...",
962
- "flags": ["dosage_inquiry"],
963
- "used_context": {...}
964
  }
965
  ```
966
 
@@ -972,20 +974,20 @@ curl -X POST http://localhost:8000/api/voice/answer \
972
 
973
  **Solutions**:
974
  1. **Check API Key Loading**:
975
- ```bash
976
- docker exec ggw-backend python -c "from app.settings import settings; print(f'API Key: {settings.ELEVENLABS_API_KEY[:20] if settings.ELEVENLABS_API_KEY else None}...')"
977
- ```
978
 
979
  2. **Verify Environment Variables**:
980
- - Ensure `ELEVENLABS_API_KEY` is in root `.env` file
981
- - Ensure `docker-compose.yml` includes the env var mapping
982
- - Restart Docker containers: `docker compose restart backend`
983
 
984
  3. **Check API Key Validity**:
985
- ```bash
986
- curl https://api.elevenlabs.io/v1/voices \
987
- -H "xi-api-key: sk_your_key_here"
988
- ```
989
 
990
  #### Issue: Gemini API 404 or 429 Errors
991
 
@@ -993,19 +995,19 @@ curl -X POST http://localhost:8000/api/voice/answer \
993
 
994
  **Solutions**:
995
  1. **Verify Model Compatibility**:
996
- ```bash
997
- docker exec ggw-backend python -c "import google.generativeai as genai; import os; genai.configure(api_key=os.environ['GEMINI_API_KEY']); models = [m.name for m in genai.list_models() if 'generateContent' in m.supported_generation_methods]; print('\n'.join(models))"
998
- ```
999
 
1000
  2. **Check Free Tier Limits**:
1001
- - Free tier model: `gemini-flash-latest`
1002
- - Paid tier models: `gemini-2.0-flash`, `gemini-2.5-flash`
1003
- - Rate limits: 15 requests/minute, 1M tokens/day
1004
 
1005
  3. **Update Model Name** (if needed):
1006
- - Edit `backend/app/services/llm_service.py`
1007
- - Change model to `gemini-flash-latest`
1008
- - Restart backend
1009
 
1010
  #### Issue: "ProfileMedication object has no attribute 'medication_name'"
1011
 
@@ -1026,16 +1028,16 @@ curl -X POST http://localhost:8000/api/voice/answer \
1026
  ### Example Conversations
1027
 
1028
  **General Health Query**:
1029
- - 👤 User: "What does my health profile show?"
1030
- - 🤖 Assistant: "You're a 49-year-old male with a healthy BMI of 21.5. Your profile shows hypertension, and you're currently taking Lisinopril. You have 5 health reports on file. Is there specific information you'd like me to explain?"
1031
 
1032
  **Lab Result Interpretation**:
1033
- - 👤 User: "Explain my recent cholesterol test"
1034
- - 🤖 Assistant: "Your recent cholesterol panel shows total cholesterol at 195 mg/dL, which is in the desirable range. Your LDL is 110 mg/dL and HDL is 55 mg/dL. Given your hypertension, maintaining these levels is important. Discuss with your doctor about your diet and medication effectiveness."
1035
 
1036
  **Medication Context**:
1037
- - 👤 User: "Why am I taking Lisinopril?"
1038
- - 🤖 Assistant: "Lisinopril is an ACE inhibitor prescribed for managing hypertension, which is noted in your health profile. It helps lower blood pressure by relaxing blood vessels. Always take it as prescribed and discuss any side effects with your healthcare provider."
1039
 
1040
  ---
1041
 
@@ -1043,21 +1045,23 @@ curl -X POST http://localhost:8000/api/voice/answer \
1043
 
1044
  Lumea's **Physics Twin** is a comprehensive digital health analysis engine that transforms raw health metrics from uploaded medical reports into deterministic organ-level health scores and real-time condition detection. It provides an interactive 3D body visualization, time-series telemetry monitoring, and AI-powered health recommendations grounded in your actual medical data.
1045
 
1046
- > ⚠️ **Medical Disclaimer**: The Physics Twin is a visualization and analysis support tool. It does NOT provide medical diagnosis or treatment. Values are computed from extracted lab data and should be verified by healthcare professionals. Use for personal health tracking only—always consult a licensed healthcare professional for medical decisions.
1047
 
1048
  ### Overview
1049
 
1050
  The Physics Twin delivers:
1051
 
1052
- - **🏥 Deterministic Organ Scoring**: Evidence-based scoring for 6 organs (kidney, heart, liver, lungs, brain, blood) based on reference ranges and clinical normalisation
1053
- - **🔬 Real Data Integration**: Pulls metrics directly from your uploaded medical reports (not simulated) via the `Observation` table
1054
- - **📊 Interactive 3D Body**: Clickable organ hotspots with animated scoring visualizations and trend indicators
1055
- - **❤️ Real-Time Telemetry**: Simulated vital signs stream via SSE (Server-Sent Events) with SpO₂, heart rate, blood pressure, stress, and glucose tracking
1056
- - **⚠️ Condition Detection**: 10 rule-based conditions (Hypertension, Tachycardia, Kidney Stress, Liver Stress, Hyperglycemia, Sleep Deprivation, etc.) automatically detected from current metrics
1057
- - **👁️ Body Impact Overlay**: 2D SVG body silhouette highlighting organs affected by detected conditions with severity-based color coding
1058
- - **💊 Smart Recommendations**: Context-aware health recommendations with YouTube educational links for each detected condition
1059
- - **📈 Time-Series History**: View per-report organ scores, historical trends, and scored snapshots over time
1060
- - **🧬 Lifestyle Integration**: Incorporates UserProfile self-reported data (sleep hours, stress level, activity level) into organ scoring
 
 
1061
 
1062
  ### How It Works
1063
 
@@ -1089,50 +1093,50 @@ LAYER 3: Condition Detection & Recommendations
1089
 
1090
  ```mermaid
1091
  graph TB
1092
- subgraph Database["PostgreSQL"]
1093
- Observation["Observation Table<br/>(extracted metrics)"]
1094
- UserProfile["UserProfile Table<br/>(lifestyle data)"]
1095
- Report["Report Table<br/>(source documents)"]
1096
- end
1097
-
1098
- subgraph Backend["Backend Services"]
1099
- PhysicsConfig["physics_config.py<br/>- Organ specs<br/>- Metric weights<br/>- Reference ranges"]
1100
- PhysicsRoute["physics.py Routes<br/>- GET /latest (auto-compute)<br/>- GET /history (per-report)<br/>- POST /metrics (manual)<br/>- GET /config"]
1101
- Scorer["Scoring Engine<br/>- compute_organ_score()<br/>- compute_all_organs()"]
1102
- ConditionEngine["conditions.py<br/>- detect_conditions()<br/>- 10 rule-based conditions<br/>- Severity mapping<br/>- Organ mapping"]
1103
- TelemetryRoute["telemetry.py Routes<br/>- GET /stream (SSE)<br/>- GET /latest<br/>- GET /history"]
1104
- end
1105
-
1106
- subgraph Frontend["Frontend Components"]
1107
- PhysicsTwin["PhysicsTwin.tsx<br/>- Main page orchestrator<br/>- Tab navigation (Twin/Metrics/History)"]
1108
- TwinViewer["TwinViewer.tsx<br/>- 3D body with Three.js<br/>- Organ hotspots<br/>- Auto-rotate"]
1109
- BodyOverlay["BodyImpactOverlay.tsx<br/>- SVG body silhouette<br/>- Animated organ zones<br/>- Gender toggle"]
1110
- OrganTelemetry["OrganTelemetryCard.tsx<br/>- Overall score display<br/>- Selected organ detail<br/>- Metric values"]
1111
- Explainability["ExplainabilityCard.tsx<br/>- Scoring breakdown<br/>- Weight contributions<br/>- Coverage/confidence"]
1112
- Recommendations["RecommendationsCard.tsx<br/>- Detected conditions<br/>- Evidence-based actions<br/>- YouTube links"]
1113
- ConditionsEngine["conditionsEngine.ts<br/>- Client-side condition detection<br/>- Rapid re-evaluation<br/>- Trend computation"]
1114
- TelemetryHook["useTelemetryStream.ts<br/>- SSE connection manager<br/>- Fallback simulation<br/>- History buffer (120 readings)"]
1115
- end
1116
-
1117
- Observation -->|Query last 90 days| PhysicsRoute
1118
- UserProfile -->|Enrich with lifestyle| PhysicsRoute
1119
- Report -->|Group snapshots by| PhysicsRoute
1120
-
1121
- PhysicsRoute -->|Load config| PhysicsConfig
1122
- PhysicsRoute -->|Compute scores| Scorer
1123
- Scorer -->|Detect conditions| ConditionEngine
1124
-
1125
- TelemetryRoute -->|Generate readings| TelemetryHook
1126
- PhysicsRoute -->|Return snapshot| PhysicsTwin
1127
-
1128
- PhysicsTwin -->|Render organs| TwinViewer
1129
- PhysicsTwin -->|Show overlay| BodyOverlay
1130
- PhysicsTwin -->|Display score| OrganTelemetry
1131
- PhysicsTwin -->|Show breakdown| Explainability
1132
- PhysicsTwin -->|Detect conditions| ConditionsEngine
1133
- ConditionsEngine -->|Map to organs| BodyOverlay
1134
- ConditionsEngine -->|Show recommendations| Recommendations
1135
- TelemetryHook -->|Stream vitals| PhysicsTwin
1136
  ```
1137
 
1138
  ### Core Concepts
@@ -1154,15 +1158,15 @@ Each metric is **normalised to 0-1** based on clinical reference ranges:
1154
 
1155
  ```
1156
  If metric_value is in reference_range [ref_min, ref_max]:
1157
- normalised_score = 1.0 (perfect)
1158
 
1159
  Else if metric_value < ref_min:
1160
- normalised_score = (metric_value - abs_min) / (ref_min - abs_min)
1161
- (linear decay to 0 at absolute minimum)
1162
 
1163
  Else if metric_value > ref_max:
1164
- normalised_score = 1.0 - (metric_value - ref_max) / (abs_max - ref_max)
1165
- (linear decay to 0 at absolute maximum)
1166
  ```
1167
 
1168
  **Organ Score** = weighted average of present metrics:
@@ -1174,9 +1178,9 @@ score = Σ(weight_i × normalised_i) / Σ(weight_present) × 100
1174
  **Overall Score** = mean of all organ scores with data
1175
 
1176
  **Status Classification**:
1177
- - **Healthy**: 75-100
1178
- - ⚠️ **Watch**: 50-74
1179
- - 🔴 **Risk**: 0-49
1180
 
1181
  #### Metric Name Alignment
1182
 
@@ -1213,49 +1217,49 @@ This ensures consistent data flow from OCR extraction → database storage → p
1213
  #### Files Modified/Created
1214
 
1215
  1. **`app/services/physics_config.py`**: Organ & metric specifications
1216
- - 6 organ definitions with metric weights
1217
- - Reference ranges & absolute bounds per metric
1218
- - `compute_organ_score()`: Deterministic scoring function
1219
- - `compute_all_organs()`: Multi-organ aggregation
1220
 
1221
  2. **`app/routes/physics.py`**: Smart API endpoints
1222
- - `GET /api/physics/latest`: Auto-compute from real DB data (90-day window)
1223
- - `GET /api/physics/history`: Per-report snapshots
1224
- - `POST /api/physics/metrics`: Manual submission (fallback)
1225
- - `GET /api/physics/config`: Frontend config download
1226
 
1227
  3. **`app/services/conditions.py`**: Rule engine
1228
- - 10 `ConditionRule` definitions
1229
- - `detect_conditions()`: Main evaluation function
1230
- - Threshold checking with severity mapping
1231
- - Organ affection mapping
1232
 
1233
  4. **`app/routes/telemetry.py`**: Simulated SSE stream
1234
- - Baseline vital signs (heart_rate, bp, spo2, glucose, etc.)
1235
- - Per-user simulation state tracking
1236
- - Realistic jitter + periodic spikes
1237
- - `GET /stream` SSE endpoint
1238
 
1239
  #### Data QueryQuery Pattern (physics.py)
1240
 
1241
  ```python
1242
  # Gather last 90 days of metrics for user
1243
  result = await db.execute(
1244
- select(Observation)
1245
- .where(
1246
- and_(
1247
- Observation.user_id == user_id,
1248
- Observation.observed_at >= cutoff # 90 days ago
1249
- )
1250
- )
1251
- .order_by(desc(Observation.observed_at))
1252
  )
1253
 
1254
  # Deduplicate: keep most recent value per metric_name
1255
  metrics = {}
1256
  for obs in sorted_observations:
1257
- if obs.metric_name not in seen:
1258
- metrics[obs.metric_name] = float(obs.value)
1259
  ```
1260
 
1261
  ### Frontend Implementation
@@ -1263,50 +1267,59 @@ for obs in sorted_observations:
1263
  #### Main Components
1264
 
1265
  1. **`PhysicsTwin.tsx`** (616 lines): Page orchestrator
1266
- - Three tabs: Twin View (3D/overlay), Metrics (vitals grid), History (trends + snapshots)
1267
- - Auto-loads latest snapshot on mount
1268
- - Streams real-time telemetry via SSE hook
1269
- - Auto-detects conditions from current metrics
1270
- - Handles organ selection state
1271
 
1272
  2. **`TwinViewer.tsx`** (297 lines): 3D body renderer
1273
- - Uses `@react-three/fiber` + `@react-three/drei` + Three.js
1274
- - Stylized torso + head mesh
1275
- - 6 animated organ orbs with onClick handlers
1276
- - Auto-rotating when no organ selected
1277
- - Smooth camera tween to selected organ
1278
- - Responsive scaling & glow effects
1279
 
1280
  3. **`BodyImpactOverlay.tsx`** (263 lines): 2D condition visualization
1281
- - SVG body silhouette (male/female toggle)
1282
- - 6 organ zones with animated pulsing
1283
- - Severity-based color coding (mild/moderate/severe)
1284
- - Condition pill badges at top
1285
- - Gender-specific body paths
1286
 
1287
  4. **`OrganTelemetryCard.tsx`** (159 lines): Score summary
1288
- - Large animated overall score display
1289
- - Selected organ detail (coverage %, status badge)
1290
- - Per-metric contribution breakdown with visual bars
1291
- - Last updated timestamp
1292
 
1293
  5. **`ExplainabilityCard.tsx`**: Scoring transparency
1294
- - Breakdown table: metric name, raw value, normalised score, weight
1295
- - Contribution % calculation
1296
- - Coverage metric (% of available metrics present)
1297
- - Confidence score (weighted average)
1298
-
1299
- 6. **`RecommendationsCard.tsx`**: Condition actions
1300
- - Dynamic pill badges grouped by severity
1301
- - Evidence-based recommendations for each condition
1302
- - YouTube search links for educational content
1303
- - Expandable/collapsible sections
 
 
 
 
 
 
 
 
1304
 
1305
  #### Supporting Files
1306
 
1307
  - **`physicsApi.ts`**: API client with types for snapshots, organs, conditions
1308
  - **`conditionsEngine.ts`**: Client-side condition detection mirroring backend rules (instant feedback on metric changes)
1309
  - **`useTelemetryStream.ts`**: SSE hook with automatic fallback to simulated vitals if SSE unavailable
 
1310
 
1311
  ### API Endpoints
1312
 
@@ -1327,69 +1340,69 @@ for obs in sorted_observations:
1327
  Response:
1328
  ```json
1329
  {
1330
- "id": "550e8400-e29b-41d4-a716-446655440000_2025-02-06T10:30:00",
1331
- "user_id": "550e8400-e29b-41d4-a716-446655440000",
1332
- "timestamp": "2025-02-06T10:30:00",
1333
- "overall_score": 72.3,
1334
- "overall_status": "Watch",
1335
- "data_source": "reports",
1336
- "raw_metrics": {
1337
- "systolic_bp": 145,
1338
- "diastolic_bp": 88,
1339
- "heart_rate": 78,
1340
- "glucose": 115,
1341
- "alt": 35,
1342
- "creatinine": 1.1,
1343
- "spo2": 97,
1344
- "hemoglobin": 14.2,
1345
- "stress_level": 5.2,
1346
- "sleep_hours": 6.5
1347
- },
1348
- "organs": {
1349
- "heart": {
1350
- "score": 68.5,
1351
- "status": "Watch",
1352
- "coverage": 0.83,
1353
- "contributions": [
1354
- {
1355
- "name": "systolic_bp",
1356
- "value": 145,
1357
- "normalised": 0.75,
1358
- "weight": 0.25,
1359
- "weighted": 0.1875,
1360
- "unit": "mmHg"
1361
- }
1362
- ]
1363
- },
1364
- "kidney": {
1365
- "score": 82.1,
1366
- "status": "Healthy",
1367
- "coverage": 1.0,
1368
- "contributions": []
1369
- }
1370
- },
1371
- "conditions": [
1372
- {
1373
- "id": "hypertension",
1374
- "name": "Hypertension",
1375
- "severity": "moderate",
1376
- "affected_organs": ["heart", "kidney", "brain"],
1377
- "trigger_metrics": { "systolic_bp": 145 },
1378
- "recommendations": [
1379
- "Reduce sodium intake to under 2,300 mg/day",
1380
- "Engage in 30 minutes of moderate exercise daily"
1381
- ],
1382
- "youtube_queries": ["how to lower blood pressure naturally"]
1383
- }
1384
- ],
1385
- "organ_conditions": {
1386
- "heart": ["hypertension"],
1387
- "kidney": ["hypertension"]
1388
- },
1389
- "organ_severities": {
1390
- "heart": "moderate",
1391
- "kidney": "mild"
1392
- }
1393
  }
1394
  ```
1395
 
@@ -1405,75 +1418,84 @@ data: {"timestamp":"2025-02-06T10:30:02Z","metrics":{"heart_rate":76,"systolic_b
1405
  ### User Experience Flow
1406
 
1407
  1. **Page Load**:
1408
- - Auto-fetches `/api/physics/latest` → loads real data from user's reports
1409
- - If user has reports: snapshot shows immediately with organ scores visible
1410
- - If no reports: Shows "No health data found" overlay with option to load demo data
1411
 
1412
  2. **3D Viewer Interaction**:
1413
- - Click any organ → auto-selects, triggers camera tween to that organ
1414
- - OrganTelemetryCard shows score breakdown (e.g., "Heart: 68.5% — Watch")
1415
- - ExplainabilityCard shows metric contributions to selected organ score
1416
- - Each metric bar shows current value vs reference range
1417
 
1418
  3. **Body Overlay Mode**:
1419
- - Click "Impact View" toggle → SVG body appears
1420
- - Detected conditions pulse with color coding (mild=yellow, moderate=orange, severe=red)
1421
- - Each organ zone highlights if affected by a condition
1422
- - Hover shows condition name
1423
 
1424
  4. **Real-Time Metrics Tab**:
1425
- - Vital signs grid shows latest values (from telemetry SSE stream)
1426
- - Trend arrows (↑ up, ↓ down, → stable) based on 10-reading window
1427
- - "Score Metrics" button submits current readings for manual analysis
1428
 
1429
  5. **History Tab**:
1430
- - Line chart of selected metric over last 60 telemetry readings
1431
- - Scored snapshots section lists per-report scores with timestamps
1432
- - Click any snapshot to replay its organ breakdown
1433
 
1434
  6. **Recommendations**:
1435
- - Dynamically updates as conditions detected/cleared
1436
- - Each condition shows severity, affected organs, evidence-based actions
1437
- - YouTube links for patient education (e.g., "DASH diet for hypertension")
1438
 
1439
  ### Key Features Breakdown
1440
 
1441
  #### Real Data, Not Simulation
1442
 
1443
- - `/api/physics/latest` queries actual `Observation` table
1444
- - Uses real extracted metrics from uploaded medical reports
1445
- - Falls back gracefully if user has zero observations (shows empty state)
1446
- - Enriches with self-reported profile data (sleep, stress)
1447
 
1448
  #### Deterministic Scoring
1449
 
1450
- - Reference ranges stored in `physics_config.py`
1451
- - All calculations deterministic (no AI/ML, pure math)
1452
- - Explainable: every score component shown in breakdown
1453
- - Aligned with clinical normalisation standards
1454
 
1455
  #### 3D Interactive Body
1456
 
1457
- - 6 clickable organ hotspots
1458
- - Smooth camera animation on selection
1459
- - Real-time score badges on each organ
1460
- - Auto-rotating when idle
1461
- - Works on mobile and desktop
1462
 
1463
  #### Condition Detection
1464
 
1465
- - 10 rule-based conditions automatically detected
1466
- - Severity mapping (mild ≤ moderate ≤ severe)
1467
- - Organ affection mapping (which organs are affected?)
1468
- - Real-time evaluation as metrics change
1469
- - Client-side + server-side dual detection for instant feedback
1470
 
1471
  #### Educational Context
1472
 
1473
- - Recommendations tailored per condition
1474
- - YouTube search links for each condition
1475
- - Evidence citations (reference ranges explained)
1476
- - Safety disclaimers on all pages
 
 
 
 
 
 
 
 
 
1477
 
1478
  ### Performance & Optimization
1479
 
@@ -1676,31 +1698,31 @@ alembic current
1676
 
1677
  ```mermaid
1678
  erDiagram
1679
- users ||--o{ reports : uploads
1680
- users ||--o{ observations : has
1681
- users ||--o{ health_metrics : has
1682
- users ||--o| user_profiles : has
1683
- users ||--o{ profile_conditions : has
1684
- users ||--o{ profile_medications : takes
1685
- users ||--o{ profile_allergies : has
1686
- users ||--o{ chat_sessions : owns
1687
- users ||--o{ report_ai_summaries : generates
1688
- users ||--o{ report_ai_comparisons : generates
1689
- users ||--o{ user_saved_medicines : saves
1690
- users ||--o{ substitute_queries : searches
1691
- users ||--o{ pharmacy_clicks : clicks
1692
- users ||--o| user_location_consent : grants
1693
-
1694
- user_profiles ||--o{ reminders : has
1695
- reminders ||--o{ reminder_events : logs
1696
-
1697
- reports ||--o{ observations : extracts
1698
- reports ||--o{ report_ai_summaries : summarized_by
1699
-
1700
- chat_sessions ||--o{ chat_messages : contains
1701
-
1702
- user_saved_medicines ||--o{ generic_catalog : references
1703
- substitute_queries ||--o{ generic_catalog : queries
1704
  ```
1705
 
1706
  ### Medicines Database Schema
@@ -1866,21 +1888,21 @@ erDiagram
1866
  Request:
1867
  ```json
1868
  {
1869
- "email": "user@example.com",
1870
- "password": "securepassword"
1871
  }
1872
  ```
1873
 
1874
  Response:
1875
  ```json
1876
  {
1877
- "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
1878
- "user": {
1879
- "id": "550e8400-e29b-41d4-a716-446655440000",
1880
- "email": "user@example.com",
1881
- "full_name": "John Doe",
1882
- "created_at": "2026-01-15T10:30:00Z"
1883
- }
1884
  }
1885
  ```
1886
 
@@ -1889,30 +1911,30 @@ Response:
1889
  Request:
1890
  ```json
1891
  {
1892
- "report_id": "550e8400-e29b-41d4-a716-446655440001",
1893
- "force_regenerate": false
1894
  }
1895
  ```
1896
 
1897
  Response:
1898
  ```json
1899
  {
1900
- "summary_json": {
1901
- "title": "Blood Panel Analysis - January 2026",
1902
- "highlights": {
1903
- "positive": ["Hemoglobin within normal range", "Glucose levels stable"],
1904
- "needs_attention": ["LDL cholesterol slightly elevated"],
1905
- "next_steps": ["Consider dietary changes", "Retest in 3 months"]
1906
- },
1907
- "plain_language_summary": "Your blood panel shows mostly healthy values...",
1908
- "key_findings": [
1909
- {"item": "LDL Cholesterol", "evidence": "142 mg/dL (ref: <100)"}
1910
- ],
1911
- "confidence": 0.85
1912
- },
1913
- "cached": false,
1914
- "generated_at": "2026-02-02T10:30:00Z",
1915
- "model_name": "grok-beta"
1916
  }
1917
  ```
1918
 
@@ -1978,24 +2000,27 @@ pytest tests/test_routes_auth.py
1978
 
1979
  ### Manual QA Checklist
1980
 
1981
- 1. Register a new account
1982
- 2. Login and verify dashboard loads
1983
- 3. Upload a PDF lab report
1984
- 4. Verify OCR extraction completes (WebSocket notification)
1985
- 5. Check extracted metrics appear in reports list
1986
- 6. Verify health index updates on dashboard
1987
- 7. Complete health profile wizard
1988
- 8. Check recommendations generate
1989
- 9. Open AI Summary page, select a report
1990
- 10. Generate AI summary, verify it displays
1991
- 11. Select 2 reports of same type, generate comparison
1992
- 12. Try selecting mixed types, verify warning appears
1993
- 13. Test Medicines feature: search for a medicine (e.g., "Aspirin 500mg")
1994
- 14. Verify substitutes appear with prices and Jan Aushadhi options highlighted
1995
- 15. Test pharmacy search by entering your location
1996
- 16. Verify nearby pharmacies appear with ratings and distance
1997
- 17. Try filtering pharmacies by type (all/jan_aushadhi/generic)
1998
- 18. Save a medicine and verify it appears in saved list
 
 
 
1999
 
2000
  ### Medicines Feature Testing
2001
 
@@ -2018,45 +2043,45 @@ pytest tests/test_routes_medicines.py
2018
  ### Medicines Manual Testing
2019
 
2020
  1. **Test Medicine Normalization**
2021
- ```bash
2022
- curl -X POST http://localhost:8000/api/medicines/normalize \
2023
- -H "Authorization: Bearer <token>" \
2024
- -H "Content-Type: application/json" \
2025
- -d '{"text": "Aspirin 500mg tablet"}'
2026
- ```
2027
- Expected: `NormalizedMedicine` with extracted salt, strength, form
2028
 
2029
  2. **Test Substitute Search (AI-Powered)**
2030
- ```bash
2031
- curl -X POST http://localhost:8000/api/medicines/substitutes/from-text \
2032
- -H "Authorization: Bearer <token>" \
2033
- -H "Content-Type: application/json" \
2034
- -d '{"text": "Crocin 650mg for fever"}'
2035
- ```
2036
- Expected: Ranked list of alternatives with prices
2037
 
2038
  3. **Test Pharmacy Search**
2039
- ```bash
2040
- curl -X GET "http://localhost:8000/api/medicines/pharmacies/nearby?lat=40.7128&lng=-74.0060&radius_m=1000&type=all" \
2041
- -H "Authorization: Bearer <token>"
2042
- ```
2043
- Expected: List of nearby pharmacies with ratings
2044
 
2045
  4. **Test Save Medicine**
2046
- ```bash
2047
- curl -X POST http://localhost:8000/api/medicines/saved \
2048
- -H "Authorization: Bearer <token>" \
2049
- -H "Content-Type: application/json" \
2050
- -d '{
2051
- "brand_name": "Aspirin",
2052
- "salt": "Acetylsalicylic Acid",
2053
- "strength": "500",
2054
- "form": "tablet",
2055
- "release_type": "immediate",
2056
- "notes": "Take with food"
2057
- }'
2058
- ```
2059
- Expected: `UserSavedMedicine` object saved to database
2060
 
2061
  ### Medicines Troubleshooting
2062
 
@@ -2064,58 +2089,58 @@ pytest tests/test_routes_medicines.py
2064
 
2065
  - **Cause**: Medicine not in `generic_catalog` table
2066
  - **Solution**:
2067
- - Verify PMBI/Jan Aushadhi seed data was loaded during migration
2068
- - Check database: `SELECT COUNT(*) FROM generic_catalog;` should show 600K+ records
2069
- - Try searching by generic name instead of brand name (e.g., "Acetylsalicylic Acid" instead of "Aspirin")
2070
- - Check logs for Grok API errors if using free-text search
2071
 
2072
  **Problem: "Google Places API 401" or pharmacy search returns empty**
2073
 
2074
  - **Cause**: `GOOGLE_PLACES_API_KEY` not set or API quota exceeded
2075
  - **Solution**:
2076
- - Verify `GOOGLE_PLACES_API_KEY` is set in `.env`: `echo $GOOGLE_PLACES_API_KEY`
2077
- - Check Google Cloud Console for API key validity and quota limits
2078
- - If quota exceeded, wait 24 hours or upgrade billing
2079
- - During development, feature falls back to mock pharmacy data when API key is missing
2080
- - Mock data is useful for frontend testing without API costs
2081
 
2082
  **Problem: Slow medicine normalization or substitute search**
2083
 
2084
  - **Cause**: Missing database indexes on high-cardinality columns
2085
  - **Solution**:
2086
- - Verify indexes exist on `generic_catalog`: `SELECT indexname FROM pg_indexes WHERE tablename='generic_catalog';`
2087
- - Should see indexes on: `product_name`, `salt`, `user_id`
2088
- - If missing, run: `alembic upgrade head` to apply latest migrations
2089
- - Check database query performance: Enable `EXPLAIN ANALYZE` in PostgreSQL
2090
- - Profile with: `SELECT pg_size_pretty(pg_total_relation_size('generic_catalog'));`
2091
 
2092
  **Problem: Grok API errors when parsing prescription images**
2093
 
2094
  - **Cause**: Invalid image format, API key expired, or API rate limit
2095
  - **Solution**:
2096
- - Verify image is clear and readable (JPG, PNG, TIFF)
2097
- - Test API key directly: `curl -H "Authorization: Bearer $GROK_API_KEY" https://api.x.ai/v1/models`
2098
- - Check Grok API status at https://status.x.ai
2099
- - Review backend logs: `docker logs <backend-container>` or `tail uvicorn.log`
2100
- - If rate-limited, implement request throttling in `medicine_normalizer.py`
2101
 
2102
  **Problem: Pharmacy search cache not updating**
2103
 
2104
  - **Cause**: In-memory cache TTL (1 hour) not expired
2105
  - **Solution**:
2106
- - Manually clear cache by restarting backend service
2107
- - Cache is per-search key (lat/lng/radius): different locations create new cache entries
2108
- - Check cache size in logs: Search for "pharmacy_cache" debug messages
2109
- - To disable caching for testing, set `PharmacyLocator.CACHE_TTL_SECONDS = 0`
2110
 
2111
  **Problem: User saved medicines not persisting**
2112
 
2113
  - **Cause**: Database migration not applied or user_id foreign key constraint
2114
  - **Solution**:
2115
- - Verify `user_saved_medicines` table exists: `\dt user_saved_medicines` in psql
2116
- - Run migrations: `cd backend && alembic upgrade head`
2117
- - Verify user exists and authenticated: Check JWT token contains valid `sub` (user_id)
2118
- - Check database logs for constraint errors: `docker logs <postgres-container>`
2119
  ---
2120
 
2121
  ## Deployment
 
1
+ # Lumea Health Platform
2
 
3
  A full-stack health companion platform for preventive health management. Upload medical reports, extract health metrics via OCR, track trends, and receive AI-powered health recommendations.
4
 
5
+ > **Disclaimer**: This platform is a support tool for personal health tracking. It does not provide medical advice, diagnosis, or treatment. Always consult a licensed healthcare professional for medical decisions.
6
 
7
  ---
8
 
 
38
  - **Receive AI recommendations** based on extracted lab values and health patterns powered by OpenRouter (Claude Sonnet 4.5)
39
  - **Compare reports over time** with AI-powered summaries and trend analysis
40
  - **Chat with an AI assistant** grounded in your personal health data
41
+ - **Memory & Knowledge Graph**: Persistent health memory with Mem0 and interconnected insights via Neo4j/Graphiti for temporal reasoning and contradiction detection
42
+ - **Voice Agent**: Speak naturally to an AI health assistant for hands-free access to personalized health insights
43
+ - **Physics Twin**: Interactive 2D organ simulation with real-time telemetry, condition detection, and evidence-based health recommendations powered by your actual medical data
44
 
45
  ---
46
 
 
68
 
69
  ```mermaid
70
  flowchart TB
71
+ subgraph Client["Frontend (React)"]
72
+ UI[React UI]
73
+ WS[WebSocket Client]
74
+ end
75
+
76
+ subgraph API["Backend (FastAPI)"]
77
+ Auth[Auth Routes]
78
+ Dashboard[Dashboard Routes]
79
+ Reports[Reports Routes]
80
+ Profile[Profile Routes]
81
+ Recommendations[Recommendations Routes]
82
+ AISummary[AI Summary Routes]
83
+ Assistant[Assistant Routes]
84
+ WSServer[WebSocket Server]
85
+ end
86
+
87
+ subgraph Services["Backend Services"]
88
+ OCR[PDF/OCR Extractor]
89
+ Classifier[Document Classifier]
90
+ MetricExtractor[Metric Extractor]
91
+ MetricsService[Metrics Service]
92
+ RecommendationEngine[Recommendation Engine]
93
+ AISummaryService[AI Summary Service]
94
+ RAG[RAG Service]
95
+ LLM[LLM Service]
96
+ end
97
+
98
+ subgraph External["External Services"]
99
+ GrokAPI[Grok API]
100
+ Ollama[Ollama / MedGemma]
101
+ Gemini[Gemini API]
102
+ end
103
+
104
+ subgraph Storage["Data Layer"]
105
+ DB[(PostgreSQL / Neon)]
106
+ ChromaDB[(ChromaDB Vector Store)]
107
+ FileStorage[File Storage]
108
+ end
109
+
110
+ UI --> Auth
111
+ UI --> Dashboard
112
+ UI --> Reports
113
+ UI --> Profile
114
+ UI --> Recommendations
115
+ UI --> AISummary
116
+ UI --> Assistant
117
+ WS <--> WSServer
118
+
119
+ Reports --> OCR
120
+ OCR --> Classifier
121
+ Classifier --> MetricExtractor
122
+ MetricExtractor --> MetricsService
123
+ MetricsService --> RecommendationEngine
124
+
125
+ AISummary --> AISummaryService
126
+ AISummaryService --> GrokAPI
127
+ Assistant --> RAG
128
+ RAG --> ChromaDB
129
+ RAG --> LLM
130
+ LLM --> Ollama
131
+ LLM --> Gemini
132
+
133
+ Auth --> DB
134
+ Dashboard --> DB
135
+ Reports --> DB
136
+ Reports --> FileStorage
137
+ Profile --> DB
138
+ Recommendations --> DB
139
+ AISummary --> DB
140
  ```
141
 
142
  ### Data Flow: Report Upload to Health Index
143
 
144
  ```mermaid
145
  sequenceDiagram
146
+ participant User
147
+ participant Frontend
148
+ participant Backend
149
+ participant OCR
150
+ participant Classifier
151
+ participant Extractor
152
+ participant DB
153
+ participant WebSocket
154
+
155
+ User->>Frontend: Upload PDF/Image
156
+ Frontend->>Backend: POST /api/reports/upload
157
+ Backend->>DB: Create report (status: uploaded)
158
+ Backend-->>Frontend: {id, status: uploaded}
159
+
160
+ Backend->>OCR: Extract text (background)
161
+ OCR-->>Backend: Raw text
162
+ Backend->>Classifier: Classify document
163
+ Classifier-->>Backend: Category + doc_type
164
+ Backend->>Extractor: Extract metrics
165
+ Extractor-->>Backend: Observations[]
166
+ Backend->>DB: Save observations + update report
167
+ Backend->>DB: Recompute health index
168
+ Backend->>WebSocket: emit report_parsed
169
+ WebSocket-->>Frontend: report_parsed event
170
+ Frontend->>Frontend: Refresh UI
171
  ```
172
 
173
  ---
 
182
  - **Condition Detection**: 10 rule-based health conditions auto-detected with severity mapping
183
  - **Body Impact Overlay**: 2D SVG visualization showing organs affected by detected conditions
184
  - **Smart Recommendations**: Evidence-based health actions with YouTube educational links per condition
185
+ - **AI Video Recommendations**: OpenRouter-powered personalized health video suggestions for each organ based on abnormal metrics, with per-organ caching
186
+ - **Explainability Cards**: Side-by-side "Improve Your Health" and "Why This Score?" panels showing video recommendations and scoring breakdowns
187
  - **Time-Series History**: View per-report organ scores and historical trends
188
  - **Lifestyle Integration**: Incorporates self-reported profile data (sleep hours, stress level)
189
 
 
198
  - Tracks conditions, symptoms, medications, supplements, allergies
199
  - Family medical history and genetic test results
200
  - **Once completed, users are never re-asked** – profile status persists in DB
201
+ - "Profile Complete" indicator with quick-edit access via Settings page
202
  - **Real-time SMS reminders** via Twilio (or mock mode for testing)
203
  - Background scheduler processes due reminders every 60 seconds
204
  - Default reminders auto-generated: medication, appointment, checkup, hydration
 
226
  - Citations from reports and observations
227
  - WebSocket streaming for real-time responses
228
 
229
+ ### Voice Agent (AI Health Assistant)
230
  - **Natural voice conversations** with your health data
231
  - **Speech-to-Text**: Browser Web Speech API for hands-free input
232
  - **AI Processing**: Google Gemini with personalized health context
 
253
 
254
  ```mermaid
255
  flowchart TB
256
+ subgraph Input["Health Data Sources"]
257
+ Profile[Health Profile]
258
+ Reports[Medical Reports]
259
+ Questionnaire[User Questionnaires]
260
+ Assistant[AI Assistant Chats]
261
+ end
262
+
263
+ subgraph Memory["Mem0 Memory Layer"]
264
+ Extraction[Fact Extraction<br/>via Groq LLM]
265
+ Storage[ChromaDB<br/>Vector Store]
266
+ Retrieval[Semantic Search]
267
+ end
268
+
269
+ subgraph Graph["Graphiti Knowledge Graph"]
270
+ Entities[Entity Nodes<br/>conditions, metrics, meds]
271
+ Relationships[Temporal Relationships<br/>affects, caused_by, treats]
272
+ Episodes[Health Episodes<br/>time-stamped events]
273
+ Insights[AI-Generated Insights<br/>contradictions, patterns]
274
+ end
275
+
276
+ subgraph DB["Neo4j Graph Database"]
277
+ Nodes[Entity Nodes]
278
+ Edges[Relationship Edges]
279
+ Indexes[Vector Indexes<br/>Ollama embeddings]
280
+ end
281
+
282
+ Input --> Extraction
283
+ Extraction --> Storage
284
+ Storage --> Retrieval
285
+
286
+ Input --> Entities
287
+ Entities --> Relationships
288
+ Relationships --> Episodes
289
+ Episodes --> Insights
290
+
291
+ Entities --> Nodes
292
+ Relationships --> Edges
293
+ Edges --> Indexes
294
+
295
+ Retrieval -.->|Enriches| Recommendations[AI Recommendations]
296
+ Insights -.->|Powers| Dashboard[Features Dashboard]
297
  ```
298
 
299
  ### Key Features
 
301
  #### 1. **Persistent Memory (Mem0)**
302
 
303
  - **Automatic Fact Extraction**: Converts natural language health data into structured facts
304
+ - "Sleep is about 5 hours per night" → Extracted memory fact
305
+ - "Has condition High Blood Pressure" → Indexed and searchable
306
+ - "BMI is 25.2 which falls in overweight range" → Computed and stored
307
 
308
  - **User-Scoped Storage**: Each user's memories are isolated and secure
309
  - **Vector-Based Retrieval**: Semantic search powered by local Ollama embeddings (nomic-embed-text)
 
312
  #### 2. **Knowledge Graph (Graphiti + Neo4j)**
313
 
314
  - **Entity Relationship Mapping**:
315
+ - Conditions linked with symptoms, medications, and risk factors
316
+ - Lab values connected to reference ranges and health implications
317
+ - Lifestyle factors (sleep, diet, exercise) linked to health outcomes
318
 
319
  - **Temporal Reasoning**:
320
+ - Track health changes over time (BP trend: 140/90 in Jan → 130/85 in Feb)
321
+ - Episode creation for significant health events
322
+ - Valid/expired relationships based on temporal context
323
 
324
  - **Contradiction Detection**:
325
+ - Identifies conflicting health data (e.g., "no allergies" vs. "allergic to dust")
326
+ - Highlights diagnostic uncertainties
327
+ - Flags medication interactions
328
 
329
  - **Graph-Powered Insights**:
330
+ - **Temporal Patterns**: "BP decreased after starting medication X"
331
+ - **Key Relationships**: "High BMI correlates with elevated cholesterol"
332
+ - **Contradictions**: "Sleep reported as both 5hrs and 8hrs - needs clarification"
333
 
334
  #### 3. **Interactive Visualization**
335
 
336
  **Features Dashboard** (`/features`) provides:
337
 
338
  - **Memory Dashboard**:
339
+ - View all extracted health facts
340
+ - Search memories semantically
341
+ - Add/delete facts manually
342
+ - Real-time sync status
343
 
344
  - **Knowledge Graph Viewer**:
345
+ - Interactive node-edge visualization
346
+ - Filter by conditions, medications, metrics
347
+ - Zoom/pan for detailed exploration
348
+ - Click nodes/edges for metadata
349
 
350
  - **AI Insights Panel**:
351
+ - Temporal insights (trend analysis)
352
+ - Relationship insights (correlation patterns)
353
+ - Contradiction insights (data conflicts)
354
+ - Each insight includes evidence and confidence score
355
 
356
  ### Technical Implementation
357
 
 
426
 
427
  Lumea includes a comprehensive medicine management system that helps users find affordable generic alternatives to prescribed medicines and locate nearby pharmacies, including government-sponsored Jan Aushadhi Kendras offering subsidized medications.
428
 
429
+ > **Medical Disclaimer**: This feature is a support tool for informational purposes only. It does NOT provide medical advice. Always consult your doctor or pharmacist before switching medicines or starting new treatments.
430
 
431
  ### Overview
432
 
 
442
  ### User Flow
443
 
444
  1. **Input Medicine Information**
445
+ - User enters brand name (e.g., "Aspirin 500mg") or uploads prescription image
446
+ - System extracts text via OCR or Grok AI parsing
447
 
448
  2. **Normalize Medicine Data**
449
+ - `MedicineNormalizer` service extracts: salt, strength, form (tablet/capsule/liquid), release type (immediate/sustained)
450
+ - Queries `GenericCatalog` table to validate and standardize extraction
451
 
452
  3. **Find Substitutes**
453
+ - `SubstituteFinder` queries database for alternatives with 4-tier ranked matching:
454
+ - **Tier 1** (1.0 score): Exact match on salt + strength + form + release type
455
+ - **Tier 2** (0.8 score): Same salt + strength + form
456
+ - **Tier 3** (0.6 score): Same salt + strength
457
+ - **Tier 4** (0.4 score): Same salt only
458
+ - Results sorted by price (Jan Aushadhi first for lowest cost)
459
 
460
  4. **Locate Pharmacies** (Optional)
461
+ - User enters location (latitude/longitude) and search radius
462
+ - `PharmacyLocator` queries Google Places API for pharmacies
463
+ - Results include Jan Aushadhi Kendras (government pharmacies) and private pharmacies
464
+ - Results cached for 1 hour to reduce API calls
465
 
466
  5. **Save & Track**
467
+ - User can save medicines to personal list with notes
468
+ - Each save creates `UserSavedMedicine` entry for quick reference
469
 
470
  ### Architecture Diagram
471
 
472
  ```mermaid
473
  flowchart TB
474
+ subgraph Frontend["Frontend (React)"]
475
+ MedicinesPage["Medicines.tsx Page"]
476
+ SubstitutePanel["Substitute Finder Panel"]
477
+ PharmacyPanel["Pharmacy Locator Panel"]
478
+ end
479
+
480
+ subgraph API["Backend API (FastAPI)"]
481
+ NormalizeRoute["POST /normalize<br/>POST /normalize/batch"]
482
+ SubstituteRoute["POST /substitutes<br/>POST /substitutes/from-text"]
483
+ PharmacyRoute["GET /pharmacies/nearby<br/>GET /pharmacies/{id}<br/>POST /pharmacies/{id}/click"]
484
+ SavedRoute["POST /saved<br/>GET /saved<br/>DELETE /saved/{id}"]
485
+ end
486
+
487
+ subgraph Services["Backend Services"]
488
+ MedicineNormalizer["MedicineNormalizer<br/>- normalize()<br/>- normalize_batch()"]
489
+ SubstituteFinder["SubstituteFinder<br/>- find_substitutes()<br/>- save_medicine()"]
490
+ PharmacyLocator["PharmacyLocator<br/>- search_nearby()<br/>- get_place_details()<br/>- 1hr cache"]
491
+ GrokService["GrokMedicineService<br/>- get_alternatives_for_text()"]
492
+ end
493
+
494
+ subgraph External["External Services"]
495
+ GooglePlaces["Google Places API<br/>(Pharmacy Search)"]
496
+ GrokAPI["Grok xAI API<br/>(Medicine Parsing)"]
497
+ end
498
+
499
+ subgraph Storage["Data Layer"]
500
+ DB[(PostgreSQL)]
501
+ GenericCatalog["GenericCatalog Table<br/>(600K+ medicines)"]
502
+ UserSavedMedicine["UserSavedMedicine Table"]
503
+ SubstituteQuery["SubstituteQuery Table<br/>(Analytics)"]
504
+ PharmacyClick["PharmacyClick Table<br/>(Analytics)"]
505
+ end
506
+
507
+ MedicinesPage --> SubstitutePanel
508
+ MedicinesPage --> PharmacyPanel
509
+ SubstitutePanel --> SubstituteRoute
510
+ PharmacyPanel --> PharmacyRoute
511
+ PharmacyPanel --> SavedRoute
512
+
513
+ SubstituteRoute --> GrokService
514
+ SubstituteRoute --> MedicineNormalizer
515
+ SubstituteRoute --> SubstituteFinder
516
+ PharmacyRoute --> PharmacyLocator
517
+
518
+ MedicineNormalizer --> DB
519
+ MedicineNormalizer --> GenericCatalog
520
+ SubstituteFinder --> DB
521
+ SubstituteFinder --> GenericCatalog
522
+ SubstituteFinder --> SubstituteQuery
523
+ PharmacyLocator --> GooglePlaces
524
+ PharmacyLocator --> PharmacyClick
525
+
526
+ GrokService --> GrokAPI
527
+ GrokAPI -->|Parsed results| SubstituteFinder
528
  ```
529
 
530
  ### API Endpoints
 
549
  Request:
550
  ```json
551
  {
552
+ "text": "Aspirin 500mg tablets"
553
  }
554
  ```
555
 
556
  Response:
557
  ```json
558
  {
559
+ "original_text": "Aspirin 500mg tablets",
560
+ "normalized": {
561
+ "brand_name": "Aspirin",
562
+ "salt": "Acetylsalicylic Acid",
563
+ "strength": "500",
564
+ "form": "tablet",
565
+ "release_type": "immediate",
566
+ "confidence": 0.95
567
+ },
568
+ "substitutes": [
569
+ {
570
+ "rank": 1,
571
+ "product_name": "Jan Aushadhi Aspirin 500mg",
572
+ "salt": "Acetylsalicylic Acid",
573
+ "strength": "500",
574
+ "form": "tablet",
575
+ "mrp": "5.00",
576
+ "is_jan_aushadhi": true,
577
+ "match_score": 1.0,
578
+ "match_reason": "Exact match: Same salt, strength, form, and type"
579
+ },
580
+ {
581
+ "rank": 2,
582
+ "product_name": "Ecosprin 500mg",
583
+ "salt": "Acetylsalicylic Acid",
584
+ "strength": "500",
585
+ "form": "tablet",
586
+ "mrp": "8.50",
587
+ "is_jan_aushadhi": false,
588
+ "match_score": 1.0,
589
+ "match_reason": "Exact match: Same salt, strength, form, and type"
590
+ }
591
+ ],
592
+ "disclaimer": "Always confirm with your doctor or pharmacist before switching medicines"
593
  }
594
  ```
595
 
 
603
  Response:
604
  ```json
605
  {
606
+ "pharmacies": [
607
+ {
608
+ "place_id": "ChIJN1blbgBQwokRzKgy6E_B_1Q",
609
+ "name": "Jan Aushadhi Kendra - Downtown",
610
+ "address": "123 Main St, New York, NY 10001",
611
+ "latitude": 40.7128,
612
+ "longitude": -74.0060,
613
+ "rating": 4.7,
614
+ "is_open": true,
615
+ "is_jan_aushadhi": true,
616
+ "phone": "+1-212-555-0123"
617
+ },
618
+ {
619
+ "place_id": "ChIJrc_p_1BQwokRzKgy6E_B_2Q",
620
+ "name": "Metro Pharmacy",
621
+ "address": "456 Broadway, New York, NY 10002",
622
+ "latitude": 40.7150,
623
+ "longitude": -74.0030,
624
+ "rating": 4.2,
625
+ "is_open": true,
626
+ "is_jan_aushadhi": false,
627
+ "phone": "+1-212-555-0456"
628
+ }
629
+ ],
630
+ "next_page_token": null,
631
+ "total_results": 2
632
  }
633
  ```
634
 
 
638
 
639
  Lumea includes a fully integrated **Voice Agent** that provides an ElevenLabs-style conversational health assistant experience. Users can speak naturally to the AI agent, which responds with personalized health guidance based on their profile, conditions, medications, allergies, and recent lab reports.
640
 
641
+ > **Medical Disclaimer**: The Voice Agent is an informational support tool. It does NOT provide medical diagnosis, dosage recommendations, or emergency care. Always consult licensed healthcare professionals for medical decisions.
642
 
643
  ### Overview
644
 
645
+ The Voice Agent enables users to interact with their health data through natural voice conversations powered by AI.
646
 
647
  **Key Capabilities**:
648
 
649
+ - **Voice Interaction**: Speak naturally using browser-based speech recognition (Web Speech API)
650
+ - **Personalized AI Responses**: Powered by Google Gemini with access to your complete health profile
651
+ - **Natural Speech Output**: Voice responses via ElevenLabs TTS (with browser fallback)
652
+ - **Medical Safety Filters**: Detects emergency keywords, dosage inquiries, and provides appropriate safety responses
653
+ - **Real-time Feedback**: Visual orb animations reflect listening/thinking/speaking states
654
+ - **Transcript View**: Optional chat-mode panel displays conversation history
655
+ - **Context-Aware**: Accesses your health conditions, medications, allergies, recent reports, and RAG-indexed data
656
 
657
  ### How It Works
658
 
659
+ The Voice Agent uses a three-stage pipeline to deliver personalized health conversations:
660
 
661
  ```mermaid
662
  graph LR
663
+ A[User Speaks] --> B[Browser STT<br/>Web Speech API]
664
+ B --> C[LLM Processing<br/>Google Gemini]
665
+ C --> D[Health Context<br/>Profile + Reports + RAG]
666
+ D --> C
667
+ C --> E[Text-to-Speech<br/>ElevenLabs API]
668
+ E --> F[Browser Fallback<br/>Web Speech API]
669
+ E --> G[User Hears Response]
670
+ F --> G
671
  ```
672
 
673
  #### Stage 1: Speech-to-Text (STT)
 
679
  #### Stage 2: AI Processing & Context Retrieval
680
  - **LLM Provider**: Google Gemini API (`gemini-flash-latest`)
681
  - **Health Context Integration**:
682
+ - User demographics (age, gender, BMI)
683
+ - Active medical conditions
684
+ - Current medications (name, dose, frequency)
685
+ - Known allergies and their severity
686
+ - Recent lab reports and observations
687
+ - Lifestyle factors (sleep, exercise, smoking, alcohol)
688
  - **RAG Enhancement**: Queries ChromaDB vector store for relevant historical health data
689
  - **Safety Layer**: HIPAA-compliant system prompt with strict medical guidelines:
690
+ - Never diagnose conditions
691
+ - Never recommend medication dosages
692
+ - Always suggest professional consultation
693
+ - Detect emergency situations and direct to 911
694
 
695
  #### Stage 3: Text-to-Speech (TTS)
696
  - **Primary**: ElevenLabs TTS API
697
+ - Model: `eleven_turbo_v2_5` (optimized for speed, free tier compatible)
698
+ - Voice: Rachel (`21m00Tcm4TlvDq8ikWAM`)
699
+ - Quality: Natural voice synthesis
700
  - **Fallback**: Browser Text-to-Speech API
701
+ - Activates automatically if ElevenLabs unavailable (503 errors)
702
+ - Uses system voices
703
+ - Zero latency, no API costs
704
 
705
  ### Real-World Benefits
706
 
707
  **For Everyday Users**:
708
+ - **Hands-Free Access**: Check health info while cooking, exercising, or commuting
709
+ - **Natural Conversations**: Ask questions in plain language, no medical jargon required
710
+ - **Accessibility**: Ideal for users with visual impairments or reading difficulties
711
+ - **Quick Insights**: Faster than navigating multiple screens and charts
712
 
713
  **For Health-Conscious Individuals**:
714
+ - **Report Interpretation**: "What do my cholesterol levels mean?"
715
+ - **Medication Context**: "Why am I taking this medication?"
716
+ - **Trend Analysis**: "How has my blood pressure changed over time?"
717
+ - **Personalized Guidance**: Responses tailored to your specific health profile
718
 
719
  **Safety & Reliability**:
720
+ - **Emergency Detection**: Recognizes crisis keywords (chest pain, can't breathe, etc.)
721
+ - **Dosage Protection**: Refuses to recommend medication changes
722
+ - **HIPAA Guidelines**: All responses include disclaimers and professional consultation advice
723
+ - **Fallback Mechanisms**: Graceful degradation if AI/TTS services unavailable
724
 
725
  ### API Endpoints
726
 
727
  | Method | Endpoint | Description | Auth | Response |
728
  |--------|----------|-------------|------|----------|
729
+ | GET | `/api/voice/context` | Get user health context summary with profile completeness status | Yes | `{profile_complete: bool, summary: {...}, has_personalization: bool}` |
730
+ | POST | `/api/voice/answer` | Generate personalized AI answer using Gemini + health context | Yes | `{answer_text: string, flags: [string], used_context: {...}}` |
731
+ | POST | `/api/voice/tts` | Convert text to speech using ElevenLabs API | Yes | Binary audio stream (MP3) |
732
+ | GET | `/api/voice/tts/status` | Check ElevenLabs TTS configuration status | Yes | `{configured: bool, voice_id: string, runtime_check: {...}}` |
733
 
734
  **Context Response Details**:
735
  ```json
736
  {
737
+ "profile_complete": true,
738
+ "has_personalization": true,
739
+ "summary": {
740
+ "name": "Darshan Ved",
741
+ "age": 49,
742
+ "gender": "Male",
743
+ "bmi": 21.5,
744
+ "conditions": ["Hypertension"],
745
+ "medications": ["Lisinopril 10mg"],
746
+ "allergies": ["Penicillin"],
747
+ "reports_count": 5,
748
+ "sleep_hours": 7,
749
+ "exercise_frequency": "3-4 times/week"
750
+ }
751
  }
752
  ```
753
 
 
766
  # ===== AI/LLM Configuration =====
767
  # Google Gemini API (Primary LLM for Voice Agent)
768
  USE_GEMINI_FALLBACK=true
769
+ GEMINI_API_KEY=AIzaSy... # Get from https://aistudio.google.com/apikey
770
 
771
  # ===== Text-to-Speech Configuration =====
772
  # ElevenLabs TTS for Voice Agent (Optional - has browser fallback)
773
+ ELEVENLABS_API_KEY=sk_... # Get from https://elevenlabs.io/
774
+ ELEVENLABS_VOICE_ID=21m00Tcm4TlvDq8ikWAM # Rachel voice (recommended)
775
  ```
776
 
777
  #### Docker Configuration
 
780
 
781
  ```yaml
782
  services:
783
+ backend:
784
+ environment:
785
+ # ... other env vars ...
786
+ - GEMINI_API_KEY=${GEMINI_API_KEY:-}
787
+ - USE_GEMINI_FALLBACK=${USE_GEMINI_FALLBACK:-true}
788
+ - ELEVENLABS_API_KEY=${ELEVENLABS_API_KEY:-}
789
+ - ELEVENLABS_VOICE_ID=${ELEVENLABS_VOICE_ID:-21m00Tcm4TlvDq8ikWAM}
790
  ```
791
 
792
  #### Getting API Keys
 
805
  3. Create new API key (starts with `sk_...`)
806
  4. Free tier includes: 10,000 characters/month
807
  5. Recommended voice IDs:
808
+ - `21m00Tcm4TlvDq8ikWAM` - Rachel (conversational, female)
809
+ - `ErXwobaYiN019PkySvjV` - Antoni (clear, male)
810
+ - `EXAVITQu4vr4xnSDxMaL` - Bella (warm, female)
811
 
812
  **Free Tier Model Compatibility**:
813
+ - Works: `gemini-flash-latest`, `gemini-1.5-flash-8b`
814
+ - Quota exceeded: `gemini-2.0-flash`, `gemini-2.5-flash` (requires paid tier)
815
 
816
  ### Technology Stack
817
 
 
849
 
850
  ```bash
851
  curl -X GET http://localhost:8000/api/voice/tts/status \
852
+ -H "Authorization: Bearer <your_jwt_token>"
853
  ```
854
 
855
  **Expected Response**:
856
  ```json
857
  {
858
+ "configured": true,
859
+ "voice_id": "21m00Tcm4TlvDq8ikWAM",
860
+ "runtime_check": {
861
+ "api_key_loaded": true,
862
+ "api_key_prefix": "sk_d78...",
863
+ "voice_id": "21m00Tcm4TlvDq8ikWAM"
864
+ }
865
  }
866
  ```
867
 
 
869
 
870
  ```bash
871
  curl -X GET http://localhost:8000/api/voice/context \
872
+ -H "Authorization: Bearer <your_jwt_token>"
873
  ```
874
 
875
  **Expected Response**:
876
  ```json
877
  {
878
+ "profile_complete": true,
879
+ "has_personalization": true,
880
+ "summary": {
881
+ "name": "Darshan Ved",
882
+ "age": 49,
883
+ "gender": "Male",
884
+ "bmi": 21.5,
885
+ "conditions": ["Hypertension"],
886
+ "medications": ["Lisinopril"],
887
+ "allergies": ["Penicillin"],
888
+ "reports_count": 5
889
+ }
890
  }
891
  ```
892
 
 
894
 
895
  ```bash
896
  curl -X POST http://localhost:8000/api/voice/answer \
897
+ -H "Authorization: Bearer <your_jwt_token>" \
898
+ -H "Content-Type: application/json" \
899
+ -d '{
900
+ "text": "What do my cholesterol levels mean?"
901
+ }'
902
  ```
903
 
904
  **Expected Response**:
905
  ```json
906
  {
907
+ "answer_text": "Based on your recent lab reports, your total cholesterol is 195 mg/dL, which falls within the desirable range (below 200 mg/dL). Your LDL cholesterol is 110 mg/dL, also in the optimal range. However, I recommend discussing these results with your healthcare provider for personalized guidance based on your hypertension condition.",
908
+ "flags": [],
909
+ "used_context": {
910
+ "has_profile": true,
911
+ "conditions_count": 1,
912
+ "medications_count": 1,
913
+ "has_rag_context": true
914
+ }
915
  }
916
  ```
917
 
 
919
 
920
  ```bash
921
  curl -X POST http://localhost:8000/api/voice/tts \
922
+ -H "Authorization: Bearer <your_jwt_token>" \
923
+ -H "Content-Type: application/json" \
924
+ -d '{"text": "Hello! Your health data shows positive trends."}' \
925
+ --output test_voice.mp3
926
 
927
  # Play the audio file
928
  # Windows: start test_voice.mp3
 
935
  **Emergency Detection Test**:
936
  ```bash
937
  curl -X POST http://localhost:8000/api/voice/answer \
938
+ -H "Authorization: Bearer <your_jwt_token>" \
939
+ -H "Content-Type: application/json" \
940
+ -d '{"text": "I am having severe chest pain"}'
941
  ```
942
 
943
  **Expected Response**:
944
  ```json
945
  {
946
+ "answer_text": "I'm detecting words that suggest this might be an emergency situation. Please call emergency services immediately (911) or go to the nearest emergency room...",
947
+ "flags": ["emergency"],
948
+ "used_context": {}
949
  }
950
  ```
951
 
952
  **Dosage Inquiry Test**:
953
  ```bash
954
  curl -X POST http://localhost:8000/api/voice/answer \
955
+ -H "Authorization: Bearer <your_jwt_token>" \
956
+ -H "Content-Type: application/json" \
957
+ -d '{"text": "Can I double my medication dosage?"}'
958
  ```
959
 
960
  **Expected Response**:
961
  ```json
962
  {
963
+ "answer_text": "I cannot provide specific dosage recommendations. Please consult your doctor or pharmacist before making any changes to your medications...",
964
+ "flags": ["dosage_inquiry"],
965
+ "used_context": {...}
966
  }
967
  ```
968
 
 
974
 
975
  **Solutions**:
976
  1. **Check API Key Loading**:
977
+ ```bash
978
+ docker exec ggw-backend python -c "from app.settings import settings; print(f'API Key: {settings.ELEVENLABS_API_KEY[:20] if settings.ELEVENLABS_API_KEY else None}...')"
979
+ ```
980
 
981
  2. **Verify Environment Variables**:
982
+ - Ensure `ELEVENLABS_API_KEY` is in root `.env` file
983
+ - Ensure `docker-compose.yml` includes the env var mapping
984
+ - Restart Docker containers: `docker compose restart backend`
985
 
986
  3. **Check API Key Validity**:
987
+ ```bash
988
+ curl https://api.elevenlabs.io/v1/voices \
989
+ -H "xi-api-key: sk_your_key_here"
990
+ ```
991
 
992
  #### Issue: Gemini API 404 or 429 Errors
993
 
 
995
 
996
  **Solutions**:
997
  1. **Verify Model Compatibility**:
998
+ ```bash
999
+ docker exec ggw-backend python -c "import google.generativeai as genai; import os; genai.configure(api_key=os.environ['GEMINI_API_KEY']); models = [m.name for m in genai.list_models() if 'generateContent' in m.supported_generation_methods]; print('\n'.join(models))"
1000
+ ```
1001
 
1002
  2. **Check Free Tier Limits**:
1003
+ - Free tier model: `gemini-flash-latest`
1004
+ - Paid tier models: `gemini-2.0-flash`, `gemini-2.5-flash`
1005
+ - Rate limits: 15 requests/minute, 1M tokens/day
1006
 
1007
  3. **Update Model Name** (if needed):
1008
+ - Edit `backend/app/services/llm_service.py`
1009
+ - Change model to `gemini-flash-latest`
1010
+ - Restart backend
1011
 
1012
  #### Issue: "ProfileMedication object has no attribute 'medication_name'"
1013
 
 
1028
  ### Example Conversations
1029
 
1030
  **General Health Query**:
1031
+ - User: "What does my health profile show?"
1032
+ - Assistant: "You're a 49-year-old male with a healthy BMI of 21.5. Your profile shows hypertension, and you're currently taking Lisinopril. You have 5 health reports on file. Is there specific information you'd like me to explain?"
1033
 
1034
  **Lab Result Interpretation**:
1035
+ - User: "Explain my recent cholesterol test"
1036
+ - Assistant: "Your recent cholesterol panel shows total cholesterol at 195 mg/dL, which is in the desirable range. Your LDL is 110 mg/dL and HDL is 55 mg/dL. Given your hypertension, maintaining these levels is important. Discuss with your doctor about your diet and medication effectiveness."
1037
 
1038
  **Medication Context**:
1039
+ - User: "Why am I taking Lisinopril?"
1040
+ - Assistant: "Lisinopril is an ACE inhibitor prescribed for managing hypertension, which is noted in your health profile. It helps lower blood pressure by relaxing blood vessels. Always take it as prescribed and discuss any side effects with your healthcare provider."
1041
 
1042
  ---
1043
 
 
1045
 
1046
  Lumea's **Physics Twin** is a comprehensive digital health analysis engine that transforms raw health metrics from uploaded medical reports into deterministic organ-level health scores and real-time condition detection. It provides an interactive 3D body visualization, time-series telemetry monitoring, and AI-powered health recommendations grounded in your actual medical data.
1047
 
1048
+ > **Medical Disclaimer**: The Physics Twin is a visualization and analysis support tool. It does NOT provide medical diagnosis or treatment. Values are computed from extracted lab data and should be verified by healthcare professionals. Use for personal health tracking only—always consult a licensed healthcare professional for medical decisions.
1049
 
1050
  ### Overview
1051
 
1052
  The Physics Twin delivers:
1053
 
1054
+ - **Deterministic Organ Scoring**: Evidence-based scoring for 6 organs (kidney, heart, liver, lungs, brain, blood) based on reference ranges and clinical normalisation
1055
+ - **Real Data Integration**: Pulls metrics directly from your uploaded medical reports (not simulated) via the `Observation` table
1056
+ - **Interactive 3D Body**: Clickable organ hotspots with animated scoring visualizations and trend indicators
1057
+ - **Real-Time Telemetry**: Simulated vital signs stream via SSE (Server-Sent Events) with SpO2, heart rate, blood pressure, stress, and glucose tracking
1058
+ - **Condition Detection**: 10 rule-based conditions (Hypertension, Tachycardia, Kidney Stress, Liver Stress, Hyperglycemia, Sleep Deprivation, etc.) automatically detected from current metrics
1059
+ - **Body Impact Overlay**: 2D SVG body silhouette highlighting organs affected by detected conditions with severity-based color coding
1060
+ - **Smart Recommendations**: Context-aware health recommendations with YouTube educational links for each detected condition
1061
+ - **AI Video Recommendations**: OpenRouter-powered personalized health video suggestions for each organ based on abnormal metrics, displayed in a side-by-side "Improve Your Health" card with per-organ caching
1062
+ - **Explainability Panel**: Side-by-side "Why This Score?" card showing metric contributions, weights, coverage, and confidence for each organ
1063
+ - **Time-Series History**: View per-report organ scores, historical trends, and scored snapshots over time
1064
+ - **Lifestyle Integration**: Incorporates UserProfile self-reported data (sleep hours, stress level, activity level) into organ scoring
1065
 
1066
  ### How It Works
1067
 
 
1093
 
1094
  ```mermaid
1095
  graph TB
1096
+ subgraph Database["PostgreSQL"]
1097
+ Observation["Observation Table<br/>(extracted metrics)"]
1098
+ UserProfile["UserProfile Table<br/>(lifestyle data)"]
1099
+ Report["Report Table<br/>(source documents)"]
1100
+ end
1101
+
1102
+ subgraph Backend["Backend Services"]
1103
+ PhysicsConfig["physics_config.py<br/>- Organ specs<br/>- Metric weights<br/>- Reference ranges"]
1104
+ PhysicsRoute["physics.py Routes<br/>- GET /latest (auto-compute)<br/>- GET /history (per-report)<br/>- POST /metrics (manual)<br/>- GET /config"]
1105
+ Scorer["Scoring Engine<br/>- compute_organ_score()<br/>- compute_all_organs()"]
1106
+ ConditionEngine["conditions.py<br/>- detect_conditions()<br/>- 10 rule-based conditions<br/>- Severity mapping<br/>- Organ mapping"]
1107
+ TelemetryRoute["telemetry.py Routes<br/>- GET /stream (SSE)<br/>- GET /latest<br/>- GET /history"]
1108
+ end
1109
+
1110
+ subgraph Frontend["Frontend Components"]
1111
+ PhysicsTwin["PhysicsTwin.tsx<br/>- Main page orchestrator<br/>- Tab navigation (Twin/Metrics/History)"]
1112
+ TwinViewer["TwinViewer.tsx<br/>- 3D body with Three.js<br/>- Organ hotspots<br/>- Auto-rotate"]
1113
+ BodyOverlay["BodyImpactOverlay.tsx<br/>- SVG body silhouette<br/>- Animated organ zones<br/>- Gender toggle"]
1114
+ OrganTelemetry["OrganTelemetryCard.tsx<br/>- Overall score display<br/>- Selected organ detail<br/>- Metric values"]
1115
+ Explainability["ExplainabilityCard.tsx<br/>- Scoring breakdown<br/>- Weight contributions<br/>- Coverage/confidence"]
1116
+ Recommendations["RecommendationsCard.tsx<br/>- Detected conditions<br/>- Evidence-based actions<br/>- YouTube links"]
1117
+ ConditionsEngine["conditionsEngine.ts<br/>- Client-side condition detection<br/>- Rapid re-evaluation<br/>- Trend computation"]
1118
+ TelemetryHook["useTelemetryStream.ts<br/>- SSE connection manager<br/>- Fallback simulation<br/>- History buffer (120 readings)"]
1119
+ end
1120
+
1121
+ Observation -->|Query last 90 days| PhysicsRoute
1122
+ UserProfile -->|Enrich with lifestyle| PhysicsRoute
1123
+ Report -->|Group snapshots by| PhysicsRoute
1124
+
1125
+ PhysicsRoute -->|Load config| PhysicsConfig
1126
+ PhysicsRoute -->|Compute scores| Scorer
1127
+ Scorer -->|Detect conditions| ConditionEngine
1128
+
1129
+ TelemetryRoute -->|Generate readings| TelemetryHook
1130
+ PhysicsRoute -->|Return snapshot| PhysicsTwin
1131
+
1132
+ PhysicsTwin -->|Render organs| TwinViewer
1133
+ PhysicsTwin -->|Show overlay| BodyOverlay
1134
+ PhysicsTwin -->|Display score| OrganTelemetry
1135
+ PhysicsTwin -->|Show breakdown| Explainability
1136
+ PhysicsTwin -->|Detect conditions| ConditionsEngine
1137
+ ConditionsEngine -->|Map to organs| BodyOverlay
1138
+ ConditionsEngine -->|Show recommendations| Recommendations
1139
+ TelemetryHook -->|Stream vitals| PhysicsTwin
1140
  ```
1141
 
1142
  ### Core Concepts
 
1158
 
1159
  ```
1160
  If metric_value is in reference_range [ref_min, ref_max]:
1161
+ normalised_score = 1.0 (perfect)
1162
 
1163
  Else if metric_value < ref_min:
1164
+ normalised_score = (metric_value - abs_min) / (ref_min - abs_min)
1165
+ (linear decay to 0 at absolute minimum)
1166
 
1167
  Else if metric_value > ref_max:
1168
+ normalised_score = 1.0 - (metric_value - ref_max) / (abs_max - ref_max)
1169
+ (linear decay to 0 at absolute maximum)
1170
  ```
1171
 
1172
  **Organ Score** = weighted average of present metrics:
 
1178
  **Overall Score** = mean of all organ scores with data
1179
 
1180
  **Status Classification**:
1181
+ - **Healthy**: 75-100
1182
+ - **Watch**: 50-74
1183
+ - **Risk**: 0-49
1184
 
1185
  #### Metric Name Alignment
1186
 
 
1217
  #### Files Modified/Created
1218
 
1219
  1. **`app/services/physics_config.py`**: Organ & metric specifications
1220
+ - 6 organ definitions with metric weights
1221
+ - Reference ranges & absolute bounds per metric
1222
+ - `compute_organ_score()`: Deterministic scoring function
1223
+ - `compute_all_organs()`: Multi-organ aggregation
1224
 
1225
  2. **`app/routes/physics.py`**: Smart API endpoints
1226
+ - `GET /api/physics/latest`: Auto-compute from real DB data (90-day window)
1227
+ - `GET /api/physics/history`: Per-report snapshots
1228
+ - `POST /api/physics/metrics`: Manual submission (fallback)
1229
+ - `GET /api/physics/config`: Frontend config download
1230
 
1231
  3. **`app/services/conditions.py`**: Rule engine
1232
+ - 10 `ConditionRule` definitions
1233
+ - `detect_conditions()`: Main evaluation function
1234
+ - Threshold checking with severity mapping
1235
+ - Organ affection mapping
1236
 
1237
  4. **`app/routes/telemetry.py`**: Simulated SSE stream
1238
+ - Baseline vital signs (heart_rate, bp, spo2, glucose, etc.)
1239
+ - Per-user simulation state tracking
1240
+ - Realistic jitter + periodic spikes
1241
+ - `GET /stream` SSE endpoint
1242
 
1243
  #### Data QueryQuery Pattern (physics.py)
1244
 
1245
  ```python
1246
  # Gather last 90 days of metrics for user
1247
  result = await db.execute(
1248
+ select(Observation)
1249
+ .where(
1250
+ and_(
1251
+ Observation.user_id == user_id,
1252
+ Observation.observed_at >= cutoff # 90 days ago
1253
+ )
1254
+ )
1255
+ .order_by(desc(Observation.observed_at))
1256
  )
1257
 
1258
  # Deduplicate: keep most recent value per metric_name
1259
  metrics = {}
1260
  for obs in sorted_observations:
1261
+ if obs.metric_name not in seen:
1262
+ metrics[obs.metric_name] = float(obs.value)
1263
  ```
1264
 
1265
  ### Frontend Implementation
 
1267
  #### Main Components
1268
 
1269
  1. **`PhysicsTwin.tsx`** (616 lines): Page orchestrator
1270
+ - Three tabs: Twin View (3D/overlay), Metrics (vitals grid), History (trends + snapshots)
1271
+ - Auto-loads latest snapshot on mount
1272
+ - Streams real-time telemetry via SSE hook
1273
+ - Auto-detects conditions from current metrics
1274
+ - Handles organ selection state
1275
 
1276
  2. **`TwinViewer.tsx`** (297 lines): 3D body renderer
1277
+ - Uses `@react-three/fiber` + `@react-three/drei` + Three.js
1278
+ - Stylized torso + head mesh
1279
+ - 6 animated organ orbs with onClick handlers
1280
+ - Auto-rotating when no organ selected
1281
+ - Smooth camera tween to selected organ
1282
+ - Responsive scaling & glow effects
1283
 
1284
  3. **`BodyImpactOverlay.tsx`** (263 lines): 2D condition visualization
1285
+ - SVG body silhouette (male/female toggle)
1286
+ - 6 organ zones with animated pulsing
1287
+ - Severity-based color coding (mild/moderate/severe)
1288
+ - Condition pill badges at top
1289
+ - Gender-specific body paths
1290
 
1291
  4. **`OrganTelemetryCard.tsx`** (159 lines): Score summary
1292
+ - Large animated overall score display
1293
+ - Selected organ detail (coverage %, status badge)
1294
+ - Per-metric contribution breakdown with visual bars
1295
+ - Last updated timestamp
1296
 
1297
  5. **`ExplainabilityCard.tsx`**: Scoring transparency
1298
+ - Breakdown table: metric name, raw value, normalised score, weight
1299
+ - Contribution % calculation
1300
+ - Coverage metric (% of available metrics present)
1301
+ - Confidence score (weighted average)
1302
+
1303
+ 6. **`YouTubeRecommendationsCard.tsx`**: AI-powered video recommendations
1304
+ - Generates organ-specific health video suggestions via OpenRouter (GPT-4o-mini)
1305
+ - Per-organ caching with `useRef<Map>` so recommendations load instantly on re-selection
1306
+ - AbortController + request ID pattern prevents stale API responses
1307
+ - Retry mechanism with cache-clearing for error recovery
1308
+ - Styled with `dash-card` CSS to match the project design system
1309
+ - Side-by-side layout with ExplainabilityCard in a 50/50 CSS grid row
1310
+
1311
+ 7. **`RecommendationsCard.tsx`**: Condition actions
1312
+ - Dynamic pill badges grouped by severity
1313
+ - Evidence-based recommendations for each condition
1314
+ - YouTube search links for educational content
1315
+ - Expandable/collapsible sections
1316
 
1317
  #### Supporting Files
1318
 
1319
  - **`physicsApi.ts`**: API client with types for snapshots, organs, conditions
1320
  - **`conditionsEngine.ts`**: Client-side condition detection mirroring backend rules (instant feedback on metric changes)
1321
  - **`useTelemetryStream.ts`**: SSE hook with automatic fallback to simulated vitals if SSE unavailable
1322
+ - **`openRouterService.ts`**: OpenRouter API client for generating video recommendations (GPT-4o-mini, with local fallback)
1323
 
1324
  ### API Endpoints
1325
 
 
1340
  Response:
1341
  ```json
1342
  {
1343
+ "id": "550e8400-e29b-41d4-a716-446655440000_2025-02-06T10:30:00",
1344
+ "user_id": "550e8400-e29b-41d4-a716-446655440000",
1345
+ "timestamp": "2025-02-06T10:30:00",
1346
+ "overall_score": 72.3,
1347
+ "overall_status": "Watch",
1348
+ "data_source": "reports",
1349
+ "raw_metrics": {
1350
+ "systolic_bp": 145,
1351
+ "diastolic_bp": 88,
1352
+ "heart_rate": 78,
1353
+ "glucose": 115,
1354
+ "alt": 35,
1355
+ "creatinine": 1.1,
1356
+ "spo2": 97,
1357
+ "hemoglobin": 14.2,
1358
+ "stress_level": 5.2,
1359
+ "sleep_hours": 6.5
1360
+ },
1361
+ "organs": {
1362
+ "heart": {
1363
+ "score": 68.5,
1364
+ "status": "Watch",
1365
+ "coverage": 0.83,
1366
+ "contributions": [
1367
+ {
1368
+ "name": "systolic_bp",
1369
+ "value": 145,
1370
+ "normalised": 0.75,
1371
+ "weight": 0.25,
1372
+ "weighted": 0.1875,
1373
+ "unit": "mmHg"
1374
+ }
1375
+ ]
1376
+ },
1377
+ "kidney": {
1378
+ "score": 82.1,
1379
+ "status": "Healthy",
1380
+ "coverage": 1.0,
1381
+ "contributions": []
1382
+ }
1383
+ },
1384
+ "conditions": [
1385
+ {
1386
+ "id": "hypertension",
1387
+ "name": "Hypertension",
1388
+ "severity": "moderate",
1389
+ "affected_organs": ["heart", "kidney", "brain"],
1390
+ "trigger_metrics": { "systolic_bp": 145 },
1391
+ "recommendations": [
1392
+ "Reduce sodium intake to under 2,300 mg/day",
1393
+ "Engage in 30 minutes of moderate exercise daily"
1394
+ ],
1395
+ "youtube_queries": ["how to lower blood pressure naturally"]
1396
+ }
1397
+ ],
1398
+ "organ_conditions": {
1399
+ "heart": ["hypertension"],
1400
+ "kidney": ["hypertension"]
1401
+ },
1402
+ "organ_severities": {
1403
+ "heart": "moderate",
1404
+ "kidney": "mild"
1405
+ }
1406
  }
1407
  ```
1408
 
 
1418
  ### User Experience Flow
1419
 
1420
  1. **Page Load**:
1421
+ - Auto-fetches `/api/physics/latest` → loads real data from user's reports
1422
+ - If user has reports: snapshot shows immediately with organ scores visible
1423
+ - If no reports: Shows "No health data found" overlay with option to load demo data
1424
 
1425
  2. **3D Viewer Interaction**:
1426
+ - Click any organ → auto-selects, triggers camera tween to that organ
1427
+ - OrganTelemetryCard shows score breakdown (e.g., "Heart: 68.5% — Watch")
1428
+ - ExplainabilityCard shows metric contributions to selected organ score
1429
+ - Each metric bar shows current value vs reference range
1430
 
1431
  3. **Body Overlay Mode**:
1432
+ - Click "Impact View" toggle → SVG body appears
1433
+ - Detected conditions pulse with color coding (mild=yellow, moderate=orange, severe=red)
1434
+ - Each organ zone highlights if affected by a condition
1435
+ - Hover shows condition name
1436
 
1437
  4. **Real-Time Metrics Tab**:
1438
+ - Vital signs grid shows latest values (from telemetry SSE stream)
1439
+ - Trend arrows (↑ up, ↓ down, → stable) based on 10-reading window
1440
+ - "Score Metrics" button submits current readings for manual analysis
1441
 
1442
  5. **History Tab**:
1443
+ - Line chart of selected metric over last 60 telemetry readings
1444
+ - Scored snapshots section lists per-report scores with timestamps
1445
+ - Click any snapshot to replay its organ breakdown
1446
 
1447
  6. **Recommendations**:
1448
+ - Dynamically updates as conditions detected/cleared
1449
+ - Each condition shows severity, affected organs, evidence-based actions
1450
+ - YouTube links for patient education (e.g., "DASH diet for hypertension")
1451
 
1452
  ### Key Features Breakdown
1453
 
1454
  #### Real Data, Not Simulation
1455
 
1456
+ - `/api/physics/latest` queries actual `Observation` table
1457
+ - Uses real extracted metrics from uploaded medical reports
1458
+ - Falls back gracefully if user has zero observations (shows empty state)
1459
+ - Enriches with self-reported profile data (sleep, stress)
1460
 
1461
  #### Deterministic Scoring
1462
 
1463
+ - Reference ranges stored in `physics_config.py`
1464
+ - All calculations deterministic (no AI/ML, pure math)
1465
+ - Explainable: every score component shown in breakdown
1466
+ - Aligned with clinical normalisation standards
1467
 
1468
  #### 3D Interactive Body
1469
 
1470
+ - 6 clickable organ hotspots
1471
+ - Smooth camera animation on selection
1472
+ - Real-time score badges on each organ
1473
+ - Auto-rotating when idle
1474
+ - Works on mobile and desktop
1475
 
1476
  #### Condition Detection
1477
 
1478
+ - 10 rule-based conditions automatically detected
1479
+ - Severity mapping (mild ≤ moderate ≤ severe)
1480
+ - Organ affection mapping (which organs are affected?)
1481
+ - Real-time evaluation as metrics change
1482
+ - Client-side + server-side dual detection for instant feedback
1483
 
1484
  #### Educational Context
1485
 
1486
+ - Recommendations tailored per condition
1487
+ - YouTube search links for each condition
1488
+ - Evidence citations (reference ranges explained)
1489
+ - Safety disclaimers on all pages
1490
+
1491
+ #### AI Video Recommendations (OpenRouter)
1492
+
1493
+ - Personalized health video suggestions generated via OpenRouter (GPT-4o-mini) for each organ
1494
+ - Video recommendations based on organ-specific abnormal metrics, not generic searches
1495
+ - Per-organ caching with `useRef<Map>` for instant retrieval on organ re-selection
1496
+ - AbortController + request ID guards prevent stale responses from overwriting current data
1497
+ - Fallback recommendations generated locally if API is unavailable
1498
+ - Displayed in a side-by-side "Improve Your Health" card alongside the "Why This Score?" explainability panel
1499
 
1500
  ### Performance & Optimization
1501
 
 
1698
 
1699
  ```mermaid
1700
  erDiagram
1701
+ users ||--o{ reports : uploads
1702
+ users ||--o{ observations : has
1703
+ users ||--o{ health_metrics : has
1704
+ users ||--o| user_profiles : has
1705
+ users ||--o{ profile_conditions : has
1706
+ users ||--o{ profile_medications : takes
1707
+ users ||--o{ profile_allergies : has
1708
+ users ||--o{ chat_sessions : owns
1709
+ users ||--o{ report_ai_summaries : generates
1710
+ users ||--o{ report_ai_comparisons : generates
1711
+ users ||--o{ user_saved_medicines : saves
1712
+ users ||--o{ substitute_queries : searches
1713
+ users ||--o{ pharmacy_clicks : clicks
1714
+ users ||--o| user_location_consent : grants
1715
+
1716
+ user_profiles ||--o{ reminders : has
1717
+ reminders ||--o{ reminder_events : logs
1718
+
1719
+ reports ||--o{ observations : extracts
1720
+ reports ||--o{ report_ai_summaries : summarized_by
1721
+
1722
+ chat_sessions ||--o{ chat_messages : contains
1723
+
1724
+ user_saved_medicines ||--o{ generic_catalog : references
1725
+ substitute_queries ||--o{ generic_catalog : queries
1726
  ```
1727
 
1728
  ### Medicines Database Schema
 
1888
  Request:
1889
  ```json
1890
  {
1891
+ "email": "user@example.com",
1892
+ "password": "securepassword"
1893
  }
1894
  ```
1895
 
1896
  Response:
1897
  ```json
1898
  {
1899
+ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
1900
+ "user": {
1901
+ "id": "550e8400-e29b-41d4-a716-446655440000",
1902
+ "email": "user@example.com",
1903
+ "full_name": "John Doe",
1904
+ "created_at": "2026-01-15T10:30:00Z"
1905
+ }
1906
  }
1907
  ```
1908
 
 
1911
  Request:
1912
  ```json
1913
  {
1914
+ "report_id": "550e8400-e29b-41d4-a716-446655440001",
1915
+ "force_regenerate": false
1916
  }
1917
  ```
1918
 
1919
  Response:
1920
  ```json
1921
  {
1922
+ "summary_json": {
1923
+ "title": "Blood Panel Analysis - January 2026",
1924
+ "highlights": {
1925
+ "positive": ["Hemoglobin within normal range", "Glucose levels stable"],
1926
+ "needs_attention": ["LDL cholesterol slightly elevated"],
1927
+ "next_steps": ["Consider dietary changes", "Retest in 3 months"]
1928
+ },
1929
+ "plain_language_summary": "Your blood panel shows mostly healthy values...",
1930
+ "key_findings": [
1931
+ {"item": "LDL Cholesterol", "evidence": "142 mg/dL (ref: <100)"}
1932
+ ],
1933
+ "confidence": 0.85
1934
+ },
1935
+ "cached": false,
1936
+ "generated_at": "2026-02-02T10:30:00Z",
1937
+ "model_name": "grok-beta"
1938
  }
1939
  ```
1940
 
 
2000
 
2001
  ### Manual QA Checklist
2002
 
2003
+ 1. Register a new account
2004
+ 2. Login and verify dashboard loads
2005
+ 3. Upload a PDF lab report
2006
+ 4. Verify OCR extraction completes (WebSocket notification)
2007
+ 5. Check extracted metrics appear in reports list
2008
+ 6. Verify health index updates on dashboard
2009
+ 7. Complete health profile wizard
2010
+ 8. Check recommendations generate
2011
+ 9. Open AI Summary page, select a report
2012
+ 10. Generate AI summary, verify it displays
2013
+ 11. Select 2 reports of same type, generate comparison
2014
+ 12. Try selecting mixed types, verify warning appears
2015
+ 13. Test Medicines feature: search for a medicine (e.g., "Aspirin 500mg")
2016
+ 14. Verify substitutes appear with prices and Jan Aushadhi options highlighted
2017
+ 15. Test pharmacy search by entering your location
2018
+ 16. Verify nearby pharmacies appear with ratings and distance
2019
+ 17. Try filtering pharmacies by type (all/jan_aushadhi/generic)
2020
+ 18. Save a medicine and verify it appears in saved list
2021
+ 19. Open Physics Twin page, verify organ scores load from reports
2022
+ 20. Click an organ, verify "Why This Score?" breakdown and "Improve Your Health" video recommendations appear side-by-side
2023
+ 21. Switch organs, verify video recommendations update and cache correctly
2024
 
2025
  ### Medicines Feature Testing
2026
 
 
2043
  ### Medicines Manual Testing
2044
 
2045
  1. **Test Medicine Normalization**
2046
+ ```bash
2047
+ curl -X POST http://localhost:8000/api/medicines/normalize \
2048
+ -H "Authorization: Bearer <token>" \
2049
+ -H "Content-Type: application/json" \
2050
+ -d '{"text": "Aspirin 500mg tablet"}'
2051
+ ```
2052
+ Expected: `NormalizedMedicine` with extracted salt, strength, form
2053
 
2054
  2. **Test Substitute Search (AI-Powered)**
2055
+ ```bash
2056
+ curl -X POST http://localhost:8000/api/medicines/substitutes/from-text \
2057
+ -H "Authorization: Bearer <token>" \
2058
+ -H "Content-Type: application/json" \
2059
+ -d '{"text": "Crocin 650mg for fever"}'
2060
+ ```
2061
+ Expected: Ranked list of alternatives with prices
2062
 
2063
  3. **Test Pharmacy Search**
2064
+ ```bash
2065
+ curl -X GET "http://localhost:8000/api/medicines/pharmacies/nearby?lat=40.7128&lng=-74.0060&radius_m=1000&type=all" \
2066
+ -H "Authorization: Bearer <token>"
2067
+ ```
2068
+ Expected: List of nearby pharmacies with ratings
2069
 
2070
  4. **Test Save Medicine**
2071
+ ```bash
2072
+ curl -X POST http://localhost:8000/api/medicines/saved \
2073
+ -H "Authorization: Bearer <token>" \
2074
+ -H "Content-Type: application/json" \
2075
+ -d '{
2076
+ "brand_name": "Aspirin",
2077
+ "salt": "Acetylsalicylic Acid",
2078
+ "strength": "500",
2079
+ "form": "tablet",
2080
+ "release_type": "immediate",
2081
+ "notes": "Take with food"
2082
+ }'
2083
+ ```
2084
+ Expected: `UserSavedMedicine` object saved to database
2085
 
2086
  ### Medicines Troubleshooting
2087
 
 
2089
 
2090
  - **Cause**: Medicine not in `generic_catalog` table
2091
  - **Solution**:
2092
+ - Verify PMBI/Jan Aushadhi seed data was loaded during migration
2093
+ - Check database: `SELECT COUNT(*) FROM generic_catalog;` should show 600K+ records
2094
+ - Try searching by generic name instead of brand name (e.g., "Acetylsalicylic Acid" instead of "Aspirin")
2095
+ - Check logs for Grok API errors if using free-text search
2096
 
2097
  **Problem: "Google Places API 401" or pharmacy search returns empty**
2098
 
2099
  - **Cause**: `GOOGLE_PLACES_API_KEY` not set or API quota exceeded
2100
  - **Solution**:
2101
+ - Verify `GOOGLE_PLACES_API_KEY` is set in `.env`: `echo $GOOGLE_PLACES_API_KEY`
2102
+ - Check Google Cloud Console for API key validity and quota limits
2103
+ - If quota exceeded, wait 24 hours or upgrade billing
2104
+ - During development, feature falls back to mock pharmacy data when API key is missing
2105
+ - Mock data is useful for frontend testing without API costs
2106
 
2107
  **Problem: Slow medicine normalization or substitute search**
2108
 
2109
  - **Cause**: Missing database indexes on high-cardinality columns
2110
  - **Solution**:
2111
+ - Verify indexes exist on `generic_catalog`: `SELECT indexname FROM pg_indexes WHERE tablename='generic_catalog';`
2112
+ - Should see indexes on: `product_name`, `salt`, `user_id`
2113
+ - If missing, run: `alembic upgrade head` to apply latest migrations
2114
+ - Check database query performance: Enable `EXPLAIN ANALYZE` in PostgreSQL
2115
+ - Profile with: `SELECT pg_size_pretty(pg_total_relation_size('generic_catalog'));`
2116
 
2117
  **Problem: Grok API errors when parsing prescription images**
2118
 
2119
  - **Cause**: Invalid image format, API key expired, or API rate limit
2120
  - **Solution**:
2121
+ - Verify image is clear and readable (JPG, PNG, TIFF)
2122
+ - Test API key directly: `curl -H "Authorization: Bearer $GROK_API_KEY" https://api.x.ai/v1/models`
2123
+ - Check Grok API status at https://status.x.ai
2124
+ - Review backend logs: `docker logs <backend-container>` or `tail uvicorn.log`
2125
+ - If rate-limited, implement request throttling in `medicine_normalizer.py`
2126
 
2127
  **Problem: Pharmacy search cache not updating**
2128
 
2129
  - **Cause**: In-memory cache TTL (1 hour) not expired
2130
  - **Solution**:
2131
+ - Manually clear cache by restarting backend service
2132
+ - Cache is per-search key (lat/lng/radius): different locations create new cache entries
2133
+ - Check cache size in logs: Search for "pharmacy_cache" debug messages
2134
+ - To disable caching for testing, set `PharmacyLocator.CACHE_TTL_SECONDS = 0`
2135
 
2136
  **Problem: User saved medicines not persisting**
2137
 
2138
  - **Cause**: Database migration not applied or user_id foreign key constraint
2139
  - **Solution**:
2140
+ - Verify `user_saved_medicines` table exists: `\dt user_saved_medicines` in psql
2141
+ - Run migrations: `cd backend && alembic upgrade head`
2142
+ - Verify user exists and authenticated: Check JWT token contains valid `sub` (user_id)
2143
+ - Check database logs for constraint errors: `docker logs <postgres-container>`
2144
  ---
2145
 
2146
  ## Deployment
backend/app/routes/physics.py CHANGED
@@ -36,6 +36,13 @@ from app.services.conditions import (
36
  get_organ_conditions,
37
  get_organ_worst_severity,
38
  )
 
 
 
 
 
 
 
39
 
40
  logger = logging.getLogger(__name__)
41
 
@@ -315,3 +322,52 @@ async def get_config(user=Depends(get_current_user)):
315
  ],
316
  }
317
  return {"organs": organs}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  get_organ_conditions,
37
  get_organ_worst_severity,
38
  )
39
+ from app.services.youtube_recommendation_service import YouTubeRecommendationService
40
+ from app.settings import Settings
41
+ from functools import lru_cache
42
+
43
+ @lru_cache()
44
+ def get_settings():
45
+ return Settings()
46
 
47
  logger = logging.getLogger(__name__)
48
 
 
322
  ],
323
  }
324
  return {"organs": organs}
325
+
326
+
327
+ class YouTubeRecommendationRequest(BaseModel):
328
+ """Request body for YouTube recommendations"""
329
+ organ: str = Field(..., description="Organ name (kidney, heart, liver, etc.)")
330
+ score: float = Field(..., description="Organ health score 0-100")
331
+ status: str = Field(..., description="Health status (Healthy, Watch, Risk)")
332
+ metrics: Dict[str, float] = Field(default_factory=dict, description="Current metric values")
333
+ conditions: List[dict] = Field(default_factory=list, description="Detected conditions")
334
+
335
+
336
+ class YouTubeRecommendationResponse(BaseModel):
337
+ """Response with YouTube video recommendations"""
338
+ organ: str
339
+ recommendations: List[Dict[str, str]]
340
+
341
+
342
+ @router.post("/youtube-recommendations", response_model=YouTubeRecommendationResponse)
343
+ async def get_youtube_recommendations(
344
+ body: YouTubeRecommendationRequest,
345
+ user=Depends(get_current_user),
346
+ ):
347
+ """
348
+ Generate YouTube video recommendations based on organ telemetry.
349
+ Uses OpenRouter AI to generate targeted search queries and returns
350
+ actual YouTube videos (if API key is available) or search links.
351
+ """
352
+ try:
353
+ settings = get_settings()
354
+ service = YouTubeRecommendationService(settings)
355
+
356
+ recommendations = await service.generate_recommendations(
357
+ organ=body.organ,
358
+ score=body.score,
359
+ status=body.status,
360
+ metrics=body.metrics,
361
+ conditions=body.conditions,
362
+ )
363
+
364
+ return {
365
+ "organ": body.organ,
366
+ "recommendations": recommendations,
367
+ }
368
+ except Exception as e:
369
+ logger.exception(f"Error generating YouTube recommendations: {e}")
370
+ raise HTTPException(
371
+ status_code=500,
372
+ detail=f"Failed to generate recommendations: {str(e)}"
373
+ )
backend/app/services/youtube_recommendation_service.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ YouTube Recommendation Service
3
+
4
+ Uses OpenRouter API to generate contextual YouTube video recommendations
5
+ based on organ telemetry data and detected health conditions.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import os
12
+ from typing import Dict, List, Optional
13
+ import httpx
14
+ from googleapiclient.discovery import build
15
+ from googleapiclient.errors import HttpError
16
+
17
+ from app.settings import Settings
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class YouTubeRecommendationService:
23
+ """Generate accurate YouTube video recommendations for health improvement"""
24
+
25
+ def __init__(self, settings: Settings):
26
+ self.settings = settings
27
+ self.openrouter_api_key = settings.OPENROUTER_API_KEY or os.getenv("OPENROUTER_API_KEY")
28
+ self.youtube_api_key = settings.google_places_api_key or os.getenv("YOUTUBE_API_KEY")
29
+
30
+ if not self.openrouter_api_key:
31
+ logger.warning("OPENROUTER_API_KEY not configured - YouTube recommendations will use fallback")
32
+
33
+ self.openrouter_base_url = "https://openrouter.ai/api/v1/chat/completions"
34
+ self.timeout = 30
35
+
36
+ async def generate_recommendations(
37
+ self,
38
+ organ: str,
39
+ score: float,
40
+ status: str,
41
+ metrics: Dict[str, float],
42
+ conditions: List[Dict],
43
+ ) -> List[Dict[str, str]]:
44
+ """
45
+ Generate YouTube video recommendations based on organ health data.
46
+
47
+ Args:
48
+ organ: Organ name (e.g., "kidney", "heart", "liver")
49
+ score: Organ health score (0-100)
50
+ status: Health status ("Healthy", "Watch", "Risk")
51
+ metrics: Current metric values affecting this organ
52
+ conditions: List of detected conditions affecting this organ
53
+
54
+ Returns:
55
+ List of video recommendations with title, videoId, thumbnail, and description
56
+ """
57
+ try:
58
+ # Generate search queries using OpenRouter
59
+ queries = await self._generate_search_queries(organ, score, status, metrics, conditions)
60
+
61
+ # Fetch actual YouTube videos for each query
62
+ recommendations = []
63
+ for query in queries[:3]: # Limit to top 3 queries to save API calls
64
+ videos = await self._search_youtube(query)
65
+ if videos:
66
+ recommendations.extend(videos[:2]) # Top 2 videos per query
67
+
68
+ # Deduplicate by video ID and limit to 5 total
69
+ seen_ids = set()
70
+ unique_recommendations = []
71
+ for video in recommendations:
72
+ if video["videoId"] not in seen_ids:
73
+ seen_ids.add(video["videoId"])
74
+ unique_recommendations.append(video)
75
+ if len(unique_recommendations) >= 5:
76
+ break
77
+
78
+ return unique_recommendations
79
+
80
+ except Exception as e:
81
+ logger.exception(f"Error generating YouTube recommendations: {e}")
82
+ return self._get_fallback_recommendations(organ, conditions)
83
+
84
+ async def _generate_search_queries(
85
+ self,
86
+ organ: str,
87
+ score: float,
88
+ status: str,
89
+ metrics: Dict[str, float],
90
+ conditions: List[Dict],
91
+ ) -> List[str]:
92
+ """Use OpenRouter AI to generate targeted YouTube search queries"""
93
+
94
+ if not self.openrouter_api_key:
95
+ return self._get_fallback_queries(organ, conditions)
96
+
97
+ # Build context for AI
98
+ condition_text = ""
99
+ if conditions:
100
+ condition_names = [c.get("name", "") for c in conditions]
101
+ condition_text = f"Detected conditions: {', '.join(condition_names)}. "
102
+
103
+ metric_text = ""
104
+ if metrics:
105
+ metric_items = [f"{k.replace('_', ' ')}: {v:.1f}" for k, v in metrics.items()]
106
+ metric_text = f"Key metrics: {', '.join(metric_items)}. "
107
+
108
+ prompt = f"""You are a medical health educator. Generate 5 specific, actionable YouTube search queries for someone with:
109
+
110
+ Organ: {organ.upper()}
111
+ Health Score: {score:.1f}/100 ({status})
112
+ {condition_text}{metric_text}
113
+
114
+ Requirements:
115
+ 1. Each query should target EVIDENCE-BASED educational content
116
+ 2. Focus on practical improvement strategies, diet, exercise, lifestyle
117
+ 3. Include specific terms like "doctor explains", "Mayo Clinic", "medical review"
118
+ 4. Avoid clickbait - prioritize credible medical sources
119
+ 5. Mix general organ health with specific condition management
120
+
121
+ Return ONLY the 5 search queries, one per line, no numbering or extra text."""
122
+
123
+ try:
124
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
125
+ response = await client.post(
126
+ self.openrouter_base_url,
127
+ headers={
128
+ "Authorization": f"Bearer {self.openrouter_api_key}",
129
+ "HTTP-Referer": "https://lumea-health.com",
130
+ "X-Title": "Lumea Health YouTube Recommendations",
131
+ },
132
+ json={
133
+ "model": "openai/gpt-4o-mini", # Fast and accurate
134
+ "messages": [{"role": "user", "content": prompt}],
135
+ "temperature": 0.7,
136
+ "max_tokens": 300,
137
+ }
138
+ )
139
+
140
+ if response.status_code == 200:
141
+ data = response.json()
142
+ content = data.get("choices", [{}])[0].get("message", {}).get("content", "")
143
+ queries = [q.strip() for q in content.strip().split("\n") if q.strip()]
144
+ logger.info(f"Generated {len(queries)} queries for {organ}: {queries}")
145
+ return queries[:5]
146
+ else:
147
+ logger.warning(f"OpenRouter returned status {response.status_code}")
148
+ return self._get_fallback_queries(organ, conditions)
149
+
150
+ except Exception as e:
151
+ logger.warning(f"Error calling OpenRouter: {e}")
152
+ return self._get_fallback_queries(organ, conditions)
153
+
154
+ async def _search_youtube(self, query: str) -> List[Dict[str, str]]:
155
+ """Search YouTube Data API v3 for videos matching the query"""
156
+
157
+ # Return search URL fallback if no API key - with generic placeholder
158
+ if not self.youtube_api_key:
159
+ logger.info(f"No YouTube API key - returning search link for: {query}")
160
+ # Generate a more deterministic video ID for better caching
161
+ video_id = f"srch{abs(hash(query)) % 1000000}"
162
+ return [{
163
+ "title": query,
164
+ "videoId": video_id,
165
+ "thumbnail": f"https://via.placeholder.com/320x180/6b9175/ffffff?text={query[:30].replace(' ', '+')}",
166
+ "description": "Click to search YouTube for medical videos on this topic",
167
+ "url": f"https://www.youtube.com/results?search_query={query.replace(' ', '+')}",
168
+ "channelTitle": "YouTube Search"
169
+ }]
170
+
171
+ try:
172
+ youtube = build("youtube", "v3", developerKey=self.youtube_api_key)
173
+
174
+ search_response = youtube.search().list(
175
+ q=query,
176
+ part="id,snippet",
177
+ maxResults=3,
178
+ type="video",
179
+ order="relevance",
180
+ videoDuration="medium", # 4-20 minutes
181
+ videoDefinition="high",
182
+ relevanceLanguage="en",
183
+ ).execute()
184
+
185
+ videos = []
186
+ for item in search_response.get("items", []):
187
+ video_id = item["id"]["videoId"]
188
+ snippet = item["snippet"]
189
+
190
+ videos.append({
191
+ "title": snippet["title"],
192
+ "videoId": video_id,
193
+ "thumbnail": snippet["thumbnails"].get("medium", {}).get("url", ""),
194
+ "description": snippet["description"][:150] + "..." if len(snippet["description"]) > 150 else snippet["description"],
195
+ "url": f"https://www.youtube.com/watch?v={video_id}",
196
+ "channelTitle": snippet["channelTitle"]
197
+ })
198
+
199
+ return videos
200
+
201
+ except HttpError as e:
202
+ logger.warning(f"YouTube API error for query '{query}': {e}")
203
+ # Return search URL fallback with generic placeholder
204
+ video_id = f"srch{abs(hash(query)) % 1000000}"
205
+ return [{
206
+ "title": query,
207
+ "videoId": video_id,
208
+ "thumbnail": f"https://via.placeholder.com/320x180/6b9175/ffffff?text={query[:30].replace(' ', '+')}",
209
+ "description": "Click to search YouTube for medical videos on this topic",
210
+ "url": f"https://www.youtube.com/results?search_query={query.replace(' ', '+')}",
211
+ "channelTitle": "YouTube Search"
212
+ }]
213
+
214
+ except Exception as e:
215
+ logger.exception(f"Error searching YouTube: {e}")
216
+ return []
217
+
218
+ def _get_fallback_queries(self, organ: str, conditions: List[Dict]) -> List[str]:
219
+ """Fallback queries when AI is unavailable"""
220
+
221
+ base_queries = {
222
+ "kidney": [
223
+ "how to improve kidney function naturally doctor explains",
224
+ "kidney disease prevention Mayo Clinic",
225
+ "best foods for kidney health medical review",
226
+ "kidney health exercises and lifestyle changes",
227
+ "understanding kidney function tests explained",
228
+ ],
229
+ "heart": [
230
+ "heart health improvement doctor guide",
231
+ "cardiovascular disease prevention explained",
232
+ "best diet for heart health evidence based",
233
+ "heart disease risk factors medical review",
234
+ "improving heart health naturally Cleveland Clinic",
235
+ ],
236
+ "liver": [
237
+ "liver health improvement natural methods",
238
+ "fatty liver disease treatment explained",
239
+ "liver detox what actually works doctor",
240
+ "liver function tests explained medical",
241
+ "best diet for liver health evidence based",
242
+ ],
243
+ "lungs": [
244
+ "lung health improvement breathing exercises",
245
+ "respiratory health doctor explains",
246
+ "lung function improvement natural methods",
247
+ "breathing techniques for better lung health",
248
+ "pulmonary health prevention medical review",
249
+ ],
250
+ "brain": [
251
+ "brain health improvement neuroplasticity",
252
+ "cognitive function optimization doctor",
253
+ "neurological health prevention methods",
254
+ "brain health diet and lifestyle medical",
255
+ "memory improvement techniques evidence based",
256
+ ],
257
+ "blood": [
258
+ "blood health optimization complete guide",
259
+ "anemia treatment natural methods doctor",
260
+ "improving blood test results naturally",
261
+ "hematology basics medical explanation",
262
+ "blood health diet and supplements",
263
+ ],
264
+ }
265
+
266
+ # Get base queries for organ
267
+ queries = base_queries.get(organ.lower(), [
268
+ f"{organ} health improvement doctor explains",
269
+ f"how to improve {organ} function naturally",
270
+ f"{organ} disease prevention medical review",
271
+ f"best diet for {organ} health evidence based",
272
+ f"{organ} health lifestyle changes doctor guide",
273
+ ])
274
+
275
+ # Add condition-specific queries
276
+ if conditions:
277
+ for condition in conditions[:2]: # Top 2 conditions
278
+ condition_name = condition.get("name", "")
279
+ if condition_name:
280
+ queries.append(f"{condition_name} treatment explained doctor")
281
+
282
+ return queries[:5]
283
+
284
+ def _get_fallback_recommendations(self, organ: str, conditions: List[Dict]) -> List[Dict[str, str]]:
285
+ """Static fallback when all APIs fail"""
286
+
287
+ queries = self._get_fallback_queries(organ, conditions)
288
+
289
+ return [{
290
+ "title": query,
291
+ "videoId": f"fallback_{i}_{abs(hash(query)) % 1000}",
292
+ "thumbnail": f"https://via.placeholder.com/320x180/6b9175/ffffff?text={query[:30].replace(' ', '+')}",
293
+ "description": "Click to search YouTube for medical videos on this topic",
294
+ "url": f"https://www.youtube.com/results?search_query={query.replace(' ', '+')}",
295
+ "channelTitle": "YouTube Search"
296
+ } for i, query in enumerate(queries)]
backend/requirements.txt CHANGED
@@ -40,6 +40,9 @@ ollama>=0.1.0
40
  google-generativeai>=0.4.0
41
  openai>=1.0.0 # Used as OpenRouter SDK (OpenAI-compatible API)
42
 
 
 
 
43
  # Local MedGemma via Hugging Face
44
  # Keep transformers on the 4.x line; 5.x requires newer torch and breaks
45
  # sentence-transformers import paths used by Mem0 huggingface embeddings.
 
40
  google-generativeai>=0.4.0
41
  openai>=1.0.0 # Used as OpenRouter SDK (OpenAI-compatible API)
42
 
43
+ # YouTube API for recommendations
44
+ google-api-python-client>=2.100.0
45
+
46
  # Local MedGemma via Hugging Face
47
  # Keep transformers on the 4.x line; 5.x requires newer torch and breaks
48
  # sentence-transformers import paths used by Mem0 huggingface embeddings.
frontend/.env.example ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # OpenRouter API Key
2
+ # Used for generating intelligent YouTube video recommendations
3
+ # Get your key from: https://openrouter.ai/keys
4
+ VITE_OPENROUTER_API_KEY=sk-or-v1-your_key_here
5
+
6
+ # Note: No YouTube Data API key needed - we use YouTube search URLs directly
frontend/src/components/HealthGraph.css CHANGED
@@ -268,19 +268,20 @@
268
  transform-origin: center center;
269
  }
270
 
271
- /* SVG Styles */
272
  .relationship-line {
273
- stroke: var(--dash-border, #e5dfd5);
274
- stroke-width: 2;
275
  fill: none;
276
  }
277
 
278
  .relationship-label {
279
- font-size: 10px;
280
- fill: var(--dash-text-muted, #999999);
281
  text-anchor: middle;
282
  pointer-events: none;
283
  font-family: var(--dash-font-sans, 'DM Sans', sans-serif);
 
284
  }
285
 
286
  .node {
@@ -294,21 +295,23 @@
294
 
295
  .node-circle {
296
  stroke: white;
297
- stroke-width: 3;
298
- filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
299
  transition: all var(--dash-transition-fast, 150ms);
300
  }
301
 
302
  .node:hover .node-circle {
303
- filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2));
 
304
  }
305
 
306
  .node-label {
307
- font-size: 10px;
308
  fill: white;
309
- font-weight: 600;
310
  pointer-events: none;
311
  font-family: var(--dash-font-sans, 'DM Sans', sans-serif);
 
312
  }
313
 
314
  /* Tooltip */
 
268
  transform-origin: center center;
269
  }
270
 
271
+ /* SVG Styles - Larger, More Readable Nodes */
272
  .relationship-line {
273
+ stroke: rgba(107, 145, 117, 0.25);
274
+ stroke-width: 2.5;
275
  fill: none;
276
  }
277
 
278
  .relationship-label {
279
+ font-size: 11px;
280
+ fill: var(--dash-text-secondary, #6b6b6b);
281
  text-anchor: middle;
282
  pointer-events: none;
283
  font-family: var(--dash-font-sans, 'DM Sans', sans-serif);
284
+ font-weight: 500;
285
  }
286
 
287
  .node {
 
295
 
296
  .node-circle {
297
  stroke: white;
298
+ stroke-width: 4;
299
+ filter: drop-shadow(0 3px 6px rgba(0, 0, 0, 0.15));
300
  transition: all var(--dash-transition-fast, 150ms);
301
  }
302
 
303
  .node:hover .node-circle {
304
+ filter: drop-shadow(0 5px 10px rgba(0, 0, 0, 0.25));
305
+ stroke-width: 5;
306
  }
307
 
308
  .node-label {
309
+ font-size: 12px;
310
  fill: white;
311
+ font-weight: 700;
312
  pointer-events: none;
313
  font-family: var(--dash-font-sans, 'DM Sans', sans-serif);
314
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
315
  }
316
 
317
  /* Tooltip */
frontend/src/components/HealthGraph.tsx CHANGED
@@ -524,7 +524,7 @@ const HealthGraph: React.FC<HealthGraphProps> = ({
524
  onMouseLeave={() => setHoveredNode(null)}
525
  >
526
  <circle
527
- r={isHovered ? 28 : 24}
528
  fill={nodeColors[node.type] || nodeColors.entity}
529
  className="node-circle"
530
  />
@@ -533,8 +533,8 @@ const HealthGraph: React.FC<HealthGraphProps> = ({
533
  textAnchor="middle"
534
  className="node-label"
535
  >
536
- {node.name.length > 12
537
- ? node.name.slice(0, 10) + '...'
538
  : node.name}
539
  </text>
540
  </g>
 
524
  onMouseLeave={() => setHoveredNode(null)}
525
  >
526
  <circle
527
+ r={isHovered ? 38 : 34}
528
  fill={nodeColors[node.type] || nodeColors.entity}
529
  className="node-circle"
530
  />
 
533
  textAnchor="middle"
534
  className="node-label"
535
  >
536
+ {node.name.length > 14
537
+ ? node.name.slice(0, 12) + '...'
538
  : node.name}
539
  </text>
540
  </g>
frontend/src/components/MemoryDashboard.css CHANGED
@@ -241,8 +241,9 @@
241
  justify-content: space-between;
242
  gap: var(--dash-spacing-md, 1rem);
243
  padding: var(--dash-spacing-md, 1rem) var(--dash-spacing-lg, 1.5rem);
244
- background: white;
245
  border: 1px solid var(--dash-border, #e5dfd5);
 
246
  border-radius: var(--dash-radius-lg, 1rem);
247
  margin-bottom: var(--dash-spacing-sm, 0.75rem);
248
  transition: all var(--dash-transition-base, 250ms);
@@ -250,7 +251,9 @@
250
 
251
  .memory-item:hover {
252
  border-color: var(--dash-accent-light, #8fb199);
253
- box-shadow: var(--dash-shadow-sm, 0 2px 8px rgba(107, 145, 117, 0.08));
 
 
254
  transform: translateY(-1px);
255
  }
256
 
 
241
  justify-content: space-between;
242
  gap: var(--dash-spacing-md, 1rem);
243
  padding: var(--dash-spacing-md, 1rem) var(--dash-spacing-lg, 1.5rem);
244
+ background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
245
  border: 1px solid var(--dash-border, #e5dfd5);
246
+ border-left: 3px solid var(--dash-accent-light, #8fb199);
247
  border-radius: var(--dash-radius-lg, 1rem);
248
  margin-bottom: var(--dash-spacing-sm, 0.75rem);
249
  transition: all var(--dash-transition-base, 250ms);
 
251
 
252
  .memory-item:hover {
253
  border-color: var(--dash-accent-light, #8fb199);
254
+ border-left-color: var(--dash-accent, #6b9175);
255
+ background: white;
256
+ box-shadow: 0 4px 12px rgba(107, 145, 117, 0.12);
257
  transform: translateY(-1px);
258
  }
259
 
frontend/src/components/dashboard/DashboardNavbar.css CHANGED
@@ -73,18 +73,19 @@
73
  .dashboard-nav {
74
  display: flex;
75
  align-items: center;
76
- gap: var(--dash-spacing-md);
77
  }
78
 
79
  .dashboard-nav-link {
80
  padding: var(--dash-spacing-sm) var(--dash-spacing-md);
81
- font-size: 0.9375rem;
82
  font-weight: 500;
83
  color: var(--dash-text-secondary);
84
  text-decoration: none;
85
  border-radius: var(--dash-radius-md);
86
  transition: all var(--dash-transition-fast);
87
  position: relative;
 
88
  }
89
 
90
  .dashboard-nav-link:hover {
 
73
  .dashboard-nav {
74
  display: flex;
75
  align-items: center;
76
+ gap: var(--dash-spacing-sm, 0.75rem);
77
  }
78
 
79
  .dashboard-nav-link {
80
  padding: var(--dash-spacing-sm) var(--dash-spacing-md);
81
+ font-size: 0.9rem;
82
  font-weight: 500;
83
  color: var(--dash-text-secondary);
84
  text-decoration: none;
85
  border-radius: var(--dash-radius-md);
86
  transition: all var(--dash-transition-fast);
87
  position: relative;
88
+ white-space: nowrap;
89
  }
90
 
91
  .dashboard-nav-link:hover {
frontend/src/components/dashboard/DashboardNavbar.tsx CHANGED
@@ -1,7 +1,7 @@
1
  import { useState } from 'react';
2
  import { Link, useLocation, useNavigate } from 'react-router-dom';
3
  import { motion, AnimatePresence } from 'framer-motion';
4
- import { Settings, LogOut, Home, LayoutDashboard, Bell, FileText, Activity, Sparkles, Pill, PhoneCall, Network } from 'lucide-react';
5
  import { logout } from '../../utils/auth';
6
  import Logo from '../ui/Logo';
7
  import './DashboardNavbar.css';
@@ -69,16 +69,6 @@ function DashboardNavbar({ userName = 'User', userStatus = '87% Healthy' }: Dash
69
 
70
  {/* Actions */}
71
  <div className="dashboard-actions">
72
- {/* Notifications */}
73
- <motion.button
74
- className="dash-btn dash-btn-icon dash-focus-ring"
75
- whileHover={{ scale: 1.05 }}
76
- whileTap={{ scale: 0.95 }}
77
- aria-label="Notifications"
78
- >
79
- <Bell size={20} />
80
- </motion.button>
81
-
82
  {/* User Menu */}
83
  <div className="dashboard-user-menu">
84
  <motion.button
 
1
  import { useState } from 'react';
2
  import { Link, useLocation, useNavigate } from 'react-router-dom';
3
  import { motion, AnimatePresence } from 'framer-motion';
4
+ import { Settings, LogOut, Home, LayoutDashboard, FileText, Activity, Sparkles, Pill, PhoneCall, Network } from 'lucide-react';
5
  import { logout } from '../../utils/auth';
6
  import Logo from '../ui/Logo';
7
  import './DashboardNavbar.css';
 
69
 
70
  {/* Actions */}
71
  <div className="dashboard-actions">
 
 
 
 
 
 
 
 
 
 
72
  {/* User Menu */}
73
  <div className="dashboard-user-menu">
74
  <motion.button
frontend/src/components/physics/ExplainabilityCard.tsx CHANGED
@@ -1,7 +1,7 @@
1
  /**
2
  * ExplainabilityCard – deterministic scoring breakdown.
3
  * Shows weights, normalised contributions, coverage/confidence.
4
- * Styled to match the calm Lumea card aesthetic.
5
  */
6
 
7
  import React from 'react';
@@ -21,17 +21,17 @@ const ExplainabilityCard: React.FC<ExplainabilityCardProps> = ({
21
  }) => {
22
  if (!selectedOrgan || !organResult) {
23
  return (
24
- <div className="dash-card physics-explainability-card">
25
  <div className="dash-card-header">
26
- <h3 className="dash-card-title">
27
  <BarChart3 size={18} />
28
- Explainability
29
  </h3>
30
  </div>
31
- <div className="dash-card-body">
32
- <div className="physics-explain-empty">
33
- <Info size={32} />
34
- <p>Select an organ to see the scoring breakdown</p>
35
  </div>
36
  </div>
37
  </div>
@@ -41,63 +41,131 @@ const ExplainabilityCard: React.FC<ExplainabilityCardProps> = ({
41
  const presentMetrics = organResult.contributions.filter((c) => c.value != null);
42
  const totalWeight = organResult.contributions.reduce((s, c) => s + c.weight, 0);
43
  const presentWeight = presentMetrics.reduce((s, c) => s + c.weight, 0);
 
44
 
45
  return (
46
- <div className="dash-card physics-explainability-card">
 
47
  <div className="dash-card-header">
48
- <h3 className="dash-card-title">
49
  <BarChart3 size={18} />
50
  Why this score?
51
  </h3>
52
- <span className="physics-explain-organ-badge">{organLabel}</span>
 
 
53
  </div>
54
 
55
- <div className="dash-card-body">
56
- {/* Summary row */}
57
- <div className="physics-explain-summary">
58
- <div className="physics-explain-stat">
59
- <span className="physics-explain-stat-value">{Math.round(organResult.score)}</span>
60
- <span className="physics-explain-stat-label">Score</span>
 
 
 
 
 
 
 
 
 
 
 
61
  </div>
62
- <div className="physics-explain-stat">
63
- <span className="physics-explain-stat-value">{Math.round(organResult.coverage * 100)}%</span>
64
- <span className="physics-explain-stat-label">Coverage</span>
 
 
65
  </div>
66
- <div className="physics-explain-stat">
67
- <span className="physics-explain-stat-value">
68
- {presentWeight > 0 ? Math.round((presentWeight / totalWeight) * 100) : 0}%
69
- </span>
70
- <span className="physics-explain-stat-label">Confidence</span>
71
  </div>
72
  </div>
73
 
74
- {/* Contributions table */}
75
- <div className="physics-explain-table">
76
- <div className="physics-explain-table-header">
 
 
 
 
 
 
 
 
 
 
 
77
  <span>Metric</span>
78
- <span>Value</span>
79
- <span>Norm</span>
80
- <span>Weight</span>
81
- <span>Contrib</span>
82
  </div>
83
- {organResult.contributions.map((c) => (
84
- <div key={c.name} className={`physics-explain-table-row ${c.value == null ? 'missing' : ''}`}>
85
- <span className="physics-explain-metric-name">{c.name}</span>
86
- <span>{c.value != null ? `${c.value}` : '—'}</span>
87
- <span>{c.normalised != null ? c.normalised.toFixed(2) : '—'}</span>
88
- <span>{(c.weight * 100).toFixed(0)}%</span>
89
- <span>{c.weighted != null ? (c.weighted * 100).toFixed(1) : ''}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  </div>
91
  ))}
92
  </div>
93
 
94
- {/* Formula note */}
95
- <div className="physics-explain-note">
96
- <Info size={14} />
97
- <span>
98
- Score = Σ (weight<sub>i</sub> × normalised<sub>i</sub>) / Σ weight<sub>present</sub> × 100.
 
 
 
 
 
 
 
 
 
99
  Missing metrics are excluded — coverage reflects data completeness.
100
- </span>
101
  </div>
102
  </div>
103
  </div>
 
1
  /**
2
  * ExplainabilityCard – deterministic scoring breakdown.
3
  * Shows weights, normalised contributions, coverage/confidence.
4
+ * Matches clean Tailwind design system.
5
  */
6
 
7
  import React from 'react';
 
21
  }) => {
22
  if (!selectedOrgan || !organResult) {
23
  return (
24
+ <div className="dash-card h-full flex flex-col">
25
  <div className="dash-card-header">
26
+ <h3 className="dash-card-title flex items-center gap-2">
27
  <BarChart3 size={18} />
28
+ Why this score?
29
  </h3>
30
  </div>
31
+ <div className="dash-card-body flex-1 flex items-center justify-center">
32
+ <div className="text-center" style={{ color: 'var(--dash-text-muted)' }}>
33
+ <Info size={48} style={{ margin: '0 auto', opacity: 0.3 }} />
34
+ <p style={{ fontSize: '0.875rem', marginTop: '0.5rem' }}>Select an organ to see details</p>
35
  </div>
36
  </div>
37
  </div>
 
41
  const presentMetrics = organResult.contributions.filter((c) => c.value != null);
42
  const totalWeight = organResult.contributions.reduce((s, c) => s + c.weight, 0);
43
  const presentWeight = presentMetrics.reduce((s, c) => s + c.weight, 0);
44
+ const confidence = presentWeight > 0 ? Math.round((presentWeight / totalWeight) * 100) : 0;
45
 
46
  return (
47
+ <div className="dash-card h-full flex flex-col">
48
+ {/* Header */}
49
  <div className="dash-card-header">
50
+ <h3 className="dash-card-title flex items-center gap-2">
51
  <BarChart3 size={18} />
52
  Why this score?
53
  </h3>
54
+ <span className={`dash-badge dash-badge-${organResult.status === 'Healthy' ? 'success' : organResult.status === 'Watch' ? 'warning' : 'danger'}`}>
55
+ {organLabel}
56
+ </span>
57
  </div>
58
 
59
+ {/* Content */}
60
+ <div className="dash-card-body flex-1" style={{ maxHeight: '500px', overflowY: 'auto' }}>
61
+ {/* Summary Stats */}
62
+ <div style={{
63
+ display: 'grid',
64
+ gridTemplateColumns: 'repeat(3, 1fr)',
65
+ gap: '1rem',
66
+ marginBottom: '1.5rem',
67
+ padding: '1rem',
68
+ background: 'var(--dash-accent-pale)',
69
+ borderRadius: 'var(--dash-radius-lg)',
70
+ }}>
71
+ <div style={{ textAlign: 'center' }}>
72
+ <div style={{ fontSize: '1.5rem', fontWeight: 'bold', color: 'var(--dash-success)' }}>
73
+ {Math.round(organResult.score)}
74
+ </div>
75
+ <div style={{ fontSize: '0.75rem', color: 'var(--dash-text-muted)', marginTop: '0.25rem' }}>Score</div>
76
  </div>
77
+ <div style={{ textAlign: 'center' }}>
78
+ <div style={{ fontSize: '1.5rem', fontWeight: 'bold', color: 'var(--dash-info)' }}>
79
+ {Math.round(organResult.coverage * 100)}%
80
+ </div>
81
+ <div style={{ fontSize: '0.75rem', color: 'var(--dash-text-muted)', marginTop: '0.25rem' }}>Coverage</div>
82
  </div>
83
+ <div style={{ textAlign: 'center' }}>
84
+ <div style={{ fontSize: '1.5rem', fontWeight: 'bold', color: 'var(--dash-accent)' }}>
85
+ {confidence}%
86
+ </div>
87
+ <div style={{ fontSize: '0.75rem', color: 'var(--dash-text-muted)', marginTop: '0.25rem' }}>Confidence</div>
88
  </div>
89
  </div>
90
 
91
+ {/* Metrics Table */}
92
+ <div style={{ marginTop: '1rem' }}>
93
+ {/* Header */}
94
+ <div style={{
95
+ display: 'grid',
96
+ gridTemplateColumns: '2fr 1fr 1fr 1fr 1fr',
97
+ gap: '0.5rem',
98
+ padding: '0.5rem 0.75rem',
99
+ fontSize: '0.75rem',
100
+ fontWeight: 600,
101
+ color: 'var(--dash-text-muted)',
102
+ textTransform: 'uppercase',
103
+ borderBottom: '2px solid var(--dash-border)',
104
+ }}>
105
  <span>Metric</span>
106
+ <span style={{ textAlign: 'right' }}>Value</span>
107
+ <span style={{ textAlign: 'right' }}>Norm</span>
108
+ <span style={{ textAlign: 'right' }}>Weight</span>
109
+ <span style={{ textAlign: 'right' }}>Contrib</span>
110
  </div>
111
+
112
+ {/* Rows */}
113
+ {organResult.contributions.map((c, index) => (
114
+ <div
115
+ key={c.name}
116
+ style={{
117
+ display: 'grid',
118
+ gridTemplateColumns: '2fr 1fr 1fr 1fr 1fr',
119
+ gap: '0.5rem',
120
+ padding: '0.625rem 0.75rem',
121
+ fontSize: '0.875rem',
122
+ background: c.value == null ? 'transparent' : (index % 2 === 0 ? 'var(--dash-surface)' : 'var(--dash-surface-hover)'),
123
+ borderRadius: 'var(--dash-radius-sm)',
124
+ color: c.value == null ? 'var(--dash-text-muted)' : 'var(--dash-text)',
125
+ transition: 'background 0.2s',
126
+ }}
127
+ onMouseEnter={(e) => {
128
+ if (c.value != null) e.currentTarget.style.background = 'var(--dash-accent-pale)';
129
+ }}
130
+ onMouseLeave={(e) => {
131
+ if (c.value != null) e.currentTarget.style.background = index % 2 === 0 ? 'var(--dash-surface)' : 'var(--dash-surface-hover)';
132
+ }}
133
+ >
134
+ <span style={{ fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
135
+ {c.name}
136
+ </span>
137
+ <span style={{ textAlign: 'right', fontFamily: 'monospace' }}>
138
+ {c.value != null ? c.value.toString() : '—'}
139
+ </span>
140
+ <span style={{ textAlign: 'right', fontFamily: 'monospace' }}>
141
+ {c.normalised != null ? c.normalised.toFixed(2) : '—'}
142
+ </span>
143
+ <span style={{ textAlign: 'right', fontWeight: 600, color: 'var(--dash-info)' }}>
144
+ {(c.weight * 100).toFixed(0)}%
145
+ </span>
146
+ <span style={{ textAlign: 'right', fontWeight: 600, color: 'var(--dash-success)' }}>
147
+ {c.weighted != null ? (c.weighted * 100).toFixed(1) : '—'}
148
+ </span>
149
  </div>
150
  ))}
151
  </div>
152
 
153
+ {/* Formula Note */}
154
+ <div style={{
155
+ marginTop: '1.5rem',
156
+ padding: '1rem',
157
+ background: 'var(--dash-accent-pale)',
158
+ border: '1px solid var(--dash-accent-light)',
159
+ borderRadius: 'var(--dash-radius-lg)',
160
+ display: 'flex',
161
+ alignItems: 'flex-start',
162
+ gap: '0.75rem',
163
+ }}>
164
+ <Info size={16} style={{ color: 'var(--dash-accent)', flexShrink: 0, marginTop: '2px' }} />
165
+ <p style={{ fontSize: '0.75rem', color: 'var(--dash-text-secondary)', lineHeight: 1.6 }}>
166
+ <strong>Score Formula:</strong> Σ (weight<sub>i</sub> × normalised<sub>i</sub>) / Σ weight<sub>present</sub> × 100.
167
  Missing metrics are excluded — coverage reflects data completeness.
168
+ </p>
169
  </div>
170
  </div>
171
  </div>
frontend/src/components/physics/YouTubeRecommendationsCard.css ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * YouTubeRecommendationsCard styles
3
+ * Matches Lumea dashboard design tokens
4
+ */
5
+
6
+ .youtube-reco-card {
7
+ /* Card inherits dash-card base styles */
8
+ display: flex;
9
+ flex-direction: column;
10
+ }
11
+
12
+ .youtube-reco-card .dash-card-body {
13
+ flex: 1;
14
+ display: flex;
15
+ flex-direction: column;
16
+ min-height: 0;
17
+ }
18
+
19
+ .youtube-reco-organ-badge {
20
+ padding: 0.35rem 0.75rem;
21
+ background: linear-gradient(135deg, var(--sage-green, #6b9175) 0%, var(--sage-dark, #4a7c59) 100%);
22
+ color: white;
23
+ border-radius: 6px;
24
+ font-size: 0.8125rem;
25
+ font-weight: 600;
26
+ letter-spacing: 0.025em;
27
+ box-shadow: 0 2px 4px rgba(107, 145, 117, 0.2);
28
+ }
29
+
30
+ .youtube-reco-organ-label {
31
+ font-size: 0.85rem;
32
+ color: var(--text-secondary, #6b7280);
33
+ font-style: italic;
34
+ }
35
+
36
+ /* Empty state */
37
+ .youtube-reco-empty {
38
+ display: flex;
39
+ flex-direction: column;
40
+ align-items: center;
41
+ justify-content: center;
42
+ padding: 2.5rem 1rem;
43
+ text-align: center;
44
+ color: var(--text-muted, #9ca3af);
45
+ }
46
+
47
+ .youtube-reco-empty svg {
48
+ color: var(--sage-green, #6b9175);
49
+ margin-bottom: 1rem;
50
+ opacity: 0.5;
51
+ }
52
+
53
+ .youtube-reco-empty p {
54
+ font-size: 0.9rem;
55
+ margin: 0;
56
+ }
57
+
58
+ .youtube-reco-empty-subtitle {
59
+ font-size: 0.8125rem !important;
60
+ color: var(--text-muted, #9ca3af) !important;
61
+ margin-top: 0.5rem !important;
62
+ }
63
+
64
+ /* Loading state */
65
+ .youtube-reco-loading {
66
+ display: flex;
67
+ flex-direction: column;
68
+ align-items: center;
69
+ justify-content: center;
70
+ padding: 2.5rem 1rem;
71
+ text-align: center;
72
+ color: var(--text-secondary, #6b7280);
73
+ }
74
+
75
+ .youtube-reco-loading .spinner {
76
+ color: var(--sage-green, #6b9175);
77
+ margin-bottom: 1rem;
78
+ animation: spin 1s linear infinite;
79
+ }
80
+
81
+ @keyframes spin {
82
+ to { transform: rotate(360deg); }
83
+ }
84
+
85
+ .youtube-reco-loading p {
86
+ font-size: 0.9rem;
87
+ margin: 0;
88
+ }
89
+
90
+ /* Error state */
91
+ .youtube-reco-error {
92
+ display: flex;
93
+ flex-direction: column;
94
+ align-items: center;
95
+ justify-content: center;
96
+ padding: 2rem 1rem;
97
+ text-align: center;
98
+ }
99
+
100
+ .youtube-reco-error svg {
101
+ color: var(--danger-color, #ef4444);
102
+ margin-bottom: 1rem;
103
+ }
104
+
105
+ .youtube-reco-error p {
106
+ font-size: 0.9rem;
107
+ color: var(--text-secondary, #6b7280);
108
+ margin: 0 0 1rem 0;
109
+ }
110
+
111
+ .youtube-reco-retry {
112
+ padding: 0.5rem 1.25rem;
113
+ background: var(--sage-green, #6b9175);
114
+ color: white;
115
+ border: none;
116
+ border-radius: 6px;
117
+ font-size: 0.875rem;
118
+ font-weight: 500;
119
+ cursor: pointer;
120
+ transition: all 0.2s ease;
121
+ }
122
+
123
+ .youtube-reco-retry:hover {
124
+ background: var(--sage-dark, #4a7c59);
125
+ transform: translateY(-1px);
126
+ }
127
+
128
+ /* Video list */
129
+ .youtube-reco-list {
130
+ display: flex;
131
+ flex-direction: column;
132
+ gap: 1rem;
133
+ flex: 1;
134
+ overflow-y: auto;
135
+ overflow-x: hidden;
136
+ padding-right: 0.25rem;
137
+ }
138
+
139
+ /* Custom scrollbar */
140
+ .youtube-reco-list::-webkit-scrollbar {
141
+ width: 6px;
142
+ }
143
+
144
+ .youtube-reco-list::-webkit-scrollbar-track {
145
+ background: rgba(107, 145, 117, 0.05);
146
+ border-radius: 3px;
147
+ }
148
+
149
+ .youtube-reco-list::-webkit-scrollbar-thumb {
150
+ background: rgba(107, 145, 117, 0.2);
151
+ border-radius: 3px;
152
+ }
153
+
154
+ .youtube-reco-list::-webkit-scrollbar-thumb:hover {
155
+ background: rgba(107, 145, 117, 0.3);
156
+ }
157
+
158
+ /* Video card */
159
+ .youtube-video-card {
160
+ display: flex;
161
+ gap: 0.875rem;
162
+ padding: 1rem;
163
+ background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
164
+ border: 1px solid rgba(107, 145, 117, 0.12);
165
+ border-radius: 8px;
166
+ cursor: pointer;
167
+ transition: all 0.25s ease;
168
+ position: relative;
169
+ min-height: 110px;
170
+ }
171
+
172
+ .youtube-video-card:hover {
173
+ transform: translateY(-2px);
174
+ box-shadow: 0 4px 12px rgba(107, 145, 117, 0.15);
175
+ border-color: var(--sage-green, #6b9175);
176
+ }
177
+
178
+ /* Video thumbnail */
179
+ .youtube-video-thumbnail {
180
+ flex-shrink: 0;
181
+ width: 180px;
182
+ height: 101px; /* 16:9 aspect ratio */
183
+ border-radius: 6px;
184
+ overflow: hidden;
185
+ background: linear-gradient(135deg, #f0f0f0 0%, #e0e0e0 100%);
186
+ position: relative;
187
+ }
188
+
189
+ .youtube-video-thumbnail img {
190
+ width: 100%;
191
+ height: 100%;
192
+ object-fit: cover;
193
+ transition: transform 0.3s ease;
194
+ }
195
+
196
+ .youtube-video-card:hover .youtube-video-thumbnail img {
197
+ transform: scale(1.05);
198
+ }
199
+
200
+ /* Play overlay */
201
+ .youtube-video-play-overlay {
202
+ position: absolute;
203
+ top: 0;
204
+ left: 0;
205
+ right: 0;
206
+ bottom: 0;
207
+ display: flex;
208
+ align-items: center;
209
+ justify-content: center;
210
+ background: rgba(0, 0, 0, 0.3);
211
+ opacity: 0;
212
+ transition: opacity 0.25s ease;
213
+ }
214
+
215
+ .youtube-video-card:hover .youtube-video-play-overlay {
216
+ opacity: 1;
217
+ }
218
+
219
+ .youtube-video-play-overlay svg {
220
+ color: white;
221
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
222
+ }
223
+
224
+ /* Video info */
225
+ .youtube-video-info {
226
+ flex: 1;
227
+ display: flex;
228
+ flex-direction: column;
229
+ gap: 0.375rem;
230
+ min-width: 0; /* Enable text truncation */
231
+ }
232
+
233
+ .youtube-video-title {
234
+ font-size: 0.9375rem;
235
+ font-weight: 600;
236
+ color: var(--text-primary, #111827);
237
+ margin: 0;
238
+ line-height: 1.35;
239
+ display: -webkit-box;
240
+ -webkit-line-clamp: 2;
241
+ -webkit-box-orient: vertical;
242
+ overflow: hidden;
243
+ transition: color 0.2s ease;
244
+ }
245
+
246
+ .youtube-video-card:hover .youtube-video-title {
247
+ color: var(--sage-green, #6b9175);
248
+ }
249
+
250
+ .youtube-video-channel {
251
+ font-size: 0.8125rem;
252
+ color: var(--text-secondary, #6b7280);
253
+ margin: 0;
254
+ font-weight: 500;
255
+ }
256
+
257
+ .youtube-video-description {
258
+ font-size: 0.8125rem;
259
+ color: var(--text-muted, #9ca3af);
260
+ margin: 0;
261
+ line-height: 1.4;
262
+ display: -webkit-box;
263
+ -webkit-line-clamp: 2;
264
+ -webkit-box-orient: vertical;
265
+ overflow: hidden;
266
+ }
267
+
268
+ /* Link icon */
269
+ .youtube-video-link-icon {
270
+ flex-shrink: 0;
271
+ align-self: flex-start;
272
+ margin-top: 0.25rem;
273
+ color: var(--text-muted, #9ca3af);
274
+ transition: color 0.2s ease;
275
+ }
276
+
277
+ .youtube-video-card:hover .youtube-video-link-icon {
278
+ color: var(--sage-green, #6b9175);
279
+ }
280
+
281
+ /* Responsive adjustments */
282
+ @media (max-width: 768px) {
283
+ .youtube-video-card {
284
+ flex-direction: column;
285
+ min-height: auto;
286
+ }
287
+
288
+ .youtube-video-thumbnail {
289
+ width: 100%;
290
+ height: 200px;
291
+ }
292
+
293
+ .youtube-video-link-icon {
294
+ position: absolute;
295
+ top: 1rem;
296
+ right: 1rem;
297
+ }
298
+ }
299
+
300
+ @media (max-width: 1200px) {
301
+ .youtube-video-thumbnail {
302
+ width: 140px;
303
+ height: 79px;
304
+ }
305
+ }
306
+
307
+ /* Compact view for smaller cards */
308
+ .youtube-reco-card.compact .youtube-video-thumbnail {
309
+ width: 120px;
310
+ height: 68px;
311
+ }
312
+
313
+ .youtube-reco-card.compact .youtube-video-title {
314
+ font-size: 0.875rem;
315
+ -webkit-line-clamp: 1;
316
+ }
317
+
318
+ .youtube-reco-card.compact .youtube-video-description {
319
+ display: none;
320
+ }
frontend/src/components/physics/YouTubeRecommendationsCard.tsx ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * YouTubeRecommendationsCard – Smart video recommendations for organ health
3
+ *
4
+ * Uses OpenRouter AI to generate video recommendations (no YouTube API needed)
5
+ * FIXED: Stable fetch with caching, no infinite loops
6
+ */
7
+
8
+ import React, { useState, useEffect, useRef } from 'react';
9
+ import { motion, AnimatePresence } from 'framer-motion';
10
+ import { Youtube, ExternalLink, Loader2, AlertCircle, PlayCircle, Clock } from 'lucide-react';
11
+ import type { OrganResult } from '../../services/physicsApi';
12
+ import { generateVideoRecommendations, type AbnormalMetric, type VideoRecommendation } from '../../services/openRouterService';
13
+
14
+ interface YouTubeRecommendationsCardProps {
15
+ selectedOrgan: string | null;
16
+ organResult: OrganResult | null;
17
+ currentMetrics: Record<string, number>;
18
+ }
19
+
20
+ const ORGAN_LABELS: Record<string, string> = {
21
+ kidney: 'Kidney',
22
+ heart: 'Heart',
23
+ liver: 'Liver',
24
+ lungs: 'Lungs',
25
+ brain: 'Brain',
26
+ blood: 'Blood',
27
+ };
28
+
29
+ // Reference ranges for metrics
30
+ const METRIC_RANGES: Record<string, { min: number; max: number }> = {
31
+ creatinine: { min: 0.6, max: 1.2 },
32
+ urea: { min: 7, max: 20 },
33
+ alt: { min: 7, max: 56 },
34
+ ast: { min: 10, max: 40 },
35
+ bilirubin_total: { min: 0.1, max: 1.2 },
36
+ systolic_bp: { min: 90, max: 120 },
37
+ diastolic_bp: { min: 60, max: 80 },
38
+ heart_rate: { min: 60, max: 100 },
39
+ glucose: { min: 70, max: 100 },
40
+ spo2: { min: 95, max: 100 },
41
+ sodium: { min: 136, max: 145 },
42
+ potassium: { min: 3.5, max: 5.0 },
43
+ };
44
+
45
+ const YouTubeRecommendationsCard: React.FC<YouTubeRecommendationsCardProps> = ({
46
+ selectedOrgan,
47
+ organResult,
48
+ currentMetrics,
49
+ }) => {
50
+ const [videos, setVideos] = useState<VideoRecommendation[]>([]);
51
+ const [loading, setLoading] = useState(false);
52
+ const [error, setError] = useState<string | null>(null);
53
+
54
+ // Cache videos per organ to avoid refetching
55
+ const cacheRef = useRef<Map<string, VideoRecommendation[]>>(new Map());
56
+
57
+ // Track current request to ignore stale responses
58
+ const requestIdRef = useRef(0);
59
+
60
+ // AbortController for cancelling requests
61
+ const abortControllerRef = useRef<AbortController | null>(null);
62
+
63
+ // Calculate abnormal metrics
64
+ const getAbnormalMetrics = (metrics: Record<string, number>): AbnormalMetric[] => {
65
+ const abnormal: AbnormalMetric[] = [];
66
+
67
+ for (const [metricKey, value] of Object.entries(metrics)) {
68
+ const range = METRIC_RANGES[metricKey];
69
+ if (!range || typeof value !== 'number') continue;
70
+
71
+ if (value < range.min) {
72
+ abnormal.push({
73
+ name: metricKey,
74
+ value,
75
+ normalRange: `${range.min}-${range.max}`,
76
+ status: 'low',
77
+ });
78
+ } else if (value > range.max) {
79
+ abnormal.push({
80
+ name: metricKey,
81
+ value,
82
+ normalRange: `${range.min}-${range.max}`,
83
+ status: 'high',
84
+ });
85
+ }
86
+ }
87
+
88
+ return abnormal;
89
+ };
90
+
91
+ useEffect(() => {
92
+ // Clear state if no organ selected
93
+ if (!selectedOrgan || !organResult) {
94
+ setVideos([]);
95
+ setLoading(false);
96
+ setError(null);
97
+ return;
98
+ }
99
+
100
+ // Check cache first
101
+ const cached = cacheRef.current.get(selectedOrgan);
102
+ if (cached) {
103
+ setVideos(cached);
104
+ setLoading(false);
105
+ setError(null);
106
+ return;
107
+ }
108
+
109
+ // Abort previous request if still running
110
+ if (abortControllerRef.current) {
111
+ abortControllerRef.current.abort();
112
+ }
113
+
114
+ // Start new fetch
115
+ const abortController = new AbortController();
116
+ abortControllerRef.current = abortController;
117
+
118
+ const currentRequestId = ++requestIdRef.current;
119
+
120
+ const fetchRecommendations = async () => {
121
+ setLoading(true);
122
+ setError(null);
123
+
124
+ try {
125
+ const abnormalMetrics = getAbnormalMetrics(currentMetrics);
126
+
127
+ // Generate video recommendations using OpenRouter AI
128
+ const recommendations = await generateVideoRecommendations(
129
+ ORGAN_LABELS[selectedOrgan] || selectedOrgan,
130
+ abnormalMetrics
131
+ );
132
+
133
+ // Only update if this is still the latest request
134
+ if (currentRequestId === requestIdRef.current && !abortController.signal.aborted) {
135
+ setVideos(recommendations);
136
+ // Cache the results
137
+ cacheRef.current.set(selectedOrgan, recommendations);
138
+ setError(null);
139
+ }
140
+ } catch (err: any) {
141
+ // Only show error if request wasn't aborted
142
+ if (!abortController.signal.aborted && currentRequestId === requestIdRef.current) {
143
+ console.error('Error fetching recommendations:', err);
144
+ setError(err.message || 'Failed to load recommendations');
145
+ setVideos([]);
146
+ }
147
+ } finally {
148
+ // Only clear loading if this is still the latest request
149
+ if (currentRequestId === requestIdRef.current && !abortController.signal.aborted) {
150
+ setLoading(false);
151
+ }
152
+ }
153
+ };
154
+
155
+ fetchRecommendations();
156
+
157
+ // Cleanup: abort request on unmount or organ change
158
+ return () => {
159
+ abortController.abort();
160
+ };
161
+ }, [selectedOrgan]); // ONLY depend on selectedOrgan, not currentMetrics!
162
+
163
+ const handleRetry = () => {
164
+ // Clear cache for this organ and refetch
165
+ if (selectedOrgan) {
166
+ cacheRef.current.delete(selectedOrgan);
167
+ // Trigger refetch by incrementing request ID
168
+ requestIdRef.current++;
169
+
170
+ const fetchRecommendations = async () => {
171
+ if (!selectedOrgan || !organResult) return;
172
+
173
+ setLoading(true);
174
+ setError(null);
175
+
176
+ try {
177
+ const abnormalMetrics = getAbnormalMetrics(currentMetrics);
178
+ const recommendations = await generateVideoRecommendations(
179
+ ORGAN_LABELS[selectedOrgan] || selectedOrgan,
180
+ abnormalMetrics
181
+ );
182
+
183
+ setVideos(recommendations);
184
+ cacheRef.current.set(selectedOrgan, recommendations);
185
+ setError(null);
186
+ } catch (err: any) {
187
+ console.error('Error fetching recommendations:', err);
188
+ setError(err.message || 'Failed to load recommendations');
189
+ } finally {
190
+ setLoading(false);
191
+ }
192
+ };
193
+
194
+ fetchRecommendations();
195
+ }
196
+ };
197
+
198
+ const openVideo = (url: string) => {
199
+ window.open(url, '_blank', 'noopener,noreferrer');
200
+ };
201
+
202
+ if (!selectedOrgan || !organResult) {
203
+ return (
204
+ <div className="rounded-2xl border border-gray-200 bg-white shadow-sm overflow-hidden flex flex-col h-full">
205
+ <div className="p-6 border-b border-gray-100 bg-gradient-to-r from-emerald-50 to-teal-50">
206
+ <div className="flex items-center justify-between">
207
+ <div className="flex items-center gap-3">
208
+ <Youtube className="text-emerald-600" size={24} />
209
+ <h3 className="text-lg font-semibold text-gray-900">Improve Your Health</h3>
210
+ </div>
211
+ </div>
212
+ </div>
213
+ <div className="flex-1 flex items-center justify-center p-8">
214
+ <div className="text-center text-gray-500 space-y-2">
215
+ <Youtube size={48} className="mx-auto opacity-30" />
216
+ <p className="text-sm font-medium">All organs performing well!</p>
217
+ <p className="text-xs text-gray-400">No specific recommendations needed</p>
218
+ </div>
219
+ </div>
220
+ </div>
221
+ );
222
+ }
223
+
224
+ return (
225
+ <div className="dash-card h-full flex flex-col">
226
+ {/* Header */}
227
+ <div className="dash-card-header">
228
+ <h3 className="dash-card-title flex items-center gap-2">
229
+ <Youtube size={18} />
230
+ Improve Your Health
231
+ </h3>
232
+ <span className={`dash-badge dash-badge-${organResult.status === 'Healthy' ? 'success' : organResult.status === 'Watch' ? 'warning' : 'danger'}`}>
233
+ {ORGAN_LABELS[selectedOrgan]}
234
+ </span>
235
+ </div>
236
+
237
+ {/* Content */}
238
+ <div className="dash-card-body flex-1" style={{ maxHeight: '500px', overflowY: 'auto' }}>
239
+ <AnimatePresence mode="wait">
240
+ {loading && (
241
+ <motion.div
242
+ key="loading"
243
+ style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '2rem 0' }}
244
+ initial={{ opacity: 0 }}
245
+ animate={{ opacity: 1 }}
246
+ exit={{ opacity: 0 }}
247
+ >
248
+ <Loader2 size={32} className="animate-spin" style={{ color: 'var(--dash-accent)' }} />
249
+ <p style={{ fontSize: '0.875rem', color: 'var(--dash-text-secondary)', marginTop: '0.75rem' }}>Finding best videos for you...</p>
250
+ </motion.div>
251
+ )}
252
+
253
+ {error && !loading && (
254
+ <motion.div
255
+ key="error"
256
+ style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '2rem 0', textAlign: 'center' }}
257
+ initial={{ opacity: 0 }}
258
+ animate={{ opacity: 1 }}
259
+ exit={{ opacity: 0 }}
260
+ >
261
+ <AlertCircle size={32} style={{ color: 'var(--dash-danger)' }} />
262
+ <p style={{ fontSize: '0.875rem', color: 'var(--dash-text-secondary)', margin: '0.75rem 0' }}>{error}</p>
263
+ <button
264
+ onClick={handleRetry}
265
+ className="dash-btn dash-btn-primary"
266
+ >
267
+ Try Again
268
+ </button>
269
+ </motion.div>
270
+ )}
271
+
272
+ {!loading && !error && videos.length > 0 && (
273
+ <motion.div
274
+ key="videos"
275
+ className="space-y-4"
276
+ initial={{ opacity: 0 }}
277
+ animate={{ opacity: 1 }}
278
+ exit={{ opacity: 0 }}
279
+ >
280
+ {videos.map((video, index) => (
281
+ <motion.div
282
+ key={`${video.title}-${index}`}
283
+ style={{
284
+ background: 'var(--dash-surface)',
285
+ border: '1px solid var(--dash-border-light)',
286
+ borderRadius: 'var(--dash-radius-lg)',
287
+ padding: '1rem',
288
+ marginBottom: '0.75rem',
289
+ cursor: 'pointer',
290
+ transition: 'all 0.2s ease',
291
+ display: 'flex',
292
+ gap: '1rem',
293
+ }}
294
+ initial={{ opacity: 0, y: 10 }}
295
+ animate={{ opacity: 1, y: 0 }}
296
+ transition={{ delay: index * 0.1 }}
297
+ onClick={() => openVideo(video.url)}
298
+ onMouseEnter={(e) => {
299
+ e.currentTarget.style.border = '1px solid var(--dash-accent-light)';
300
+ e.currentTarget.style.boxShadow = 'var(--dash-shadow-md)';
301
+ }}
302
+ onMouseLeave={(e) => {
303
+ e.currentTarget.style.border = '1px solid var(--dash-border-light)';
304
+ e.currentTarget.style.boxShadow = 'none';
305
+ }}
306
+ >
307
+ {/* Thumbnail */}
308
+ <div style={{
309
+ flexShrink: 0,
310
+ width: '120px',
311
+ height: '80px',
312
+ background: 'linear-gradient(135deg, var(--dash-accent-pale), var(--dash-accent-light))',
313
+ borderRadius: 'var(--dash-radius-md)',
314
+ display: 'flex',
315
+ alignItems: 'center',
316
+ justifyContent: 'center',
317
+ position: 'relative',
318
+ overflow: 'hidden',
319
+ }}>
320
+ <Youtube size={32} style={{ color: 'var(--dash-accent)', opacity: 0.5 }} />
321
+ <div style={{
322
+ position: 'absolute',
323
+ inset: 0,
324
+ display: 'flex',
325
+ alignItems: 'center',
326
+ justifyContent: 'center',
327
+ transition: 'opacity 0.2s',
328
+ }}>
329
+ <PlayCircle size={32} style={{ color: 'white', opacity: 0.8 }} />
330
+ </div>
331
+ </div>
332
+
333
+ {/* Video Info */}
334
+ <div style={{ flex: 1, minWidth: 0 }}>
335
+ <h4 style={{
336
+ fontSize: '0.875rem',
337
+ fontWeight: 600,
338
+ color: 'var(--dash-text)',
339
+ marginBottom: '0.25rem',
340
+ lineHeight: 1.3,
341
+ display: '-webkit-box',
342
+ WebkitLineClamp: 2,
343
+ WebkitBoxOrient: 'vertical',
344
+ overflow: 'hidden',
345
+ }}>
346
+ {video.title}
347
+ </h4>
348
+ <p style={{
349
+ fontSize: '0.75rem',
350
+ color: 'var(--dash-text-secondary)',
351
+ marginBottom: '0.5rem',
352
+ display: '-webkit-box',
353
+ WebkitLineClamp: 2,
354
+ WebkitBoxOrient: 'vertical',
355
+ overflow: 'hidden',
356
+ }}>
357
+ {video.description}
358
+ </p>
359
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.75rem', color: 'var(--dash-text-muted)' }}>
360
+ <Clock size={12} />
361
+ <span>{video.duration}</span>
362
+ </div>
363
+ </div>
364
+
365
+ {/* External Link Icon */}
366
+ <div style={{ flexShrink: 0 }}>
367
+ <ExternalLink size={16} style={{ color: 'var(--dash-text-muted)' }} />
368
+ </div>
369
+ </motion.div>
370
+ ))}
371
+ </motion.div>
372
+ )}
373
+
374
+ {!loading && !error && videos.length === 0 && (
375
+ <motion.div
376
+ key="empty"
377
+ style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '2rem 0', textAlign: 'center' }}
378
+ initial={{ opacity: 0 }}
379
+ animate={{ opacity: 1 }}
380
+ exit={{ opacity: 0 }}
381
+ >
382
+ <Youtube size={48} style={{ color: 'var(--dash-border)', opacity: 0.5 }} />
383
+ <p style={{ fontSize: '0.875rem', color: 'var(--dash-text-muted)', marginTop: '0.5rem' }}>No recommendations available</p>
384
+ </motion.div>
385
+ )}
386
+ </AnimatePresence>
387
+ </div>
388
+ </div>
389
+ );
390
+ };
391
+
392
+ export default YouTubeRecommendationsCard;
frontend/src/components/ui/PdfPreview.css CHANGED
@@ -2,9 +2,12 @@
2
  position: relative;
3
  width: 100%;
4
  height: 100%;
 
5
  background: #f5f5f5;
6
  border-radius: 0.5rem;
7
- overflow: hidden;
 
 
8
  }
9
 
10
  .pdf-preview-iframe {
 
2
  position: relative;
3
  width: 100%;
4
  height: 100%;
5
+ min-height: 0;
6
  background: #f5f5f5;
7
  border-radius: 0.5rem;
8
+ overflow: auto;
9
+ display: flex;
10
+ flex-direction: column;
11
  }
12
 
13
  .pdf-preview-iframe {
frontend/src/pages/Features.css CHANGED
@@ -1,74 +1,73 @@
1
- /* Features Page Styles */
2
 
3
  .features-page {
4
  min-height: 100vh;
5
- background: var(--dash-bg);
6
  position: relative;
7
  overflow-x: hidden;
8
  }
9
 
10
- /* Animated Background */
11
  .features-background {
12
  position: fixed;
13
  inset: 0;
14
  z-index: 0;
15
  overflow: hidden;
16
  pointer-events: none;
 
 
 
 
17
  }
18
 
19
  .features-bg-blob {
20
  position: absolute;
21
  border-radius: 50%;
22
- filter: blur(120px);
23
- opacity: 0.4;
24
- animation: blobFloat 20s ease-in-out infinite;
25
  }
26
 
27
  .features-bg-blob-1 {
28
- width: 600px;
29
- height: 600px;
30
- background: var(--dash-accent);
31
- top: -200px;
32
- right: -200px;
33
  animation-delay: 0s;
34
  }
35
 
36
  .features-bg-blob-2 {
37
- width: 500px;
38
- height: 500px;
39
- background: var(--dash-info);
40
- bottom: -150px;
41
- left: -150px;
42
- animation-delay: -7s;
43
  }
44
 
45
  .features-bg-blob-3 {
46
- width: 400px;
47
- height: 400px;
48
- background: var(--dash-warning);
49
- top: 50%;
50
  left: 50%;
51
  transform: translate(-50%, -50%);
52
- animation-delay: -14s;
53
  }
54
 
55
  @keyframes blobFloat {
56
-
57
- 0%,
58
- 100% {
59
  transform: translate(0, 0) scale(1);
60
  }
61
-
62
  25% {
63
- transform: translate(20px, -30px) scale(1.05);
64
  }
65
-
66
  50% {
67
- transform: translate(-20px, 20px) scale(0.95);
68
  }
69
-
70
  75% {
71
- transform: translate(30px, 10px) scale(1.02);
72
  }
73
  }
74
 
@@ -76,12 +75,13 @@
76
  .features-content {
77
  position: relative;
78
  z-index: 1;
79
- padding: 100px 24px 60px;
80
  }
81
 
82
  .features-container {
83
- max-width: 1200px;
84
  margin: 0 auto;
 
85
  }
86
 
87
  .features-loading {
@@ -97,109 +97,139 @@
97
  /* Hero Section */
98
  .features-hero {
99
  text-align: center;
100
- padding: 60px 0 80px;
 
 
101
  }
102
 
103
  .hero-badge {
104
  display: inline-flex;
105
  align-items: center;
106
  gap: 8px;
107
- padding: 8px 16px;
108
- background: rgba(106, 145, 117, 0.15);
109
- border: 1px solid rgba(106, 145, 117, 0.3);
110
- border-radius: 100px;
111
- color: var(--dash-accent);
 
112
  font-size: 13px;
113
- font-weight: 500;
114
  margin-bottom: 24px;
 
115
  }
116
 
117
  .features-hero h1 {
118
- font-size: 3.5rem;
119
  font-weight: 700;
120
- color: var(--dash-text);
121
  margin: 0 0 20px;
122
- line-height: 1.1;
 
 
123
  }
124
 
125
  .gradient-text {
126
- background: linear-gradient(135deg, var(--dash-accent), var(--dash-info));
127
- -webkit-background-clip: text;
128
- -webkit-text-fill-color: transparent;
129
- background-clip: text;
130
  }
131
 
132
  .hero-description {
133
- font-size: 1.15rem;
134
- color: var(--dash-text-muted);
135
- max-width: 640px;
136
- margin: 0 auto 48px;
137
  line-height: 1.7;
 
138
  }
139
 
140
- /* Hero Features Grid */
141
  .hero-features {
142
  display: grid;
143
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
144
  gap: 24px;
145
- max-width: 960px;
146
- margin: 0 auto 48px;
147
  text-align: left;
148
  }
149
 
150
  .hero-feature {
151
  display: flex;
152
- gap: 16px;
153
- padding: 20px;
154
- background: var(--dash-card-bg);
155
- border: 1px solid var(--dash-border);
156
- border-radius: 16px;
157
- transition: all 0.2s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  }
159
 
160
  .hero-feature:hover {
161
- transform: translateY(-2px);
162
- border-color: var(--dash-accent);
163
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
 
 
 
 
 
164
  }
165
 
166
  .hero-feature-icon {
167
- width: 44px;
168
- height: 44px;
169
- border-radius: 12px;
170
  display: flex;
171
  align-items: center;
172
  justify-content: center;
173
  flex-shrink: 0;
 
 
174
  }
175
 
176
  .hero-feature-icon.memory {
177
- background: rgba(156, 136, 255, 0.15);
178
- color: #9c88ff;
179
  }
180
 
181
  .hero-feature-icon.graph {
182
- background: rgba(122, 156, 198, 0.15);
183
- color: var(--dash-info);
184
  }
185
 
186
  .hero-feature-icon.ai {
187
- background: rgba(106, 145, 117, 0.15);
188
- color: var(--dash-accent);
189
  }
190
 
191
  .hero-feature-content h4 {
192
- margin: 0 0 6px;
193
- font-size: 15px;
194
  font-weight: 600;
195
- color: var(--dash-text);
 
196
  }
197
 
198
  .hero-feature-content p {
199
  margin: 0;
200
- font-size: 13px;
201
- color: var(--dash-text-muted);
202
- line-height: 1.5;
 
203
  }
204
 
205
  .scroll-indicator {
@@ -207,23 +237,43 @@
207
  opacity: 0.5;
208
  }
209
 
210
- /* Flow Diagram */
211
  .features-flow {
212
- padding: 48px 0 64px;
213
- text-align: center;
 
 
 
 
 
 
214
  }
215
 
216
  .features-flow h2 {
217
- font-size: 1.5rem;
218
- color: var(--dash-text);
219
- margin: 0 0 32px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  }
221
 
222
  .flow-diagram {
223
  display: flex;
224
  align-items: center;
225
  justify-content: center;
226
- gap: 16px;
227
  flex-wrap: wrap;
228
  }
229
 
@@ -231,60 +281,68 @@
231
  display: flex;
232
  flex-direction: column;
233
  align-items: center;
234
- gap: 12px;
235
  }
236
 
237
  .flow-icon {
238
- width: 64px;
239
- height: 64px;
240
- border-radius: 16px;
241
- background: var(--dash-card-bg);
242
- border: 2px solid var(--dash-border);
 
243
  display: flex;
244
  align-items: center;
245
  justify-content: center;
246
- color: var(--dash-text);
247
- transition: all 0.2s ease;
 
248
  }
249
 
250
  .flow-icon.memory {
251
- border-color: #9c88ff;
252
- color: #9c88ff;
 
253
  }
254
 
255
  .flow-icon.graph {
256
- border-color: var(--dash-info);
257
- color: var(--dash-info);
 
258
  }
259
 
260
  .flow-icon.ai {
261
- border-color: var(--dash-accent);
262
- color: var(--dash-accent);
 
263
  }
264
 
265
  .flow-step span {
266
- font-size: 13px;
267
- color: var(--dash-text-muted);
268
- font-weight: 500;
 
269
  }
270
 
271
  .flow-arrow {
272
- color: var(--dash-text-muted);
273
- opacity: 0.4;
 
274
  }
275
 
276
  /* Sections */
277
  .features-section {
278
- margin-bottom: 48px;
279
  }
280
 
281
  .section-header {
282
  display: flex;
283
  align-items: center;
284
  justify-content: space-between;
285
- margin-bottom: 20px;
286
  flex-wrap: wrap;
287
  gap: 16px;
 
288
  }
289
 
290
  .section-header-left {
@@ -294,53 +352,62 @@
294
  }
295
 
296
  .section-icon {
297
- width: 44px;
298
- height: 44px;
299
- padding: 10px;
300
- border-radius: 12px;
301
  flex-shrink: 0;
 
302
  }
303
 
304
  .section-icon.memory {
305
- background: rgba(156, 136, 255, 0.15);
306
- color: #9c88ff;
307
  }
308
 
309
  .section-icon.graph {
310
- background: rgba(122, 156, 198, 0.15);
311
- color: var(--dash-info);
312
  }
313
 
314
  .section-header h2 {
315
  margin: 0;
316
- font-size: 1.35rem;
317
- color: var(--dash-text);
 
 
 
318
  }
319
 
320
  .section-header p {
321
  margin: 4px 0 0;
322
  font-size: 14px;
323
- color: var(--dash-text-muted);
 
324
  }
325
 
326
  .section-badge {
327
  display: inline-flex;
328
  align-items: center;
329
  gap: 6px;
330
- padding: 6px 12px;
331
- background: var(--dash-card-bg);
332
- border: 1px solid var(--dash-border);
333
- border-radius: 100px;
 
334
  font-size: 12px;
335
- color: var(--dash-text-muted);
 
336
  }
337
 
338
  .section-content {
339
- background: var(--dash-card-bg);
340
- border: 1px solid var(--dash-border);
341
- border-radius: 20px;
342
- padding: 4px;
 
343
  overflow: hidden;
 
344
  }
345
 
346
  .section-content-large {
@@ -353,56 +420,139 @@
353
  background: transparent;
354
  border: none;
355
  border-radius: 0;
 
356
  }
357
 
358
  .section-content .memory-header,
359
  .section-content .health-graph-header {
360
  cursor: default;
361
- background: rgba(255, 255, 255, 0.02);
362
- border-radius: 16px 16px 0 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  padding: 16px 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  }
365
 
366
  /* CTA Section */
367
  .features-cta {
368
  text-align: center;
369
- padding: 60px 0;
370
- margin-top: 32px;
 
 
 
 
 
 
371
  }
372
 
373
  .features-cta h3 {
374
- font-size: 1.75rem;
375
- color: var(--dash-text);
376
- margin: 0 0 12px;
 
 
 
377
  }
378
 
379
  .features-cta p {
380
- color: var(--dash-text-muted);
381
- margin: 0 0 28px;
 
 
 
382
  }
383
 
384
  .cta-button {
385
  display: inline-flex;
386
  align-items: center;
387
  gap: 10px;
388
- padding: 14px 28px;
389
- background: linear-gradient(135deg, var(--dash-accent), var(--dash-accent-dark));
390
  color: white;
391
  border: none;
392
- border-radius: 12px;
393
- font-size: 15px;
394
  font-weight: 600;
395
  cursor: pointer;
396
- transition: all 0.2s ease;
 
 
397
  }
398
 
399
  .cta-button:hover {
400
  transform: translateY(-2px);
401
- box-shadow: 0 8px 24px rgba(106, 145, 117, 0.3);
402
  }
403
 
404
  /* Responsive */
405
  @media (max-width: 768px) {
 
 
 
 
406
  .features-hero h1 {
407
  font-size: 2.25rem;
408
  }
@@ -411,6 +561,10 @@
411
  grid-template-columns: 1fr;
412
  }
413
 
 
 
 
 
414
  .flow-diagram {
415
  flex-direction: column;
416
  }
@@ -422,6 +576,11 @@
422
  .section-header {
423
  flex-direction: column;
424
  align-items: flex-start;
 
 
 
 
 
425
  }
426
  }
427
 
 
1
+ /* Features Page Styles - Lumea Theme */
2
 
3
  .features-page {
4
  min-height: 100vh;
5
+ background: linear-gradient(135deg, #f8f5f0 0%, #f0ebe3 100%);
6
  position: relative;
7
  overflow-x: hidden;
8
  }
9
 
10
+ /* Animated Background with Radial Gradients */
11
  .features-background {
12
  position: fixed;
13
  inset: 0;
14
  z-index: 0;
15
  overflow: hidden;
16
  pointer-events: none;
17
+ background:
18
+ radial-gradient(circle at 20% 30%, rgba(107, 145, 117, 0.08) 0%, transparent 50%),
19
+ radial-gradient(circle at 80% 70%, rgba(217, 169, 98, 0.06) 0%, transparent 50%),
20
+ radial-gradient(circle at 50% 50%, rgba(122, 156, 198, 0.04) 0%, transparent 60%);
21
  }
22
 
23
  .features-bg-blob {
24
  position: absolute;
25
  border-radius: 50%;
26
+ filter: blur(140px);
27
+ opacity: 0.15;
28
+ animation: blobFloat 25s ease-in-out infinite;
29
  }
30
 
31
  .features-bg-blob-1 {
32
+ width: 500px;
33
+ height: 500px;
34
+ background: radial-gradient(circle, rgba(107, 145, 117, 0.3), transparent);
35
+ top: -150px;
36
+ right: -150px;
37
  animation-delay: 0s;
38
  }
39
 
40
  .features-bg-blob-2 {
41
+ width: 450px;
42
+ height: 450px;
43
+ background: radial-gradient(circle, rgba(217, 169, 98, 0.25), transparent);
44
+ bottom: -100px;
45
+ left: -100px;
46
+ animation-delay: -8s;
47
  }
48
 
49
  .features-bg-blob-3 {
50
+ width: 350px;
51
+ height: 350px;
52
+ background: radial-gradient(circle, rgba(122, 156, 198, 0.2), transparent);
53
+ top: 40%;
54
  left: 50%;
55
  transform: translate(-50%, -50%);
56
+ animation-delay: -16s;
57
  }
58
 
59
  @keyframes blobFloat {
60
+ 0%, 100% {
 
 
61
  transform: translate(0, 0) scale(1);
62
  }
 
63
  25% {
64
+ transform: translate(30px, -40px) scale(1.08);
65
  }
 
66
  50% {
67
+ transform: translate(-30px, 30px) scale(0.92);
68
  }
 
69
  75% {
70
+ transform: translate(40px, 15px) scale(1.04);
71
  }
72
  }
73
 
 
75
  .features-content {
76
  position: relative;
77
  z-index: 1;
78
+ padding: 110px 24px 80px;
79
  }
80
 
81
  .features-container {
82
+ max-width: 1300px;
83
  margin: 0 auto;
84
+ padding: 0 var(--dash-spacing-lg, 1.5rem);
85
  }
86
 
87
  .features-loading {
 
97
  /* Hero Section */
98
  .features-hero {
99
  text-align: center;
100
+ padding: 40px 0 60px;
101
+ max-width: 900px;
102
+ margin: 0 auto;
103
  }
104
 
105
  .hero-badge {
106
  display: inline-flex;
107
  align-items: center;
108
  gap: 8px;
109
+ padding: 8px 18px;
110
+ background: rgba(232, 243, 235, 0.7);
111
+ backdrop-filter: blur(8px);
112
+ border: 1px solid rgba(107, 145, 117, 0.2);
113
+ border-radius: var(--dash-radius-full, 9999px);
114
+ color: var(--dash-accent-dark, #4a7c59);
115
  font-size: 13px;
116
+ font-weight: 600;
117
  margin-bottom: 24px;
118
+ box-shadow: 0 2px 8px rgba(107, 145, 117, 0.1);
119
  }
120
 
121
  .features-hero h1 {
122
+ font-size: 3.25rem;
123
  font-weight: 700;
124
+ color: var(--dash-text, #2d2d2d);
125
  margin: 0 0 20px;
126
+ line-height: 1.15;
127
+ font-family: var(--dash-font-serif, 'Playfair Display', Georgia, serif);
128
+ letter-spacing: -0.02em;
129
  }
130
 
131
  .gradient-text {
132
+ color: var(--dash-accent, #6b9175);
133
+ position: relative;
 
 
134
  }
135
 
136
  .hero-description {
137
+ font-size: 1.125rem;
138
+ color: var(--dash-text-secondary, #6b6b6b);
139
+ max-width: 680px;
140
+ margin: 0 auto 56px;
141
  line-height: 1.7;
142
+ font-family: var(--dash-font-sans, 'DM Sans', sans-serif);
143
  }
144
 
145
+ /* Hero Features Grid - Glassy Cards */
146
  .hero-features {
147
  display: grid;
148
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
149
  gap: 24px;
150
+ max-width: 1100px;
151
+ margin: 0 auto 64px;
152
  text-align: left;
153
  }
154
 
155
  .hero-feature {
156
  display: flex;
157
+ gap: 18px;
158
+ padding: 24px;
159
+ background: rgba(255, 255, 255, 0.5);
160
+ backdrop-filter: blur(12px);
161
+ border: 1px solid rgba(107, 145, 117, 0.15);
162
+ border-radius: var(--dash-radius-xl, 1.5rem);
163
+ box-shadow: 0 8px 24px rgba(107, 145, 117, 0.08);
164
+ transition: all var(--dash-transition-base, 250ms);
165
+ position: relative;
166
+ overflow: hidden;
167
+ }
168
+
169
+ .hero-feature::before {
170
+ content: '';
171
+ position: absolute;
172
+ top: 0;
173
+ left: 0;
174
+ width: 4px;
175
+ height: 100%;
176
+ background: linear-gradient(180deg, var(--dash-accent) 0%, var(--dash-accent-light) 100%);
177
+ opacity: 0;
178
+ transition: opacity var(--dash-transition-base, 250ms);
179
  }
180
 
181
  .hero-feature:hover {
182
+ transform: translateY(-4px);
183
+ border-color: rgba(107, 145, 117, 0.3);
184
+ background: rgba(255, 255, 255, 0.7);
185
+ box-shadow: 0 12px 32px rgba(107, 145, 117, 0.15);
186
+ }
187
+
188
+ .hero-feature:hover::before {
189
+ opacity: 1;
190
  }
191
 
192
  .hero-feature-icon {
193
+ width: 52px;
194
+ height: 52px;
195
+ border-radius: var(--dash-radius-lg, 1rem);
196
  display: flex;
197
  align-items: center;
198
  justify-content: center;
199
  flex-shrink: 0;
200
+ position: relative;
201
+ background: linear-gradient(135deg, rgba(107, 145, 117, 0.15), rgba(107, 145, 117, 0.08));
202
  }
203
 
204
  .hero-feature-icon.memory {
205
+ background: linear-gradient(135deg, rgba(107, 145, 117, 0.2), rgba(107, 145, 117, 0.1));
206
+ color: var(--dash-accent, #6b9175);
207
  }
208
 
209
  .hero-feature-icon.graph {
210
+ background: linear-gradient(135deg, rgba(122, 156, 198, 0.2), rgba(122, 156, 198, 0.1));
211
+ color: var(--dash-info, #7a9cc6);
212
  }
213
 
214
  .hero-feature-icon.ai {
215
+ background: linear-gradient(135deg, rgba(74, 124, 89, 0.2), rgba(74, 124, 89, 0.1));
216
+ color: var(--dash-accent-dark, #4a7c59);
217
  }
218
 
219
  .hero-feature-content h4 {
220
+ margin: 0 0 4px;
221
+ font-size: 16px;
222
  font-weight: 600;
223
+ color: var(--dash-text, #2d2d2d);
224
+ font-family: var(--dash-font-sans, 'DM Sans', sans-serif);
225
  }
226
 
227
  .hero-feature-content p {
228
  margin: 0;
229
+ font-size: 14px;
230
+ color: var(--dash-text-secondary, #6b6b6b);
231
+ line-height: 1.6;
232
+ font-family: var(--dash-font-sans, 'DM Sans', sans-serif);
233
  }
234
 
235
  .scroll-indicator {
 
237
  opacity: 0.5;
238
  }
239
 
240
+ /* Flow Diagram - Tinted Container */
241
  .features-flow {
242
+ padding: 48px;
243
+ margin: 0 auto 64px;
244
+ max-width: 1100px;
245
+ background: rgba(255, 255, 255, 0.4);
246
+ backdrop-filter: blur(16px);
247
+ border: 1px solid rgba(107, 145, 117, 0.12);
248
+ border-radius: var(--dash-radius-xl, 1.5rem);
249
+ box-shadow: 0 8px 24px rgba(107, 145, 117, 0.08);
250
  }
251
 
252
  .features-flow h2 {
253
+ font-size: 1.75rem;
254
+ color: var(--dash-text, #2d2d2d);
255
+ margin: 0 0 12px;
256
+ text-align: center;
257
+ font-family: var(--dash-font-serif, 'Playfair Display', Georgia, serif);
258
+ font-weight: 600;
259
+ letter-spacing: -0.01em;
260
+ }
261
+
262
+ .features-flow > p {
263
+ text-align: center;
264
+ color: var(--dash-text-secondary, #6b6b6b);
265
+ font-size: 14px;
266
+ margin: 0 0 40px;
267
+ max-width: 600px;
268
+ margin-left: auto;
269
+ margin-right: auto;
270
  }
271
 
272
  .flow-diagram {
273
  display: flex;
274
  align-items: center;
275
  justify-content: center;
276
+ gap: 20px;
277
  flex-wrap: wrap;
278
  }
279
 
 
281
  display: flex;
282
  flex-direction: column;
283
  align-items: center;
284
+ gap: 14px;
285
  }
286
 
287
  .flow-icon {
288
+ width: 72px;
289
+ height: 72px;
290
+ border-radius: var(--dash-radius-xl, 1.5rem);
291
+ background: rgba(255, 255, 255, 0.7);
292
+ backdrop-filter: blur(8px);
293
+ border: 2px solid rgba(107, 145, 117, 0.2);
294
  display: flex;
295
  align-items: center;
296
  justify-content: center;
297
+ color: var(--dash-text, #2d2d2d);
298
+ transition: all var(--dash-transition-base, 250ms);
299
+ box-shadow: 0 4px 12px rgba(107, 145, 117, 0.08);
300
  }
301
 
302
  .flow-icon.memory {
303
+ border-color: rgba(107, 145, 117, 0.3);
304
+ color: var(--dash-accent, #6b9175);
305
+ background: linear-gradient(135deg, rgba(232, 243, 235, 0.8), rgba(255, 255, 255, 0.7));
306
  }
307
 
308
  .flow-icon.graph {
309
+ border-color: rgba(122, 156, 198, 0.3);
310
+ color: var(--dash-info, #7a9cc6);
311
+ background: linear-gradient(135deg, rgba(122, 156, 198, 0.15), rgba(255, 255, 255, 0.7));
312
  }
313
 
314
  .flow-icon.ai {
315
+ border-color: rgba(74, 124, 89, 0.3);
316
+ color: var(--dash-accent-dark, #4a7c59);
317
+ background: linear-gradient(135deg, rgba(232, 243, 235, 0.8), rgba(255, 255, 255, 0.7));
318
  }
319
 
320
  .flow-step span {
321
+ font-size: 14px;
322
+ color: var(--dash-text-secondary, #6b6b6b);
323
+ font-weight: 600;
324
+ text-align: center;
325
  }
326
 
327
  .flow-arrow {
328
+ color: var(--dash-accent-light, #8fb199);
329
+ opacity: 0.5;
330
+ font-size: 20px;
331
  }
332
 
333
  /* Sections */
334
  .features-section {
335
+ margin-bottom: 56px;
336
  }
337
 
338
  .section-header {
339
  display: flex;
340
  align-items: center;
341
  justify-content: space-between;
342
+ margin-bottom: 24px;
343
  flex-wrap: wrap;
344
  gap: 16px;
345
+ padding: 0 var(--dash-spacing-md, 1rem);
346
  }
347
 
348
  .section-header-left {
 
352
  }
353
 
354
  .section-icon {
355
+ width: 48px;
356
+ height: 48px;
357
+ padding: 12px;
358
+ border-radius: var(--dash-radius-lg, 1rem);
359
  flex-shrink: 0;
360
+ backdrop-filter: blur(8px);
361
  }
362
 
363
  .section-icon.memory {
364
+ background: linear-gradient(135deg, rgba(107, 145, 117, 0.15), rgba(107, 145, 117, 0.08));
365
+ color: var(--dash-accent, #6b9175);
366
  }
367
 
368
  .section-icon.graph {
369
+ background: linear-gradient(135deg, rgba(122, 156, 198, 0.15), rgba(122, 156, 198, 0.08));
370
+ color: var(--dash-info, #7a9cc6);
371
  }
372
 
373
  .section-header h2 {
374
  margin: 0;
375
+ font-size: 1.5rem;
376
+ color: var(--dash-text, #2d2d2d);
377
+ font-family: var(--dash-font-serif, 'Playfair Display', Georgia, serif);
378
+ font-weight: 600;
379
+ letter-spacing: -0.01em;
380
  }
381
 
382
  .section-header p {
383
  margin: 4px 0 0;
384
  font-size: 14px;
385
+ color: var(--dash-text-secondary, #6b6b6b);
386
+ font-family: var(--dash-font-sans, 'DM Sans', sans-serif);
387
  }
388
 
389
  .section-badge {
390
  display: inline-flex;
391
  align-items: center;
392
  gap: 6px;
393
+ padding: 6px 14px;
394
+ background: rgba(255, 255, 255, 0.6);
395
+ backdrop-filter: blur(8px);
396
+ border: 1px solid rgba(107, 145, 117, 0.15);
397
+ border-radius: var(--dash-radius-full, 9999px);
398
  font-size: 12px;
399
+ font-weight: 600;
400
+ color: var(--dash-accent-dark, #4a7c59);
401
  }
402
 
403
  .section-content {
404
+ background: rgba(255, 255, 255, 0.5);
405
+ backdrop-filter: blur(12px);
406
+ border: 1px solid rgba(107, 145, 117, 0.12);
407
+ border-radius: var(--dash-radius-xl, 1.5rem);
408
+ padding: 6px;
409
  overflow: hidden;
410
+ box-shadow: 0 8px 24px rgba(107, 145, 117, 0.08);
411
  }
412
 
413
  .section-content-large {
 
420
  background: transparent;
421
  border: none;
422
  border-radius: 0;
423
+ box-shadow: none;
424
  }
425
 
426
  .section-content .memory-header,
427
  .section-content .health-graph-header {
428
  cursor: default;
429
+ background: rgba(255, 255, 255, 0.4);
430
+ backdrop-filter: blur(8px);
431
+ border-radius: var(--dash-radius-lg, 1rem) var(--dash-radius-lg, 1rem) 0 0;
432
+ padding: 18px 24px;
433
+ border-bottom: 1px solid rgba(107, 145, 117, 0.1);
434
+ }
435
+
436
+ /* Memory Dashboard GitHub-style */
437
+ .section-content .memory-list {
438
+ padding: 20px;
439
+ background: transparent;
440
+ }
441
+
442
+ .section-content .memory-item {
443
+ background: rgba(255, 255, 255, 0.45);
444
+ backdrop-filter: blur(8px);
445
+ border: 1px solid rgba(107, 145, 117, 0.15);
446
+ border-left: 3px solid var(--dash-accent, #6b9175);
447
+ border-radius: var(--dash-radius-lg, 1rem);
448
  padding: 16px 20px;
449
+ margin-bottom: 12px;
450
+ box-shadow: 0 2px 8px rgba(107, 145, 117, 0.06);
451
+ transition: all var(--dash-transition-base, 250ms);
452
+ }
453
+
454
+ .section-content .memory-item:hover {
455
+ background: rgba(255, 255, 255, 0.65);
456
+ border-left-color: var(--dash-accent-dark, #4a7c59);
457
+ box-shadow: 0 4px 16px rgba(107, 145, 117, 0.12);
458
+ transform: translateX(2px);
459
+ }
460
+
461
+ .section-content .memory-search {
462
+ background: rgba(255, 255, 255, 0.5);
463
+ backdrop-filter: blur(8px);
464
+ border: 1px solid rgba(107, 145, 117, 0.15);
465
+ border-radius: var(--dash-radius-lg, 1rem);
466
+ padding: 10px 16px;
467
+ }
468
+
469
+ .section-content .memory-search:focus-within {
470
+ background: rgba(255, 255, 255, 0.7);
471
+ border-color: var(--dash-accent, #6b9175);
472
+ box-shadow: 0 0 0 3px rgba(232, 243, 235, 0.5);
473
+ }
474
+
475
+ /* Graph Canvas Improvements */
476
+ .section-content .health-graph-canvas-wrapper {
477
+ position: relative;
478
+ padding: var(--dash-spacing-xl, 2rem);
479
+ background: linear-gradient(135deg, rgba(232, 243, 235, 0.2) 0%, rgba(255, 255, 255, 0.3) 100%);
480
+ backdrop-filter: blur(8px);
481
+ margin: 16px;
482
+ border-radius: var(--dash-radius-lg, 1rem);
483
+ border: 1px solid rgba(107, 145, 117, 0.12);
484
+ overflow: visible;
485
+ }
486
+
487
+ .section-content .health-graph-canvas {
488
+ min-height: 450px;
489
+ }
490
+
491
+ .section-content .health-graph-toolbar {
492
+ background: rgba(255, 255, 255, 0.5);
493
+ backdrop-filter: blur(8px);
494
+ border-bottom: 1px solid rgba(107, 145, 117, 0.1);
495
+ padding: 16px 24px;
496
  }
497
 
498
  /* CTA Section */
499
  .features-cta {
500
  text-align: center;
501
+ padding: 80px 32px;
502
+ margin: 64px auto 0;
503
+ max-width: 800px;
504
+ background: rgba(255, 255, 255, 0.4);
505
+ backdrop-filter: blur(16px);
506
+ border: 1px solid rgba(107, 145, 117, 0.15);
507
+ border-radius: var(--dash-radius-xl, 1.5rem);
508
+ box-shadow: 0 8px 32px rgba(107, 145, 117, 0.1);
509
  }
510
 
511
  .features-cta h3 {
512
+ font-size: 2rem;
513
+ color: var(--dash-text, #2d2d2d);
514
+ margin: 0 0 16px;
515
+ font-family: var(--dash-font-serif, 'Playfair Display', Georgia, serif);
516
+ font-weight: 600;
517
+ letter-spacing: -0.01em;
518
  }
519
 
520
  .features-cta p {
521
+ color: var(--dash-text-secondary, #6b6b6b);
522
+ margin: 0 0 32px;
523
+ font-family: var(--dash-font-sans, 'DM Sans', sans-serif);
524
+ font-size: 1.05rem;
525
+ line-height: 1.6;
526
  }
527
 
528
  .cta-button {
529
  display: inline-flex;
530
  align-items: center;
531
  gap: 10px;
532
+ padding: 14px 32px;
533
+ background: linear-gradient(135deg, var(--dash-accent, #6b9175) 0%, var(--dash-accent-dark, #4a7c59) 100%);
534
  color: white;
535
  border: none;
536
+ border-radius: var(--dash-radius-lg, 1rem);
537
+ font-size: 16px;
538
  font-weight: 600;
539
  cursor: pointer;
540
+ transition: all var(--dash-transition-base, 250ms);
541
+ font-family: var(--dash-font-sans, 'DM Sans', sans-serif);
542
+ box-shadow: 0 4px 16px rgba(107, 145, 117, 0.25);
543
  }
544
 
545
  .cta-button:hover {
546
  transform: translateY(-2px);
547
+ box-shadow: 0 8px 24px rgba(107, 145, 117, 0.35);
548
  }
549
 
550
  /* Responsive */
551
  @media (max-width: 768px) {
552
+ .features-content {
553
+ padding: 100px 16px 60px;
554
+ }
555
+
556
  .features-hero h1 {
557
  font-size: 2.25rem;
558
  }
 
561
  grid-template-columns: 1fr;
562
  }
563
 
564
+ .features-flow {
565
+ padding: 32px 24px;
566
+ }
567
+
568
  .flow-diagram {
569
  flex-direction: column;
570
  }
 
576
  .section-header {
577
  flex-direction: column;
578
  align-items: flex-start;
579
+ padding: 0;
580
+ }
581
+
582
+ .features-cta {
583
+ padding: 48px 24px;
584
  }
585
  }
586
 
frontend/src/pages/Features.tsx CHANGED
@@ -320,6 +320,7 @@ function Features() {
320
  viewport={{ once: true }}
321
  >
322
  <h2>How Your Data Flows</h2>
 
323
  <div className="flow-diagram">
324
  <div className="flow-step">
325
  <div className="flow-icon">
 
320
  viewport={{ once: true }}
321
  >
322
  <h2>How Your Data Flows</h2>
323
+ <p>Every piece of health information follows a carefully designed path through our intelligent systems</p>
324
  <div className="flow-diagram">
325
  <div className="flow-step">
326
  <div className="flow-icon">
frontend/src/pages/PhysicsTwin.css CHANGED
@@ -49,6 +49,40 @@
49
  gap: var(--dash-spacing-lg);
50
  }
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  /* ------------------------------------------------------------------ */
53
  /* Twin card (left column) */
54
  /* ------------------------------------------------------------------ */
@@ -299,6 +333,20 @@
299
  /* ------------------------------------------------------------------ */
300
  /* Explainability card */
301
  /* ------------------------------------------------------------------ */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  .physics-explainability-card .dash-card-title {
303
  display: flex;
304
  align-items: center;
 
49
  gap: var(--dash-spacing-lg);
50
  }
51
 
52
+ /* Two cards side-by-side row: YouTube (left) + Explainability (right) */
53
+ .physics-cards-row {
54
+ display: grid;
55
+ grid-template-columns: 1fr 1fr;
56
+ gap: var(--dash-spacing-lg);
57
+ align-items: stretch;
58
+ margin-top: var(--dash-spacing-lg);
59
+ }
60
+
61
+ .physics-cards-row-item {
62
+ min-width: 0;
63
+ height: 100%;
64
+ }
65
+
66
+ .physics-cards-row-item > .dash-card {
67
+ height: 100%;
68
+ min-height: 480px;
69
+ max-height: 700px;
70
+ display: flex;
71
+ flex-direction: column;
72
+ }
73
+
74
+ .physics-cards-row-item > .dash-card .dash-card-body {
75
+ flex: 1;
76
+ overflow-y: auto;
77
+ overflow-x: hidden;
78
+ }
79
+
80
+ @media (max-width: 768px) {
81
+ .physics-cards-row {
82
+ grid-template-columns: 1fr;
83
+ }
84
+ }
85
+
86
  /* ------------------------------------------------------------------ */
87
  /* Twin card (left column) */
88
  /* ------------------------------------------------------------------ */
 
333
  /* ------------------------------------------------------------------ */
334
  /* Explainability card */
335
  /* ------------------------------------------------------------------ */
336
+ .physics-explainability-card {
337
+ display: flex;
338
+ flex-direction: column;
339
+ }
340
+
341
+ .physics-explainability-card .dash-card-body {
342
+ flex: 1;
343
+ display: flex;
344
+ flex-direction: column;
345
+ min-height: 0;
346
+ overflow-y: auto;
347
+ overflow-x: hidden;
348
+ }
349
+
350
  .physics-explainability-card .dash-card-title {
351
  display: flex;
352
  align-items: center;
frontend/src/pages/PhysicsTwin.tsx CHANGED
@@ -24,7 +24,8 @@ import DashboardNavbar from '../components/dashboard/DashboardNavbar';
24
  import OrganTelemetryCard from '../components/physics/OrganTelemetryCard';
25
  import ExplainabilityCard from '../components/physics/ExplainabilityCard';
26
  import BodyImpactOverlay from '../components/physics/BodyImpactOverlay';
27
- import RecommendationsCard from '../components/physics/RecommendationsCard';
 
28
  import {
29
  getLatestSnapshot,
30
  submitMetrics,
@@ -242,6 +243,31 @@ function PhysicsTwin() {
242
  ? snapshot.organs[selectedOrgan]
243
  : null;
244
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  // Current metric value from telemetry
246
  const currentMetrics = telemetry.current?.metrics ?? snapshot?.raw_metrics ?? {};
247
 
@@ -602,15 +628,25 @@ function PhysicsTwin() {
602
  organResult={selectedResult}
603
  lastUpdated={telemetry.current?.timestamp ?? snapshot?.timestamp ?? null}
604
  />
 
 
605
 
 
 
 
 
 
 
 
 
 
 
606
  <ExplainabilityCard
607
  selectedOrgan={selectedOrgan}
608
  organResult={selectedResult}
609
  organLabel={selectedOrgan ? (ORGAN_LABELS[selectedOrgan] || selectedOrgan) : ''}
610
  />
611
-
612
- <RecommendationsCard conditions={conditions} />
613
- </motion.div>
614
  </div>
615
 
616
  {/* Data quality disclaimer */}
 
24
  import OrganTelemetryCard from '../components/physics/OrganTelemetryCard';
25
  import ExplainabilityCard from '../components/physics/ExplainabilityCard';
26
  import BodyImpactOverlay from '../components/physics/BodyImpactOverlay';
27
+ import YouTubeRecommendationsCard from '../components/physics/YouTubeRecommendationsCard';
28
+ import '../components/physics/YouTubeRecommendationsCard.css';
29
  import {
30
  getLatestSnapshot,
31
  submitMetrics,
 
243
  ? snapshot.organs[selectedOrgan]
244
  : null;
245
 
246
+ // Find worst performing organ for recommendations
247
+ const worstOrgan = useMemo(() => {
248
+ if (!snapshot?.organs) return null;
249
+
250
+ let worst: { organ: string; score: number; result: OrganResult } | null = null;
251
+
252
+ for (const [organKey, organData] of Object.entries(snapshot.organs)) {
253
+ // Skip if score is 100 (perfect health)
254
+ if (organData.score >= 100) continue;
255
+
256
+ if (!worst || organData.score < worst.score) {
257
+ worst = { organ: organKey, score: organData.score, result: organData };
258
+ }
259
+ }
260
+
261
+ return worst;
262
+ }, [snapshot]);
263
+
264
+ // Auto-select worst organ if nothing selected
265
+ useEffect(() => {
266
+ if (!selectedOrgan && worstOrgan) {
267
+ setSelectedOrgan(worstOrgan.organ);
268
+ }
269
+ }, [worstOrgan, selectedOrgan]);
270
+
271
  // Current metric value from telemetry
272
  const currentMetrics = telemetry.current?.metrics ?? snapshot?.raw_metrics ?? {};
273
 
 
628
  organResult={selectedResult}
629
  lastUpdated={telemetry.current?.timestamp ?? snapshot?.timestamp ?? null}
630
  />
631
+ </motion.div>
632
+ </div>
633
 
634
+ {/* Full-width two-column layout: YouTube (left) + Explainability (right) */}
635
+ <div className="physics-cards-row">
636
+ <div className="physics-cards-row-item">
637
+ <YouTubeRecommendationsCard
638
+ selectedOrgan={selectedOrgan}
639
+ organResult={selectedResult}
640
+ currentMetrics={currentMetrics}
641
+ />
642
+ </div>
643
+ <div className="physics-cards-row-item">
644
  <ExplainabilityCard
645
  selectedOrgan={selectedOrgan}
646
  organResult={selectedResult}
647
  organLabel={selectedOrgan ? (ORGAN_LABELS[selectedOrgan] || selectedOrgan) : ''}
648
  />
649
+ </div>
 
 
650
  </div>
651
 
652
  {/* Data quality disclaimer */}
frontend/src/pages/ReportSummary.css CHANGED
@@ -369,7 +369,9 @@
369
 
370
  /* Center Panel - Preview */
371
  .center-panel .panel-card {
372
- min-height: 500px;
 
 
373
  }
374
 
375
  .preview-panel {
@@ -409,6 +411,7 @@
409
  /* Preview Single */
410
  .preview-single {
411
  flex: 1;
 
412
  display: flex;
413
  flex-direction: column;
414
  }
@@ -435,8 +438,11 @@
435
 
436
  .preview-frame {
437
  flex: 1;
438
- min-height: 400px;
439
  background: #f8f8f8;
 
 
 
440
  }
441
 
442
  .preview-iframe {
@@ -444,6 +450,7 @@
444
  height: 100%;
445
  min-height: 500px;
446
  border: none;
 
447
  }
448
 
449
  .preview-image {
@@ -455,8 +462,10 @@
455
  /* Preview Multi (Grid) */
456
  .preview-multi {
457
  flex: 1;
 
458
  display: flex;
459
  flex-direction: column;
 
460
  }
461
 
462
  .preview-multi-header {
 
369
 
370
  /* Center Panel - Preview */
371
  .center-panel .panel-card {
372
+ min-height: 0;
373
+ display: flex;
374
+ flex-direction: column;
375
  }
376
 
377
  .preview-panel {
 
411
  /* Preview Single */
412
  .preview-single {
413
  flex: 1;
414
+ min-height: 0;
415
  display: flex;
416
  flex-direction: column;
417
  }
 
438
 
439
  .preview-frame {
440
  flex: 1;
441
+ min-height: 0;
442
  background: #f8f8f8;
443
+ overflow: hidden;
444
+ display: flex;
445
+ flex-direction: column;
446
  }
447
 
448
  .preview-iframe {
 
450
  height: 100%;
451
  min-height: 500px;
452
  border: none;
453
+ flex: 1;
454
  }
455
 
456
  .preview-image {
 
462
  /* Preview Multi (Grid) */
463
  .preview-multi {
464
  flex: 1;
465
+ min-height: 0;
466
  display: flex;
467
  flex-direction: column;
468
+ overflow-y: auto;
469
  }
470
 
471
  .preview-multi-header {
frontend/src/services/openRouterService.ts ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * OpenRouter Service
3
+ * Uses AI to generate YouTube video recommendations based on health metrics.
4
+ * NO YouTube API needed - generates video titles and search URLs directly.
5
+ */
6
+
7
+ const OPENROUTER_API_KEY = import.meta.env.VITE_OPENROUTER_API_KEY || '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';
8
+ const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1/chat/completions';
9
+
10
+ export interface AbnormalMetric {
11
+ name: string;
12
+ value: number;
13
+ normalRange: string;
14
+ status: 'high' | 'low';
15
+ }
16
+
17
+ export interface VideoRecommendation {
18
+ title: string;
19
+ description: string;
20
+ url: string;
21
+ duration: string;
22
+ }
23
+
24
+ export async function generateVideoRecommendations(
25
+ organ: string,
26
+ abnormalMetrics: AbnormalMetric[]
27
+ ): Promise<VideoRecommendation[]> {
28
+ // Use fallback if API not configured
29
+ if (!OPENROUTER_API_KEY || OPENROUTER_API_KEY.includes('your_key')) {
30
+ console.warn('OpenRouter API key not configured, using fallback recommendations');
31
+ return generateFallbackRecommendations(organ, abnormalMetrics);
32
+ }
33
+
34
+ try {
35
+ const prompt = buildPrompt(organ, abnormalMetrics);
36
+
37
+ const response = await fetch(OPENROUTER_BASE_URL, {
38
+ method: 'POST',
39
+ headers: {
40
+ 'Authorization': `Bearer ${OPENROUTER_API_KEY}`,
41
+ 'Content-Type': 'application/json',
42
+ 'HTTP-Referer': window.location.origin,
43
+ 'X-Title': 'Lumea Health - Physics Twin',
44
+ },
45
+ body: JSON.stringify({
46
+ model: 'openai/gpt-4o-mini',
47
+ messages: [
48
+ {
49
+ role: 'system',
50
+ content: 'You are a health education expert. Generate 6 YouTube video recommendations with titles (max 60 chars), descriptions (max 100 chars), and search queries (5-8 words). Return ONLY valid JSON: {"videos":[{"title":"...","description":"...","searchQuery":"..."}]}',
51
+ },
52
+ {
53
+ role: 'user',
54
+ content: prompt,
55
+ },
56
+ ],
57
+ temperature: 0.8,
58
+ max_tokens: 1200,
59
+ }),
60
+ });
61
+
62
+ if (!response.ok) {
63
+ throw new Error(`OpenRouter API error: ${response.status}`);
64
+ }
65
+
66
+ const data = await response.json();
67
+ const content = data.choices?.[0]?.message?.content?.trim() || '';
68
+
69
+ if (content) {
70
+ // Try to parse JSON response
71
+ const parsed = JSON.parse(content);
72
+ const videos = parsed.videos || [];
73
+
74
+ if (Array.isArray(videos) && videos.length > 0) {
75
+ return videos.slice(0, 6).map((v: any, index: number) => ({
76
+ title: v.title || `${organ} Health Video ${index + 1}`,
77
+ description: v.description || 'Learn how to improve your health naturally',
78
+ url: `https://www.youtube.com/results?search_query=${encodeURIComponent(v.searchQuery || `${organ} health`)}`,
79
+ duration: estimateDuration(v.searchQuery || ''),
80
+ }));
81
+ }
82
+ }
83
+
84
+ return generateFallbackRecommendations(organ, abnormalMetrics);
85
+ } catch (error) {
86
+ console.error('OpenRouter recommendation generation failed:', error);
87
+ return generateFallbackRecommendations(organ, abnormalMetrics);
88
+ }
89
+ }
90
+
91
+ function buildPrompt(organ: string, abnormalMetrics: AbnormalMetric[]): string {
92
+ if (abnormalMetrics.length === 0) {
93
+ return `Generate 6 YouTube video recommendations for improving ${organ} health. Include titles about: natural remedies, diet tips, exercises, lifestyle changes, supplements, and prevention. Make titles engaging and search queries specific.`;
94
+ }
95
+
96
+ const issues = abnormalMetrics
97
+ .slice(0, 3)
98
+ .map(m => `${m.name} is ${m.status} (normal: ${m.normalRange})`)
99
+ .join(', ');
100
+
101
+ return `Generate 6 YouTube video recommendations for ${organ} health when ${issues}. Include: diet changes, exercises, supplements, stress management, sleep tips, and medical advice. Make titles specific and actionable.`;
102
+ }
103
+
104
+ function generateFallbackRecommendations(organ: string, abnormalMetrics: AbnormalMetric[]): VideoRecommendation[] {
105
+ const organLower = organ.toLowerCase();
106
+ const topIssue = abnormalMetrics[0]?.name.toLowerCase() || 'health';
107
+
108
+ return [
109
+ {
110
+ title: `How to Improve ${organ} Health Naturally`,
111
+ description: 'Evidence-based tips for better organ function and overall wellness',
112
+ url: `https://www.youtube.com/results?search_query=improve+${organLower}+health+naturally`,
113
+ duration: '10-15 min',
114
+ },
115
+ {
116
+ title: `Best Foods for ${organ} Health`,
117
+ description: 'Nutritional guide for optimal function and disease prevention',
118
+ url: `https://www.youtube.com/results?search_query=best+foods+for+${organLower}`,
119
+ duration: '8-12 min',
120
+ },
121
+ {
122
+ title: `${organ} Function: What You Need to Know`,
123
+ description: 'Understanding how your organ works and signs of problems',
124
+ url: `https://www.youtube.com/results?search_query=${organLower}+function+explained`,
125
+ duration: '12-18 min',
126
+ },
127
+ {
128
+ title: `Exercises for ${organ} Health`,
129
+ description: 'Physical activities to support and improve organ function',
130
+ url: `https://www.youtube.com/results?search_query=exercises+for+${organLower}+health`,
131
+ duration: '15-20 min',
132
+ },
133
+ {
134
+ title: `Managing ${topIssue} for Better ${organ} Health`,
135
+ description: 'Targeted strategies for your specific health concerns',
136
+ url: `https://www.youtube.com/results?search_query=${topIssue}+${organLower}+health`,
137
+ duration: '10-15 min',
138
+ },
139
+ {
140
+ title: `${organ} Health Supplements and Remedies`,
141
+ description: 'Natural support options and what science says',
142
+ url: `https://www.youtube.com/results?search_query=${organLower}+supplements+remedies`,
143
+ duration: '8-12 min',
144
+ },
145
+ ];
146
+ }
147
+
148
+ function estimateDuration(searchQuery: string): string {
149
+ // Estimate duration based on query complexity
150
+ const words = searchQuery.split(' ').length;
151
+ if (words <= 3) return '5-10 min';
152
+ if (words <= 5) return '8-15 min';
153
+ return '10-20 min';
154
+ }
frontend/src/services/youtubeService.ts ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * YouTube Data API v3 Service
3
+ * Fetches real video data with thumbnails
4
+ */
5
+
6
+ export interface YouTubeVideo {
7
+ videoId: string;
8
+ title: string;
9
+ channelTitle: string;
10
+ publishedAt: string;
11
+ thumbnailUrl: string;
12
+ url: string;
13
+ description: string;
14
+ }
15
+
16
+ interface YouTubeSearchResponse {
17
+ items: Array<{
18
+ id: { videoId: string };
19
+ snippet: {
20
+ title: string;
21
+ channelTitle: string;
22
+ publishedAt: string;
23
+ thumbnails: {
24
+ default?: { url: string };
25
+ medium?: { url: string };
26
+ high?: { url: string };
27
+ };
28
+ description: string;
29
+ };
30
+ }>;
31
+ }
32
+
33
+ const YOUTUBE_API_KEY = import.meta.env.VITE_YOUTUBE_API_KEY;
34
+ const YOUTUBE_API_BASE = 'https://www.googleapis.com/youtube/v3';
35
+
36
+ export async function searchYouTubeVideos(query: string): Promise<YouTubeVideo[]> {
37
+ if (!YOUTUBE_API_KEY) {
38
+ console.warn('YouTube API key not configured');
39
+ // Return placeholder data that opens YouTube search
40
+ return [{
41
+ videoId: 'placeholder',
42
+ title: query,
43
+ channelTitle: 'Search YouTube',
44
+ publishedAt: new Date().toISOString(),
45
+ thumbnailUrl: '',
46
+ url: `https://www.youtube.com/results?search_query=${encodeURIComponent(query)}`,
47
+ description: 'Click to search YouTube for this topic',
48
+ }];
49
+ }
50
+
51
+ try {
52
+ const params = new URLSearchParams({
53
+ part: 'snippet',
54
+ type: 'video',
55
+ maxResults: '6',
56
+ videoEmbeddable: 'true',
57
+ safeSearch: 'strict',
58
+ relevanceLanguage: 'en',
59
+ q: query,
60
+ key: YOUTUBE_API_KEY,
61
+ });
62
+
63
+ const response = await fetch(`${YOUTUBE_API_BASE}/search?${params}`);
64
+
65
+ if (!response.ok) {
66
+ throw new Error(`YouTube API error: ${response.statusText}`);
67
+ }
68
+
69
+ const data: YouTubeSearchResponse = await response.json();
70
+
71
+ return data.items.map(item => {
72
+ const thumbnails = item.snippet.thumbnails;
73
+ const thumbnailUrl =
74
+ thumbnails.medium?.url ||
75
+ thumbnails.high?.url ||
76
+ thumbnails.default?.url ||
77
+ '';
78
+
79
+ return {
80
+ videoId: item.id.videoId,
81
+ title: item.snippet.title,
82
+ channelTitle: item.snippet.channelTitle,
83
+ publishedAt: item.snippet.publishedAt,
84
+ thumbnailUrl,
85
+ url: `https://www.youtube.com/watch?v=${item.id.videoId}`,
86
+ description: item.snippet.description,
87
+ };
88
+ });
89
+ } catch (error) {
90
+ console.error('Error fetching YouTube videos:', error);
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ export function formatTimeAgo(dateString: string): string {
96
+ const date = new Date(dateString);
97
+ const now = new Date();
98
+ const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
99
+
100
+ if (seconds < 60) return 'just now';
101
+ const minutes = Math.floor(seconds / 60);
102
+ if (minutes < 60) return `${minutes}m ago`;
103
+ const hours = Math.floor(minutes / 60);
104
+ if (hours < 24) return `${hours}h ago`;
105
+ const days = Math.floor(hours / 24);
106
+ if (days < 30) return `${days}d ago`;
107
+ const months = Math.floor(days / 30);
108
+ if (months < 12) return `${months}mo ago`;
109
+ const years = Math.floor(months / 12);
110
+ return `${years}y ago`;
111
+ }