File size: 21,364 Bytes
91b9f55
 
f4102c2
91b9f55
f4102c2
91b9f55
f4102c2
91b9f55
f4102c2
9881306
f4102c2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91b9f55
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
---
title: BeatSense
emoji: 🩺
colorFrom: blue
colorTo: indigo
sdk: docker
app_port: 7860
pinned: false
license: mit
short_description: Cardiovascular risk workstation (FastAPI + Next.js)
---

<p align="center">
  <img src="src/img/logo.png" alt="BeatSense" width="260" />
</p>

<h1 align="center">BeatSense β€” Cardiovascular Risk Intelligence</h1>

<p align="center">
  <a href="https://huggingface.co/spaces/devrup404/BeatSense">
    <img src="src/img/qr_hf.png" alt="QR β€” Hugging Face Space" width="160"/>
  </a>
</p>

<p align="center">
  <sub><b>Scan to open the Hugging Face Space</b></sub>
</p>

<p align="center">
  <a href="README.md"><img src="https://img.shields.io/badge/English-0d1a1f?style=flat&logoColor=7fd1c6&labelColor=0a1014" alt="English" /></a>
  Β·
  <a href="README.es.md"><img src="https://img.shields.io/badge/Espa%C3%B1ol-0d1a1f?style=flat&logoColor=7fd1c6&labelColor=0a1014" alt="EspaΓ±ol" /></a>
</p>

<p align="center">
  <img src="https://img.shields.io/badge/Python-3.11-0a1014?logo=python&logoColor=7fd1c6&labelColor=0d1a1f" />
  <img src="https://img.shields.io/badge/FastAPI-0.115-0a1014?logo=fastapi&logoColor=7fd1c6&labelColor=0d1a1f" />
  <img src="https://img.shields.io/badge/scikit--learn-1.8-0a1014?logo=scikitlearn&logoColor=7fd1c6&labelColor=0d1a1f" />
  <img src="https://img.shields.io/badge/XGBoost-3.x-0a1014?logoColor=7fd1c6&labelColor=0d1a1f" />
  <img src="https://img.shields.io/badge/Optuna-4.x-0a1014?logoColor=7fd1c6&labelColor=0d1a1f" />
  <img src="https://img.shields.io/badge/uv-package%20mgr-0a1014?logoColor=7fd1c6&labelColor=0d1a1f" />
  <img src="https://img.shields.io/badge/Next.js-16-0a1014?logo=nextdotjs&logoColor=7fd1c6&labelColor=0d1a1f" />
  <img src="https://img.shields.io/badge/React-19-0a1014?logo=react&logoColor=7fd1c6&labelColor=0d1a1f" />
  <img src="https://img.shields.io/badge/TypeScript-5-0a1014?logo=typescript&logoColor=7fd1c6&labelColor=0d1a1f" />
  <img src="https://img.shields.io/badge/Tailwind-v4-0a1014?logo=tailwindcss&logoColor=7fd1c6&labelColor=0d1a1f" />
  <img src="https://img.shields.io/badge/TanStack%20Query-5-0a1014?logoColor=7fd1c6&labelColor=0d1a1f" />
  <img src="https://img.shields.io/badge/Recharts-2-0a1014?logoColor=7fd1c6&labelColor=0d1a1f" />
  <img src="https://img.shields.io/badge/pnpm-10-0a1014?logo=pnpm&logoColor=7fd1c6&labelColor=0d1a1f" />
  <img src="https://img.shields.io/badge/Streamlit-1.x-0a1014?logo=streamlit&logoColor=7fd1c6&labelColor=0d1a1f" />
  <img src="https://img.shields.io/badge/Docker-compose-0a1014?logo=docker&logoColor=7fd1c6&labelColor=0d1a1f" />
</p>

<p align="center">
  <a href="https://medium.com/@devrup404/most-clinical-machine-learning-prototypes-share-the-same-fate-2c357e91e112"><img src="https://img.shields.io/badge/Medium-Read%20the%20article-0a1014?logo=medium&logoColor=7fd1c6&labelColor=0d1a1f" alt="Read on Medium" /></a>
