Spaces:
Sleeping
Sleeping
darved2305 commited on
Commit ·
5beb8a2
1
Parent(s): b718129
ui errors fix and readme update with new features implemented
Browse files- README.md +438 -14
- backend/.env.example +1 -1
- backend/app/main.py +2 -2
- backend/app/routes/__init__.py +1 -1
- backend/app/routes/documents.py +1 -1
- backend/app/services/__init__.py +1 -1
- backend/app/services/document_classifier.py +1 -1
- backend/app/services/document_processing.py +1 -1
- backend/app/services/grok_extractor.py +1 -1
- backend/app/services/metric_extractor.py +1 -1
- backend/src/__init__.py +1 -1
- backend/src/app.py +3 -3
- backend/src/routes/__init__.py +1 -1
- backend/src/services/__init__.py +1 -1
- backend/start.bat +1 -1
- backend/tests/__init__.py +1 -1
- frontend/assets/logo.png +0 -0
- frontend/index.html +2 -1
- frontend/package.json +1 -1
- frontend/src/assets/logo.png +0 -0
- frontend/src/components/ArtOfNaturalRemedies.tsx +7 -1
- frontend/src/components/Footer.tsx +3 -3
- frontend/src/components/Header.css +3 -1
- frontend/src/components/Header.tsx +12 -30
- frontend/src/components/Hero.css +5 -5
- frontend/src/components/HowWeCanHelp.css +3 -3
- frontend/src/components/dashboard/DashboardNavbar.tsx +2 -11
- frontend/src/components/ui/Logo.css +29 -0
- frontend/src/components/ui/Logo.tsx +22 -0
- frontend/src/components/ui/PdfPreview.css +48 -0
- frontend/src/components/ui/PdfPreview.tsx +65 -0
- frontend/src/components/ui/Tooltip.css +84 -0
- frontend/src/components/ui/Tooltip.tsx +29 -0
- frontend/src/components/ui/index.ts +3 -0
- frontend/src/i18n/locales/en.json +7 -7
- frontend/src/i18n/locales/hi.json +7 -7
- frontend/src/i18n/locales/mr.json +7 -7
- frontend/src/index.css +4 -0
- frontend/src/pages/Dashboard.tsx +14 -7
- frontend/src/pages/DashboardExtras.css +27 -0
- frontend/src/pages/ReportSummary.css +25 -0
- frontend/src/pages/ReportSummary.tsx +104 -114
- frontend/src/pages/Reports.tsx +85 -35
- frontend/src/pages/ReportsExtras.css +126 -0
README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
#
|
| 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 |
-
|
| 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 |
-
| `
|
| 289 |
-
| `
|
| 290 |
-
| `
|
| 291 |
-
| `
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 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="
|
| 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": "
|
| 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
|
| 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:
|
| 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
|
| 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:
|
| 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:
|
| 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:
|
| 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:
|
| 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 |
-
|
| 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 |
-
|
| 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="
|
| 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": "
|
| 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
|
| 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
|
| 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
|
| 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
|
| 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>
|
|
|
|
| 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": "
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
| 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 |
-
<
|
| 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:
|
| 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 |
-
<
|
| 28 |
-
|
| 29 |
-
|
| 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 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 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:
|
| 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:
|
| 89 |
font-weight: 500;
|
| 90 |
-
padding:
|
| 91 |
-
border-radius:
|
| 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:
|
| 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: #
|
| 111 |
transition: all 0.3s ease;
|
| 112 |
}
|
| 113 |
|
| 114 |
.service-view-link:hover {
|
| 115 |
gap: 14px;
|
| 116 |
-
color: #
|
| 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 |
-
<
|
| 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.
|
| 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
|
| 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.
|
| 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": "
|
| 100 |
"items": {
|
| 101 |
"isDoctor": {
|
| 102 |
-
"question": "Is
|
| 103 |
-
"answer": "No.
|
| 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,
|
| 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": "अपनी जांच रिपोर्ट अपलोड करें, उनका विश्लेषण प्राप्त करें, और नियमित जांच व दवाइयों के लिए स्मार्ट स्मरण पाएं।
|
| 18 |
"disclaimer": "यह चिकित्सा सलाह नहीं है। निदान/उपचार के लिए हमेशा लाइसेंस प्राप्त चिकित्सक से परामर्श करें।",
|
| 19 |
"cta": {
|
| 20 |
"upload": "रिपोर्ट अपलोड करें",
|
|
@@ -50,7 +50,7 @@
|
|
| 50 |
}
|
| 51 |
},
|
| 52 |
"howItWorks": {
|
| 53 |
-
"title": "
|
| 54 |
"subtitle": "रिपोर्ट अपलोड करें, जानकारी पाएं, और एक सुरक्षित मंच पर रोकथाम देखभाल के शीर्ष पर रहें।",
|
| 55 |
"features": {
|
| 56 |
"ocr": {
|
|
@@ -69,7 +69,7 @@
|
|
| 69 |
"cta": "सभी विशेषताएं देखें"
|
| 70 |
},
|
| 71 |
"belief": {
|
| 72 |
-
"message": "अधिकतर स्वास्थ्य समस्याएं आपातकाल के रूप में शुरू नहीं होतीं। वे चुपचाप शुरू होती हैं।
|
| 73 |
"tagline": "एक सुझाव प्रणाली के रूप में बनाया गया, डॉक्टर के रूप में नहीं।"
|
| 74 |
},
|
| 75 |
"news": {
|
|
@@ -96,11 +96,11 @@
|
|
| 96 |
"faq": {
|
| 97 |
"title": "रोकथाम की",
|
| 98 |
"titleHighlight": "कला",
|
| 99 |
-
"subtitle": "
|
| 100 |
"items": {
|
| 101 |
"isDoctor": {
|
| 102 |
-
"question": "क्या
|
| 103 |
-
"answer": "नहीं।
|
| 104 |
},
|
| 105 |
"reports": {
|
| 106 |
"question": "मैं कौन सी रिपोर्ट अपलोड कर सकता हूं?",
|
|
@@ -112,7 +112,7 @@
|
|
| 112 |
},
|
| 113 |
"reminders": {
|
| 114 |
"question": "स्मरण कैसे व्यक्तिगत किए जाते हैं?",
|
| 115 |
-
"answer": "आपकी रिपोर्ट इतिहास, उम्र और पिछली जांच तिथियों के आधार पर,
|
| 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": "तुमचे तपासणी अहवाल अपलोड करा, त्यांचे विश्लेषण मिळवा, आणि नियमित तपासणी व औषधांसाठी स्मार्ट स्मरणपत्र मिळवा।
|
| 18 |
"disclaimer": "हा वैद्यकीय सल्ला नाही. निदान/उपचारासाठी नेहमी परवानाधारक वैद्यांचा सल्ला घ्या।",
|
| 19 |
"cta": {
|
| 20 |
"upload": "अहवाल अपलोड करा",
|
|
@@ -50,7 +50,7 @@
|
|
| 50 |
}
|
| 51 |
},
|
| 52 |
"howItWorks": {
|
| 53 |
-
"title": "
|
| 54 |
"subtitle": "अहवाल अपलोड करा, माहिती मिळवा आणि एका सुरक्षित व्यासपीठावर प्रतिबंधात्मक काळजीच्या शीर्षस्थानी रहा।",
|
| 55 |
"features": {
|
| 56 |
"ocr": {
|
|
@@ -69,7 +69,7 @@
|
|
| 69 |
"cta": "सर्व वैशिष्ट्ये पहा"
|
| 70 |
},
|
| 71 |
"belief": {
|
| 72 |
-
"message": "बहुतेक आरोग्य समस्या आणीबाणी म्हणून सुरू होत नाहीत
|
| 73 |
"tagline": "एक सूचना प्रणाली म्हणून तयार केले, डॉक्टर म्हणून नाही।"
|
| 74 |
},
|
| 75 |
"news": {
|
|
@@ -96,11 +96,11 @@
|
|
| 96 |
"faq": {
|
| 97 |
"title": "प्रतिबंधाची",
|
| 98 |
"titleHighlight": "कला",
|
| 99 |
-
"subtitle": "
|
| 100 |
"items": {
|
| 101 |
"isDoctor": {
|
| 102 |
-
"question": "
|
| 103 |
-
"answer": "नाही।
|
| 104 |
},
|
| 105 |
"reports": {
|
| 106 |
"question": "मी कोणते अहवाल अपलोड करू शकतो?",
|
|
@@ -112,7 +112,7 @@
|
|
| 112 |
},
|
| 113 |
"reminders": {
|
| 114 |
"question": "स्मरणपत्र वैयक्तिक कसे केली जातात?",
|
| 115 |
-
"answer": "तुमच्या अहवाल इतिहास, वय आणि मागील तपासणी तारखांच्या आधारे,
|
| 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 |
-
<
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 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 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 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 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
<div
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
<
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
src={`${API_BASE}${report.preview_url}`}
|
| 462 |
-
alt={report.filename}
|
| 463 |
-
className="preview-grid-image"
|
| 464 |
/>
|
| 465 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
</div>
|
| 467 |
-
|
| 468 |
-
|
| 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 |
-
<
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
<
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
|
|
|
| 523 |
|
| 524 |
-
|
| 525 |
-
|
| 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
|
| 330 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
>
|
| 607 |
-
<
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|