# Project export: ReHeart

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: Preventing fatal readmissions with AI-powered cardiac recovery monitoring
- Devpost: https://devpost.com/software/reheart
- GitHub: https://github.com/chiragbhansali/reheart-treehacks
- Team: 1 GitHub contributor(s) — Chirag Bhansali (2 commits)

## Devpost submission (written by the team)

### Inspiration

Every year, nearly 1 in 4 heart attack survivors is readmitted within 30 days — often due to missed warning signs, medication errors, or delayed follow-up. The most dangerous period isn’t during hospitalization — it’s the silent gap after discharge. We were inspired by a simple question: What if every heart attack survivor had an AI copilot watching over their recovery at home? ReHeart exists to close that dangerous 30-day gap.

### What it does

ReHeart is an AI-powered cardiac recovery copilot that monitors patients in the critical 30 days after heart attack discharge. Patients complete daily voice check-ins describing how they feel. ReHeart: Extracts cardiac-relevant symptoms (chest pain, dyspnea, edema, palpitations) Applies structured post-MI risk stratification Maps symptoms to potential clinical deterioration Generates guideline-aligned recommended actions Escalates RED or YELLOW cases to a care team dashboard Instead of reactive care, ReHeart provides continuous, AI-driven post-discharge monitoring.

### How we built it

We built ReHeart as a multi-layer clinical reasoning pipeline: Step 1 - Voice Input Web-based voice recording Transcription using Whisper Step 2 - Clinical Intelligence MedGemma deployed on Modal Structured symptom extraction into strict JSON Risk stratification (GREEN / YELLOW / RED) Condition inference (e.g., recurrent ischemia, heart failure decompensation) Guideline-aligned action recommendation Step 3 - Safety Guardrails Conservative escalation rules Higher-risk default when uncertain Human-in-the-loop alerting Step 4 - Care Team Dashboard Patient list with risk badges Alert queue Structured clinical summaries Trend visualization The system is designed as an executable clinical decision engine.

### Challenges we ran into

1. Reliable clinical reasoning Getting structured, consistent medical JSON output required tight schema constraints and prompt tuning. 2. Balancing safety vs over-alerting Too sensitive → constant RED flags. Too lax → unsafe system. We had to encode conservative but practical escalation logic. 3. Demo fragility Voice AI, multimodal inputs, and real-time reasoning introduce many failure points. We built fallback pathways and seeded demo data to ensure reliability.

### Accomplishments we're proud of

Built an end-to-end AI clinical reasoning pipeline in 18 hours. Transformed cardiology guidelines into executable decision logic. Created structured escalation outputs instead of vague chatbot responses. Demonstrated a realistic Day 5 crisis detection scenario. Designed the system to prioritize safety and human oversight. Most importantly, we built something that feels like it could truly prevent a readmission.

### What's next

Short-term: Add longitudinal recovery modeling Improve condition inference confidence scoring Pilot with cardiology practices Mid-term: Expand to heart failure, post-CABG surgery, and arrhythmias Integrate with EHR systems Add real-time escalation capabilities Long-term vision: ReHeart becomes the AI copilot for every cardiac patient’s recovery journey — continuously monitoring, guiding, and preventing avoidable deterioration at scale. Our goal is simple - make surviving a heart attack safer than ever before.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 18 recognized source files, 126 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (33 of 33)

```
.mcp.json
backend/.gitignore
backend/main.py
backend/prompts.py
backend/prompts/system.txt
backend/prompts/user.txt
backend/pyproject.toml
backend/README.md
backend/uv.lock
CLAUDE.md
frontend/.gitignore
frontend/eslint.config.mjs
frontend/next.config.ts
frontend/package.json
frontend/postcss.config.mjs
frontend/README.md
frontend/src/app/dashboard/page.tsx
frontend/src/app/globals.css
frontend/src/app/layout.tsx
frontend/src/app/page.tsx
frontend/src/app/trends/page.tsx
frontend/stitch-designs/daily-voice-checkin/index.html
frontend/tsconfig.json
idea.md
llm/.gitignore
llm/.python-version
llm/pyproject.toml
llm/README.md
llm/uv.lock
llm/vllm_medgemma.py
supabase/migrations/20260215000000_create_patient_monitoring_schema.sql
supabase/seed.sql
types/database.types.ts
```

### Dependencies