</p>

> **BeatSense** is a cardiovascular risk assessment platform born from a question: what would a coronary risk tool actually feel like if it were built for the nurses station of a real hospital, instead of for a notebook demo?
>
> Around a trained scikit-learn model (918 patients, twelve clinical features), it ships three coordinated surfaces: a FastAPI service that exposes the model and persists every assessment, a Next.js 16 clinical workstation with sidebar navigation, dark mode, English and Spanish, and a result rendered as a printable thermal-style ticket ready to clip to the patient record, plus a legacy Streamlit dashboard kept alive for quick demos. The model itself is a Logistic Regression with a custom 0.4 decision threshold, picked because in cardiology a false negative is far more dangerous than a false positive, an interpretable choice that recovers 94% of true positives.
>
> The repo is end-to-end: notebooks for EDA and training, a typed REST API, a production-grade clinical UI, internationalization with zero hardcoded strings, and theme tokens out of the box.

---

## Table of Contents

| | |
|---|---|
| Overview | Live Demo |
| Features | Architecture |
| Project Structure | Tech Stack |
| Dataset | Model Selection |
| Getting Started | Make Targets |
| API Reference | Frontend Highlights |
| Internationalization | Theming |
| Testing | Docker |

---

## Overview

**BeatSense** predicts the probability of coronary heart disease from clinical patient data. It ships three independent surfaces sharing the same trained model:

- A **FastAPI** REST backend that wraps the production model (models/modelo_coronario.pkl).
- A **Next.js 16** clinical workstation UI β€” sidebar, ticket-style report, dark mode, EN/ES β€” for nurses and clinicians.
- A legacy **Streamlit** dashboard (still operational) for quick demos.

The trained model is a **Logistic Regression** with a custom decision threshold of 0.4, chosen because in cardiology the cost of a false negative (missing a sick patient) far exceeds that of a false positive.

---

## Live Demo

<p>
  <a href="https://huggingface.co/spaces/devrup404/BeatSense"><img src="https://img.shields.io/badge/Hugging%20Face-Open%20Space-0a1014?style=flat&logo=huggingface&logoColor=7fd1c6&labelColor=0d1a1f" alt="Hugging Face Space" /></a>
</p>

---

## Features

| Surface | Highlights |
|---|---|
| πŸ€– **ML pipeline** | EDA + survival analysis (Kaplan–Meier, Cox), preprocessing, 4 models compared, Optuna tuning, full notebooks. |
| πŸš€ **REST API** | FastAPI service with /predict, /history, /stats, /eda, /model-info, persistent CSV history, CORS enabled. |
| 🩺 **Clinical UI** | Next.js 16 + Tailwind v4 + shadcn-style components. Sidebar navigation, nurse-station header, KPI dashboard, filtered patient table, EDA charts, model insights. |
| πŸ–¨οΈ **Printable ticket** | Risk result rendered as a thermal-printer ticket: monospace font, dashed dividers, barcode, window.print(). |
| 🌍 **i18n** | English (default) + Spanish, zero hardcoded strings, JSON dictionaries, cookie + Accept-Language detection. |
| πŸŒ“ **Dark mode** | First-class next-themes integration, dark by default, no flash on first paint. |
| πŸ“¦ **Containerized** | Docker / docker-compose for the Streamlit surface. |
| πŸ› οΈ **Dual toolchain** | uv for Python, pnpm for the Next.js workspace. One make dev boots both servers. |

---

