darved2305 commited on
Commit
5beb8a2
·
1 Parent(s): b718129

ui errors fix and readme update with new features implemented

Browse files
Files changed (44) hide show
  1. README.md +438 -14
  2. backend/.env.example +1 -1
  3. backend/app/main.py +2 -2
  4. backend/app/routes/__init__.py +1 -1
  5. backend/app/routes/documents.py +1 -1
  6. backend/app/services/__init__.py +1 -1
  7. backend/app/services/document_classifier.py +1 -1
  8. backend/app/services/document_processing.py +1 -1
  9. backend/app/services/grok_extractor.py +1 -1
  10. backend/app/services/metric_extractor.py +1 -1
  11. backend/src/__init__.py +1 -1
  12. backend/src/app.py +3 -3
  13. backend/src/routes/__init__.py +1 -1
  14. backend/src/services/__init__.py +1 -1
  15. backend/start.bat +1 -1
  16. backend/tests/__init__.py +1 -1
  17. frontend/assets/logo.png +0 -0
  18. frontend/index.html +2 -1
  19. frontend/package.json +1 -1
  20. frontend/src/assets/logo.png +0 -0
  21. frontend/src/components/ArtOfNaturalRemedies.tsx +7 -1
  22. frontend/src/components/Footer.tsx +3 -3
  23. frontend/src/components/Header.css +3 -1
  24. frontend/src/components/Header.tsx +12 -30
  25. frontend/src/components/Hero.css +5 -5
  26. frontend/src/components/HowWeCanHelp.css +3 -3
  27. frontend/src/components/dashboard/DashboardNavbar.tsx +2 -11
  28. frontend/src/components/ui/Logo.css +29 -0
  29. frontend/src/components/ui/Logo.tsx +22 -0
  30. frontend/src/components/ui/PdfPreview.css +48 -0
  31. frontend/src/components/ui/PdfPreview.tsx +65 -0
  32. frontend/src/components/ui/Tooltip.css +84 -0
  33. frontend/src/components/ui/Tooltip.tsx +29 -0
  34. frontend/src/components/ui/index.ts +3 -0
  35. frontend/src/i18n/locales/en.json +7 -7
  36. frontend/src/i18n/locales/hi.json +7 -7
  37. frontend/src/i18n/locales/mr.json +7 -7
  38. frontend/src/index.css +4 -0
  39. frontend/src/pages/Dashboard.tsx +14 -7
  40. frontend/src/pages/DashboardExtras.css +27 -0
  41. frontend/src/pages/ReportSummary.css +25 -0
  42. frontend/src/pages/ReportSummary.tsx +104 -114
  43. frontend/src/pages/Reports.tsx +85 -35
  44. frontend/src/pages/ReportsExtras.css +126 -0
README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Co-Code GGW 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
 