- backend/pyproject.toml: fastapi@>=0.115.0, httpx@>=0.28.1, openai@>=2.21.0, python-dotenv@>=1.2.1, python-multipart@>=0.0.12, supabase@>=2.10.0, uvicorn@>=0.32.0
- frontend/package.json: @tailwindcss/postcss@^4.1.18, @types/node@^20, @types/react@^19, @types/react-dom@^19, babel-plugin-react-compiler@1.0.0, eslint@^9, eslint-config-next@16.1.6, next@16.1.6, postcss@^8.5.6, react@19.2.3, react-dom@19.2.3, tailwindcss@^4.1.18, typescript@^5
- llm/pyproject.toml: modal@>=1.3.3

### Recent commits (newest first)

- Final commit
- init commit

## Key source files (fetched from GitHub, selected and truncated for size)

### idea.md

```markdown
# Post-MI Monitoring System (36-Hour Hackathon Spec)

## 1. Overview

AI-powered monitoring system for the 30 days after myocardial infarction (MI) discharge.

Primary goal:
Detect early warning signs (chest pain, dyspnea, medication issues) via voice + image inputs and escalate risk to care teams before readmission.

Core demo scenario:
Day 5 post-discharge → patient reports exertional chest tightness → system classifies RED → alert shown on dashboard → nurse follow-up simulated.

---

# 2. System Architecture

## Frontend

- React Web App (mobile-first)
- Two views:
    - Patient App
    - Care Team Dashboard

## Backend

- FastAPI (Python)
- REST API
- SQLite (or Supabase) for storage

## AI Components

### 1. Transcription

- Whisper API (or faster-whisper local)

### 2. Clinical Reasoning

- MedGemma on Modal
- Fallback: GPT/Claude/Gemini

### 3. Vision (optional for demo)

- GPT-4 Vision (BP reading, medication bottles)

---

# 3. Core Features (MVP Required)

## 3.1 Patient Voice Check-In

### Flow

1. Patient records voice note
2. Audio → transcription
3. Transcript → MedGemma
4. MedGemma returns structured JSON
5. If follow-up needed → ask 1–2 additional questions
6. Final risk classification stored

---

## 3.2 Clinical Analysis Contract

### Endpoint

POST /analyze_checkin

### Input

```json
{
	"patient_id": "string",
	"transcript": "string",
	"day_post_discharge": 5
}
```

### Output

```json
{
	"symptoms": {
		"chest_pain": {
			"present": true,
			"severity": 6,
			"exertional": true,
			"similar_to_index_mi": true
		},
		"shortness_of_breath": {
			"present": true,
			"exertional": true
		},
		"palpitations": false,
		"edema": false
	},
	"risk_level": "RED",
	"rationale": "Exertional chest pain with dyspnea on Day 5 post-MI.",
	"next_question": "On a scale of 1-10, how severe is the pain?"
}
```

Risk levels:

- GREEN → routine monitoring
- YELLOW → urgent clinic review
- RED → emergency escalation

---

## 3.3 Risk Stratification Rules (Guardrails)

Even if LLM handles reasoning, enforce minimal safety rules:

RED if:

- Chest pain + dyspnea
- Exertional chest pain
- Severity ≥ 6 and similar to index MI
- Any explicit “worse than heart attack”

YELLOW if:

- Mild chest discomfort
- Elevated BP > 150 systolic
- Medication non-adherence

GREEN:

- No concerning symptoms

Backend validates risk override if needed.

---

## 3.4 Care Team Dashboard

### Patient List View

- Name
- Day post-discharge
- Last check-in timestamp
- Risk badge (color-coded)

### Patient Detail View

- Latest transcript
- Extracted symptom JSON
- Risk rationale
- BP readings (if available)
- Alert history

### Alert Queue

Filter by:

- RED
- YELLOW
- Unresolved alerts

Action buttons:

- Acknowledge
- Mark resolved

---

# 4. Optional (If Time Allows)

## 4.1 BP Image Upload

### Endpoint

POST /analyze_bp_image

### Output

```json
{
	"systolic": 152,
	"diastolic": 96,
	"confidence": 0.88
}
```

UI must:

- Show extracted value
- Ask patient to confirm

--
[truncated — 2023 more characters]
```

### CLAUDE.md

```markdown
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Post-MI (Myocardial Infarction) Monitoring System - An AI-powered monitoring system for the 30 days after heart attack discharge. The system detects early warning signs (chest pain, dyspnea, medication issues) via voice and image inputs, then escalates risk to care teams before readmission.

**Tech Stack:**
- Backend: FastAPI + Python (uv) + Supabase
- Frontend: Next.js + React + Tailwind
- LLM: MedGemma 1.5 4B IT served via vLLM on Modal
- Transcription: Whisper API or faster-whisper local
- Vision (optional): GPT-4 Vision for BP readings and medication bottles

## Repository Structure

```
llm/                    # MedGemma deployment on Modal
├── vllm_medgemma.py   # Modal app for vLLM inference server
└── pyproject.toml     # Python dependencies (managed by uv)