## Architecture

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      HTTPS/REST       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Next.js 16 frontend     β”‚  ────────────────▢    β”‚ FastAPI service         β”‚
β”‚ β€’ App Router            β”‚                       β”‚ β€’ /predict              β”‚
β”‚ β€’ Server + Client       β”‚                       β”‚ β€’ /history /stats       β”‚
β”‚   Components            β”‚                       β”‚ β€’ /eda /model-info      β”‚
β”‚ β€’ TanStack Query        β”‚  ◀────────────────    β”‚ β€’ Loads .pkl + scaler   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      JSON              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚                                                β”‚
              β”‚ static assets                                   β”‚ joblib.load
              β–Ό                                                β–Ό
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚ /public  β”‚                                β”‚ models/             β”‚
        β”‚ logu1.pngβ”‚                                β”‚  β€’ modelo_coronario β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                β”‚  β€’ scaler_coronario β”‚
                                                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                              β–²
                                                              β”‚ persists
                                                              β”‚
                                                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                    β”‚ docs/                β”‚
                                                    β”‚ historial_pacientes  β”‚
                                                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

---

## Project Structure

```
BeatSense/
β”œβ”€β”€ api/                              # FastAPI backend
β”‚   └── main.py                       # /predict, /history, /stats, /eda, /model-info
β”‚
β”œβ”€β”€ app.py                            # Legacy Streamlit app (still works)
β”‚
β”œβ”€β”€ frontend/                         # Next.js 16 clinical workstation
β”‚   β”œβ”€β”€ public/
β”‚   β”‚   └── logu1.png                 # Logo (auto-cropped for tight framing)
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ app/
β”‚   β”‚   β”‚   β”œβ”€β”€ layout.tsx            # Root layout β€” ThemeProvider lives here
β”‚   β”‚   β”‚   β”œβ”€β”€ globals.css           # Tailwind v4 + design tokens + print CSS
β”‚   β”‚   β”‚   └── [lang]/               # Locale-scoped routes
β”‚   β”‚   β”‚       β”œβ”€β”€ layout.tsx        # CRM shell (Sidebar + Header) + Providers
β”‚   β”‚   β”‚       β”œβ”€β”€ page.tsx          # Dashboard (KPIs + recent + pie chart)
β”‚   β”‚   β”‚       β”œβ”€β”€ new-evaluation/   # Clinical form β†’ ticket result
β”‚   β”‚   β”‚       β”œβ”€β”€ patients/         # Filterable history table + CSV export
β”‚   β”‚   β”‚       β”œβ”€β”€ eda/              # Dataset analytics (Recharts)
β”‚   β”‚   β”‚       β”œβ”€β”€ model/            # Model metrics + comparison
β”‚   β”‚   β”‚       └── settings/         # Language + theme switcher
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”œβ”€β”€ ui/                   # Button, Card, Input, Label, Select, Badge
β”‚   β”‚   β”‚   β”œβ”€β”€ layout/               # Sidebar, Header, PageHeader, toggles
β”‚   β”‚   β”‚   β”œβ”€β”€ forms/                # EvaluationForm (RHF + Zod)
β”‚   β”‚   β”‚   β”œβ”€β”€ ticket/               # Printable result ticket
β”‚   β”‚   β”‚   β”œβ”€β”€ charts/               # EDA charts
β”‚   β”‚   β”‚   β”œβ”€β”€ dashboard/            # Dashboard client widgets
β”‚   β”‚   β”‚   β”œβ”€β”€ patients/             # Patient table
β”‚   β”‚   β”‚   β”œβ”€β”€ model/                # Model insights
β”‚   β”‚   β”‚   β”œβ”€β”€ settings/             # Settings panel
β”‚   β”‚   β”‚   └── providers.tsx         # ThemeRoot + I18nProvider + QueryClient
β”‚   β”‚   β”œβ”€β”€ i18n/
β”‚   β”‚   β”‚   β”œβ”€β”€ config.ts             # Locale list + defaultLocale
β”‚   β”‚   β”‚   β”œβ”€β”€ context.tsx           # Client-side useT() hook
β”‚   β”‚   β”‚   β”œβ”€β”€ dictionaries.ts       # Server-only loader
β”‚   β”‚   β”‚   └── dictionaries/
β”‚   β”‚   β”‚       β”œβ”€β”€ en.json
β”‚   β”‚   β”‚       └── es.json
β”‚   β”‚   β”œβ”€β”€ lib/
β”‚   β”‚   β”‚   β”œβ”€β”€ api.ts                # Typed REST client (TanStack-friendly)
β”‚   β”‚   β”‚   └── utils.ts              # cn() helper
β”‚   β”‚   └── proxy.ts                  # Next 16 middleware: locale routing
β”‚   β”œβ”€β”€ package.json
β”‚   └── tsconfig.json
β”‚
β”œβ”€β”€ data/                             # Heart Disease dataset (918 records)
β”‚   β”œβ”€β”€ heart.csv                     # Raw
β”‚   β”œβ”€β”€ heart_procesado_g.csv         # Processed by Gema
β”‚   β”œβ”€β”€ heart_procesado_i.csv         # Processed by Isrodam
β”‚   └── heart_processed_R.csv         # Processed by Roberto
β”‚
β”œβ”€β”€ models/                           # Persisted scikit-learn artifacts
β”‚   β”œβ”€β”€ modelo_coronario.pkl
β”‚   β”œβ”€β”€ scaler_coronario.pkl
β”‚   └── model_R.pkl
β”‚
β”œβ”€β”€ notebook/                         # EDA + training notebooks
β”‚   β”œβ”€β”€ eda_heart_disease_p.ipynb     # EDA + survival analysis (Paloma)
β”‚   β”œβ”€β”€ eda_heart_failure_g.ipynb     # EDA (Gema)
β”‚   β”œβ”€β”€ eda_heart_failure_i.ipynb     # EDA (Isrodam)
β”‚   β”œβ”€β”€ eda_heart_failure_R.ipynb     # EDA (Roberto)
β”‚   β”œβ”€β”€ entrenamiento_modelo_final.ipynb
β”‚   β”œβ”€β”€ entrenamiento_modelo_g.ipynb
β”‚   └── entrenamiento_modelo_i.ipynb
β”‚
β”œβ”€β”€ report/                           # Evaluation reports (PDF)
β”œβ”€β”€ docs/historial_pacientes.csv      # Patient history (written by /predict)
β”œβ”€β”€ src/img/qr_app.png                # QR code to live demo
β”œβ”€β”€ assets/                           # Static assets (lottie, etc.)
β”œβ”€β”€ .streamlit/                       # Streamlit theming
β”œβ”€β”€ Makefile                          # uv + pnpm orchestration
β”œβ”€β”€ dockerfile
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ pyproject.toml                    # uv-managed Python deps
└── uv.lock
```