@@ -12,6 +12,7 @@ A full-stack health companion platform for preventive health management. Upload
12
  - [Tech Stack](#tech-stack)
13
  - [Architecture](#architecture)
14
  - [Features](#features)
 
15
  - [Getting Started](#getting-started)
16
  - [Environment Variables](#environment-variables)
17
  - [Database](#database)
@@ -26,7 +27,7 @@ A full-stack health companion platform for preventive health management. Upload
26
 
27
  ## Overview
28
 
29
- Co-Code GGW is a unified medical companion platform that enables users to:
30
 
31
  - **Upload medical reports** (PDF, images) and automatically extract health metrics via OCR
32
  - **Track health profiles** with comprehensive intake forms (conditions, medications, allergies, family history)
@@ -198,6 +199,218 @@ sequenceDiagram
198
 
199
  ---
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  ## Getting Started
202
 
203
  ### Prerequisites
@@ -277,18 +490,27 @@ Services:
277
 
278
  Create a `.env` file in the `backend/` directory:
279
 
280
- | Variable | Description | Example |
281
- |----------|-------------|---------|
282
- | `DATABASE_URL` | PostgreSQL connection string (asyncpg) | `postgresql+asyncpg://user:pass@host:5432/db` |
283
- | `JWT_SECRET` | Secret key for JWT signing | `your-secret-key-min-32-chars` |
284
- | `JWT_ALGORITHM` | JWT algorithm | `HS256` |
285
- | `ACCESS_TOKEN_EXPIRE_MINUTES` | Token expiration | `10080` (7 days) |
286
- | `FRONTEND_ORIGIN` | CORS allowed origin | `http://localhost:5173` |
287
- | `GROK_API_KEY` | xAI Grok API key (for AI summaries) | `gsk_...` |
288
- | `GEMINI_API_KEY` | Google Gemini API key (optional fallback) | `AIza...` |
289
- | `OLLAMA_BASE_URL` | Ollama server URL (optional) | `http://localhost:11434` |
290
- | `OLLAMA_MODEL` | Ollama model name | `medgemma:4b` |
291
- | `USE_GEMINI_FALLBACK` | Enable Gemini fallback | `true` |
 
 
 
 
 
 
 
 
 
292
 
293
  See [backend/.env.example](backend/.env.example) for a complete template.
294
 
@@ -332,6 +554,11 @@ alembic current
332
  | `chat_messages` | Chat message history |
333
  | `report_ai_summaries` | Cached AI report summaries |
334
  | `report_ai_comparisons` | Cached AI report comparisons |
 
 
 
 
 
335
 
336
  ### Entity Relationships
337
 
@@ -347,13 +574,85 @@ erDiagram
347
  users ||--o{ chat_sessions : owns
348
  users ||--o{ report_ai_summaries : generates
349
  users ||--o{ report_ai_comparisons : generates
 
 
 
 
350
 
351
  reports ||--o{ observations : extracts
352
  reports ||--o{ report_ai_summaries : summarized_by
353
 
354
  chat_sessions ||--o{ chat_messages : contains
 
 
 
355
  ```
356
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  ---
358
 
359
  ## API Reference
@@ -552,7 +851,132 @@ pytest tests/test_routes_auth.py
552
  10. ☐ Generate AI summary, verify it displays
553
  11. ☐ Select 2 reports of same type, generate comparison
554
  12. ☐ Try selecting mixed types, verify warning appears
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
556
  ---
557
 
558
  ## 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
 
 
12
  - [Tech Stack](#tech-stack)
13
  - [Architecture](#architecture)
14
  - [Features](#features)
15
+ - [Medicines: Find Cheap Alternatives](#medicines-find-cheap-alternatives)
16
  - [Getting Started](#getting-started)
17
  - [Environment Variables](#environment-variables)
18
  - [Database](#database)
 
27
 
28
  ## Overview
29
 
30
+ Lumea is a unified medical companion platform that enables users to:
31
 
32
  - **Upload medical reports** (PDF, images) and automatically extract health metrics via OCR
33
  - **Track health profiles** with comprehensive intake forms (conditions, medications, allergies, family history)
 
199
 
200
  ---
201
 
202
+ ## Medicines: Find Cheap Alternatives
203
+
204
+ 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.
205
+
206
+ > ⚠️ **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.
207
+
208
+ ### Overview
209
+
210
+ The Medicines feature enables users to:
211
+
212
+ - **Search for medicines** by brand name or free-text queries
213
+ - **Upload prescriptions** and automatically extract medicines via OCR/AI
214
+ - **Find affordable alternatives** with ranked matching by clinical equivalence
215
+ - **Compare prices** including Jan Aushadhi (government-fixed price) options
216
+ - **Locate nearby pharmacies** including generic and Jan Aushadhi pharmacies
217
+ - **Track medications** with personal notes and dosage schedules
218
+
219
+ ### User Flow
220
+
221
+ 1. **Input Medicine Information**
222
+ - User enters brand name (e.g., "Aspirin 500mg") or uploads prescription image
223
+ - System extracts text via OCR or Grok AI parsing
224
+
225
+ 2. **Normalize Medicine Data**
226
+ - `MedicineNormalizer` service extracts: salt, strength, form (tablet/capsule/liquid), release type (immediate/sustained)
227
+ - Queries `GenericCatalog` table to validate and standardize extraction
228
+
229
+ 3. **Find Substitutes**
230
+ - `SubstituteFinder` queries database for alternatives with 4-tier ranked matching:
231
+ - **Tier 1** (1.0 score): Exact match on salt + strength + form + release type
232
+ - **Tier 2** (0.8 score): Same salt + strength + form
233
+ - **Tier 3** (0.6 score): Same salt + strength
234
+ - **Tier 4** (0.4 score): Same salt only
235
+ - Results sorted by price (Jan Aushadhi first for lowest cost)
236
+
237
+ 4. **Locate Pharmacies** (Optional)
238
+ - User enters location (latitude/longitude) and search radius
239
+ - `PharmacyLocator` queries Google Places API for pharmacies
240
+ - Results include Jan Aushadhi Kendras (government pharmacies) and private pharmacies
241
+ - Results cached for 1 hour to reduce API calls
242
+
243
+ 5. **Save & Track**
244
+ - User can save medicines to personal list with notes
245
+ - Each save creates `UserSavedMedicine` entry for quick reference
246
+
247
+ ### Architecture Diagram
248
+
249
+ ```mermaid
250
+ flowchart TB
251
+ subgraph Frontend["Frontend (React)"]
252
+ MedicinesPage["Medicines.tsx Page"]
253
+ SubstitutePanel["Substitute Finder Panel"]
254
+ PharmacyPanel["Pharmacy Locator Panel"]
255
+ end
256
+
257
+ subgraph API["Backend API (FastAPI)"]
258
+ NormalizeRoute["POST /normalize<br/>POST /normalize/batch"]
259
+ SubstituteRoute["POST /substitutes<br/>POST /substitutes/from-text"]
260
+ PharmacyRoute["GET /pharmacies/nearby<br/>GET /pharmacies/{id}<br/>POST /pharmacies/{id}/click"]
261
+ SavedRoute["POST /saved<br/>GET /saved<br/>DELETE /saved/{id}"]
262
+ end
263
+
264
+ subgraph Services["Backend Services"]
265
+ MedicineNormalizer["MedicineNormalizer<br/>- normalize()<br/>- normalize_batch()"]
266
+ SubstituteFinder["SubstituteFinder<br/>- find_substitutes()<br/>- save_medicine()"]
267
+ PharmacyLocator["PharmacyLocator<br/>- search_nearby()<br/>- get_place_details()<br/>- 1hr cache"]
268
+ GrokService["GrokMedicineService<br/>- get_alternatives_for_text()"]
269
+ end
270
+
271
+ subgraph External["External Services"]
272
+ GooglePlaces["Google Places API<br/>(Pharmacy Search)"]
273
+ GrokAPI["Grok xAI API<br/>(Medicine Parsing)"]
274
+ end
275
+
276
+ subgraph Storage["Data Layer"]
277
+ DB[(PostgreSQL)]
278
+ GenericCatalog["GenericCatalog Table<br/>(600K+ medicines)"]
279
+ UserSavedMedicine["UserSavedMedicine Table"]
280
+ SubstituteQuery["SubstituteQuery Table<br/>(Analytics)"]
281
+ PharmacyClick["PharmacyClick Table<br/>(Analytics)"]
282
+ end
283
+
284
+ MedicinesPage --> SubstitutePanel
285
+ MedicinesPage --> PharmacyPanel
286
+ SubstitutePanel --> SubstituteRoute
287
+ PharmacyPanel --> PharmacyRoute
288
+ PharmacyPanel --> SavedRoute
289
+
290
+ SubstituteRoute --> GrokService
291
+ SubstituteRoute --> MedicineNormalizer
292
+ SubstituteRoute --> SubstituteFinder
293
+ PharmacyRoute --> PharmacyLocator
294
+
295
+ MedicineNormalizer --> DB
296
+ MedicineNormalizer --> GenericCatalog
297
+ SubstituteFinder --> DB
298
+ SubstituteFinder --> GenericCatalog
299
+ SubstituteFinder --> SubstituteQuery
300
+ PharmacyLocator --> GooglePlaces
301
+ PharmacyLocator --> PharmacyClick
302
+
303
+ GrokService --> GrokAPI
304
+ GrokAPI -->|Parsed results| SubstituteFinder
305
+ ```
306
+
307
+ ### API Endpoints
308
+
309
+ | Method | Endpoint | Description | Auth | Key Parameters |
310
+ |--------|----------|-------------|------|-----------------|
311
+ | POST | `/api/medicines/normalize` | Normalize single medicine text | Yes | `text: str` → Returns `NormalizedMedicine` |
312
+ | POST | `/api/medicines/normalize/batch` | Normalize multiple medicines (prescription lines) | Yes | `texts: List[str]` → Returns `List[NormalizedMedicine]` |
313
+ | POST | `/api/medicines/substitutes` | Find substitutes from structured data | Yes | `salt, strength, form, release_type` → Returns ranked substitutes |
314
+ | POST | `/api/medicines/substitutes/from-text` | Find substitutes from free-form text (AI-powered) | Yes | `text: str` → Grok AI parsing → Returns alternatives with prices |
315
+ | GET | `/api/medicines/pharmacies/nearby` | Search nearby pharmacies by location | Yes | `lat, lng, radius_m=1000, type=all, page_token` → Returns paginated pharmacies |
316
+ | GET | `/api/medicines/pharmacies/{place_id}` | Get pharmacy details | Yes | `place_id: str` → Returns address, phone, hours, rating |
317
+ | POST | `/api/medicines/pharmacies/{place_id}/click` | Log pharmacy interaction (analytics) | Yes | `action: directions\|call\|website` |
318
+ | POST | `/api/medicines/saved` | Save medicine to user list | Yes | `brand_name, salt, strength, form, notes` |
319
+ | GET | `/api/medicines/saved` | Get user's saved medicines | Yes | Returns list of `UserSavedMedicine` |
320
+ | DELETE | `/api/medicines/saved/{medicine_id}` | Delete saved medicine | Yes | `medicine_id: UUID` |
321
+
322
+ ### Request/Response Examples
323
+
324
+ **POST /api/medicines/substitutes/from-text**
325
+
326
+ Request:
327
+ ```json
328
+ {
329
+ "text": "Aspirin 500mg tablets"
330
+ }
331
+ ```
332
+
333
+ Response:
334
+ ```json
335
+ {
336
+ "original_text": "Aspirin 500mg tablets",
337
+ "normalized": {
338
+ "brand_name": "Aspirin",
339
+ "salt": "Acetylsalicylic Acid",
340
+ "strength": "500",
341
+ "form": "tablet",
342
+ "release_type": "immediate",
343
+ "confidence": 0.95
344
+ },
345
+ "substitutes": [
346
+ {
347
+ "rank": 1,
348
+ "product_name": "Jan Aushadhi Aspirin 500mg",
349
+ "salt": "Acetylsalicylic Acid",
350
+ "strength": "500",
351
+ "form": "tablet",
352
+ "mrp": "5.00",
353
+ "is_jan_aushadhi": true,
354
+ "match_score": 1.0,
355
+ "match_reason": "Exact match: Same salt, strength, form, and type"
356
+ },
357
+ {
358
+ "rank": 2,
359
+ "product_name": "Ecosprin 500mg",
360
+ "salt": "Acetylsalicylic Acid",
361
+ "strength": "500",
362
+ "form": "tablet",
363
+ "mrp": "8.50",
364
+ "is_jan_aushadhi": false,
365
+ "match_score": 1.0,
366
+ "match_reason": "Exact match: Same salt, strength, form, and type"
367
+ }
368
+ ],
369
+ "disclaimer": "Always confirm with your doctor or pharmacist before switching medicines"
370
+ }
371
+ ```
372
+
373
+ **GET /api/medicines/pharmacies/nearby**
374
+
375
+ Request:
376
+ ```
377
+ GET /api/medicines/pharmacies/nearby?lat=40.7128&lng=-74.0060&radius_m=1000&type=all
378
+ ```
379
+
380
+ Response:
381
+ ```json
382
+ {
383
+ "pharmacies": [
384
+ {
385
+ "place_id": "ChIJN1blbgBQwokRzKgy6E_B_1Q",
386
+ "name": "Jan Aushadhi Kendra - Downtown",
387
+ "address": "123 Main St, New York, NY 10001",
388
+ "latitude": 40.7128,
389
+ "longitude": -74.0060,
390
+ "rating": 4.7,
391
+ "is_open": true,
392
+ "is_jan_aushadhi": true,
393
+ "phone": "+1-212-555-0123"
394
+ },
395
+ {
396
+ "place_id": "ChIJrc_p_1BQwokRzKgy6E_B_2Q",
397
+ "name": "Metro Pharmacy",
398
+ "address": "456 Broadway, New York, NY 10002",
399
+ "latitude": 40.7150,
400
+ "longitude": -74.0030,
401
+ "rating": 4.2,
402
+ "is_open": true,
403
+ "is_jan_aushadhi": false,
404
+ "phone": "+1-212-555-0456"
405
+ }
406
+ ],
407
+ "next_page_token": null,
408
+ "total_results": 2
409
+ }
410
+ ```
411
+
412
+ ---
413
+
414
  ## Getting Started
415
 
416
  ### Prerequisites
 
490
 
491
  Create a `.env` file in the `backend/` directory:
492
 
493
+ | Variable | Description | Example | Required |
494
+ |----------|-------------|---------|----------|
495
+ | `DATABASE_URL` | PostgreSQL connection string (asyncpg) | `postgresql+asyncpg://user:pass@host:5432/db` | Yes |
496
+ | `JWT_SECRET` | Secret key for JWT signing | `your-secret-key-min-32-chars` | Yes |
497
+ | `JWT_ALGORITHM` | JWT algorithm | `HS256` | No (default: HS256) |
498
+ | `ACCESS_TOKEN_EXPIRE_MINUTES` | Token expiration | `10080` (7 days) | No (default: 10080) |
499
+ | `FRONTEND_ORIGIN` | CORS allowed origin | `http://localhost:5173` | Yes |
500
+ | `GROK_API_KEY` | xAI Grok API key (for AI summaries & medicine parsing) | `gsk_...` | Yes |
501
+ | `XAI_API_BASE` | xAI API endpoint base URL | `https://api.x.ai/v1` | No (default provided) |
502
+ | `GROK_MODEL` | Grok model identifier | `grok-beta` | No (default: grok-beta) |
503
+ | `GOOGLE_PLACES_API_KEY` | Google Maps API key (for pharmacy search) | `AIzaSyD...` | No (fallback to mock data) |
504
+ | `GEMINI_API_KEY` | Google Gemini API key (optional fallback for summaries) | `AIza...` | No |
505
+ | `OLLAMA_BASE_URL` | Ollama server URL (optional) | `http://localhost:11434` | No |
506
+ | `OLLAMA_MODEL` | Ollama model name | `medgemma:4b` | No |
507
+ | `USE_GEMINI_FALLBACK` | Enable Gemini fallback when Grok unavailable | `true` | No (default: false) |
508
+
509
+ **Medicines Feature Notes:**
510
+
511
+ - **GROK_API_KEY**: Required for free-text medicine parsing (e.g., "Aspirin 500mg tablets" → structured data)
512
+ - **GOOGLE_PLACES_API_KEY**: Optional for pharmacy search. If not set, returns mock pharmacy data for testing
513
+ - **XAI_API_BASE** & **GROK_MODEL**: Typically use defaults; modify only for different xAI endpoints or model versions
514
 
515
  See [backend/.env.example](backend/.env.example) for a complete template.
516
 
 
554
  | `chat_messages` | Chat message history |
555
  | `report_ai_summaries` | Cached AI report summaries |
556
  | `report_ai_comparisons` | Cached AI report comparisons |
557
+ | `generic_catalog` | Medicine database (600K+ products, indexed by salt/product_name) |
558
+ | `user_saved_medicines` | User's saved medicines (brand name, dosage, notes) |
559
+ | `substitute_queries` | Analytics: medicine searches and results |
560
+ | `pharmacy_clicks` | Analytics: pharmacy interaction tracking (directions, calls, website) |
561
+ | `user_location_consent` | Location permissions for pharmacy search |
562
 
563
  ### Entity Relationships
564
 
 
574
  users ||--o{ chat_sessions : owns
575
  users ||--o{ report_ai_summaries : generates
576
  users ||--o{ report_ai_comparisons : generates
577
+ users ||--o{ user_saved_medicines : saves
578
+ users ||--o{ substitute_queries : searches
579
+ users ||--o{ pharmacy_clicks : clicks
580
+ users ||--o| user_location_consent : grants
581
 
582
  reports ||--o{ observations : extracts
583
  reports ||--o{ report_ai_summaries : summarized_by
584
 
585
  chat_sessions ||--o{ chat_messages : contains
586
+
587
+ user_saved_medicines ||--o{ generic_catalog : references
588
+ substitute_queries ||--o{ generic_catalog : queries
589
  ```
590
 
591
+ ### Medicines Database Schema
592
+
593
+ **generic_catalog** - Seeded from PMBI (Pharmaceutical Market Bureau of India) data
594
+
595
+ | Column | Type | Notes |
596
+ |--------|------|-------|
597
+ | `id` | UUID | Primary key |
598
+ | `product_name` | VARCHAR | Brand/product name (indexed) |
599
+ | `salt` | VARCHAR | Active pharmaceutical ingredient (indexed) |
600
+ | `strength` | VARCHAR | e.g., "500mg", "100mcg" |
601
+ | `form` | VARCHAR | e.g., "tablet", "capsule", "liquid", "injection" |
602
+ | `release_type` | VARCHAR | e.g., "immediate", "sustained" |
603
+ | `mrp` | DECIMAL | Maximum retail price |
604
+ | `manufacturer` | VARCHAR | Manufacturing company |
605
+ | `source` | VARCHAR | Data source (e.g., "jan_aushadhi", "pmbi") |
606
+ | `is_jan_aushadhi` | BOOLEAN | Government-fixed price indicator |
607
+ | `created_at` | TIMESTAMP | Record creation time |
608
+
609
+ **user_saved_medicines** - User's tracked medicines
610
+
611
+ | Column | Type | Notes |
612
+ |--------|------|-------|
613
+ | `id` | UUID | Primary key |
614
+ | `user_id` | UUID | Foreign key → users |
615
+ | `original_name` | VARCHAR | User-entered medicine name |
616
+ | `salt` | VARCHAR | Normalized salt |
617
+ | `strength` | VARCHAR | Normalized strength |
618
+ | `form` | VARCHAR | Normalized form |
619
+ | `release_type` | VARCHAR | Normalized release type |
620
+ | `schedule_json` | JSONB | Optional: dosage schedule (morning, noon, evening) |
621
+ | `notes` | TEXT | User's personal notes |
622
+ | `created_at` | TIMESTAMP | When medicine was saved |
623
+
624
+ **substitute_queries** - Analytics table
625
+
626
+ | Column | Type | Notes |
627
+ |--------|------|-------|
628
+ | `id` | UUID | Primary key |
629
+ | `user_id` | UUID | Foreign key → users |
630
+ | `query_raw` | VARCHAR | Original user input |
631
+ | `normalized_json` | JSONB | Extracted fields (salt, strength, form, release_type) |
632
+ | `results_json` | JSONB | Array of matching substitutes |
633
+ | `results_count` | INTEGER | Number of alternatives found |
634
+ | `created_at` | TIMESTAMP | Query timestamp |
635
+
636
+ **pharmacy_clicks** - Analytics table
637
+
638
+ | Column | Type | Notes |
639
+ |--------|------|-------|
640
+ | `id` | UUID | Primary key |
641
+ | `user_id` | UUID | Foreign key → users |
642
+ | `place_id` | VARCHAR | Google Places API place ID |
643
+ | `place_name` | VARCHAR | Pharmacy name |
644
+ | `mode` | VARCHAR | Action: "directions", "call", "website" |
645
+ | `created_at` | TIMESTAMP | Click timestamp |
646
+
647
+ **user_location_consent** - Privacy tracking
648
+
649
+ | Column | Type | Notes |
650
+ |--------|------|-------|
651
+ | `id` | UUID | Primary key |
652
+ | `user_id` | UUID | Foreign key → users (unique) |
653
+ | `consent` | BOOLEAN | Location permission granted/revoked |
654
+ | `updated_at` | TIMESTAMP | Last update |
655
+
656
  ---
657
 
658
  ## API Reference
 
851
  10. ☐ Generate AI summary, verify it displays
852
  11. ☐ Select 2 reports of same type, generate comparison
853
  12. ☐ Try selecting mixed types, verify warning appears
854
+ 13. ☐ Test Medicines feature: search for a medicine (e.g., "Aspirin 500mg")
855
+ 14. ☐ Verify substitutes appear with prices and Jan Aushadhi options highlighted
856
+ 15. ☐ Test pharmacy search by entering your location
857
+ 16. ☐ Verify nearby pharmacies appear with ratings and distance
858
+ 17. ☐ Try filtering pharmacies by type (all/jan_aushadhi/generic)
859
+ 18. ☐ Save a medicine and verify it appears in saved list
860
+
861
+ ### Medicines Feature Testing
862
+
863
+ ```bash
864
+ cd backend
865
+
866
+ # Test medicines normalization
867
+ pytest tests/test_medicines_normalizer.py
868
+
869
+ # Test substitute finder
870
+ pytest tests/test_medicines_substitutes.py
871
+
872
+ # Test pharmacy locator
873
+ pytest tests/test_medicines_pharmacy_locator.py
874
+
875
+ # Test medicines routes
876
+ pytest tests/test_routes_medicines.py
877
+ ```
878
 
879
+ ### Medicines Manual Testing
880
+
881
+ 1. **Test Medicine Normalization**
882
+ ```bash
883
+ curl -X POST http://localhost:8000/api/medicines/normalize \
884
+ -H "Authorization: Bearer <token>" \
885
+ -H "Content-Type: application/json" \
886
+ -d '{"text": "Aspirin 500mg tablet"}'
887
+ ```
888
+ Expected: `NormalizedMedicine` with extracted salt, strength, form
889
+
890
+ 2. **Test Substitute Search (AI-Powered)**
891
+ ```bash
892
+ curl -X POST http://localhost:8000/api/medicines/substitutes/from-text \
893
+ -H "Authorization: Bearer <token>" \
894
+ -H "Content-Type: application/json" \
895
+ -d '{"text": "Crocin 650mg for fever"}'
896
+ ```
897
+ Expected: Ranked list of alternatives with prices
898
+
899
+ 3. **Test Pharmacy Search**
900
+ ```bash
901
+ curl -X GET "http://localhost:8000/api/medicines/pharmacies/nearby?lat=40.7128&lng=-74.0060&radius_m=1000&type=all" \
902
+ -H "Authorization: Bearer <token>"
903
+ ```
904
+ Expected: List of nearby pharmacies with ratings
905
+
906
+ 4. **Test Save Medicine**
907
+ ```bash
908
+ curl -X POST http://localhost:8000/api/medicines/saved \
909
+ -H "Authorization: Bearer <token>" \
910
+ -H "Content-Type: application/json" \
911
+ -d '{
912
+ "brand_name": "Aspirin",
913
+ "salt": "Acetylsalicylic Acid",
914
+ "strength": "500",
915
+ "form": "tablet",
916
+ "release_type": "immediate",
917
+ "notes": "Take with food"
918
+ }'
919
+ ```
920
+ Expected: `UserSavedMedicine` object saved to database
921
+
922
+ ### Medicines Troubleshooting
923
+
924
+ **Problem: "No alternatives found" when searching for a medicine**
925
+
926
+ - **Cause**: Medicine not in `generic_catalog` table
927
+ - **Solution**:
928
+ - Verify PMBI/Jan Aushadhi seed data was loaded during migration
929
+ - Check database: `SELECT COUNT(*) FROM generic_catalog;` should show 600K+ records
930
+ - Try searching by generic name instead of brand name (e.g., "Acetylsalicylic Acid" instead of "Aspirin")
931
+ - Check logs for Grok API errors if using free-text search
932
+
933
+ **Problem: "Google Places API 401" or pharmacy search returns empty**
934
+
935
+ - **Cause**: `GOOGLE_PLACES_API_KEY` not set or API quota exceeded
936
+ - **Solution**:
937
+ - Verify `GOOGLE_PLACES_API_KEY` is set in `.env`: `echo $GOOGLE_PLACES_API_KEY`
938
+ - Check Google Cloud Console for API key validity and quota limits
939
+ - If quota exceeded, wait 24 hours or upgrade billing
940
+ - During development, feature falls back to mock pharmacy data when API key is missing
941
+ - Mock data is useful for frontend testing without API costs
942
+
943
+ **Problem: Slow medicine normalization or substitute search**
944
+
945
+ - **Cause**: Missing database indexes on high-cardinality columns
946
+ - **Solution**:
947
+ - Verify indexes exist on `generic_catalog`: `SELECT indexname FROM pg_indexes WHERE tablename='generic_catalog';`
948
+ - Should see indexes on: `product_name`, `salt`, `user_id`
949
+ - If missing, run: `alembic upgrade head` to apply latest migrations
950
+ - Check database query performance: Enable `EXPLAIN ANALYZE` in PostgreSQL
951
+ - Profile with: `SELECT pg_size_pretty(pg_total_relation_size('generic_catalog'));`
952
+
953
+ **Problem: Grok API errors when parsing prescription images**
954
+
955
+ - **Cause**: Invalid image format, API key expired, or API rate limit
956
+ - **Solution**:
957
+ - Verify image is clear and readable (JPG, PNG, TIFF)
958
+ - Test API key directly: `curl -H "Authorization: Bearer $GROK_API_KEY" https://api.x.ai/v1/models`
959
+ - Check Grok API status at https://status.x.ai
960
+ - Review backend logs: `docker logs <backend-container>` or `tail uvicorn.log`
961
+ - If rate-limited, implement request throttling in `medicine_normalizer.py`
962
+
963
+ **Problem: Pharmacy search cache not updating**
964
+
965
+ - **Cause**: In-memory cache TTL (1 hour) not expired
966
+ - **Solution**:
967
+ - Manually clear cache by restarting backend service
968
+ - Cache is per-search key (lat/lng/radius): different locations create new cache entries
969
+ - Check cache size in logs: Search for "pharmacy_cache" debug messages
970
+ - To disable caching for testing, set `PharmacyLocator.CACHE_TTL_SECONDS = 0`
971
+
972
+ **Problem: User saved medicines not persisting**
973
+
974
+ - **Cause**: Database migration not applied or user_id foreign key constraint
975
+ - **Solution**:
976
+ - Verify `user_saved_medicines` table exists: `\dt user_saved_medicines` in psql
977
+ - Run migrations: `cd backend && alembic upgrade head`
978
+ - Verify user exists and authenticated: Check JWT token contains valid `sub` (user_id)
979
+ - Check database logs for constraint errors: `docker logs <postgres-container>`
980
  ---
981
 
982
  ## Deployment
backend/.env.example CHANGED
@@ -1,5 +1,5 @@
1
  # =============================================================================
2
- # Co-Code GGW Backend Environment Variables
3
  # =============================================================================
4
  # Copy this file to .env and fill in your values
5
  # cp .env.example .env
 
1
  # =============================================================================
2
+ # Lumea Backend Environment Variables
3
  # =============================================================================
4
  # Copy this file to .env and fill in your values
5
  # cp .env.example .env
backend/app/main.py CHANGED
@@ -46,7 +46,7 @@ async def lifespan(app: FastAPI):
46
  except Exception as e:
47
  logger.debug(f"DB engine dispose: {e}")
48
 
49
- app = FastAPI(title="Co-Code GGW Health Platform API", lifespan=lifespan)
50
 
51
  # CORS configuration - allow both localhost ports and Docker
52
  origins = [
@@ -87,7 +87,7 @@ app.include_router(medicines_router)
87
  @app.get("/")
88
  async def root():
89
  return {
90
- "message": "Co-Code GGW Health Platform API",
91
  "version": "2.0",
92
  "endpoints": {
93
  "auth": "/api/auth",
 
46
  except Exception as e:
47
  logger.debug(f"DB engine dispose: {e}")
48
 
49
+ app = FastAPI(title="Lumea Health Platform API", lifespan=lifespan)
50
 
51
  # CORS configuration - allow both localhost ports and Docker
52
  origins = [
 
87
  @app.get("/")
88
  async def root():
89
  return {
90
+ "message": "Lumea Health Platform API",
91
  "version": "2.0",
92
  "endpoints": {
93
  "auth": "/api/auth",
backend/app/routes/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- Routes module for the Co-Code GGW Health Platform API.
3
  """
4
  from . import auth, health, dashboard, reports, assistant, recommendations, websocket, ai_summary, medicines
5
 
 
1
  """
2
+ Routes module for the Lumea Health Platform API.
3
  """
4
  from . import auth, health, dashboard, reports, assistant, recommendations, websocket, ai_summary, medicines
5
 
backend/app/routes/documents.py CHANGED
@@ -7,7 +7,7 @@ Provides endpoints for:
7
  - Missing data task management
8
  - Reprocessing
9
 
10
- Author: Co-Code GGW Health Platform
11
  """
12
  import logging
13
  from datetime import datetime
 
7
  - Missing data task management
8
  - Reprocessing
9
 
10
+ Author: Lumea Health Platform
11
  """
12
  import logging
13
  from datetime import datetime
backend/app/services/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- Services module for the Co-Code GGW Health Platform.
3
  """
4
  from .recommendation_service import RecommendationService, get_user_recommendations
5
  from .document_classifier import classify_document, ClassificationResult
 
1
  """
2
+ Services module for the Lumea Health Platform.
3
  """
4
  from .recommendation_service import RecommendationService, get_user_recommendations
5
  from .document_classifier import classify_document, ClassificationResult
backend/app/services/document_classifier.py CHANGED
@@ -6,7 +6,7 @@ Uses pattern matching with priority-ordered rules to classify documents into:
6
  - Categories: Lab, Dental, MRI, X-ray, Prescription, Sleep
7
  - Document Types: Blood Panel, Lipid Panel, Checkup, Brain Scan, Chest, etc.
8
 
9
- Author: Co-Code GGW Health Platform
10
  """
11
  import re
12
  import logging
 
6
  - Categories: Lab, Dental, MRI, X-ray, Prescription, Sleep
7
  - Document Types: Blood Panel, Lipid Panel, Checkup, Brain Scan, Chest, etc.
8
 
9
+ Author: Lumea Health Platform
10
  """
11
  import re
12
  import logging
backend/app/services/document_processing.py CHANGED
@@ -10,7 +10,7 @@ Orchestrates the full document processing flow:
10
  6. Database persistence
11
  7. Recompute trigger
12
 
13
- Author: Co-Code GGW Health Platform
14
  """
15
  import logging
16
  import uuid
 
10
  6. Database persistence
11
  7. Recompute trigger
12
 
13
+ Author: Lumea Health Platform
14
  """
15
  import logging
16
  import uuid
backend/app/services/grok_extractor.py CHANGED
@@ -13,7 +13,7 @@ HARD RULES:
13
  - Prefer null for missing values + create MissingDataTask
14
  - Always validate JSON against schema before accepting
15
 
16
- Author: Co-Code GGW Health Platform
17
  """
18
  import json
19
  import logging
 
13
  - Prefer null for missing values + create MissingDataTask
14
  - Always validate JSON against schema before accepting
15
 
16
+ Author: Lumea Health Platform
17
  """
18
  import json
19
  import logging
backend/app/services/metric_extractor.py CHANGED
@@ -9,7 +9,7 @@ Handles:
9
  - Unit normalization
10
  - Missing parameter detection
11
 
12
- Author: Co-Code GGW Health Platform
13
  """
14
  import re
15
  import logging
 
9
  - Unit normalization
10
  - Missing parameter detection
11
 
12
+ Author: Lumea Health Platform
13
  """
14
  import re
15
  import logging
backend/src/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- Co-Code GGW Health Platform Backend
3
 
4
  Structured following the monolithic REST API pattern:
5
  - config/ Application config, environment variables, database setup
 
1
  """
2
+ Lumea Health Platform Backend
3
 
4
  Structured following the monolithic REST API pattern:
5
  - config/ Application config, environment variables, database setup
backend/src/app.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- Co-Code GGW Health Platform API
3
 
4
  Main application entry point following monolithic REST API structure.
5
  """
@@ -20,7 +20,7 @@ async def lifespan(app: FastAPI):
20
  yield
21
 
22
 
23
- app = FastAPI(title="Co-Code GGW Health Platform API", lifespan=lifespan)
24
 
25
  # CORS configuration - allow both localhost ports and Docker
26
  origins = [
@@ -58,7 +58,7 @@ app.include_router(websocket_router)
58
  @app.get("/")
59
  async def root():
60
  return {
61
- "message": "Co-Code GGW Health Platform API",
62
  "version": "2.0",
63
  "endpoints": {
64
  "auth": "/api/auth",
 
1
  """
2
+ Lumea Health Platform API
3
 
4
  Main application entry point following monolithic REST API structure.
5
  """
 
20
  yield
21
 
22
 
23
+ app = FastAPI(title="Lumea Health Platform API", lifespan=lifespan)
24
 
25
  # CORS configuration - allow both localhost ports and Docker
26
  origins = [
 
58
  @app.get("/")
59
  async def root():
60
  return {
61
+ "message": "Lumea Health Platform API",
62
  "version": "2.0",
63
  "endpoints": {
64
  "auth": "/api/auth",
backend/src/routes/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- Routes module for the Co-Code GGW Health Platform API.
3
  """
4
  from . import auth, health, dashboard, reports, assistant, recommendations, websocket
5
 
 
1
  """
2
+ Routes module for the Lumea Health Platform API.
3
  """
4
  from . import auth, health, dashboard, reports, assistant, recommendations, websocket
5
 
backend/src/services/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- Services module for the Co-Code GGW Health Platform.
3
  """
4
  from .recommendation_service import RecommendationService, get_user_recommendations
5
  from .report_service import ReportService
 
1
  """
2
+ Services module for the Lumea Health Platform.
3
  """
4
  from .recommendation_service import RecommendationService, get_user_recommendations
5
  from .report_service import ReportService
backend/start.bat CHANGED
@@ -1,5 +1,5 @@
1
  @echo off
2
- echo Starting Co-Code GGW Backend...
3
  echo.
4
 
5
  if not exist venv (
 
1
  @echo off
2
+ echo Starting Lumea Backend...
3
  echo.
4
 
5
  if not exist venv (
backend/tests/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
  """
2
- Test suite for the Co-Code GGW Health Platform backend.
3
  """
 
1
  """
2
+ Test suite for the Lumea Health Platform backend.
3
  """
frontend/assets/logo.png ADDED
frontend/index.html CHANGED
@@ -4,7 +4,8 @@
4
  <meta charset="UTF-8" />
5
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Co-Code GGW - Unified Medical Platform</title>\n <meta name="description" content="Upload your lab reports, get them interpreted, and receive smart reminders for checkups + medicines. Co-Code GGW helps you stay preventive—without becoming your doctor." />
 
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
  <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
 
4
  <meta charset="UTF-8" />
5
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Lumea - Unified Medical Platform</title>
8
+ <meta name="description" content="Upload your lab reports, get them interpreted, and receive smart reminders for checkups + medicines. Lumea helps you stay preventive—without becoming your doctor." />
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
  <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
frontend/package.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "name": "co-code-ggw",
3
  "private": true,
4
  "version": "1.0.0",
5
  "type": "module",
 
1
  {
2
+ "name": "lumea",
3
  "private": true,
4
  "version": "1.0.0",
5
  "type": "module",
frontend/src/assets/logo.png ADDED
frontend/src/components/ArtOfNaturalRemedies.tsx CHANGED
@@ -136,7 +136,13 @@ function ArtOfNaturalRemedies() {
136
  viewport={{ once: true }}
137
  transition={{ delay: 0.5, duration: 0.6 }}
138
  >
139
- <img src="https://images.unsplash.com/photo-1505751104628-0448e3c8c7b0?w=400&h=300&fit=crop" alt="Digital health records" />
 
 
 
 
 
 
140
  </motion.div>
141
  </div>
142
  </div>
 
136
  viewport={{ once: true }}
137
  transition={{ delay: 0.5, duration: 0.6 }}
138
  >
139
+ <img
140
+ src="https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=400&h=300&fit=crop"
141
+ alt="Digital health records"
142
+ onError={(e) => {
143
+ e.currentTarget.src = 'https://images.unsplash.com/photo-1576091160399-112ba8d25d1d?w=400&h=300&fit=crop'
144
+ }}
145
+ />
146
  </motion.div>
147
  </div>
148
  </div>
frontend/src/components/Footer.tsx CHANGED
@@ -1,5 +1,6 @@
1
  import { motion } from 'framer-motion'
2
  import { useTranslation } from 'react-i18next'
 
3
  import './Footer.css'
4
 
5
  function Footer() {
@@ -16,7 +17,7 @@ function Footer() {
16
  viewport={{ once: true }}
17
  transition={{ duration: 0.6 }}
18
  >
19
- <h3 className="footer-heading">Co-Code GGW</h3>
20
  <p className="footer-text">
21
  {t('footer.description')}
22
  </p>
@@ -45,8 +46,7 @@ function Footer() {
45
  transition={{ delay: 0.2, duration: 0.6 }}
46
  >
47
  <div className="footer-logo">
48
- <span className="footer-logo-main">Co-Code</span>
49
- <span className="footer-logo-sub">GGW</span>
50
  </div>
51
  <h3 className="footer-cta-text">
52
  {t('footer.cta.title')}
 
1
  import { motion } from 'framer-motion'
2
  import { useTranslation } from 'react-i18next'
3
+ import Logo from './ui/Logo'
4
  import './Footer.css'
5
 
6
  function Footer() {
 
17
  viewport={{ once: true }}
18
  transition={{ duration: 0.6 }}
19
  >
20
+ <h3 className="footer-heading">Lumea</h3>
21
  <p className="footer-text">
22
  {t('footer.description')}
23
  </p>
 
46
  transition={{ delay: 0.2, duration: 0.6 }}
47
  >
48
  <div className="footer-logo">
49
+ <Logo variant="footer" />
 
50
  </div>
51
  <h3 className="footer-cta-text">
52
  {t('footer.cta.title')}
frontend/src/components/Header.css CHANGED
@@ -1,9 +1,11 @@
1
  .header {
2
- padding: 20px 60px;
3
  background-color: #f5f0e8;
4
  position: sticky;
5
  top: 0;
6
  z-index: 100;
 
 
7
  }
8
 
9
  .header-container {
 
1
  .header {
2
+ padding: 1rem 3.75rem;
3
  background-color: #f5f0e8;
4
  position: sticky;
5
  top: 0;
6
  z-index: 100;
7
+ margin: 0;
8
+ border: 0;
9
  }
10
 
11
  .header-container {
frontend/src/components/Header.tsx CHANGED
@@ -1,6 +1,7 @@
1
  import { motion } from 'framer-motion'
2
  import { useTranslation } from 'react-i18next'
3
  import { Link } from 'react-router-dom'
 
4
  import LanguageSwitcher from './LanguageSwitcher'
5
  import './Header.css'
6
 
@@ -8,8 +9,6 @@ function Header() {
8
  const { t } = useTranslation()
9
 
10
  const navLinks = [
11
- { label: 'Dashboard', href: '/dashboard', isRoute: true },
12
- { label: 'Reports', href: '/reports', isRoute: true },
13
  { label: t('header.nav.features'), href: '#features' },
14
  { label: t('header.nav.howItWorks'), href: '#how-it-works' },
15
  { label: t('header.nav.privacySecurity'), href: '#privacy' },
@@ -24,15 +23,9 @@ function Header() {
24
  transition={{ duration: 0.8, ease: 'easeOut' }}
25
  >
26
  <div className="header-container">
27
- <motion.a
28
- href="/"
29
- className="logo"
30
- whileHover={{ scale: 1.05 }}
31
- transition={{ type: 'spring', stiffness: 400 }}
32
- >
33
- <span className="logo-main">Co-Code</span>
34
- <span className="logo-sub">GGW</span>
35
- </motion.a>
36
 
37
  <nav className="nav">
38
  <ul className="nav-list">
@@ -44,25 +37,14 @@ function Header() {
44
  animate={{ y: 0, opacity: 1 }}
45
  transition={{ delay: 0.1 * index, duration: 0.5 }}
46
  >
47
- {link.isRoute ? (
48
- <Link to={link.href} className="nav-link">
49
- <motion.span
50
- whileHover={{ scale: 1.1, color: '#4a7c59' }}
51
- transition={{ type: 'spring', stiffness: 300 }}
52
- >
53
- {link.label}
54
- </motion.span>
55
- </Link>
56
- ) : (
57
- <motion.a
58
- href={link.href}
59
- className="nav-link"
60
- whileHover={{ scale: 1.1, color: '#4a7c59' }}
61
- transition={{ type: 'spring', stiffness: 300 }}
62
- >
63
- {link.label}
64
- </motion.a>
65
- )}
66
  </motion.li>
67
  ))}
68
  </ul>
 
1
  import { motion } from 'framer-motion'
2
  import { useTranslation } from 'react-i18next'
3
  import { Link } from 'react-router-dom'
4
+ import Logo from './ui/Logo'
5
  import LanguageSwitcher from './LanguageSwitcher'
6
  import './Header.css'
7
 
 
9
  const { t } = useTranslation()
10
 
11
  const navLinks = [
 
 
12
  { label: t('header.nav.features'), href: '#features' },
13
  { label: t('header.nav.howItWorks'), href: '#how-it-works' },
14
  { label: t('header.nav.privacySecurity'), href: '#privacy' },
 
23
  transition={{ duration: 0.8, ease: 'easeOut' }}
24
  >
25
  <div className="header-container">
26
+ <Link to="/">
27
+ <Logo variant="header" />
28
+ </Link>
 
 
 
 
 
 
29
 
30
  <nav className="nav">
31
  <ul className="nav-list">
 
37
  animate={{ y: 0, opacity: 1 }}
38
  transition={{ delay: 0.1 * index, duration: 0.5 }}
39
  >
40
+ <motion.a
41
+ href={link.href}
42
+ className="nav-link"
43
+ whileHover={{ scale: 1.1, color: '#4a7c59' }}
44
+ transition={{ type: 'spring', stiffness: 300 }}
45
+ >
46
+ {link.label}
47
+ </motion.a>
 
 
 
 
 
 
 
 
 
 
 
48
  </motion.li>
49
  ))}
50
  </ul>
frontend/src/components/Hero.css CHANGED
@@ -1,6 +1,6 @@
1
  .hero {
2
- min-height: 100vh;
3
- padding: 80px 40px 60px;
4
  background-color: #f5f0e8;
5
  display: flex;
6
  align-items: center;
@@ -85,10 +85,10 @@
85
  background-color: #4a7c59;
86
  color: white;
87
  font-family: 'DM Sans', sans-serif;
88
- font-size: 19px;
89
  font-weight: 500;
90
- padding: 22px 48px;
91
- border-radius: 40px;
92
  transition: all 0.3s ease;
93
  display: inline-block;
94
  }
 
1
  .hero {
2
+ min-height: calc(100vh - 80px);
3
+ padding: 2rem 2.5rem 3.75rem;
4
  background-color: #f5f0e8;
5
  display: flex;
6
  align-items: center;
 
85
  background-color: #4a7c59;
86
  color: white;
87
  font-family: 'DM Sans', sans-serif;
88
+ font-size: 1rem;
89
  font-weight: 500;
90
+ padding: 0.875rem 2.5rem;
91
+ border-radius: 2rem;
92
  transition: all 0.3s ease;
93
  display: inline-block;
94
  }
frontend/src/components/HowWeCanHelp.css CHANGED
@@ -47,7 +47,7 @@
47
  .btn-view-all {
48
  display: inline-block;
49
  background-color: #4a7c59;
50
- color: white;
51
  font-family: 'DM Sans', sans-serif;
52
  font-size: 18px;
53
  font-weight: 500;
@@ -107,13 +107,13 @@
107
  font-family: 'DM Sans', sans-serif;
108
  font-size: 17px;
109
  font-weight: 500;
110
- color: #2d2d2d;
111
  transition: all 0.3s ease;
112
  }
113
 
114
  .service-view-link:hover {
115
  gap: 14px;
116
- color: #4a7c59;
117
  }
118
 
119
  .view-icon {
 
47
  .btn-view-all {
48
  display: inline-block;
49
  background-color: #4a7c59;
50
+ color: #f5f1e8;
51
  font-family: 'DM Sans', sans-serif;
52
  font-size: 18px;
53
  font-weight: 500;
 
107
  font-family: 'DM Sans', sans-serif;
108
  font-size: 17px;
109
  font-weight: 500;
110
+ color: #f5f1e8;
111
  transition: all 0.3s ease;
112
  }
113
 
114
  .service-view-link:hover {
115
  gap: 14px;
116
+ color: #ffffff;
117
  }
118
 
119
  .view-icon {
frontend/src/components/dashboard/DashboardNavbar.tsx CHANGED
@@ -3,6 +3,7 @@ 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 } from 'lucide-react';
5
  import { logout } from '../../utils/auth';
 
6
  import './DashboardNavbar.css';
7
 
8
  interface DashboardNavbarProps {
@@ -42,17 +43,7 @@ function DashboardNavbar({ userName = 'User', userStatus = '87% Healthy' }: Dash
42
  <div className="dashboard-navbar-container">
43
  {/* Logo */}
44
  <Link to="/" className="dashboard-logo">
45
- <motion.div
46
- className="dashboard-logo-icon"
47
- whileHover={{ rotate: 5, scale: 1.05 }}
48
- transition={{ type: 'spring', stiffness: 400 }}
49
- >
50
- CC
51
- </motion.div>
52
- <div className="dashboard-logo-text">
53
- <span className="dashboard-logo-main">Co-Code</span>
54
- <span className="dashboard-logo-sub">GGW Health</span>
55
- </div>
56
  </Link>
57
 
58
  {/* Navigation Links */}
 
3
  import { motion, AnimatePresence } from 'framer-motion';
4
  import { Settings, LogOut, Home, LayoutDashboard, Bell, FileText, Activity, Sparkles, Pill } from 'lucide-react';
5
  import { logout } from '../../utils/auth';
6
+ import Logo from '../ui/Logo';
7
  import './DashboardNavbar.css';
8
 
9
  interface DashboardNavbarProps {
 
43
  <div className="dashboard-navbar-container">
44
  {/* Logo */}
45
  <Link to="/" className="dashboard-logo">
46
+ <Logo variant="dashboard" />
 
 
 
 
 
 
 
 
 
 
47
  </Link>
48
 
49
  {/* Navigation Links */}
frontend/src/components/ui/Logo.css ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .logo-component {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ cursor: pointer;
5
+ }
6
+
7
+ .logo-image {
8
+ height: auto;
9
+ object-fit: contain;
10
+ }
11
+
12
+ .logo-component.header .logo-image {
13
+ height: 50px;
14
+ }
15
+
16
+ .logo-component.footer .logo-image {
17
+ height: 45px;
18
+ }
19
+
20
+ .logo-component.dashboard .logo-image {
21
+ height: 40px;
22
+ }
23
+
24
+ @media (max-width: 768px) {
25
+ .logo-image {
26
+ height: 35px;
27
+ }
28
+ }
29
+ }
frontend/src/components/ui/Logo.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { motion } from 'framer-motion'
2
+ import './Logo.css'
3
+ import logoImage from '../../assets/logo.png'
4
+
5
+ interface LogoProps {
6
+ className?: string
7
+ variant?: 'header' | 'footer' | 'dashboard'
8
+ }
9
+
10
+ function Logo({ className = '', variant = 'header' }: LogoProps) {
11
+ return (
12
+ <motion.div
13
+ className={`logo-component ${variant} ${className}`}
14
+ whileHover={{ scale: 1.05 }}
15
+ transition={{ type: 'spring', stiffness: 400 }}
16
+ >
17
+ <img src={logoImage} alt="Lumea" className="logo-image" />
18
+ </motion.div>
19
+ )
20
+ }
21
+
22
+ export default Logo
frontend/src/components/ui/PdfPreview.css ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .pdf-preview-container {
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 {
11
+ width: 100%;
12
+ height: 100%;
13
+ border: none;
14
+ }
15
+
16
+ .pdf-preview-loading,
17
+ .pdf-preview-error,
18
+ .pdf-preview-empty {
19
+ display: flex;
20
+ flex-direction: column;
21
+ align-items: center;
22
+ justify-content: center;
23
+ height: 100%;
24
+ gap: 1rem;
25
+ color: #666;
26
+ }
27
+
28
+ .pdf-preview-error {
29
+ color: #dc2626;
30
+ }
31
+
32
+ .error-detail {
33
+ font-size: 0.875rem;
34
+ color: #999;
35
+ }
36
+
37
+ .spin {
38
+ animation: spin 1s linear infinite;
39
+ }
40
+
41
+ @keyframes spin {
42
+ from {
43
+ transform: rotate(0deg);
44
+ }
45
+ to {
46
+ transform: rotate(360deg);
47
+ }
48
+ }
frontend/src/components/ui/PdfPreview.tsx ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react'
2
+ import { Loader2, AlertCircle, FileText } from 'lucide-react'
3
+ import './PdfPreview.css'
4
+
5
+ interface PdfPreviewProps {
6
+ fileUrl: string
7
+ fileName?: string
8
+ className?: string
9
+ }
10
+
11
+ function PdfPreview({ fileUrl, fileName, className = '' }: PdfPreviewProps) {
12
+ const [loading, setLoading] = useState(true)
13
+ const [error, setError] = useState(false)
14
+
15
+ const handleLoad = () => {
16
+ setLoading(false)
17
+ setError(false)
18
+ }
19
+
20
+ const handleError = () => {
21
+ setLoading(false)
22
+ setError(true)
23
+ }
24
+
25
+ if (!fileUrl) {
26
+ return (
27
+ <div className={`pdf-preview-empty ${className}`}>
28
+ <FileText size={48} />
29
+ <p>No file selected</p>
30
+ </div>
31
+ )
32
+ }
33
+
34
+ return (
35
+ <div className={`pdf-preview-container ${className}`}>
36
+ {loading && (
37
+ <div className="pdf-preview-loading">
38
+ <Loader2 size={32} className="spin" />
39
+ <p>Loading PDF...</p>
40
+ </div>
41
+ )}
42
+
43
+ {error && (
44
+ <div className="pdf-preview-error">
45
+ <AlertCircle size={48} />
46
+ <p>Failed to load PDF</p>
47
+ <p className="error-detail">{fileName || 'Unknown file'}</p>
48
+ </div>
49
+ )}
50
+
51
+ {!error && (
52
+ <iframe
53
+ src={fileUrl}
54
+ title={fileName || 'PDF Preview'}
55
+ className="pdf-preview-iframe"
56
+ onLoad={handleLoad}
57
+ onError={handleError}
58
+ style={{ display: loading ? 'none' : 'block' }}
59
+ />
60
+ )}
61
+ </div>
62
+ )
63
+ }
64
+
65
+ export default PdfPreview
frontend/src/components/ui/Tooltip.css ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .tooltip-wrapper {
2
+ position: relative;
3
+ display: inline-flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ }
7
+
8
+ .tooltip-content {
9
+ position: absolute;
10
+ background: #2d3748;
11
+ color: #fff;
12
+ padding: 0.5rem 0.75rem;
13
+ border-radius: 0.375rem;
14
+ font-size: 0.875rem;
15
+ white-space: nowrap;
16
+ z-index: 1000;
17
+ pointer-events: none;
18
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
19
+ }
20
+
21
+ .tooltip-top {
22
+ bottom: calc(100% + 8px);
23
+ left: 50%;
24
+ transform: translateX(-50%);
25
+ }
26
+
27
+ .tooltip-bottom {
28
+ top: calc(100% + 8px);
29
+ left: 50%;
30
+ transform: translateX(-50%);
31
+ }
32
+
33
+ .tooltip-left {
34
+ right: calc(100% + 8px);
35
+ top: 50%;
36
+ transform: translateY(-50%);
37
+ }
38
+
39
+ .tooltip-right {
40
+ left: calc(100% + 8px);
41
+ top: 50%;
42
+ transform: translateY(-50%);
43
+ }
44
+
45
+ /* Arrow */
46
+ .tooltip-content::before {
47
+ content: '';
48
+ position: absolute;
49
+ width: 0;
50
+ height: 0;
51
+ border-style: solid;
52
+ }
53
+
54
+ .tooltip-top::before {
55
+ top: 100%;
56
+ left: 50%;
57
+ transform: translateX(-50%);
58
+ border-width: 6px 6px 0 6px;
59
+ border-color: #2d3748 transparent transparent transparent;
60
+ }
61
+
62
+ .tooltip-bottom::before {
63
+ bottom: 100%;
64
+ left: 50%;
65
+ transform: translateX(-50%);
66
+ border-width: 0 6px 6px 6px;
67
+ border-color: transparent transparent #2d3748 transparent;
68
+ }
69
+
70
+ .tooltip-left::before {
71
+ left: 100%;
72
+ top: 50%;
73
+ transform: translateY(-50%);
74
+ border-width: 6px 0 6px 6px;
75
+ border-color: transparent transparent transparent #2d3748;
76
+ }
77
+
78
+ .tooltip-right::before {
79
+ right: 100%;
80
+ top: 50%;
81
+ transform: translateY(-50%);
82
+ border-width: 6px 6px 6px 0;
83
+ border-color: transparent #2d3748 transparent transparent;
84
+ }
frontend/src/components/ui/Tooltip.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ReactNode, useState } from 'react'
2
+ import './Tooltip.css'
3
+
4
+ interface TooltipProps {
5
+ children: ReactNode
6
+ content: string
7
+ position?: 'top' | 'bottom' | 'left' | 'right'
8
+ }
9
+
10
+ function Tooltip({ children, content, position = 'top' }: TooltipProps) {
11
+ const [isVisible, setIsVisible] = useState(false)
12
+
13
+ return (
14
+ <div
15
+ className="tooltip-wrapper"
16
+ onMouseEnter={() => setIsVisible(true)}
17
+ onMouseLeave={() => setIsVisible(false)}
18
+ >
19
+ {children}
20
+ {isVisible && (
21
+ <div className={`tooltip-content tooltip-${position}`}>
22
+ {content}
23
+ </div>
24
+ )}
25
+ </div>
26
+ )
27
+ }
28
+
29
+ export default Tooltip
frontend/src/components/ui/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export { default as Logo } from './Logo'
2
+ export { default as Tooltip } from './Tooltip'
3
+ export { default as PdfPreview } from './PdfPreview'
frontend/src/i18n/locales/en.json CHANGED
@@ -14,7 +14,7 @@
14
  "tagline": "First priority is",
15
  "taglineHighlight": "your health",
16
  "subtitle": "Track it. Understand it. Prevent it.",
17
- "description": "Upload your lab reports, get them interpreted, and receive smart reminders for checkups + medicines. Co-Code GGW helps you stay preventive—without becoming your doctor.",
18
  "disclaimer": "Not medical advice. Always consult a licensed clinician for diagnosis/treatment.",
19
  "cta": {
20
  "upload": "Upload Reports",
@@ -50,7 +50,7 @@
50
  }
51
  },
52
  "howItWorks": {
53
- "title": "How Co-Code GGW Works",
54
  "subtitle": "Upload reports, get insights, and stay on top of preventive care—all in one secure platform.",
55
  "features": {
56
  "ocr": {
@@ -69,7 +69,7 @@
69
  "cta": "View All Features"
70
  },
71
  "belief": {
72
- "message": "Most health issues don't start as emergencies. They start quietly. Co-Code GGW keeps your health history organized and nudges you at the right time—so prevention becomes automatic.",
73
  "tagline": "Built as a suggestion system, not a doctor."
74
  },
75
  "news": {
@@ -96,11 +96,11 @@
96
  "faq": {
97
  "title": "The Art Of",
98
  "titleHighlight": "Prevention",
99
- "subtitle": "Co-Code GGW makes preventive care simple. Upload reports, track trends, and get nudges at the right time—so staying healthy doesn't feel like work.",
100
  "items": {
101
  "isDoctor": {
102
- "question": "Is Co-Code GGW a doctor?",
103
- "answer": "No. Co-Code GGW is not a doctor and does not provide medical advice, diagnosis, or treatment. It is a suggestion system that helps you organize lab reports, track health trends, and receive smart reminders—always consult a licensed clinician for medical decisions."
104
  },
105
  "reports": {
106
  "question": "What reports can I upload?",
@@ -112,7 +112,7 @@
112
  },
113
  "reminders": {
114
  "question": "How are reminders personalized?",
115
- "answer": "Based on your report history, age, and past checkup dates, Co-Code GGW suggests when to schedule follow-ups, refill medicines, or repeat tests. Reminders adapt dynamically as you upload new reports."
116
  },
117
  "security": {
118
  "question": "How do you keep my data secure?",
 
14
  "tagline": "First priority is",
15
  "taglineHighlight": "your health",
16
  "subtitle": "Track it. Understand it. Prevent it.",
17
+ "description": "Upload your lab reports, get them interpreted, and receive smart reminders for checkups + medicines. Lumea helps you stay preventive—without becoming your doctor.",
18
  "disclaimer": "Not medical advice. Always consult a licensed clinician for diagnosis/treatment.",
19
  "cta": {
20
  "upload": "Upload Reports",
 
50
  }
51
  },
52
  "howItWorks": {
53
+ "title": "How Lumea Works",
54
  "subtitle": "Upload reports, get insights, and stay on top of preventive care—all in one secure platform.",
55
  "features": {
56
  "ocr": {
 
69
  "cta": "View All Features"
70
  },
71
  "belief": {
72
+ "message": "Most health issues don't start as emergencies. They start quietly. Lumea keeps your health history organized and nudges you at the right time—so prevention becomes automatic.",
73
  "tagline": "Built as a suggestion system, not a doctor."
74
  },
75
  "news": {
 
96
  "faq": {
97
  "title": "The Art Of",
98
  "titleHighlight": "Prevention",
99
+ "subtitle": "Lumea makes preventive care simple. Upload reports, track trends, and get nudges at the right time—so staying healthy doesn't feel like work.",
100
  "items": {
101
  "isDoctor": {
102
+ "question": "Is Lumea a doctor?",
103
+ "answer": "No. Lumea is not a doctor and does not provide medical advice, diagnosis, or treatment. It is a suggestion system that helps you organize lab reports, track health trends, and receive smart reminders—always consult a licensed clinician for medical decisions."
104
  },
105
  "reports": {
106
  "question": "What reports can I upload?",
 
112
  },
113
  "reminders": {
114
  "question": "How are reminders personalized?",
115
+ "answer": "Based on your report history, age, and past checkup dates, Lumea suggests when to schedule follow-ups, refill medicines, or repeat tests. Reminders adapt dynamically as you upload new reports."
116
  },
117
  "security": {
118
  "question": "How do you keep my data secure?",
frontend/src/i18n/locales/hi.json CHANGED
@@ -14,7 +14,7 @@
14
  "tagline": "सबसे पहली प्राथमिकता है",
15
  "taglineHighlight": "आपका स्वास्थ्य",
16
  "subtitle": "ट्रैक करें। समझें। रोकें।",
17
- "description": "अपनी जांच रिपोर्ट अपलोड करें, उनका विश्लेषण प्राप्त करें, और नियमित जांच व दवाइयों के लिए स्मार्ट स्मरण पाएं। Co-Code GGW आपको रोकथाम में मदद करता है—आपके डॉक्टर बने बिना।",
18
  "disclaimer": "यह चिकित्सा सलाह नहीं है। निदान/उपचार के लिए हमेशा लाइसेंस प्राप्त चिकित्सक से परामर्श करें।",
19
  "cta": {
20
  "upload": "रिपोर्ट अपलोड करें",
@@ -50,7 +50,7 @@
50
  }
51
  },
52
  "howItWorks": {
53
- "title": "Co-Code GGW कैसे काम करता है",
54
  "subtitle": "रिपोर्ट अपलोड करें, जानकारी पाएं, और एक सुरक्षित मंच पर रोकथाम देखभाल के शीर्ष पर रहें।",
55
  "features": {
56
  "ocr": {
@@ -69,7 +69,7 @@
69
  "cta": "सभी विशेषताएं देखें"
70
  },
71
  "belief": {
72
- "message": "अधिकतर स्वास्थ्य समस्याएं आपातकाल के रूप में शुरू नहीं होतीं। वे चुपचाप शुरू होती हैं। Co-Code GGW आपके स्वास्थ्य इतिहास को व्यवस्थित रखता है और सही समय पर आपको याद दिलाता है—ताकि रोकथाम स्वचालित हो जाए।",
73
  "tagline": "एक सुझाव प्रणाली के रूप में बनाया गया, डॉक्टर के रूप में नहीं।"
74
  },
75
  "news": {
@@ -96,11 +96,11 @@
96
  "faq": {
97
  "title": "रोकथाम की",
98
  "titleHighlight": "कला",
99
- "subtitle": "Co-Code GGW रोकथाम देखभाल को सरल बनाता है। रिपोर्ट अपलोड करें, रुझान ट्रैक करें, और सही समय पर याद दिलाएं—ताकि स्वस्थ रहना काम जैसा न लगे।",
100
  "items": {
101
  "isDoctor": {
102
- "question": "क्या Co-Code GGW एक डॉक्टर है?",
103
- "answer": "नहीं। Co-Code GGW डॉक्टर नहीं है और चिकित्सा सलाह, निदान या उपचार प्रदान नहीं करता है। यह एक सुझाव प्रणाली है जो आपको जांच रिपोर्ट व्यवस्थित करने, स्वास्थ्य रुझान ट्रैक करने और स्मार्ट स्मरण प्राप्त करने में मदद करती है—चिकित्सा निर्णयों के लिए हमेशा लाइसेंस प्राप्त चिकित्सक से परामर्श करें।"
104
  },
105
  "reports": {
106
  "question": "मैं कौन सी रिपोर्ट अपलोड कर सकता हूं?",
@@ -112,7 +112,7 @@
112
  },
113
  "reminders": {
114
  "question": "स्मरण कैसे व्यक्तिगत किए जाते हैं?",
115
- "answer": "आपकी रिपोर्ट इतिहास, उम्र और पिछली जांच तिथियों के आधार पर, Co-Code GGW सुझाव देता है कि फॉलो-अप कब शेड्यूल करें, दवाइयां फिर से भरें या परीक्षण दोहराएं। स्मरण गतिशील रूप से अनुकूलित होते हैं जैसे-जैसे आप नई रिपोर्ट अपलोड करते हैं।"
116
  },
117
  "security": {
118
  "question": "आप मेरा डेटा कैसे सुरक्षित रखते हैं?",
 
14
  "tagline": "सबसे पहली प्राथमिकता है",
15
  "taglineHighlight": "आपका स्वास्थ्य",
16
  "subtitle": "ट्रैक करें। समझें। रोकें।",
17
+ "description": "अपनी जांच रिपोर्ट अपलोड करें, उनका विश्लेषण प्राप्त करें, और नियमित जांच व दवाइयों के लिए स्मार्ट स्मरण पाएं। Lumea आपको रोकथाम में मदद करता है—आपके डॉक्टर बने बिना।",
18
  "disclaimer": "यह चिकित्सा सलाह नहीं है। निदान/उपचार के लिए हमेशा लाइसेंस प्राप्त चिकित्सक से परामर्श करें।",
19
  "cta": {
20
  "upload": "रिपोर्ट अपलोड करें",
 
50
  }
51
  },
52
  "howItWorks": {
53
+ "title": "Lumea कैसे काम करता है",
54
  "subtitle": "रिपोर्ट अपलोड करें, जानकारी पाएं, और एक सुरक्षित मंच पर रोकथाम देखभाल के शीर्ष पर रहें।",
55
  "features": {
56
  "ocr": {
 
69
  "cta": "सभी विशेषताएं देखें"
70
  },
71
  "belief": {
72
+ "message": "अधिकतर स्वास्थ्य समस्याएं आपातकाल के रूप में शुरू नहीं होतीं। वे चुपचाप शुरू होती हैं। Lumea आपके स्वास्थ्य इतिहास को व्यवस्थित रखता है और सही समय पर आपको याद दिलाता है—ताकि रोकथाम स्वचालित हो जाए।",
73
  "tagline": "एक सुझाव प्रणाली के रूप में बनाया गया, डॉक्टर के रूप में नहीं।"
74
  },
75
  "news": {
 
96
  "faq": {
97
  "title": "रोकथाम की",
98
  "titleHighlight": "कला",
99
+ "subtitle": "Lumea रोकथाम देखभाल को सरल बनाता है। रिपोर्ट अपलोड करें, रुझान ट्रैक करें, और सही समय पर याद दिलाएं—ताकि स्वस्थ रहना काम जैसा न लगे।",
100
  "items": {
101
  "isDoctor": {
102
+ "question": "क्या Lumea एक डॉक्टर है?",
103
+ "answer": "नहीं। Lumea डॉक्टर नहीं है और चिकित्सा सलाह, निदान या उपचार प्रदान नहीं करता है। यह एक सुझाव प्रणाली है जो आपको जांच रिपोर्ट व्यवस्थित करने, स्वास्थ्य रुझान ट्रैक करने और स्मार्ट स्मरण प्राप्त करने में मदद करती है—चिकित्सा निर्णयों के लिए हमेशा लाइसेंस प्राप्त चिकित्सक से परामर्श करें।"
104
  },
105
  "reports": {
106
  "question": "मैं कौन सी रिपोर्ट अपलोड कर सकता हूं?",
 
112
  },
113
  "reminders": {
114
  "question": "स्मरण कैसे व्यक्तिगत किए जाते हैं?",
115
+ "answer": "आपकी रिपोर्ट इतिहास, उम्र और पिछली जांच तिथियों के आधार पर, Lumea सुझाव देता है कि फॉलो-अप कब शेड्यूल करें, दवाइयां फिर से भरें या परीक्षण दोहराएं। स्मरण गतिशील रूप से अनुकूलित होते हैं जैसे-जैसे आप नई रिपोर्ट अपलोड करते हैं।"
116
  },
117
  "security": {
118
  "question": "आप मेरा डेटा कैसे सुरक्षित रखते हैं?",
frontend/src/i18n/locales/mr.json CHANGED
@@ -14,7 +14,7 @@
14
  "tagline": "पहिली प्राधान्यता आहे",
15
  "taglineHighlight": "तुमचे आरोग्य",
16
  "subtitle": "ट्रॅक करा। समजून घ्या। प्रतिबंध करा।",
17
- "description": "तुमचे तपासणी अहवाल अपलोड करा, त्यांचे विश्लेषण मिळवा, आणि नियमित तपासणी व औषधांसाठी स्मार्ट स्मरणपत्र मिळवा। Co-Code GGW तुम्हाला प्रतिबंधात मदत करते—तुमचे डॉक्टर न बनता।",
18
  "disclaimer": "हा वैद्यकीय सल्ला नाही. निदान/उपचारासाठी नेहमी परवानाधारक वैद्यांचा सल्ला घ्या।",
19
  "cta": {
20
  "upload": "अहवाल अपलोड करा",
@@ -50,7 +50,7 @@
50
  }
51
  },
52
  "howItWorks": {
53
- "title": "Co-Code GGW कसे काम करते",
54
  "subtitle": "अहवाल अपलोड करा, माहिती मिळवा आणि एका सुरक्षित व्यासपीठावर प्रतिबंधात्मक काळजीच्या शीर्षस्थानी रहा।",
55
  "features": {
56
  "ocr": {
@@ -69,7 +69,7 @@
69
  "cta": "सर्व वैशिष्ट्ये पहा"
70
  },
71
  "belief": {
72
- "message": "बहुतेक आरोग्य समस्या आणीबाणी म्हणून सुरू होत नाहीत. त्या शांतपणे सुरू होतात। Co-Code GGW तुमचा आरोग्य इतिहास व्यवस्थित ठेवते आणि योग्य वेळी तुम्हाला आठवण करून देते—जेणेकरून प्रतिबंध स्वयंचलित होईल।",
73
  "tagline": "एक सूचना प्रणाली म्हणून तयार केले, डॉक्टर म्हणून नाही।"
74
  },
75
  "news": {
@@ -96,11 +96,11 @@
96
  "faq": {
97
  "title": "प्रतिबंधाची",
98
  "titleHighlight": "कला",
99
- "subtitle": "Co-Code GGW प्रतिबंधात्मक काळजी सोपी करते। अहवाल अपलोड करा, ट्रेंड ट्रॅक करा आणि योग्य वेळी आठवण करून द्या—जेणेकरून निरोगी राहणे कामासारखे वाटणार नाही।",
100
  "items": {
101
  "isDoctor": {
102
- "question": "Co-Code GGW डॉक्टर आहे का?",
103
- "answer": "नाही। Co-Code GGW डॉक्टर नाही आणि वैद्यकीय सल्ला, निदान किंवा उपचार प्रदान करत नाही। ही एक सूचना प्रणाली आहे जी तुम्हाला तपासणी अहवाल व्यवस्थित करण्यात, आरोग्य ट्रेंड ट्रॅक करण्यात आणि स्मार्ट स्मरणपत्र प्राप्त करण्यात मदत करते—वैद्यकीय निर्णयांसाठी नेहमी परवानाधारक व��द्यांचा सल्ला घ्या।"
104
  },
105
  "reports": {
106
  "question": "मी कोणते अहवाल अपलोड करू शकतो?",
@@ -112,7 +112,7 @@
112
  },
113
  "reminders": {
114
  "question": "स्मरणपत्र वैयक्तिक कसे केली जातात?",
115
- "answer": "तुमच्या अहवाल इतिहास, वय आणि मागील तपासणी तारखांच्या आधारे, Co-Code GGW सुचवते की फॉलो-अप केव्हा शेड्यूल करावे, औषधे पुन्हा भरावीत किंवा चाचण्या पुन्हा करावीत। स्मरणपत्र डायनॅमिकपणे जुळवले जातात जसे तुम्ही नवीन अहवाल अपलोड करता।"
116
  },
117
  "security": {
118
  "question": "तुम्ही माझा डेटा कसा सुरक्षित ठेवता?",
 
14
  "tagline": "पहिली प्राधान्यता आहे",
15
  "taglineHighlight": "तुमचे आरोग्य",
16
  "subtitle": "ट्रॅक करा। समजून घ्या। प्रतिबंध करा।",
17
+ "description": "तुमचे तपासणी अहवाल अपलोड करा, त्यांचे विश्लेषण मिळवा, आणि नियमित तपासणी व औषधांसाठी स्मार्ट स्मरणपत्र मिळवा। Lumea तुम्हाला प्रतिबंधात मदत करते—तुमचे डॉक्टर न बनता।",
18
  "disclaimer": "हा वैद्यकीय सल्ला नाही. निदान/उपचारासाठी नेहमी परवानाधारक वैद्यांचा सल्ला घ्या।",
19
  "cta": {
20
  "upload": "अहवाल अपलोड करा",
 
50
  }
51
  },
52
  "howItWorks": {
53
+ "title": "Lumea कसे काम करते",
54
  "subtitle": "अहवाल अपलोड करा, माहिती मिळवा आणि एका सुरक्षित व्यासपीठावर प्रतिबंधात्मक काळजीच्या शीर्षस्थानी रहा।",
55
  "features": {
56
  "ocr": {
 
69
  "cta": "सर्व वैशिष्ट्ये पहा"
70
  },
71
  "belief": {
72
+ "message": "बहुतेक आरोग्य समस्या आणीबाणी म्हणून सुरू होत नाहीत त्या शांतपणे सुरू होतात। Lumea तुमचा आरोग्य इतिहास व्यवस्थित ठेवते आणि योग्य वेळी तुम्हाला आठवण करून देते—जेणेकरून प्रतिबंध स्वयंचलित होईल।",
73
  "tagline": "एक सूचना प्रणाली म्हणून तयार केले, डॉक्टर म्हणून नाही।"
74
  },
75
  "news": {
 
96
  "faq": {
97
  "title": "प्रतिबंधाची",
98
  "titleHighlight": "कला",
99
+ "subtitle": "Lumea प्रतिबंधात्मक काळजी सोपी करते। अहवाल अपलोड करा, ट्रेंड ट्रॅक करा आणि योग्य वेळी आठवण करून द्या—जेणेकरून निरोगी राहणे कामासारखे वाटणार नाही।",
100
  "items": {
101
  "isDoctor": {
102
+ "question": "Lumea डॉक्टर आहे का?",
103
+ "answer": "नाही। Lumea डॉक्टर नाही आणि वैद्यकीय सल्ला, निदान किंवा उपचार प्रदान करत नाही। ही एक सूचना प्रणाली आहे जी तुम्हाला तपासणी अहवाल व्यवस्थित करण्यात, आरोग्य ट्रेंड ट्रॅक करण्यात आणि स्मार्ट स्मरणपत्र प्राप्त करण्यात मदत करते—वैद्यकीय निर्णयांसाठी नेहमी परवानाधारक वद्यांचा सल्ला घ्या।"
104
  },
105
  "reports": {
106
  "question": "मी कोणते अहवाल अपलोड करू शकतो?",
 
112
  },
113
  "reminders": {
114
  "question": "स्मरणपत्र वैयक्तिक कसे केली जातात?",
115
+ "answer": "तुमच्या अहवाल इतिहास, वय आणि मागील तपासणी तारखांच्या आधारे, Lumea सुचवते की फॉलो-अप केव्हा शेड्यूल करावे, औषधे पुन्हा भरावीत किंवा चाचण्या पुन्हा करावीत। स्मरणपत्र डायनॅमिकपणे जुळवले जातात जसे तुम्ही नवीन अहवाल अपलोड करता।"
116
  },
117
  "security": {
118
  "question": "तुम्ही माझा डेटा कसा सुरक्षित ठेवता?",
frontend/src/index.css CHANGED
@@ -6,6 +6,8 @@
6
 
7
  html {
8
  font-size: 16px;
 
 
9
  }
10
 
11
  body {
@@ -15,6 +17,8 @@ body {
15
  line-height: 1.6;
16
  -webkit-font-smoothing: antialiased;
17
  -moz-osx-font-smoothing: grayscale;
 
 
18
  }
19
 
20
  a {
 
6
 
7
  html {
8
  font-size: 16px;
9
+ margin: 0;
10
+ padding: 0;
11
  }
12
 
13
  body {
 
17
  line-height: 1.6;
18
  -webkit-font-smoothing: antialiased;
19
  -moz-osx-font-smoothing: grayscale;
20
+ margin: 0;
21
+ padding: 0;
22
  }
23
 
24
  a {
frontend/src/pages/Dashboard.tsx CHANGED
@@ -1,7 +1,7 @@
1
  import { useState, useEffect, useCallback } from 'react';
2
  import { motion } from 'framer-motion';
3
  import { Upload } from 'lucide-react';
4
- import { useNavigate } from 'react-router-dom';
5
  import DashboardNavbar from '../components/dashboard/DashboardNavbar';
6
  import HealthIndexCard from '../components/dashboard/HealthIndexCard';
7
  import HealthSummaryCard from '../components/dashboard/HealthSummaryCard';
@@ -13,6 +13,7 @@ import { API_BASE_URL } from '../config/api';
13
  import '../styles/dashboardTokens.css';
14
  import '../styles/dashboardBase.css';
15
  import './Dashboard.css';
 
16
 
17
  interface UserSummary {
18
  id: string;
@@ -230,12 +231,18 @@ function Dashboard() {
230
  animate={{ opacity: 1, y: 0 }}
231
  transition={{ duration: 0.5, delay: 0.3 }}
232
  >
233
- <RecommendationsPanel
234
- authToken={authToken || undefined}
235
- apiBaseUrl={API_BASE_URL}
236
- maxInitialDisplay={3}
237
- refreshTrigger={recommendationsRefreshTrigger}
238
- />
 
 
 
 
 
 
239
  </motion.div>
240
 
241
  {/* Bottom Row: Trends Chart */}
 
1
  import { useState, useEffect, useCallback } from 'react';
2
  import { motion } from 'framer-motion';
3
  import { Upload } from 'lucide-react';
4
+ import { useNavigate, Link } from 'react-router-dom';
5
  import DashboardNavbar from '../components/dashboard/DashboardNavbar';
6
  import HealthIndexCard from '../components/dashboard/HealthIndexCard';
7
  import HealthSummaryCard from '../components/dashboard/HealthSummaryCard';
 
13
  import '../styles/dashboardTokens.css';
14
  import '../styles/dashboardBase.css';
15
  import './Dashboard.css';
16
+ import './DashboardExtras.css';
17
 
18
  interface UserSummary {
19
  id: string;
 
231
  animate={{ opacity: 1, y: 0 }}
232
  transition={{ duration: 0.5, delay: 0.3 }}
233
  >
234
+ <div className="recommendations-dashboard-wrapper">
235
+ <RecommendationsPanel
236
+ authToken={authToken || undefined}
237
+ apiBaseUrl={API_BASE_URL}
238
+ maxInitialDisplay={3}
239
+ refreshTrigger={recommendationsRefreshTrigger}
240
+ variant="dashboard"
241
+ />
242
+ <Link to="/recommendations" className="view-detailed-recommendations-btn">
243
+ View Detailed Recommendations
244
+ </Link>
245
+ </div>
246
  </motion.div>
247
 
248
  {/* Bottom Row: Trends Chart */}
frontend/src/pages/DashboardExtras.css ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Additional Dashboard styles for recommendations wrapper */
2
+ .recommendations-dashboard-wrapper {
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: 1rem;
6
+ }
7
+
8
+ .view-detailed-recommendations-btn {
9
+ align-self: flex-start;
10
+ padding: 0.75rem 1.5rem;
11
+ background-color: #4a7c59;
12
+ color: white;
13
+ font-family: 'DM Sans', sans-serif;
14
+ font-size: 0.875rem;
15
+ font-weight: 500;
16
+ border-radius: 0.5rem;
17
+ transition: all 0.3s ease;
18
+ display: inline-flex;
19
+ align-items: center;
20
+ gap: 0.5rem;
21
+ }
22
+
23
+ .view-detailed-recommendations-btn:hover {
24
+ background-color: #3d6849;
25
+ transform: translateY(-2px);
26
+ box-shadow: 0 4px 12px rgba(74, 124, 89, 0.3);
27
+ }
frontend/src/pages/ReportSummary.css CHANGED
@@ -582,6 +582,31 @@
582
  max-height: calc(100vh - 200px);
583
  position: sticky;
584
  top: 100px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
585
  }
586
 
587
  /* AI Empty State */
 
582
  max-height: calc(100vh - 200px);
583
  position: sticky;
584
  top: 100px;
585
+ display: flex;
586
+ flex-direction: column;
587
+ }
588
+
589
+ .ai-results-panel {
590
+ display: flex;
591
+ flex-direction: column;
592
+ max-height: calc(100vh - 200px);
593
+ }
594
+
595
+ .ai-results-panel .ai-tabs {
596
+ flex-shrink: 0;
597
+ }
598
+
599
+ .ai-results-panel .ai-content {
600
+ flex: 1;
601
+ overflow-y: auto;
602
+ max-height: calc(100vh - 350px);
603
+ }
604
+
605
+ .ai-results-panel .ai-action-footer {
606
+ flex-shrink: 0;
607
+ padding: 16px 20px;
608
+ border-top: 1px solid #f0f0f0;
609
+ background: white;
610
  }
611
 
612
  /* AI Empty State */
frontend/src/pages/ReportSummary.tsx CHANGED
@@ -18,6 +18,7 @@ import {
18
  Grid,
19
  } from 'lucide-react';
20
  import DashboardNavbar from '../components/dashboard/DashboardNavbar';
 
21
  import './ReportSummary.css';
22
 
23
  import { API_BASE_URL } from '../config/api';
@@ -406,6 +407,11 @@ function ReportSummary() {
406
 
407
  if (selectedReports.length === 1) {
408
  const report = selectedReports[0];
 
 
 
 
 
409
  return (
410
  <div className="preview-single">
411
  <div className="preview-header">
@@ -413,25 +419,17 @@ function ReportSummary() {
413
  <span className="preview-date">{formatDate(report.report_date || report.uploaded_at)}</span>
414
  </div>
415
  <div className="preview-frame">
416
- {report.file_type === '.pdf' ? (
417
- <iframe
418
- src={`${API_BASE}${report.preview_url}`}
419
- title={report.filename}
420
- className="preview-iframe"
421
- />
422
- ) : (
423
- <img
424
- src={`${API_BASE}${report.preview_url}`}
425
- alt={report.filename}
426
- className="preview-image"
427
- />
428
- )}
429
  </div>
430
  </div>
431
  );
432
  }
433
 
434
  // Multiple reports - grid view
 
435
  return (
436
  <div className="preview-multi">
437
  <div className="preview-multi-header">
@@ -439,42 +437,39 @@ function ReportSummary() {
439
  <span>{selectedReports.length} Reports Selected</span>
440
  </div>
441
  <div className={`preview-grid grid-${Math.min(selectedReports.length, 3)}`}>
442
- {selectedReports.map(report => (
443
- <div
444
- key={report.id}
445
- className={`preview-grid-item ${focusedPreviewId === report.id ? 'focused' : ''}`}
446
- onClick={() => setFocusedPreviewId(focusedPreviewId === report.id ? null : report.id)}
447
- >
448
- <div className="preview-grid-item-header">
449
- <span className="preview-grid-filename">{report.filename}</span>
450
- <span className="preview-grid-date">{formatDate(report.report_date || report.uploaded_at)}</span>
451
- </div>
452
- <div className="preview-grid-content">
453
- {report.file_type === '.pdf' ? (
454
- <iframe
455
- src={`${API_BASE}${report.preview_url}#toolbar=0&navpanes=0`}
456
- title={report.filename}
457
- className="preview-grid-iframe"
458
- />
459
- ) : (
460
- <img
461
- src={`${API_BASE}${report.preview_url}`}
462
- alt={report.filename}
463
- className="preview-grid-image"
464
  />
465
- )}
 
 
 
 
 
 
 
 
 
466
  </div>
467
- <button
468
- className="preview-grid-expand"
469
- onClick={(e) => {
470
- e.stopPropagation();
471
- window.open(`${API_BASE}${report.preview_url}`, '_blank');
472
- }}
473
- >
474
- <Maximize2 size={14} />
475
- </button>
476
- </div>
477
- ))}
478
  </div>
479
  </div>
480
  );
@@ -495,34 +490,35 @@ function ReportSummary() {
495
  }
496
 
497
  return (
498
- <div className="ai-panel">
499
- {/* Tabs */}
500
- <div className="ai-tabs">
501
- <button
502
- className={`ai-tab ${activeTab === 'summary' ? 'active' : ''}`}
503
- onClick={() => setActiveTab('summary')}
504
- disabled={selectedIds.size !== 1}
505
- >
506
- Summary
507
- </button>
508
- <button
509
- className={`ai-tab ${activeTab === 'comparison' ? 'active' : ''}`}
510
- onClick={() => setActiveTab('comparison')}
511
- disabled={selectedIds.size < 2}
512
- >
513
- Comparison
514
- </button>
515
- <button
516
- className={`ai-tab ${activeTab === 'metrics' ? 'active' : ''}`}
517
- onClick={() => setActiveTab('metrics')}
518
- disabled={selectedIds.size === 0}
519
- >
520
- Metrics
521
- </button>
522
- </div>
 
523
 
524
- {/* Tab Content */}
525
- <div className="ai-content">
526
  {/* Summary Tab */}
527
  {activeTab === 'summary' && (
528
  <>
@@ -749,7 +745,41 @@ function ReportSummary() {
749
  </div>
750
  )}
751
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
752
  </div>
 
753
  );
754
  };
755
 
@@ -919,46 +949,6 @@ function ReportSummary() {
919
  </div>
920
  </div>
921
  </div>
922
-
923
- {/* Floating Action Button */}
924
- <AnimatePresence>
925
- {selectedIds.size > 0 && (
926
- <motion.div
927
- className="floating-action"
928
- initial={{ opacity: 0, y: 50 }}
929
- animate={{ opacity: 1, y: 0 }}
930
- exit={{ opacity: 0, y: 50 }}
931
- >
932
- {selectedIds.size === 1 ? (
933
- <button
934
- className="action-btn primary"
935
- onClick={() => generateSummary()}
936
- disabled={summaryResult.loading}
937
- >
938
- {summaryResult.loading ? (
939
- <Loader2 size={18} className="spinner" />
940
- ) : (
941
- <Sparkles size={18} />
942
- )}
943
- Generate Summary
944
- </button>
945
- ) : (
946
- <button
947
- className="action-btn primary"
948
- onClick={() => generateComparison()}
949
- disabled={comparisonResult.loading || !validateSelection().valid}
950
- >
951
- {comparisonResult.loading ? (
952
- <Loader2 size={18} className="spinner" />
953
- ) : (
954
- <Sparkles size={18} />
955
- )}
956
- Compare Selected ({selectedIds.size})
957
- </button>
958
- )}
959
- </motion.div>
960
- )}
961
- </AnimatePresence>
962
  </div>
963
  </div>
964
  );
 
18
  Grid,
19
  } from 'lucide-react';
20
  import DashboardNavbar from '../components/dashboard/DashboardNavbar';
21
+ import { PdfPreview } from '../components/ui';
22
  import './ReportSummary.css';
23
 
24
  import { API_BASE_URL } from '../config/api';
 
407
 
408
  if (selectedReports.length === 1) {
409
  const report = selectedReports[0];
410
+ const token = localStorage.getItem('access_token');
411
+ const fileUrl = report.preview_url
412
+ ? `${API_BASE}${report.preview_url}${token ? `?token=${token}` : ''}`
413
+ : '';
414
+
415
  return (
416
  <div className="preview-single">
417
  <div className="preview-header">
 
419
  <span className="preview-date">{formatDate(report.report_date || report.uploaded_at)}</span>
420
  </div>
421
  <div className="preview-frame">
422
+ <PdfPreview
423
+ fileUrl={fileUrl}
424
+ fileName={report.filename}
425
+ />
 
 
 
 
 
 
 
 
 
426
  </div>
427
  </div>
428
  );
429
  }
430
 
431
  // Multiple reports - grid view
432
+ const token = localStorage.getItem('access_token');
433
  return (
434
  <div className="preview-multi">
435
  <div className="preview-multi-header">
 
437
  <span>{selectedReports.length} Reports Selected</span>
438
  </div>
439
  <div className={`preview-grid grid-${Math.min(selectedReports.length, 3)}`}>
440
+ {selectedReports.map(report => {
441
+ const fileUrl = report.preview_url
442
+ ? `${API_BASE}${report.preview_url}${token ? `?token=${token}` : ''}`
443
+ : '';
444
+
445
+ return (
446
+ <div
447
+ key={report.id}
448
+ className={`preview-grid-item ${focusedPreviewId === report.id ? 'focused' : ''}`}
449
+ onClick={() => setFocusedPreviewId(focusedPreviewId === report.id ? null : report.id)}
450
+ >
451
+ <div className="preview-grid-item-header">
452
+ <span className="preview-grid-filename">{report.filename}</span>
453
+ <span className="preview-grid-date">{formatDate(report.report_date || report.uploaded_at)}</span>
454
+ </div>
455
+ <div className="preview-grid-content">
456
+ <PdfPreview
457
+ fileUrl={fileUrl}
458
+ fileName={report.filename}
 
 
 
459
  />
460
+ </div>
461
+ <button
462
+ className="preview-grid-expand"
463
+ onClick={(e) => {
464
+ e.stopPropagation();
465
+ window.open(fileUrl, '_blank');
466
+ }}
467
+ >
468
+ <Maximize2 size={14} />
469
+ </button>
470
  </div>
471
+ );
472
+ })}
 
 
 
 
 
 
 
 
 
473
  </div>
474
  </div>
475
  );
 
490
  }
491
 
492
  return (
493
+ <>
494
+ <div className="ai-panel">
495
+ {/* Tabs */}
496
+ <div className="ai-tabs">
497
+ <button
498
+ className={`ai-tab ${activeTab === 'summary' ? 'active' : ''}`}
499
+ onClick={() => setActiveTab('summary')}
500
+ disabled={selectedIds.size !== 1}
501
+ >
502
+ Summary
503
+ </button>
504
+ <button
505
+ className={`ai-tab ${activeTab === 'comparison' ? 'active' : ''}`}
506
+ onClick={() => setActiveTab('comparison')}
507
+ disabled={selectedIds.size < 2}
508
+ >
509
+ Comparison
510
+ </button>
511
+ <button
512
+ className={`ai-tab ${activeTab === 'metrics' ? 'active' : ''}`}
513
+ onClick={() => setActiveTab('metrics')}
514
+ disabled={selectedIds.size === 0}
515
+ >
516
+ Metrics
517
+ </button>
518
+ </div>
519
 
520
+ {/* Tab Content */}
521
+ <div className="ai-content">
522
  {/* Summary Tab */}
523
  {activeTab === 'summary' && (
524
  <>
 
745
  </div>
746
  )}
747
  </div>
748
+
749
+ {/* Action Footer - Generate Summary / Compare Button */}
750
+ <div className="ai-action-footer">
751
+ {selectedIds.size === 1 ? (
752
+ <button
753
+ className="action-btn primary"
754
+ onClick={() => generateSummary()}
755
+ disabled={summaryResult.loading}
756
+ style={{ width: '100%' }}
757
+ >
758
+ {summaryResult.loading ? (
759
+ <Loader2 size={18} className="spinner" />
760
+ ) : (
761
+ <Sparkles size={18} />
762
+ )}
763
+ Generate Summary
764
+ </button>
765
+ ) : (
766
+ <button
767
+ className="action-btn primary"
768
+ onClick={() => generateComparison()}
769
+ disabled={comparisonResult.loading || !validateSelection().valid}
770
+ style={{ width: '100%' }}
771
+ >
772
+ {comparisonResult.loading ? (
773
+ <Loader2 size={18} className="spinner" />
774
+ ) : (
775
+ <Sparkles size={18} />
776
+ )}
777
+ Compare Selected ({selectedIds.size})
778
+ </button>
779
+ )}
780
+ </div>
781
  </div>
782
+ </>
783
  );
784
  };
785
 
 
949
  </div>
950
  </div>
951
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
952
  </div>
953
  </div>
954
  );
frontend/src/pages/Reports.tsx CHANGED
@@ -12,12 +12,15 @@ import {
12
  Clock,
13
  AlertCircle,
14
  FolderOpen,
 
15
  } from 'lucide-react';
16
  import DashboardNavbar from '../components/dashboard/DashboardNavbar';
17
  import { useWebSocket } from '../hooks/useWebSocket';
18
  import { HealthProfileCard } from '../components/profile';
 
19
  import { API_BASE_URL } from '../config/api';
20
  import './Reports.css';
 
21
 
22
  // Types
23
  interface Report {
@@ -74,6 +77,7 @@ function Reports() {
74
  const [uploadError, setUploadError] = useState<string | null>(null);
75
  const fileInputRef = useRef<HTMLInputElement>(null);
76
  const itemsPerPage = 5;
 
77
 
78
  // WebSocket for real-time updates
79
  useWebSocket({
@@ -326,15 +330,15 @@ function Reports() {
326
  if (!token) return;
327
 
328
  try {
329
- const response = await fetch(`${API_BASE_URL}/api/reports/${report.id}/debug`, {
330
- headers: { Authorization: `Bearer ${token}` },
 
 
 
331
  });
332
- if (response.ok) {
333
- const data = await response.json();
334
- alert(`Report: ${report.name}\nStatus: ${data.status}\nExtraction Method: ${data.extraction_method || 'N/A'}\nMetrics Found: ${data.extracted_metrics_count}\nText Length: ${data.text_length} chars\n\nPreview:\n${data.text_preview?.substring(0, 500) || 'No text'}`);
335
- }
336
  } catch (error) {
337
  console.error('Error viewing report:', error);
 
338
  }
339
  };
340
 
@@ -584,35 +588,43 @@ function Reports() {
584
  </span>
585
  </td>
586
  <td className="actions-cell operations-col">
587
- <button
588
- className="action-btn"
589
- onClick={() => handleView(report)}
590
- title="View"
591
- >
592
- <Eye size={16} />
593
- </button>
594
- <button
595
- className="action-btn"
596
- onClick={() => handleReprocess(report)}
597
- title="Reprocess OCR"
598
- disabled={report.status === 'processing'}
599
- >
600
- <RefreshCw size={16} className={report.status === 'processing' ? 'spinning' : ''} />
601
- </button>
602
- <button
603
- className="action-btn"
604
- onClick={() => handleDelete(report)}
605
- title="Delete"
606
- >
607
- <Trash2 size={16} />
608
- </button>
609
- <button
610
- className="action-btn"
611
- onClick={() => handleDownload(report)}
612
- title="Download"
613
- >
614
- <Download size={16} />
615
- </button>
 
 
 
 
 
 
 
 
616
  </td>
617
  </motion.tr>
618
  ))}
@@ -654,6 +666,44 @@ function Reports() {
654
  </div>
655
  </div>
656
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
657
  </div>
658
  );
659
  }
 
12
  Clock,
13
  AlertCircle,
14
  FolderOpen,
15
+ X,
16
  } from 'lucide-react';
17
  import DashboardNavbar from '../components/dashboard/DashboardNavbar';
18
  import { useWebSocket } from '../hooks/useWebSocket';
19
  import { HealthProfileCard } from '../components/profile';
20
+ import { Tooltip, PdfPreview } from '../components/ui';
21
  import { API_BASE_URL } from '../config/api';
22
  import './Reports.css';
23
+ import './ReportsExtras.css';
24
 
25
  // Types
26
  interface Report {
 
77
  const [uploadError, setUploadError] = useState<string | null>(null);
78
  const fileInputRef = useRef<HTMLInputElement>(null);
79
  const itemsPerPage = 5;
80
+ const [pdfPreview, setPdfPreview] = useState<{ reportId: string; fileName: string; url: string } | null>(null);
81
 
82
  // WebSocket for real-time updates
83
  useWebSocket({
 
330
  if (!token) return;
331
 
332
  try {
333
+ const downloadUrl = `${API_BASE_URL}/api/reports/${report.id}/download?token=${token}`;
334
+ setPdfPreview({
335
+ reportId: report.id,
336
+ fileName: report.name,
337
+ url: downloadUrl,
338
  });
 
 
 
 
339
  } catch (error) {
340
  console.error('Error viewing report:', error);
341
+ alert('Failed to load PDF preview');
342
  }
343
  };
344
 
 
588
  </span>
589
  </td>
590
  <td className="actions-cell operations-col">
591
+ <Tooltip content="View Document">
592
+ <button
593
+ className="action-btn"
594
+ onClick={() => handleView(report)}
595
+ aria-label="View"
596
+ >
597
+ <Eye size={16} />
598
+ </button>
599
+ </Tooltip>
600
+ <Tooltip content="Reload/Reprocess">
601
+ <button
602
+ className="action-btn"
603
+ onClick={() => handleReprocess(report)}
604
+ aria-label="Reprocess OCR"
605
+ disabled={report.status === 'processing'}
606
+ >
607
+ <RefreshCw size={16} className={report.status === 'processing' ? 'spinning' : ''} />
608
+ </button>
609
+ </Tooltip>
610
+ <Tooltip content="Delete">
611
+ <button
612
+ className="action-btn"
613
+ onClick={() => handleDelete(report)}
614
+ aria-label="Delete"
615
+ >
616
+ <Trash2 size={16} />
617
+ </button>
618
+ </Tooltip>
619
+ <Tooltip content="Download">
620
+ <button
621
+ className="action-btn"
622
+ onClick={() => handleDownload(report)}
623
+ aria-label="Download"
624
+ >
625
+ <Download size={16} />
626
+ </button>
627
+ </Tooltip>
628
  </td>
629
  </motion.tr>
630
  ))}
 
666
  </div>
667
  </div>
668
  </div>
669
+
670
+ {/* PDF Preview Modal */}
671
+ <AnimatePresence>
672
+ {pdfPreview && (
673
+ <motion.div
674
+ className="pdf-modal-overlay"
675
+ initial={{ opacity: 0 }}
676
+ animate={{ opacity: 1 }}
677
+ exit={{ opacity: 0 }}
678
+ onClick={() => setPdfPreview(null)}
679
+ >
680
+ <motion.div
681
+ className="pdf-modal-content"
682
+ initial={{ scale: 0.9, opacity: 0 }}
683
+ animate={{ scale: 1, opacity: 1 }}
684
+ exit={{ scale: 0.9, opacity: 0 }}
685
+ onClick={(e) => e.stopPropagation()}
686
+ >
687
+ <div className="pdf-modal-header">
688
+ <h3>{pdfPreview.fileName}</h3>
689
+ <button
690
+ className="pdf-modal-close"
691
+ onClick={() => setPdfPreview(null)}
692
+ aria-label="Close"
693
+ >
694
+ <X size={24} />
695
+ </button>
696
+ </div>
697
+ <div className="pdf-modal-body">
698
+ <PdfPreview
699
+ fileUrl={pdfPreview.url}
700
+ fileName={pdfPreview.fileName}
701
+ />
702
+ </div>
703
+ </motion.div>
704
+ </motion.div>
705
+ )}
706
+ </AnimatePresence>
707
  </div>
708
  );
709
  }
frontend/src/pages/ReportsExtras.css ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* PDF Preview Modal */
2
+ .pdf-modal-overlay {
3
+ position: fixed;
4
+ inset: 0;
5
+ background: rgba(0, 0, 0, 0.75);
6
+ display: flex;
7
+ align-items: center;
8
+ justify-content: center;
9
+ z-index: 9999;
10
+ padding: 2rem;
11
+ }
12
+
13
+ .pdf-modal-content {
14
+ background: white;
15
+ border-radius: 0.75rem;
16
+ width: 100%;
17
+ max-width: 1200px;
18
+ height: 90vh;
19
+ display: flex;
20
+ flex-direction: column;
21
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
22
+ }
23
+
24
+ .pdf-modal-header {
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: space-between;
28
+ padding: 1.5rem;
29
+ border-bottom: 1px solid #e5e7eb;
30
+ }
31
+
32
+ .pdf-modal-header h3 {
33
+ margin: 0;
34
+ font-size: 1.25rem;
35
+ font-weight: 600;
36
+ color: #1f2937;
37
+ }
38
+
39
+ .pdf-modal-close {
40
+ background: transparent;
41
+ border: none;
42
+ cursor: pointer;
43
+ padding: 0.5rem;
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: center;
47
+ border-radius: 0.375rem;
48
+ transition: background 0.2s;
49
+ }
50
+
51
+ .pdf-modal-close:hover {
52
+ background: #f3f4f6;
53
+ }
54
+
55
+ .pdf-modal-body {
56
+ flex: 1;
57
+ overflow: hidden;
58
+ padding: 1rem;
59
+ }
60
+
61
+ /* Table responsiveness fixes */
62
+ .reports-table-container {
63
+ width: 100%;
64
+ overflow-x: auto;
65
+ -webkit-overflow-scrolling: touch;
66
+ }
67
+
68
+ .reports-table {
69
+ width: 100%;
70
+ min-width: 100%;
71
+ border-collapse: collapse;
72
+ table-layout: auto;
73
+ }
74
+
75
+ .reports-table th,
76
+ .reports-table td {
77
+ padding: 0.75rem 1rem;
78
+ text-align: left;
79
+ white-space: nowrap;
80
+ }
81
+
82
+ .reports-table .doc-name {
83
+ max-width: 300px;
84
+ overflow: hidden;
85
+ text-overflow: ellipsis;
86
+ white-space: nowrap;
87
+ }
88
+
89
+ .operations-col {
90
+ width: auto;
91
+ min-width: 180px;
92
+ white-space: nowrap;
93
+ }
94
+
95
+ .actions-cell {
96
+ display: flex;
97
+ gap: 0.5rem;
98
+ align-items: center;
99
+ justify-content: flex-start;
100
+ }
101
+
102
+ .action-btn {
103
+ flex-shrink: 0;
104
+ }
105
+
106
+ /* Responsive adjustments */
107
+ @media (max-width: 1200px) {
108
+ .reports-table th:not(.checkbox-col):not(.operations-col),
109
+ .reports-table td:not(.checkbox-col):not(.operations-col) {
110
+ padding: 0.5rem 0.75rem;
111
+ }
112
+
113
+ .reports-table .doc-name {
114
+ max-width: 200px;
115
+ }
116
+ }
117
+
118
+ @media (max-width: 768px) {
119
+ .pdf-modal-overlay {
120
+ padding: 1rem;
121
+ }
122
+
123
+ .pdf-modal-content {
124
+ height: 85vh;
125
+ }
126
+ }