backend/               # FastAPI backend ✅ IMPLEMENTED
├── main.py            # FastAPI app with /api/checkin endpoint
├── prompts.py         # MedGemma prompt management
├── test_checkin.py    # Test suite
├── SETUP.md           # Setup and testing guide
└── SUPABASE_INTEGRATION.md  # Integration documentation

supabase/              # Database schema ✅ IMPLEMENTED
├── migrations/        # Version-controlled SQL migrations
├── seed.sql           # Demo data
└── README.md          # Schema documentation

types/                 # TypeScript types ✅ IMPLEMENTED
└── database.types.ts  # Auto-generated from Supabase

frontend/              # Next.js frontend ✅ IMPLEMENTED
├── app/page.tsx       # Patient voice check-in interface
├── app/dashboard/page.tsx  # Clinical monitoring dashboard
├── app/globals.css    # Theme and styles
└── README.md          # Frontend documentation
experiments/           # Experimental code
idea.md                # Complete project specification
```

## MedGemma API (Modal + vLLM)

### Deployment Commands

```bash
# Navigate to llm directory
cd llm

# Install dependencies with uv
uv sync

# Test the vLLM server locally (spins up Modal container)
modal run vllm_medgemma.py

# Deploy to Modal
modal deploy vllm_medgemma.py

# Check Modal app status
modal app list
modal app logs example-vllm-inference
```

### API Endpoint

The deployed vLLM server provides an OpenAI-compatible chat completions endpoint:

```bash
curl https://dev-chiragbhansali--example-vllm-inference-serve.modal.run/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "llm",
    "messages": [
      {"role": "user", "content": "Explain what a neural network is in one paragraph."}
    ]
  }'
```

**Key Details:**
- Model served: `google/medgemma-1.5-4b-it`
- Served model name: `llm`
- GPU: 1x L4
- Max model length: 8192 tokens
- Implementation: transformers (recommended for MedGemma)
- Scaledown window: 15 minutes of inactivity
- Port: 8000 (internal)

### Configuration Notes

- `FAST_BOOT = True`: Optimized for frequent cold starts (
[truncated — 7614 more characters]
```

### llm/pyproject.toml

```
[project]
name = "llm"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
    "modal>=1.3.3",
]

```

### backend/pyproject.toml

```
[project]
name = "medgemma-backend"
version = "0.1.0"
description = "FastAPI backend for Post-MI monitoring system"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
    "fastapi>=0.115.0",
    "uvicorn>=0.32.0",
    "python-multipart>=0.0.12",
    "supabase>=2.10.0",
    "openai>=2.21.0",
    "python-dotenv>=1.2.1",
    "httpx>=0.28.1",
]

```

### frontend/package.json

```
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "next": "16.1.6",
    "postcss": "^8.5.6",
    "react": "19.2.3",
    "react-dom": "19.2.3"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4.1.18",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "babel-plugin-react-compiler": "1.0.0",
    "eslint": "^9",
    "eslint-config-next": "16.1.6",
    "tailwindcss": "^4.1.18",
    "typescript": "^5"
  }
}

```

### backend/main.py