---

## Tech Stack

### Backend / ML

- **Python 3.11** managed with **uv**
- **FastAPI** + **Uvicorn** β€” REST API
- **scikit-learn** β€” Logistic Regression, RF, KNN; StandardScaler
- **XGBoost** β€” alternative model evaluated
- **Optuna** β€” hyperparameter search
- **pandas / numpy** β€” data wrangling
- **joblib** β€” model persistence
- **Streamlit** β€” legacy interactive dashboard
- **pytest** β€” tests

### Frontend

- **Next.js 16** (App Router, Turbopack, Server Components)
- **React 19** + **TypeScript 5**
- **Tailwind CSS v4** with custom design tokens
- **shadcn-style UI** built on **Radix UI** primitives
- **TanStack Query 5** β€” server-state caching
- **React Hook Form 7** + **Zod 4** β€” typed forms with runtime validation
- **Recharts** β€” analytics charts
- **next-themes** β€” dark/light theme
- **next-intl-style i18n** using Next.js native dictionaries
- **lucide-react** β€” icon set
- **pnpm 10** β€” workspace package manager

### DevOps

- **Make** β€” single command interface
- **Docker / docker-compose** β€” containerised Streamlit deployment
- **GitHub Actions** β€” CI/CD
- **Render** β€” production hosting (Streamlit demo)

---

## Dataset

918 records, 12 features:

