Add HTML templates for medical report generator with navigation, upload, edit, and preview functionalities

- Created base template with navigation and layout structure
- Implemented upload.html for patient data and file uploads
- Developed edit.html for editing calculated metrics
- Added preview.html for displaying generated report previews
- Enhanced user experience with Tailwind CSS styling
This commit is contained in:
bolade
2025-11-17 17:15:44 +01:00
parent 4f97691ff9
commit 83f50882e2
14 changed files with 1726 additions and 1770 deletions
+3 -1
View File
@@ -8,4 +8,6 @@ data/
/data
/reports
/reports
/temp
+335 -13
View File
@@ -5,13 +5,17 @@ This API provides a single endpoint that accepts all required files
and patient information, then generates a comprehensive medical report.
"""
import os
import shutil
import tempfile
import uuid
from pathlib import Path
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.responses import FileResponse
from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
from jinja2 import Environment, FileSystemLoader
from pydantic import BaseModel
from starlette.middleware.sessions import SessionMiddleware
from services.report_generator import ReportGeneratorService
app = FastAPI(
@@ -20,6 +24,12 @@ app = FastAPI(
version="2.0.0",
)
# Add session middleware
app.add_middleware(SessionMiddleware, secret_key=os.getenv("SECRET_KEY", "your-secret-key-change-in-production"))
# Setup templates
jinja_env = Environment(loader=FileSystemLoader("app/templates"))
# Define output directories
GRAPHS_DIR = Path("graphs")
GRAPHS_DIR.mkdir(exist_ok=True)
@@ -27,6 +37,9 @@ GRAPHS_DIR.mkdir(exist_ok=True)
REPORTS_DIR = Path("reports")
REPORTS_DIR.mkdir(exist_ok=True)
TEMP_DIR = Path("temp")
TEMP_DIR.mkdir(exist_ok=True)
# Initialize report generator service
report_service = ReportGeneratorService(
template_dir="app/report_gen",
@@ -42,18 +55,327 @@ class ReportResponse(BaseModel):
analysis_data: dict
@app.get("/")
async def root():
"""Root endpoint with API information"""
return {
"message": "Medical Report Generation API",
"version": "2.0.0",
"endpoints": {
"generate_report": "POST /generate-report",
"download_report": "GET /download-report/{filename}",
"health": "GET /health",
},
def render_template(template_name: str, context: dict) -> HTMLResponse:
"""Helper function to render Jinja2 templates"""
template = jinja_env.get_template(template_name)
html_content = template.render(**context)
return HTMLResponse(content=html_content)
@app.get("/", response_class=HTMLResponse)
async def root(request: Request):
"""Root endpoint - Upload form page"""
return render_template("upload.html", {"request": request, "session": request.session})
@app.post("/upload")
async def upload_files(
request: Request,
first_name: str = Form(...),
last_name: str = Form(...),
age: int = Form(...),
height: str = Form(...),
weight: str = Form(...),
gender: str = Form(...),
focus: str = Form(default="Endurance"),
session_id: str = Form(default="default"),
spirometry_pdf: UploadFile = File(...),
pnoe_csv: UploadFile = File(...),
seca_excel: UploadFile = File(...),
):
"""Handle file upload and generate report"""
# Validate file types
if not spirometry_pdf.filename.endswith(".pdf"):
return render_template("upload.html", {
"request": request,
"session": request.session,
"error": "Spirometry file must be a PDF"
})
if not pnoe_csv.filename.endswith(".csv"):
return render_template("upload.html", {
"request": request,
"session": request.session,
"error": "Pnoe file must be a CSV"
})
if not seca_excel.filename.endswith((".xlsx", ".xls")):
return render_template("upload.html", {
"request": request,
"session": request.session,
"error": "SECA file must be an Excel file (.xlsx or .xls)"
})
# Create session-specific temp directory
session_uuid = str(uuid.uuid4())
session_temp_dir = TEMP_DIR / session_uuid
session_temp_dir.mkdir(exist_ok=True, parents=True)
# Save uploaded files
spirometry_path = session_temp_dir / f"spirometry_{spirometry_pdf.filename}"
pnoe_path = session_temp_dir / f"pnoe_{pnoe_csv.filename}"
seca_path = session_temp_dir / f"seca_{seca_excel.filename}"
try:
# Write files
with open(spirometry_path, "wb") as f:
shutil.copyfileobj(spirometry_pdf.file, f)
with open(pnoe_path, "wb") as f:
shutil.copyfileobj(pnoe_csv.file, f)
with open(seca_path, "wb") as f:
shutil.copyfileobj(seca_excel.file, f)
# Prepare patient information
patient_name = f"{first_name} {last_name}"
patient_info = {
"patient_name": patient_name,
"first_name": first_name,
"last_name": last_name,
"age": age,
"height": height,
"weight": weight,
"gender": gender,
"focus": focus,
"session_id": session_id,
}
# Generate report
result = await report_service.generate_report(
spirometry_pdf_path=str(spirometry_path),
pnoe_csv_path=str(pnoe_path),
seca_excel_path=str(seca_path),
patient_info=patient_info,
)
# Store in session
request.session["patient_info"] = patient_info
request.session["temp_dir"] = str(session_temp_dir)
request.session["report_path"] = result["report_path"]
request.session["graphs_generated"] = result["graphs_generated"]
request.session["analysis_data"] = result["analysis_data"]
# Extract spirometry CSV path (it's saved in data_dir by the service)
from services.spirometry_table_extractor import extract_spirometry_table_from_pdf
from services.context_generator import ContextGenerator
from pathlib import Path as PathLib
# The spirometry CSV is extracted during report generation
# We need to find it or extract it again
data_dir = PathLib("data")
spirometry_csv_path = data_dir / f"spirometry_{Path(spirometry_pdf.filename).stem}.csv"
# If it doesn't exist, extract it
if not spirometry_csv_path.exists():
spirometry_csv_path = extract_spirometry_table_from_pdf(
str(spirometry_path), output_dir=str(data_dir)
)
spirometry_csv_path = PathLib(spirometry_csv_path)
# Get calculated metrics for display and editing
context_gen = ContextGenerator()
context_gen.load_data(
str(pnoe_path),
str(spirometry_csv_path),
str(seca_path)
)
context_gen.extract_patient_info(last_name) # Extract patient info
spirometry_metrics = context_gen.calculate_spirometry_metrics()
pnoe_metrics = context_gen.calculate_pnoe_metrics()
# Store metrics in session
request.session["metrics"] = {
"spirometry": spirometry_metrics,
"pnoe": pnoe_metrics,
}
request.session["spirometry_csv_path"] = str(spirometry_csv_path)
return RedirectResponse(url="/preview", status_code=303)
except Exception as e:
import traceback
error_details = traceback.format_exc()
print(f"ERROR: {error_details}")
return render_template("upload.html", {
"request": request,
"session": request.session,
"error": f"Error generating report: {str(e)}"
})
finally:
# Close file handles
spirometry_pdf.file.close()
pnoe_csv.file.close()
seca_excel.file.close()
@app.get("/preview", response_class=HTMLResponse)
async def preview(request: Request):
"""Preview generated report"""
if not request.session.get("report_path"):
return RedirectResponse(url="/", status_code=303)
return render_template("preview.html", {"request": request, "session": request.session})
@app.get("/graphs/{filename}")
async def serve_graph(filename: str):
"""Serve graph images"""
graph_path = GRAPHS_DIR / filename
if not graph_path.exists():
raise HTTPException(status_code=404, detail="Graph not found")
return FileResponse(path=graph_path, media_type="image/png")
@app.get("/edit", response_class=HTMLResponse)
async def edit_form(request: Request):
"""Display edit metrics form"""
if not request.session.get("metrics"):
return RedirectResponse(url="/", status_code=303)
return render_template("edit.html", {"request": request, "session": request.session})
@app.post("/edit")
async def edit_metrics(request: Request):
"""Handle metric edits and regenerate report"""
if not request.session.get("temp_dir") or not request.session.get("patient_info"):
return RedirectResponse(url="/", status_code=303)
# Get form data
form_data = await request.form()
# Build metric overrides
metric_overrides = {
"pnoe": {},
"spirometry": {}
}
# Pnoe overrides
if form_data.get("vo2_max"):
metric_overrides["pnoe"]["vo2_max"] = float(form_data["vo2_max"])
if form_data.get("vo2_max_per_kg"):
metric_overrides["pnoe"]["vo2_max_per_kg"] = float(form_data["vo2_max_per_kg"])
if form_data.get("peak_vt"):
metric_overrides["pnoe"]["peak_vt"] = float(form_data["peak_vt"])
if form_data.get("peak_vt_hr"):
metric_overrides["pnoe"]["peak_vt_hr"] = float(form_data["peak_vt_hr"])
if form_data.get("fat_max_value"):
metric_overrides["pnoe"]["fat_max_value"] = float(form_data["fat_max_value"])
if form_data.get("fat_max_hr"):
metric_overrides["pnoe"]["fat_max_hr"] = float(form_data["fat_max_hr"])
# VT1 and VT2 overrides
if form_data.get("vt1_hr") or form_data.get("vt1_speed") or form_data.get("vt1_time"):
metric_overrides["pnoe"]["vt1"] = {
"HeartRate": float(form_data.get("vt1_hr", 0)),
"Speed": float(form_data.get("vt1_speed", 0)),
"Time": float(form_data.get("vt1_time", 0))
}
if form_data.get("vt2_hr") or form_data.get("vt2_speed") or form_data.get("vt2_time"):
metric_overrides["pnoe"]["vt2"] = {
"HeartRate": float(form_data.get("vt2_hr", 0)),
"Speed": float(form_data.get("vt2_speed", 0)),
"Time": float(form_data.get("vt2_time", 0))
}
# Heart rate zones
for i in range(1, 6):
zone_key = f"zone{i}_bpm"
if form_data.get(zone_key):
metric_overrides["pnoe"][zone_key] = form_data[zone_key]
# Spirometry overrides
if form_data.get("fvc_best"):
metric_overrides["spirometry"]["fvc_best"] = float(form_data["fvc_best"])
if form_data.get("fvc_pred"):
metric_overrides["spirometry"]["fvc_pred"] = float(form_data["fvc_pred"])
if form_data.get("fev1_best"):
metric_overrides["spirometry"]["fev1_best"] = float(form_data["fev1_best"])
if form_data.get("fev1_pred"):
metric_overrides["spirometry"]["fev1_pred"] = float(form_data["fev1_pred"])
if form_data.get("fev1_fvc_pct_best"):
metric_overrides["spirometry"]["fev1_fvc_pct_best"] = float(form_data["fev1_fvc_pct_best"])
if form_data.get("fev1_fvc_pct_pred"):
metric_overrides["spirometry"]["fev1_fvc_pct_pred"] = float(form_data["fev1_fvc_pct_pred"])
try:
# Get file paths from session
temp_dir = Path(request.session["temp_dir"])
patient_info = request.session["patient_info"]
# Find files in temp directory
spirometry_path = None
pnoe_path = None
seca_path = None
for file_path in temp_dir.iterdir():
if file_path.name.startswith("spirometry_"):
spirometry_path = file_path
elif file_path.name.startswith("pnoe_"):
pnoe_path = file_path
elif file_path.name.startswith("seca_"):
seca_path = file_path
if not all([spirometry_path, pnoe_path, seca_path]):
raise ValueError("Could not find all uploaded files")
# Regenerate report with overrides
result = await report_service.generate_report(
spirometry_pdf_path=str(spirometry_path),
pnoe_csv_path=str(pnoe_path),
seca_excel_path=str(seca_path),
patient_info=patient_info,
metric_overrides=metric_overrides if (metric_overrides["pnoe"] or metric_overrides["spirometry"]) else None,
)
# Update session with new report
request.session["report_path"] = result["report_path"]
request.session["graphs_generated"] = result["graphs_generated"]
request.session["analysis_data"] = result["analysis_data"]
# Recalculate metrics with overrides
from services.context_generator import ContextGenerator
context_gen = ContextGenerator()
spirometry_csv_path = request.session.get("spirometry_csv_path", "")
if not spirometry_csv_path or not Path(spirometry_csv_path).exists():
from services.spirometry_table_extractor import extract_spirometry_table_from_pdf
from pathlib import Path as PathLib
data_dir = PathLib("data")
spirometry_csv_path = extract_spirometry_table_from_pdf(
str(spirometry_path), output_dir=str(data_dir)
)
spirometry_csv_path = str(PathLib(spirometry_csv_path))
context_gen.load_data(
str(pnoe_path),
spirometry_csv_path,
str(seca_path)
)
context_gen.extract_patient_info(patient_info.get("last_name", ""))
spirometry_overrides = metric_overrides.get("spirometry", {})
pnoe_overrides = metric_overrides.get("pnoe", {})
spirometry_metrics = context_gen.calculate_spirometry_metrics(spirometry_overrides)
pnoe_metrics = context_gen.calculate_pnoe_metrics(pnoe_overrides)
# Update metrics in session
request.session["metrics"] = {
"spirometry": spirometry_metrics,
"pnoe": pnoe_metrics,
}
request.session["spirometry_csv_path"] = spirometry_csv_path
return RedirectResponse(url="/preview", status_code=303)
except Exception as e:
import traceback
error_details = traceback.format_exc()
print(f"ERROR: {error_details}")
return render_template("edit.html", {
"request": request,
"session": request.session,
"error": f"Error regenerating report: {str(e)}"
})
@app.get("/health")
+102 -27
View File
@@ -101,41 +101,108 @@ class ContextGenerator:
}
return self.patient_info
def calculate_spirometry_metrics(self) -> Dict:
def calculate_spirometry_metrics(self, metric_overrides: Optional[Dict] = None) -> Dict:
"""Calculate spirometry-related metrics"""
if metric_overrides is None:
metric_overrides = {}
metrics = {}
for param in ["FVC", "FEV1", "FEV1/FVC%"]:
row = self.spirometry_df.loc[
self.spirometry_df["Parameters"].str.strip() == param
]
if not row.empty:
param_key = param.lower().replace("/", "_").replace("%", "_pct")
metrics[f"{param_key}_best"] = row["Best"].values[0]
metrics[f"{param_key}_pred"] = row["%Pred."].values[0]
param_key = param.lower().replace("/", "_").replace("%", "_pct")
if f"{param_key}_best" in metric_overrides:
metrics[f"{param_key}_best"] = float(metric_overrides[f"{param_key}_best"])
else:
row = self.spirometry_df.loc[
self.spirometry_df["Parameters"].str.strip() == param
]
if not row.empty:
metrics[f"{param_key}_best"] = row["Best"].values[0]
if f"{param_key}_pred" in metric_overrides:
metrics[f"{param_key}_pred"] = float(metric_overrides[f"{param_key}_pred"])
else:
row = self.spirometry_df.loc[
self.spirometry_df["Parameters"].str.strip() == param
]
if not row.empty:
metrics[f"{param_key}_pred"] = row["%Pred."].values[0]
return metrics
def calculate_pnoe_metrics(self) -> Dict:
def calculate_pnoe_metrics(self, metric_overrides: Optional[Dict] = None) -> Dict:
"""Calculate all Pnoe-derived metrics"""
if metric_overrides is None:
metric_overrides = {}
metrics = {}
metrics["vo2_max"] = self.pnoe_df["VO2(ml/min)_smoothed"].max()
metrics["vo2_max_per_kg"] = metrics["vo2_max"] / self.patient_info["weight"]
# VO2 Max metrics
if "vo2_max" in metric_overrides:
metrics["vo2_max"] = float(metric_overrides["vo2_max"])
else:
metrics["vo2_max"] = self.pnoe_df["VO2(ml/min)_smoothed"].max()
if "vo2_max_per_kg" in metric_overrides:
metrics["vo2_max_per_kg"] = float(metric_overrides["vo2_max_per_kg"])
else:
metrics["vo2_max_per_kg"] = metrics["vo2_max"] / self.patient_info["weight"]
peak_vt_idx = self.pnoe_df["VT(l)_smoothed"].idxmax()
peak_vt_row = self.pnoe_df.loc[peak_vt_idx]
metrics["peak_vt"] = peak_vt_row["VT(l)_smoothed"]
metrics["peak_vt_hr"] = peak_vt_row["HR(bpm)_smoothed"]
# Peak VT metrics
if "peak_vt" in metric_overrides:
metrics["peak_vt"] = float(metric_overrides["peak_vt"])
# Need to get HR from override or calculate
if "peak_vt_hr" in metric_overrides:
metrics["peak_vt_hr"] = float(metric_overrides["peak_vt_hr"])
else:
peak_vt_idx = self.pnoe_df["VT(l)_smoothed"].idxmax()
peak_vt_row = self.pnoe_df.loc[peak_vt_idx]
metrics["peak_vt_hr"] = peak_vt_row["HR(bpm)_smoothed"]
else:
peak_vt_idx = self.pnoe_df["VT(l)_smoothed"].idxmax()
peak_vt_row = self.pnoe_df.loc[peak_vt_idx]
metrics["peak_vt"] = peak_vt_row["VT(l)_smoothed"]
metrics["peak_vt_hr"] = peak_vt_row["HR(bpm)_smoothed"]
fat_max_idx = self.pnoe_df["FAT_smoothed"].idxmax()
fat_max_row = self.pnoe_df.loc[fat_max_idx]
metrics["fat_max_value"] = fat_max_row["FAT_smoothed"]
metrics["fat_max_hr"] = fat_max_row["HR(bpm)_smoothed"]
# Fat Max metrics
if "fat_max_value" in metric_overrides:
metrics["fat_max_value"] = float(metric_overrides["fat_max_value"])
if "fat_max_hr" in metric_overrides:
metrics["fat_max_hr"] = float(metric_overrides["fat_max_hr"])
else:
fat_max_idx = self.pnoe_df["FAT_smoothed"].idxmax()
fat_max_row = self.pnoe_df.loc[fat_max_idx]
metrics["fat_max_hr"] = fat_max_row["HR(bpm)_smoothed"]
else:
fat_max_idx = self.pnoe_df["FAT_smoothed"].idxmax()
fat_max_row = self.pnoe_df.loc[fat_max_idx]
metrics["fat_max_value"] = fat_max_row["FAT_smoothed"]
metrics["fat_max_hr"] = fat_max_row["HR(bpm)_smoothed"]
vt1, vt2 = self._detect_thresholds()
metrics["vt1"] = vt1
metrics["vt2"] = vt2
# VT1 and VT2 thresholds
if "vt1" in metric_overrides:
metrics["vt1"] = metric_overrides["vt1"]
else:
vt1, _ = self._detect_thresholds()
metrics["vt1"] = vt1
if "vt2" in metric_overrides:
metrics["vt2"] = metric_overrides["vt2"]
else:
_, vt2 = self._detect_thresholds()
metrics["vt2"] = vt2
zones = self._calculate_hr_zones(vt1, vt2, fat_max_row)
metrics.update(zones)
# Heart rate zones
if any(f"zone{i}_bpm" in metric_overrides for i in range(1, 6)):
for i in range(1, 6):
zone_key = f"zone{i}_bpm"
if zone_key in metric_overrides:
metrics[zone_key] = metric_overrides[zone_key]
else:
fat_max_idx = self.pnoe_df["FAT_smoothed"].idxmax()
fat_max_row = self.pnoe_df.loc[fat_max_idx]
zones = self._calculate_hr_zones(metrics["vt1"], metrics["vt2"], fat_max_row)
metrics.update(zones)
return metrics
def _detect_thresholds(self) -> Tuple[Optional[Dict], Optional[Dict]]:
@@ -195,12 +262,20 @@ class ContextGenerator:
return zones
def generate_all_contexts(
self, patient_name: str, graphs: Dict[str, str]
self, patient_name: str, graphs: Dict[str, str], metric_overrides: Optional[Dict] = None
) -> List[Dict]:
"""Main method to generate all page contexts"""
if metric_overrides is None:
metric_overrides = {}
self.extract_patient_info(patient_name)
spirometry_metrics = self.calculate_spirometry_metrics()
pnoe_metrics = self.calculate_pnoe_metrics()
# Extract metric overrides for spirometry and pnoe
spirometry_overrides = metric_overrides.get("spirometry", {})
pnoe_overrides = metric_overrides.get("pnoe", {})
spirometry_metrics = self.calculate_spirometry_metrics(spirometry_overrides)
pnoe_metrics = self.calculate_pnoe_metrics(pnoe_overrides)
contexts = []
contexts.append(
+3 -2
View File
@@ -6,7 +6,7 @@ It processes data, generates graphs, and creates PDF reports.
"""
from pathlib import Path
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional
import pandas as pd
from jinja2 import Environment, FileSystemLoader
@@ -287,6 +287,7 @@ class ReportGeneratorService:
seca_excel_path: str,
patient_info: Dict[str, Any],
output_filename: str = None,
metric_overrides: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Generate complete medical report from uploaded files.
@@ -387,7 +388,7 @@ class ReportGeneratorService:
pnoe_csv_path, str(spirometry_csv_path), seca_excel_path
)
context_list = self.context_generator.generate_all_contexts(
patient_name, graphs_dict
patient_name, graphs_dict, metric_overrides=metric_overrides
)
# Step 5: Calculate analysis metrics
+41
View File
@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Medical Report Generator{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
}
</style>
{% block extra_head %}{% endblock %}
</head>
<body class="bg-gray-50 min-h-screen">
<nav class="bg-white shadow-sm border-b">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<h1 class="text-xl font-bold text-gray-900">ISHP Report Generator</h1>
</div>
<div class="flex items-center space-x-4">
<a href="/" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Upload</a>
{% if session.get('report_path') %}
<a href="/preview" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Preview</a>
<a href="/edit" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Edit Metrics</a>
{% endif %}
</div>
</div>
</div>
</nav>
<main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
{% block content %}{% endblock %}
</main>
{% block extra_scripts %}{% endblock %}
</body>
</html>
+197
View File
@@ -0,0 +1,197 @@
{% extends "base.html" %}
{% block title %}Edit Metrics - Report Generator{% endblock %}
{% block content %}
<div class="px-4 py-6 sm:px-0">
{% if not session.get('metrics') %}
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<p class="text-yellow-800">No metrics found. Please <a href="/" class="underline">generate a report</a> first.</p>
</div>
{% else %}
{% if error %}
<div class="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<p class="text-red-800">{{ error }}</p>
</div>
{% endif %}
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold text-gray-900">Edit Calculated Metrics</h2>
<a href="/preview" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Back to Preview
</a>
</div>
<form action="/edit" method="post" class="space-y-8">
<!-- Pnoe Metrics Section -->
<div class="border-b border-gray-200 pb-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Pnoe Metrics</h3>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<label for="vo2_max" class="block text-sm font-medium text-gray-700">VO2 Max (ml/min)</label>
<input type="number" step="0.01" name="vo2_max" id="vo2_max"
value="{{ '%.2f'|format(session.metrics.pnoe['vo2_max']) if session.metrics.pnoe.get('vo2_max') else '' }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
<div>
<label for="vo2_max_per_kg" class="block text-sm font-medium text-gray-700">VO2 Max per kg (ml/min/kg)</label>
<input type="number" step="0.01" name="vo2_max_per_kg" id="vo2_max_per_kg"
value="{{ '%.2f'|format(session.metrics.pnoe['vo2_max_per_kg']) if session.metrics.pnoe.get('vo2_max_per_kg') else '' }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
<div>
<label for="peak_vt" class="block text-sm font-medium text-gray-700">Peak VT (L)</label>
<input type="number" step="0.01" name="peak_vt" id="peak_vt"
value="{{ '%.2f'|format(session.metrics.pnoe['peak_vt']) if session.metrics.pnoe.get('peak_vt') else '' }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
<div>
<label for="peak_vt_hr" class="block text-sm font-medium text-gray-700">Peak VT HR (bpm)</label>
<input type="number" step="1" name="peak_vt_hr" id="peak_vt_hr"
value="{{ '%.0f'|format(session.metrics.pnoe['peak_vt_hr']) if session.metrics.pnoe.get('peak_vt_hr') else '' }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
<div>
<label for="fat_max_value" class="block text-sm font-medium text-gray-700">Fat Max Value (kcal/min)</label>
<input type="number" step="0.01" name="fat_max_value" id="fat_max_value"
value="{{ '%.2f'|format(session.metrics.pnoe['fat_max_value']) if session.metrics.pnoe.get('fat_max_value') else '' }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
<div>
<label for="fat_max_hr" class="block text-sm font-medium text-gray-700">Fat Max HR (bpm)</label>
<input type="number" step="1" name="fat_max_hr" id="fat_max_hr"
value="{{ '%.0f'|format(session.metrics.pnoe['fat_max_hr']) if session.metrics.pnoe.get('fat_max_hr') else '' }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
</div>
</div>
<!-- VT1 Section -->
<div class="border-b border-gray-200 pb-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">VT1 Threshold</h3>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-3">
<div>
<label for="vt1_hr" class="block text-sm font-medium text-gray-700">Heart Rate (bpm)</label>
<input type="number" step="1" name="vt1_hr" id="vt1_hr"
value="{{ '%.0f'|format(session.metrics.pnoe['vt1']['HeartRate']) if session.metrics.pnoe.get('vt1') and session.metrics.pnoe['vt1'].get('HeartRate') else '' }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
<div>
<label for="vt1_speed" class="block text-sm font-medium text-gray-700">Speed (mph)</label>
<input type="number" step="0.01" name="vt1_speed" id="vt1_speed"
value="{{ '%.2f'|format(session.metrics.pnoe['vt1']['Speed']) if session.metrics.pnoe.get('vt1') and session.metrics.pnoe['vt1'].get('Speed') else '' }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
<div>
<label for="vt1_time" class="block text-sm font-medium text-gray-700">Time (sec)</label>
<input type="number" step="1" name="vt1_time" id="vt1_time"
value="{{ '%.0f'|format(session.metrics.pnoe['vt1']['Time']) if session.metrics.pnoe.get('vt1') and session.metrics.pnoe['vt1'].get('Time') else '' }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
</div>
</div>
<!-- VT2 Section -->
<div class="border-b border-gray-200 pb-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">VT2 Threshold</h3>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-3">
<div>
<label for="vt2_hr" class="block text-sm font-medium text-gray-700">Heart Rate (bpm)</label>
<input type="number" step="1" name="vt2_hr" id="vt2_hr"
value="{{ '%.0f'|format(session.metrics.pnoe['vt2']['HeartRate']) if session.metrics.pnoe.get('vt2') and session.metrics.pnoe['vt2'].get('HeartRate') else '' }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
<div>
<label for="vt2_speed" class="block text-sm font-medium text-gray-700">Speed (mph)</label>
<input type="number" step="0.01" name="vt2_speed" id="vt2_speed"
value="{{ '%.2f'|format(session.metrics.pnoe['vt2']['Speed']) if session.metrics.pnoe.get('vt2') and session.metrics.pnoe['vt2'].get('Speed') else '' }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
<div>
<label for="vt2_time" class="block text-sm font-medium text-gray-700">Time (sec)</label>
<input type="number" step="1" name="vt2_time" id="vt2_time"
value="{{ '%.0f'|format(session.metrics.pnoe['vt2']['Time']) if session.metrics.pnoe.get('vt2') and session.metrics.pnoe['vt2'].get('Time') else '' }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
</div>
</div>
<!-- Heart Rate Zones -->
<div class="border-b border-gray-200 pb-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Heart Rate Zones</h3>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-5">
{% for i in range(1, 6) %}
{% set zone_key = "zone" + i|string + "_bpm" %}
<div>
<label for="{{ zone_key }}" class="block text-sm font-medium text-gray-700">Zone {{ i }} (e.g., 120-140bpm)</label>
<input type="text" name="{{ zone_key }}" id="{{ zone_key }}"
value="{{ session.metrics.pnoe[zone_key] if session.metrics.pnoe.get(zone_key) else '' }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
{% endfor %}
</div>
</div>
<!-- Spirometry Metrics -->
{% if session.metrics.spirometry %}
<div class="border-b border-gray-200 pb-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Spirometry Metrics</h3>
<div class="space-y-4">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label for="fvc_best" class="block text-sm font-medium text-gray-700">FVC Best (L)</label>
<input type="number" step="0.01" name="fvc_best" id="fvc_best"
value="{{ '%.2f'|format(session.metrics.spirometry['fvc_best']) if session.metrics.spirometry.get('fvc_best') else '' }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
<div>
<label for="fvc_pred" class="block text-sm font-medium text-gray-700">FVC % Predicted</label>
<input type="number" step="0.1" name="fvc_pred" id="fvc_pred"
value="{{ '%.1f'|format(session.metrics.spirometry['fvc_pred']) if session.metrics.spirometry.get('fvc_pred') else '' }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
<div>
<label for="fev1_best" class="block text-sm font-medium text-gray-700">FEV1 Best (L)</label>
<input type="number" step="0.01" name="fev1_best" id="fev1_best"
value="{{ '%.2f'|format(session.metrics.spirometry['fev1_best']) if session.metrics.spirometry.get('fev1_best') else '' }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
<div>
<label for="fev1_pred" class="block text-sm font-medium text-gray-700">FEV1 % Predicted</label>
<input type="number" step="0.1" name="fev1_pred" id="fev1_pred"
value="{{ '%.1f'|format(session.metrics.spirometry['fev1_pred']) if session.metrics.spirometry.get('fev1_pred') else '' }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
<div>
<label for="fev1_fvc_pct_best" class="block text-sm font-medium text-gray-700">FEV1/FVC% Best</label>
<input type="number" step="0.01" name="fev1_fvc_pct_best" id="fev1_fvc_pct_best"
value="{{ '%.2f'|format(session.metrics.spirometry['fev1_fvc_pct_best']) if session.metrics.spirometry.get('fev1_fvc_pct_best') else '' }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
<div>
<label for="fev1_fvc_pct_pred" class="block text-sm font-medium text-gray-700">FEV1/FVC% % Predicted</label>
<input type="number" step="0.1" name="fev1_fvc_pct_pred" id="fev1_fvc_pct_pred"
value="{{ '%.1f'|format(session.metrics.spirometry['fev1_fvc_pct_pred']) if session.metrics.spirometry.get('fev1_fvc_pct_pred') else '' }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
</div>
</div>
</div>
{% endif %}
<!-- Submit Button -->
<div class="flex justify-end">
<button type="submit"
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Regenerate Report
</button>
</div>
</form>
</div>
</div>
{% endif %}
</div>
{% endblock %}
+180
View File
@@ -0,0 +1,180 @@
{% extends "base.html" %}
{% block title %}Report Preview - Report Generator{% endblock %}
{% block content %}
<div class="px-4 py-6 sm:px-0">
{% if not session.get('report_path') %}
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<p class="text-yellow-800">No report found. Please <a href="/" class="underline">upload files</a> first.</p>
</div>
{% else %}
<div class="bg-white shadow rounded-lg mb-6">
<div class="px-4 py-5 sm:p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold text-gray-900">Generated Report Preview</h2>
<div class="flex space-x-3">
<a href="/edit" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Edit Metrics
</a>
<a href="/download-report/{{ session.report_path.split('/')[-1] }}" class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700">
Download PDF
</a>
</div>
</div>
<!-- Patient Information -->
<div class="border-b border-gray-200 pb-6 mb-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Patient Information</h3>
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4">
<div>
<p class="text-sm text-gray-500">Name</p>
<p class="text-base font-medium text-gray-900">{{ session.patient_info['patient_name'] }}</p>
</div>
<div>
<p class="text-sm text-gray-500">Age</p>
<p class="text-base font-medium text-gray-900">{{ session.patient_info['age'] }}</p>
</div>
<div>
<p class="text-sm text-gray-500">Height</p>
<p class="text-base font-medium text-gray-900">{{ session.patient_info['height'] }}</p>
</div>
<div>
<p class="text-sm text-gray-500">Weight</p>
<p class="text-base font-medium text-gray-900">{{ session.patient_info['weight'] }}</p>
</div>
</div>
</div>
<!-- Calculated Metrics -->
{% if session.metrics %}
<div class="space-y-6">
<!-- Pnoe Metrics -->
<div>
<h3 class="text-lg font-medium text-gray-900 mb-4">Pnoe Metrics</h3>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{% if session.metrics.pnoe.get('vo2_max') %}
<div class="bg-gray-50 p-4 rounded-lg">
<p class="text-sm text-gray-500">VO2 Max</p>
<p class="text-2xl font-bold text-gray-900">{{ "%.2f"|format(session.metrics.pnoe['vo2_max']) }} ml/min</p>
</div>
{% endif %}
{% if session.metrics.pnoe.get('vo2_max_per_kg') %}
<div class="bg-gray-50 p-4 rounded-lg">
<p class="text-sm text-gray-500">VO2 Max per kg</p>
<p class="text-2xl font-bold text-gray-900">{{ "%.2f"|format(session.metrics.pnoe['vo2_max_per_kg']) }} ml/min/kg</p>
</div>
{% endif %}
{% if session.metrics.pnoe.get('peak_vt') %}
<div class="bg-gray-50 p-4 rounded-lg">
<p class="text-sm text-gray-500">Peak VT</p>
<p class="text-2xl font-bold text-gray-900">{{ "%.2f"|format(session.metrics.pnoe['peak_vt']) }} L</p>
<p class="text-sm text-gray-500 mt-1">HR: {{ "%.0f"|format(session.metrics.pnoe['peak_vt_hr']) }} bpm</p>
</div>
{% endif %}
{% if session.metrics.pnoe.get('fat_max_value') %}
<div class="bg-gray-50 p-4 rounded-lg">
<p class="text-sm text-gray-500">Fat Max Value</p>
<p class="text-2xl font-bold text-gray-900">{{ "%.2f"|format(session.metrics.pnoe['fat_max_value']) }} kcal/min</p>
<p class="text-sm text-gray-500 mt-1">HR: {{ "%.0f"|format(session.metrics.pnoe['fat_max_hr']) }} bpm</p>
</div>
{% endif %}
</div>
</div>
<!-- VT1 and VT2 -->
{% if session.metrics.pnoe.get('vt1') or session.metrics.pnoe.get('vt2') %}
<div>
<h3 class="text-lg font-medium text-gray-900 mb-4">Ventilatory Thresholds</h3>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
{% if session.metrics.pnoe.get('vt1') %}
<div class="bg-blue-50 p-4 rounded-lg">
<p class="text-sm font-medium text-blue-900 mb-2">VT1</p>
<p class="text-sm text-blue-700">Heart Rate: {{ "%.0f"|format(session.metrics.pnoe['vt1']['HeartRate']) }} bpm</p>
<p class="text-sm text-blue-700">Speed: {{ "%.2f"|format(session.metrics.pnoe['vt1']['Speed']) }} mph</p>
<p class="text-sm text-blue-700">Time: {{ "%.0f"|format(session.metrics.pnoe['vt1']['Time']) }} sec</p>
</div>
{% endif %}
{% if session.metrics.pnoe.get('vt2') %}
<div class="bg-green-50 p-4 rounded-lg">
<p class="text-sm font-medium text-green-900 mb-2">VT2</p>
<p class="text-sm text-green-700">Heart Rate: {{ "%.0f"|format(session.metrics.pnoe['vt2']['HeartRate']) }} bpm</p>
<p class="text-sm text-green-700">Speed: {{ "%.2f"|format(session.metrics.pnoe['vt2']['Speed']) }} mph</p>
<p class="text-sm text-green-700">Time: {{ "%.0f"|format(session.metrics.pnoe['vt2']['Time']) }} sec</p>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Heart Rate Zones -->
{% if session.metrics.pnoe.get('zone1_bpm') %}
<div>
<h3 class="text-lg font-medium text-gray-900 mb-4">Heart Rate Zones</h3>
<div class="grid grid-cols-1 gap-2 sm:grid-cols-5">
{% for i in range(1, 6) %}
{% set zone_key = "zone" + i|string + "_bpm" %}
{% if session.metrics.pnoe.get(zone_key) %}
<div class="bg-gray-50 p-3 rounded-lg text-center">
<p class="text-xs text-gray-500">Zone {{ i }}</p>
<p class="text-sm font-medium text-gray-900">{{ session.metrics.pnoe[zone_key] }}</p>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% endif %}
<!-- Spirometry Metrics -->
{% if session.metrics.spirometry %}
<div>
<h3 class="text-lg font-medium text-gray-900 mb-4">Spirometry Metrics</h3>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
{% if session.metrics.spirometry.get('fvc_best') %}
<div class="bg-gray-50 p-4 rounded-lg">
<p class="text-sm text-gray-500">FVC Best</p>
<p class="text-2xl font-bold text-gray-900">{{ "%.2f"|format(session.metrics.spirometry['fvc_best']) }} L</p>
<p class="text-sm text-gray-500 mt-1">{{ "%.1f"|format(session.metrics.spirometry['fvc_pred']) }}% predicted</p>
</div>
{% endif %}
{% if session.metrics.spirometry.get('fev1_best') %}
<div class="bg-gray-50 p-4 rounded-lg">
<p class="text-sm text-gray-500">FEV1 Best</p>
<p class="text-2xl font-bold text-gray-900">{{ "%.2f"|format(session.metrics.spirometry['fev1_best']) }} L</p>
<p class="text-sm text-gray-500 mt-1">{{ "%.1f"|format(session.metrics.spirometry['fev1_pred']) }}% predicted</p>
</div>
{% endif %}
{% if session.metrics.spirometry.get('fev1_fvc_pct_best') %}
<div class="bg-gray-50 p-4 rounded-lg">
<p class="text-sm text-gray-500">FEV1/FVC%</p>
<p class="text-2xl font-bold text-gray-900">{{ "%.2f"|format(session.metrics.spirometry['fev1_fvc_pct_best']) }}%</p>
<p class="text-sm text-gray-500 mt-1">{{ "%.1f"|format(session.metrics.spirometry['fev1_fvc_pct_pred']) }}% predicted</p>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% endif %}
<!-- Graphs Section -->
{% if session.graphs_generated %}
<div class="mt-8">
<h3 class="text-lg font-medium text-gray-900 mb-4">Generated Graphs</h3>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
{% for graph in session.graphs_generated %}
<div class="bg-gray-50 p-4 rounded-lg">
<p class="text-sm font-medium text-gray-700 mb-2">{{ graph.name|replace('_', ' ')|title }}</p>
<img src="/graphs/{{ graph.path.split('/')[-1] }}" alt="{{ graph.name }}" class="w-full h-auto rounded">
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% endblock %}
+106
View File
@@ -0,0 +1,106 @@
{% extends "base.html" %}
{% block title %}Upload Patient Data - Report Generator{% endblock %}
{% block content %}
<div class="px-4 py-6 sm:px-0">
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h2 class="text-2xl font-bold text-gray-900 mb-6">Upload Patient Data and Files</h2>
{% if error %}
<div class="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<p class="text-red-800">{{ error }}</p>
</div>
{% endif %}
<form action="/upload" method="post" enctype="multipart/form-data" class="space-y-6">
<!-- Patient Information Section -->
<div class="border-b border-gray-200 pb-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Patient Information</h3>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<label for="first_name" class="block text-sm font-medium text-gray-700">First Name</label>
<input type="text" name="first_name" id="first_name" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
<div>
<label for="last_name" class="block text-sm font-medium text-gray-700">Last Name</label>
<input type="text" name="last_name" id="last_name" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
<div>
<label for="age" class="block text-sm font-medium text-gray-700">Age</label>
<input type="number" name="age" id="age" required min="1" max="120"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
<div>
<label for="height" class="block text-sm font-medium text-gray-700">Height (e.g., 5'4" or 165cm)</label>
<input type="text" name="height" id="height" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border"
placeholder="5'4&quot;">
</div>
<div>
<label for="weight" class="block text-sm font-medium text-gray-700">Weight (e.g., 123lbs or 56kg)</label>
<input type="text" name="weight" id="weight" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border"
placeholder="123lbs">
</div>
<div>
<label for="gender" class="block text-sm font-medium text-gray-700">Gender</label>
<select name="gender" id="gender" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
<option value="">Select...</option>
<option value="male">Male</option>
<option value="female">Female</option>
<option value="other">Other</option>
</select>
</div>
<div>
<label for="focus" class="block text-sm font-medium text-gray-700">Training Focus</label>
<input type="text" name="focus" id="focus" value="Endurance"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
<div>
<label for="session_id" class="block text-sm font-medium text-gray-700">Session ID</label>
<input type="text" name="session_id" id="session_id" value="default"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
</div>
</div>
</div>
<!-- File Upload Section -->
<div class="border-b border-gray-200 pb-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Upload Files</h3>
<div class="space-y-4">
<div>
<label for="spirometry_pdf" class="block text-sm font-medium text-gray-700">Spirometry PDF</label>
<input type="file" name="spirometry_pdf" id="spirometry_pdf" accept=".pdf" required
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100">
</div>
<div>
<label for="pnoe_csv" class="block text-sm font-medium text-gray-700">Pnoe CSV</label>
<input type="file" name="pnoe_csv" id="pnoe_csv" accept=".csv" required
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100">
</div>
<div>
<label for="seca_excel" class="block text-sm font-medium text-gray-700">SECA Excel</label>
<input type="file" name="seca_excel" id="seca_excel" accept=".xlsx,.xls" required
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100">
</div>
</div>
</div>
<!-- Submit Button -->
<div class="flex justify-end">
<button type="submit"
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Generate Report
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
+46
View File
@@ -766,6 +766,52 @@
"print(f\"FAT (smoothed): {max_fat_smoothed_row['FAT_smoothed']:.3f} kcal/min\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3521220f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Estimated RMR from data: 1385 kcal/day\n"
]
}
],
"source": [
"# Step 1: Filter resting phase (usually lowest VO2 or MET values)\n",
"rest_phase = df[df['MET'] <= 1.1] # assuming <1.1 MET means rest\n",
"\n",
"# Step 2: Compute resting metabolic rate\n",
"rmr = rest_phase['EE(kcal/day)'].mean()\n",
"\n",
"print(f\"Estimated RMR from data: {rmr:.0f} kcal/day\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "524e4cba",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Resting phase fuel mix: Fats 32.9%, Carbs 67.1%\n"
]
}
],
"source": [
"rest_phase = df[df['RER'] == 0.9] # filter rest data\n",
"fat_rest = rest_phase['FAT(%)'].mean()\n",
"carb_rest = rest_phase['CARBS(%)'].mean()\n",
"\n",
"print(f\"Resting phase fuel mix: Fats {fat_rest:.1f}%, Carbs {carb_rest:.1f}%\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
+713 -1727
View File
File diff suppressed because one or more lines are too long