```python
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pathlib import Path
import shutil
from datetime import datetime, date
import os
import httpx
from openai import OpenAI
from dotenv import load_dotenv
from pydantic import BaseModel
from prompts import get_system_prompt, get_user_prompt
from supabase import create_client, Client
import json
from typing import Optional

# Load environment variables
load_dotenv()

# Initialize OpenAI client
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

# Initialize Supabase client
SUPABASE_URL = os.getenv("SUPABASE_URL")
SUPABASE_SERVICE_ROLE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY")

if not SUPABASE_URL or not SUPABASE_SERVICE_ROLE_KEY or "<YOUR_SERVICE_ROLE_KEY_HERE>" in SUPABASE_SERVICE_ROLE_KEY:
    print("⚠️  WARNING: Supabase credentials not configured. Set SUPABASE_SERVICE_ROLE_KEY in .env")
    supabase: Optional[Client] = None
else:
    supabase: Client = create_client(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY)
    print("✅ Supabase client initialized")

# MedGemma API endpoint (Modal deployment)
MEDGEMMA_API_URL = os.getenv(
    "MEDGEMMA_API_URL",
    "https://dev-chiragbhansali--example-vllm-inference-serve.modal.run/v1/chat/completions"
)

app = FastAPI(title="Post-MI Monitoring API")

# Configure CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:3000",
        "http://127.0.0.1:3000",
    ],  # Frontend URLs
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Create uploads directory if it doesn't exist
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)


@app.get("/")
async def root():
    return {"message": "Post-MI Monitoring API is running"}


@app.post("/api/upload-audio")
async def upload_audio(audio: UploadFile = File(...)):
    """
    Receive audio file from frontend and transcribe using OpenAI Whisper.
    """
    try:
        # Generate unique filename with timestamp
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = f"recording_{timestamp}.webm"
        file_path = UPLOAD_DIR / filename

        # Save the file
        with file_path.open("wb") as buffer:
            shutil.copyfileobj(audio.file, buffer)

        file_size_kb = file_path.stat().st_size / 1024

        # Transcribe audio using OpenAI Whisper
        with open(file_path, "rb") as audio_file:
            # Use medical context prompt for better accuracy
            transcription = openai_client.audio.transcriptions.create(
                model="gpt-4o-transcribe",
                file=audio_file,
                response_format="text",
                # prompt="Patient describing post-myocardial infarction recovery symptoms including chest pain, shortness of breath, fatigue, and medication adherence."
            )

        return {
            "status": "success",
            "message": "Audio transcribed successfully",
            "filename": filename,
            "size_kb": round(file_size_kb, 2),
            "transcript": transcription,
            "saved_path": str(file_path)
        }

    except Exception as e:
        raise HTTPException(
            status_code=500,
            detail=f"Failed to process audio: {str(e)}"
        )


@app.get("/health")
async def health_check():
    return {"status": "healthy"}


# Pydantic models for requests
class AnalyzeRequest(BaseModel):
    transcript: str
    patient_id: str
    day_post_discharge: Optional[int] = None  # Auto-calculated if not provided
    baseline: str = ""


@app.post("/api/analyze-checkin")
async def analyze_checkin(request: AnalyzeRequest):
    """
    Analyze patient check-in transcript using MedGemma.
    Saves to database and creates alerts if needed.
    Returns structured symptom extraction and risk classification.
    """
    if not supabase:
        raise HTTPException(
            status_code=503,
            detail="Database not configured. Set SUPABASE_SERVICE_ROLE_KEY in .env"
        )

    try:
        # Calculate days post-discharge if not provided
        day_post_discharge = request.day_post_discharge
        if day_post_discharge is None:
            day_post_discharge = await calculate_days_post_discharge(request.patient_id)

        # Get prompts
        system_prompt = get_system_prompt()
        user_prompt = get_user_prompt(
            transcript=request.transcript,
            day_post_discharge=day_post_discharge,
            baseline=request.baseline
        )

        # Call MedGemma API (OpenAI-compatible format)
        async with httpx.AsyncClient(timeout=60.0) as client:
            response = await client.post(
                MEDGEMMA_API_URL,
                json={
                    "model": "llm",
                    "messages": [
                        {"role": "system", "content": system_prompt},
                        {"role": "user", "content": user_prompt}
                    ],
                    "temperature": 0.1,  # Low temperature for consistent medical analysis
                    "max_tokens": 1024*2
                }
            )
            response.raise_for_status()

            result = response.json()

            print(result)

            # Extract the assistant's response
            analysis_text = result["choices"][0]["message"]["content"]

            # Strip markdown code fences if present
            analysis_text = analysis_text.strip()
            if analysis_text.startswith("```json"):
                analysis_text = analysis_text[7:]  # Remove ```json
            elif analysis_text.startswith("```"):
                analysis_text = analysis_text[3:]  # Remove ```
            if analysis_text.endswith("```"):
                analysis_text = analysis_text[:-3]  # Remove trailing ```
            analysis_text = analysis_text.strip()

            # Parse JSON response from MedGemma
            import json
            analysis = json.loads(ana
[truncated — 12752 more characters]
```

### frontend/src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Inter, DM_Serif_Display } from "next/font/google";
import "./globals.css";

const inter = Inter({
  variable: "--font-inter",
  subsets: ["latin"],
});

const dmSerifDisplay = DM_Serif_Display({
  variable: "--font-display",
  subsets: ["latin"],
  weight: "400",
});

export const metadata: Metadata = {
  title: "Heart Health Monitor - Daily Check-In",
  description: "Post-MI monitoring system for cardiac health recovery",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <head>
        <link
          href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined"
          rel="stylesheet"
        />
      </head>
      <body
        className={`${inter.variable} ${dmSerifDisplay.variable} antialiased`}
        suppressHydrationWarning
      >
        {children}
      </body>
    </html>
  );
}

```

### frontend/src/app/page.tsx

```typescript
'use client';

import { useState, useRef } from 'react';
import Link from 'next/link';

export default function Home() {
	const [isListening, setIsListening] = useState(false);
	const [isUploading, setIsUploading] = useState(false);
	const [transcript, setTranscript] = useState<string>('');
	const [isEditing, setIsEditing] = useState(false);
	const [isAnalyzing, setIsAnalyzing] = useState(false);
	const [analysis, setAnalysis] = useState<any>(null);
	const mediaRecorderRef = useRef<MediaRecorder | null>(null);
	const audioChunksRef = useRef<Blob[]>([]);

	const handleMicClick = async () => {
		if (!isListening) {
			// Start recording
			try {
				const stream = await navigator.mediaDevices.getUserMedia({
					audio: true,
				});
				const mediaRecorder = new MediaRecorder(stream);
				mediaRecorderRef.current = mediaRecorder;
				audioChunksRef.current = [];

				mediaRecorder.ondataavailable = (event) => {
					if (event.data.size > 0) {
						audioChunksRef.current.push(event.data);
					}
				};

				mediaRecorder.onstop = async () => {
					const audioBlob = new Blob(audioChunksRef.current, {
						type: 'audio/webm',
					});
					await sendAudioToServer(audioBlob);
					stream.getTracks().forEach((track) => track.stop());
				};

				mediaRecorder.start();
				setIsListening(true);
			} catch (error) {
				console.error('Error accessing microphone:', error);
				alert('Could not access microphone. Please check permissions.');
			}
		} else {
			// Stop recording
			if (mediaRecorderRef.current) {
				mediaRecorderRef.current.stop();
				setIsListening(false);
			}
		}
	};

	const sendAudioToServer = async (audioBlob: Blob) => {
		setIsUploading(true);
		try {
			const formData = new FormData();
			formData.append('audio', audioBlob, 'recording.webm');

			const response = await fetch(
				'http://localhost:8000/api/upload-audio',
				{
					method: 'POST',
					body: formData,
				},
			);

			if (response.ok) {
				const data = await response.json();
				console.log('Server response:', data);
				setTranscript(data.transcript);
				setIsEditing(true);
			} else {
				console.error('Upload failed:', response.statusText);
				alert('Failed to upload audio');
			}
		} catch (error) {
			console.error('Error uploading audio:', error);
			alert('Error uploading audio. Is the backend running?');
		} finally {
			setIsUploading(false);
		}
	};

	const submitForAnalysis = async () => {
		setIsAnalyzing(true);
		try {
			// TODO: Get actual patient ID from auth/session
			const PATIENT_ID = 'ae4975b3-a623-479a-8977-a77bb1a1a2fe'; // John Smith

			const response = await fetch(
				'http://localhost:8000/api/analyze-checkin',
				{
					method: 'POST',
					headers: {
						'Content-Type': 'application/json',
					},
					body: JSON.stringify({
						patient_id: PATIENT_ID,
						transcript: transcript,
						day_post_discharge: 5, // TODO: Make this dynamic
						baseline: '',
					}),
				},
			);

			if (response.ok) {
				const data = await response.json();
				console.log('Analysis:', data);
				setAnalysis(data.analysis);
				setIsEditing(false);
			} else {
				const errorData = await response.json().catch(() => ({ detail: response.statusText }));
				console.error('Analysis failed:', errorData);
				alert(`Failed to analyze check-in: ${errorData.detail || response.statusText}`);
			}
		} catch (error) {
			console.error('Error analyzing:', error);
			alert('Error analyzing. Is the backend running?');
		} finally {
			setIsAnalyzing(false);
		}
	};

	return (
		<div className="bg-[#f9f6f0] text-gray-900 antialiased min-h-screen flex flex-col items-center pb-24">
			{/* Main Content */}
			<main className="flex-1 w-full max-w-md px-6 flex flex-col items-center pt-12">
				{/* Header */}
				<header className="text-center mb-10">
					<h1 className="font-display text-4xl text-[#332211] leading-tight">
						How are you
						<br />
						feeling today?
					</h1>
					<p className="mt-4 text-gray-500 font-medium px-4">
						Describe your recovery progress. Our AI is listening to
						support your heart health.
					</p>
				</header>

				{/* Voice Recording Circle */}
				<div className="relative w-full aspect-square flex items-center justify-center">
					{/* Pulse Rings */}
					{isListening && (
						<div className="absolute inset-0 flex items-center justify-center">
							<div className="pulse-circle absolute w-72 h-72 rounded-full bg-primary/10"></div>
							<div
								className="pulse-circle absolute w-56 h-56 rounded-full bg-primary/20"
								style={{ animationDelay: '-1s' }}
							></div>
						</div>
					)}

					{/* Main Circle */}
					<div className="relative w-64 h-64 bg-white rounded-full shadow-2xl shadow-black/5 flex flex-col items-center justify-center border border-white/50">
						{/* Waveform (only show when listening) */}
						{isListening && (
							<div className="flex items-center gap-1 h-8 mb-6">
								<div className="waveform-bar w-1 bg-primary/40 h-2 rounded-full"></div>
								<div className="waveform-bar w-1 bg-primary/60 h-5 rounded-full"></div>
								<div className="waveform-bar w-1 bg-primary h-8 rounded-full"></div>
								<div className="waveform-bar w-1 bg-primary/60 h-5 rounded-full"></div>
								<div className="waveform-bar w-1 bg-primary/40 h-2 rounded-full"></div>
							</div>
						)}

						{/* Microphone Button */}
						<button
							onClick={handleMicClick}
							className="w-20 h-20 bg-primary rounded-full flex items-center justify-center text-white shadow-lg shadow-primary/30 hover:shadow-primary/40 transition-shadow"
						>
							<span className="material-icons-outlined text-4xl">
								mic
							</span>
						</button>

						{/* Status Text */}
						<span className="mt-4 text-xs font-semibold uppercase tracking-widest text-primary/60">
							{isUploading
								? 'Uploading...'
								: isListening
									? 'Listening'
									: 'Click to speak'}
						</span>
					</div>
				</div>

				{/* Transcr
[truncated — 7085 more characters]
```

### frontend/src/app/trends/page.tsx

```typescript
'use client';

import { useState, useEffect } from 'react';
import Link from 'next/link';

interface CheckIn {
	id: string;
	timestamp: string;
	day_post_discharge: number;
	risk_level: string;
	transcript: string;
	rationale: string;
	structured_symptoms: any;
	bp_reading?: {
		systolic: number;
		diastolic: number;
	};
}

interface Patient {
	name: string;
	medical_record_number: string;
	discharge_date: string;
}

export default function TrendsPage() {
	const [checkins, setCheckins] = useState<CheckIn[]>([]);
	const [patient, setPatient] = useState<Patient | null>(null);
	const [loading, setLoading] = useState(true);
	const [selectedView, setSelectedView] = useState<'timeline' | 'stats'>(
		'timeline',
	);

	// TODO: Get actual patient ID from auth/session
	const PATIENT_ID = 'ae4975b3-a623-479a-8977-a77bb1a1a2fe'; // John Smith from seed data

	useEffect(() => {
		fetchTrends();
	}, []);

	const fetchTrends = async () => {
		try {
			const response = await fetch(
				`http://localhost:8000/api/patients/${PATIENT_ID}/checkins`,
			);

			if (response.ok) {
				const data = await response.json();
				setPatient(data.patient);
				setCheckins(data.checkins);
			}
		} catch (error) {
			console.error('Error fetching trends:', error);
		} finally {
			setLoading(false);
		}
	};

	const getRiskColor = (riskLevel: string) => {
		switch (riskLevel) {
			case 'RED':
				return 'bg-red-100 text-red-700 border-red-200';
			case 'YELLOW':
				return 'bg-yellow-100 text-yellow-700 border-yellow-200';
			case 'GREEN':
				return 'bg-green-100 text-green-700 border-green-200';
			default:
				return 'bg-gray-100 text-gray-600 border-gray-200';
		}
	};

	const getRiskIcon = (riskLevel: string) => {
		switch (riskLevel) {
			case 'RED':
				return 'warning';
			case 'YELLOW':
				return 'error_outline';
			case 'GREEN':
				return 'check_circle';
			default:
				return 'info';
		}
	};

	const formatDate = (timestamp: string) => {
		const date = new Date(timestamp);
		const today = new Date();
		const yesterday = new Date(today);
		yesterday.setDate(yesterday.getDate() - 1);

		if (date.toDateString() === today.toDateString()) {
			return `Today at ${date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}`;
		} else if (date.toDateString() === yesterday.toDateString()) {
			return `Yesterday at ${date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}`;
		} else {
			return date.toLocaleDateString('en-US', {
				month: 'short',
				day: 'numeric',
				hour: 'numeric',
				minute: '2-digit',
			});
		}
	};

	const calculateStats = () => {
		if (checkins.length === 0) return null;

		const redCount = checkins.filter((c) => c.risk_level === 'RED').length;
		const yellowCount = checkins.filter(
			(c) => c.risk_level === 'YELLOW',
		).length;
		const greenCount = checkins.filter(
			(c) => c.risk_level === 'GREEN',
		).length;

		// Calculate average BP if available
		const bpReadings = checkins
			.filter((c) => c.bp_reading)
			.map((c) => c.bp_reading!);
		const avgSystolic =
			bpReadings.length > 0
				? Math.round(
						bpReadings.reduce((sum, bp) => sum + bp.systolic, 0) /
							bpReadings.length,
					)
				: null;
		const avgDiastolic =
			bpReadings.length > 0
				? Math.round(
						bpReadings.reduce((sum, bp) => sum + bp.diastolic, 0) /
							bpReadings.length,
					)
				: null;

		return {
			totalCheckins: checkins.length,
			redCount,
			yellowCount,
			greenCount,
			avgSystolic,
			avgDiastolic,
			daysPostDischarge: checkins[0]?.day_post_discharge || 0,
		};
	};

	const stats = calculateStats();

	if (loading) {
		return (
			<div className="min-h-screen bg-[#f9f6f0] flex items-center justify-center">
				<div className="text-center">
					<div className="w-12 h-12 border-4 border-primary/20 border-t-primary rounded-full animate-spin mx-auto mb-4"></div>
					<p className="text-gray-600 font-medium">
						Loading trends...
					</p>
				</div>
			</div>
		);
	}

	return (
		<div className="bg-[#f9f6f0] text-gray-900 antialiased min-h-screen pb-24">
			{/* Header */}

			<main className="max-w-md mx-auto px-6 py-6">
				{/* Stats Summary */}
				{stats && (
					<div className="mb-6">
						<div className="bg-white rounded-2xl p-5 border border-gray-200 mb-4">
							<div className="flex items-center justify-between mb-4">
								<h2 className="font-bold text-[#332211] text-lg">
									Recovery Overview
								</h2>
								<div className="bg-primary/10 text-primary px-3 py-1 rounded-full text-sm font-bold">
									Day {stats.daysPostDischarge}
								</div>
							</div>

							<div className="grid grid-cols-3 gap-3 mb-4">
								<div className="bg-green-50 rounded-xl p-3 border border-green-200">
									<div className="text-2xl font-bold text-green-700">
										{stats.greenCount}
									</div>
									<div className="text-xs text-green-600 font-medium">
										Stable
									</div>
								</div>
								<div className="bg-yellow-50 rounded-xl p-3 border border-yellow-200">
									<div className="text-2xl font-bold text-yellow-700">
										{stats.yellowCount}
									</div>
									<div className="text-xs text-yellow-600 font-medium">
										Caution
									</div>
								</div>
								<div className="bg-red-50 rounded-xl p-3 border border-red-200">
									<div className="text-2xl font-bold text-red-700">
										{stats.redCount}
									</div>
									<div className="text-xs text-red-600 font-medium">
										Alert
									</div>
								</div>
							</div>

							<div className="text-center text-sm text-gray-600">
								<span className="font-semibold text-[#332211]">
									{stats.totalCheckins}
								</span>{' '}
								total check-ins recorded
							</div>
						</div>

						{/* Blood Pressure Average */}
						{stats.avgSystolic && stats.avgDiastolic && (
							<div className="bg-white rounded-2xl p-5 border border-gray-200">
								<div className="flex items-center gap-3 mb-3">
								
[truncated — 5487 more characters]
```

### frontend/src/app/dashboard/page.tsx

```typescript
'use client';

import { useState, useEffect } from 'react';

interface Patient {
	patient_id: string;
	name: string;
	medical_record_number: string;
	days_post_discharge: number;
	last_risk_level: string | null;
	last_checkin_time: string | null;
	open_alerts_count: number;
	latest_red_alert: string | null;
}

interface Alert {
	alert_id: string;
	patient_name: string;
	medical_record_number: string;
	risk_level: string;
	status: string;
	day_post_discharge: number;
	transcript: string;
	rationale: string;
	structured_symptoms: any;
	alert_time: string;
	acknowledged_by: string | null;
	notes: string | null;
}

export default function Dashboard() {
	const [activeTab, setActiveTab] = useState<'patients' | 'alerts'>(
		'alerts',
	);
	const [patients, setPatients] = useState<Patient[]>([]);
	const [alerts, setAlerts] = useState<Alert[]>([]);
	const [selectedPatient, setSelectedPatient] = useState<Patient | null>(
		null,
	);
	const [selectedAlert, setSelectedAlert] = useState<Alert | null>(null);
	const [loading, setLoading] = useState(true);
	const [acknowledging, setAcknowledging] = useState(false);

	useEffect(() => {
		fetchData();
		// Poll every 30 seconds for updates
		const interval = setInterval(fetchData, 30000);
		return () => clearInterval(interval);
	}, []);

	const fetchData = async () => {
		try {
			const [patientsRes, alertsRes] = await Promise.all([
				fetch('http://localhost:8000/api/patients'),
				fetch('http://localhost:8000/api/alerts?status=open'),
			]);

			if (patientsRes.ok && alertsRes.ok) {
				const patientsData = await patientsRes.json();
				const alertsData = await alertsRes.json();
				setPatients(patientsData.patients);
				setAlerts(alertsData.alerts);
			}
		} catch (error) {
			console.error('Error fetching data:', error);
		} finally {
			setLoading(false);
		}
	};

	const acknowledgeAlert = async (
		alertId: string,
		status: 'acknowledged' | 'resolved',
		notes: string,
	) => {
		setAcknowledging(true);
		try {
			const response = await fetch(
				`http://localhost:8000/api/alerts/${alertId}`,
				{
					method: 'PATCH',
					headers: { 'Content-Type': 'application/json' },
					body: JSON.stringify({
						status,
						acknowledged_by: 'Clinical Staff', // TODO: Replace with actual user
						notes,
					}),
				},
			);

			if (response.ok) {
				fetchData(); // Refresh data
				setSelectedAlert(null);
			}
		} catch (error) {
			console.error('Error acknowledging alert:', error);
		} finally {
			setAcknowledging(false);
		}
	};

	const getRiskColor = (riskLevel: string | null) => {
		if (!riskLevel) return 'bg-gray-100 text-gray-600';
		switch (riskLevel) {
			case 'RED':
				return 'bg-red-100 text-red-700';
			case 'YELLOW':
				return 'bg-yellow-100 text-yellow-700';
			case 'GREEN':
				return 'bg-green-100 text-green-700';
			default:
				return 'bg-gray-100 text-gray-600';
		}
	};

	const formatTimestamp = (timestamp: string | null) => {
		if (!timestamp) return 'Never';
		const date = new Date(timestamp);
		const now = new Date();
		const diffMs = now.getTime() - date.getTime();
		const diffMins = Math.floor(diffMs / 60000);

		if (diffMins < 60) return `${diffMins}m ago`;
		if (diffMins < 1440) return `${Math.floor(diffMins / 60)}h ago`;
		return `${Math.floor(diffMins / 1440)}d ago`;
	};

	if (loading) {
		return (
			<div className="min-h-screen bg-[#f9f6f0] flex items-center justify-center">
				<div className="text-center">
					<div className="w-12 h-12 border-4 border-primary/20 border-t-primary rounded-full animate-spin mx-auto mb-4"></div>
					<p className="text-gray-600 font-medium">
						Loading dashboard...
					</p>
				</div>
			</div>
		);
	}

	return (
		<div className="min-h-screen bg-[#f9f6f0]">
			{/* Header */}
			<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
				<div className="max-w-7xl mx-auto px-6 py-5">
					<div className="flex items-center justify-between">
						<div>
							<h1 className="font-display text-3xl text-[#332211]">
								Clinical Dashboard
							</h1>
							<p className="text-sm text-gray-500 mt-1">
								Post-MI Patient Monitoring
							</p>
						</div>
						<div className="flex items-center gap-4">
							<button
								onClick={fetchData}
								className="p-2 text-gray-600 hover:bg-gray-100 rounded-xl transition-colors"
								title="Refresh"
							>
								<span className="material-icons-outlined">
									refresh
								</span>
							</button>
							<div className="flex items-center gap-2 bg-green-50 px-4 py-2 rounded-xl border border-green-200">
								<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
								<span className="text-sm font-semibold text-green-700">
									Live
								</span>
							</div>
						</div>
					</div>
				</div>
			</header>

			<div className="max-w-7xl mx-auto px-6 py-8">
				{/* Stats Cards */}
				<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
					<div className="bg-white rounded-2xl p-5 border border-gray-200">
						<div className="flex items-center justify-between mb-2">
							<span className="text-gray-500 text-sm font-medium">
								Total Patients
							</span>
							<span className="material-icons-outlined text-gray-400 text-xl">
								people_outline
							</span>
						</div>
						<p className="text-3xl font-bold text-[#332211]">
							{patients.length}
						</p>
					</div>

					<div className="bg-red-50 rounded-2xl p-5 border border-red-200">
						<div className="flex items-center justify-between mb-2">
							<span className="text-red-700 text-sm font-semibold">
								Critical Alerts
							</span>
							<span className="material-icons-outlined text-red-500 text-xl">
								warning
							</span>
						</div>
						<p className="text-3xl font-bold text-red-700">
							{
								alerts.filter((a) => a.risk_level === 'RED')
									.length
							}
						</p>
					</div>

					<div className="bg-yellow-50 rounded-2xl p-5 border border-yellow-20
[truncated — 13994 more characters]
```

[8 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]