| Variable | Description | Type |
|---|---|---|
| Age | Patient age | Numeric |
| Sex | Sex (M/F) | Categorical |
| ChestPainType | Type (ATA, NAP, ASY, TA) | Categorical |
| RestingBP | Resting blood pressure (mm Hg) | Numeric |
| Cholesterol | Serum cholesterol (mg/dl) | Numeric |
| FastingBS | Fasting blood sugar > 120 mg/dl (0/1) | Binary |
| RestingECG | Resting ECG result | Categorical |
| MaxHR | Max heart rate achieved | Numeric |
| ExerciseAngina | Exercise-induced angina (Y/N) | Binary |
| Oldpeak | ST depression | Numeric |
| ST_Slope | ST segment slope | Categorical |
| HeartDisease | **Target** β€” heart disease (0/1) | Binary |

The production model uses **12 engineered features** (Age, Sex, MaxHR, FastingBS, ExerciseAngina, one-hot of ChestPainType, one-hot of ST_Slope). Only Age and MaxHR are scaled.

---

## Model Selection

| Model | Accuracy | Recall (class 1) | ROC-AUC | Notes |
|---|---|---|---|---|
| **Logistic Regression** *(threshold 0.4)* | **88 %** | **0.94** | **0.926** | βœ… Selected |
| Random Forest | ~87 % | ~0.88 | ~0.91 | Higher raw accuracy, lower recall |
| K-NN | ~82 % | ~0.83 | ~0.87 | Scale-sensitive |
| XGBoost | ~86 % | ~0.87 | ~0.90 | Strong, less interpretable |

### Why Logistic Regression?

1. **Highest recall (0.94)** β€” at threshold 0.4 it catches 94% of truly sick patients. A false negative in cardiology is far worse than a false positive.
2. **No overfitting** β€” train/test gap of only 0.007. 5-fold CV: 84.74% Β± 2.91%.
3. **Clinical transparency** β€” linear coefficients let clinicians see exactly which features drive each prediction.

> Full analysis:
>
> <a href="notebook/entrenamiento_modelo_final.ipynb"><img src="https://img.shields.io/badge/notebook-entrenamiento__modelo__final.ipynb-0a1014?style=flat&logo=jupyter&logoColor=7fd1c6&labelColor=0d1a1f" alt="Notebook" /></a>
> <a href="report/entrenamiento_modelo_final.pdf"><img src="https://img.shields.io/badge/report-entrenamiento__modelo__final.pdf-0a1014?style=flat&logo=adobeacrobatreader&logoColor=7fd1c6&labelColor=0d1a1f" alt="Report" /></a>

---

## Getting Started

### Prerequisites

- Python >=3.11, <3.13
- <a href="https://docs.astral.sh/uv/"><img src="https://img.shields.io/badge/uv-Python%20toolchain-0a1014?style=flat&logoColor=7fd1c6&labelColor=0d1a1f" alt="uv" /></a>
- <a href="https://pnpm.io/"><img src="https://img.shields.io/badge/pnpm-%E2%89%A510%20Node.js%20toolchain-0a1014?style=flat&logo=pnpm&logoColor=7fd1c6&labelColor=0d1a1f" alt="pnpm" /></a>
- Node.js >=20

### Install

```bash
# Python deps
make install

# Frontend deps
make frontend-install
```

### Run everything (recommended)

```bash
make dev
```

This spawns:

<p>
  <a href="http://localhost:8000/docs"><img src="https://img.shields.io/badge/FastAPI-localhost:8000/docs-0a1014?style=flat&logo=fastapi&logoColor=7fd1c6&labelColor=0d1a1f" alt="FastAPI" /></a>
  <a href="http://localhost:3000"><img src="https://img.shields.io/badge/Next.js-localhost:3000-0a1014?style=flat&logo=nextdotjs&logoColor=7fd1c6&labelColor=0d1a1f" alt="Next.js" /></a>
</p>

### Run individually

```bash
make api          # FastAPI only
make frontend     # Next.js only
make streamlit    # Legacy Streamlit dashboard
```

---

## Make Targets

| Target | Purpose |
|---|---|
| make install | uv sync β€” install Python deps |
| make frontend-install | pnpm install β€” install frontend deps |
| make dev | Run **both** FastAPI + Next.js in parallel |
| make api | Run FastAPI only (uvicorn :8000) |
| make frontend | Run Next.js only (pnpm dev :3000) |
| make frontend-build | Production build of Next.js |
| make streamlit | Run legacy Streamlit app |
| make docker-up / docker-down / docker-build / docker-logs | Docker Compose lifecycle |
| make jupyter | Launch JupyterLab |
| make test | Run pytest |
| make clean | Remove caches and .next/ |

---

## API Reference

<p>
  <b>Base URL</b>&nbsp;
  <a href="http://localhost:8000"><img src="https://img.shields.io/badge/localhost:8000-0a1014?style=flat&logoColor=7fd1c6&labelColor=0d1a1f" alt="localhost:8000" /></a>
</p>

| Method | Path | Description |
|---|---|---|
| GET | /health | Liveness + model load status |
| POST | /predict | Run cardiovascular risk prediction |
| GET | /history | Return all saved patient assessments |
| GET | /stats | Aggregate KPIs (totals, high-risk %, today) |
| GET | /eda | Aggregated EDA payload for charts |
| GET | /model-info | Metrics of the selected model + alternatives |

### POST /predict payload

```json
{
  "patient_id": "P-001",
  "age": 55,
  "sex": "M",
  "chest_pain_type": "ASY",
  "max_hr": 130,
  "fasting_bs": 0,
  "exercise_angina": "Y",
  "st_slope": "Flat",
  "resting_bp": 140,
  "cholesterol": 240,
  "resting_ecg": "Normal",
  "oldpeak": 1.2
}
```

Response:

```json
{
  "patient_id": "P-001",
  "timestamp": "2026-06-08T10:32:41",
  "probability": 94.28,
  "classification": "HIGH",
  "threshold": 40.0,
  "probabilities": [0.057, 0.943]
}
```

---

## Frontend Highlights

- **/[lang]** Clinical dashboard β€” KPI cards, recent assessments, risk distribution pie.
- **/[lang]/new-evaluation** Clinical form in three sections (Identification, Vitals, Symptoms/ECG) with RHF + Zod validation. On submit, the answer renders as a printable **ticket** (window.print() switches the print stylesheet so only the ticket prints, like a thermal receipt).
- **/[lang]/patients** Searchable, filterable history table (high / low / all). CSV export client-side.
- **/[lang]/eda** Charts: age histogram, sex distribution, chest pain distribution, scatter Max HR vs Age coloured by outcome.
- **/[lang]/model** Selected model card, comparison bar chart, rationale paragraph.
- **/[lang]/settings** Language + theme switcher.

The layout is a **CRM/EHR shell** with a sticky dark sidebar (logo + navigation + institution badge), a header bar with patient search, shift indicator, language switch, theme toggle and a faux nurse-station avatar.

---

## Internationalization

- Built on **Next.js 16 native dictionaries** (app/[lang]/...).
- All UI strings live in frontend/src/i18n/dictionaries/{en,es}.json.
- Locale is resolved in this order by frontend/src/proxy.ts:
  1. locale cookie (set by the language switcher),
  2. Accept-Language header,
  3. fallback to **English**.
- useT() exposes the dictionary to client components.

Add a new locale by creating {xx}.json, adding xx to locales in src/i18n/config.ts, and registering it in src/i18n/dictionaries.ts.

---

## Theming

- Dark mode is the **default**.
- Theme tokens are defined as CSS variables in globals.css (:root for light, .dark for dark) and bridged to Tailwind v4 via @theme inline.
- ThemeProvider is mounted at the **root layout** to avoid hydration issues with next-themes.

---

## Testing

```bash
make test                  # pytest
cd frontend && pnpm build  # type-check + production build
```

---

## Docker

```bash
make docker-up      # Build and run Streamlit container
make docker-logs    # Stream logs
make docker-down    # Stop
```

> Containerising the Next.js + FastAPI duo is on the roadmap.

---