Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- .env.example +1 -0
- .gitignore +7 -0
- Dockerfile +11 -0
- README.md +89 -9
- main.py +186 -0
- models/diabetes_model.sav +0 -0
- models/diabetes_scaler.sav +0 -0
- models/heart_model.pkl +3 -0
- models/loader.py +45 -0
- models/stroke_model.pkl +3 -0
- requirements.txt +10 -0
- static/style.css +447 -0
- templates/base.html +230 -0
- templates/diabetes.html +191 -0
- templates/heart.html +209 -0
- templates/index.html +108 -0
- templates/stroke.html +292 -0
.env.example
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
GROQ_API_KEY=your_groq_api_key_here
|
.gitignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
.pytest_cache/
|
| 5 |
+
.venv/
|
| 6 |
+
venv/
|
| 7 |
+
*.log
|
Dockerfile
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY . /app
|
| 6 |
+
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
EXPOSE 7860
|
| 10 |
+
|
| 11 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,12 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
short_description: 'PredictLab is a full-stack Machine Learning web application '
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PredictLab: Machine Learning Portfolio Web App
|
| 2 |
+
|
| 3 |
+

|
| 4 |
+

|
| 5 |
+

|
| 6 |
+
|
| 7 |
+
A premium, production-ready full-stack machine learning portfolio application built with FastAPI and customized with an elegant Dark Modern Medical/Tech UI. It showcases predictive models (Pima Diabetes, Stroke Risk, House Price) and automatically integrates a Llama 3-powered AI Assistant to contextualize predictions for users.
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## 📸 Screenshots
|
| 12 |
+
|
| 13 |
+
*(Add screenshots of your application here — e.g., the homepage grid and one of the prediction forms with the Chatbot open).*
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## ✨ Features
|
| 18 |
+
|
| 19 |
+
- **Three Integrated Machine Learning Models**: Built-in support for Diabetes (SVM), Stroke Risk (Classification), and House Price (Regression) prediction models.
|
| 20 |
+
- **Graceful Fallbacks**: The application handles missing models fluidly, displaying visually pleasing error placeholders while keeping the rest of the application fully functional.
|
| 21 |
+
- **Premium UI & UX**: Gorgeous dashboard styling using a combination of Tailwind CSS and custom, deeply customized animations. Includes robust micro-interactions and async fetch routines for seamless loading.
|
| 22 |
+
- **Context-Aware Groq AI Assistant**: A globally persistent chatbot that understands what page you are on, the exact medical inputs provided, what the model predicted, and gently provides domain expertise based on Llama 3 logic via the Groq API.
|
| 23 |
+
- **Fully Responsive**: Looks flawless on Desktop, Tablet, and Mobile.
|
| 24 |
+
- **No Page-Reload Architecture**: Entirely JS-fetch driven forms.
|
| 25 |
+
|
| 26 |
+
---
|
| 27 |
+
|
| 28 |
+
## 🚀 Setup Instructions
|
| 29 |
+
|
| 30 |
+
1. **Clone the repository:**
|
| 31 |
+
```bash
|
| 32 |
+
git clone <repo>
|
| 33 |
+
cd ml-portfolio
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
2. **Install dependencies:**
|
| 37 |
+
It is recommended to run this within a clean Python Virtual Environment.
|
| 38 |
+
```bash
|
| 39 |
+
pip install -r requirements.txt
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
3. **Configure Environment Variables:**
|
| 43 |
+
```bash
|
| 44 |
+
cp .env.example .env
|
| 45 |
+
```
|
| 46 |
+
Open `.env` and configure your API keys:
|
| 47 |
+
`GROQ_API_KEY=your_key_here`
|
| 48 |
+
|
| 49 |
+
4. **Add Trained `.sav` Models:**
|
| 50 |
+
Place your pre-trained models within the `/models` directory using the following exact filenames:
|
| 51 |
+
- `diabetes_model.sav` & `diabetes_scaler.sav`
|
| 52 |
+
- `stroke_model.sav`
|
| 53 |
+
- `house_model.sav`
|
| 54 |
+
If these are omitted, the application will simply show "Model not loaded" for those respective predictors.
|
| 55 |
+
|
| 56 |
+
5. **Start the Uvicorn Server:**
|
| 57 |
+
```bash
|
| 58 |
+
uvicorn main:app --reload
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
6. **View the Application:**
|
| 62 |
+
Open [http://localhost:8000](http://localhost:8000) in your modern web browser.
|
| 63 |
+
|
| 64 |
---
|
| 65 |
+
|
| 66 |
+
## 🛠️ How to Add New Models
|
| 67 |
+
|
| 68 |
+
1. **Create an HTML UI**: Duplicate an existing prediction page in `templates/` and link it in `base.html`. Make sure it submits required payload features.
|
| 69 |
+
2. **Update loader config**: Edit `models/loader.py` to recognize your newly appended `.sav` file.
|
| 70 |
+
3. **Register Route in `main.py`**: Add a new asynchronous API route under `/predict/yours` handling your specific inference transformation arrays.
|
| 71 |
+
|
|
|
|
| 72 |
---
|
| 73 |
|
| 74 |
+
## 📁 Folder Structure
|
| 75 |
+
|
| 76 |
+
```text
|
| 77 |
+
ml-portfolio/
|
| 78 |
+
├── main.py # Main FastAPI App, Routes, and Groq Chat implementation.
|
| 79 |
+
├── requirements.txt # Python dependencies.
|
| 80 |
+
├── README.md # Project instructions.
|
| 81 |
+
├── .env # Secrets & API Keys.
|
| 82 |
+
├── models/ # Contains ML assets and custom scikit model loader block.
|
| 83 |
+
│ └── loader.py
|
| 84 |
+
├── templates/ # Jinja2 Layout Files.
|
| 85 |
+
│ ├── base.html # The global layout, includes Navbar and universal AI Chat widget.
|
| 86 |
+
│ ├── index.html # Landing Dashboard.
|
| 87 |
+
│ ├── diabetes.html # Diabetes prediction form layout.
|
| 88 |
+
│ ├── stroke.html # Stroke prediction layout.
|
| 89 |
+
│ └── house.html # House price prediction interface.
|
| 90 |
+
└── static/
|
| 91 |
+
└── style.css # Vanilla CSS enhancements overriding standard Tailwind styling for a premium aesthetic.
|
| 92 |
+
```
|
main.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, Request
|
| 2 |
+
from fastapi.responses import HTMLResponse, JSONResponse
|
| 3 |
+
from fastapi.staticfiles import StaticFiles
|
| 4 |
+
from fastapi.templating import Jinja2Templates
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
from typing import Optional, Any
|
| 7 |
+
from groq import Groq
|
| 8 |
+
import os
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
import numpy as np
|
| 11 |
+
import pandas as pd
|
| 12 |
+
|
| 13 |
+
# Internal model loader
|
| 14 |
+
from models.loader import load_models
|
| 15 |
+
|
| 16 |
+
# Load environment vars
|
| 17 |
+
load_dotenv()
|
| 18 |
+
|
| 19 |
+
app = FastAPI(title="PredictLab ML Portfolio")
|
| 20 |
+
try:
|
| 21 |
+
groq_client = Groq(api_key=os.getenv("GROQ_API_KEY"))
|
| 22 |
+
except Exception as e:
|
| 23 |
+
print(f"Failed to initialize Groq client: {e}")
|
| 24 |
+
groq_client = None
|
| 25 |
+
|
| 26 |
+
# Mount static and templates
|
| 27 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 28 |
+
templates = Jinja2Templates(directory="templates")
|
| 29 |
+
|
| 30 |
+
# Load models safely
|
| 31 |
+
models, scalers = load_models()
|
| 32 |
+
|
| 33 |
+
# Route: Homepage
|
| 34 |
+
@app.get("/", response_class=HTMLResponse)
|
| 35 |
+
async def read_root(request: Request):
|
| 36 |
+
return templates.TemplateResponse("index.html", {"request": request})
|
| 37 |
+
|
| 38 |
+
# Route: Diabetes Predictor
|
| 39 |
+
@app.get("/diabetes", response_class=HTMLResponse)
|
| 40 |
+
async def read_diabetes(request: Request):
|
| 41 |
+
return templates.TemplateResponse("diabetes.html", {"request": request})
|
| 42 |
+
|
| 43 |
+
# Route: Stroke Predictor
|
| 44 |
+
@app.get("/stroke", response_class=HTMLResponse)
|
| 45 |
+
async def read_stroke(request: Request):
|
| 46 |
+
return templates.TemplateResponse("stroke.html", {"request": request})
|
| 47 |
+
|
| 48 |
+
# Route: Heart Predictor
|
| 49 |
+
@app.get("/heart", response_class=HTMLResponse)
|
| 50 |
+
async def read_heart(request: Request):
|
| 51 |
+
return templates.TemplateResponse("heart.html", {"request": request})
|
| 52 |
+
|
| 53 |
+
# Prediction Endpoints
|
| 54 |
+
@app.post("/predict/diabetes")
|
| 55 |
+
async def predict_diabetes(data: dict):
|
| 56 |
+
if not models.get("diabetes"):
|
| 57 |
+
return JSONResponse(status_code=500, content={"error": "Diabetes model not loaded yet. Coming soon!"})
|
| 58 |
+
try:
|
| 59 |
+
# Construct DataFrame with exactly the same column names the scaler/model expects
|
| 60 |
+
df = pd.DataFrame([{
|
| 61 |
+
"Pregnancies": float(data["pregnancies"]),
|
| 62 |
+
"Glucose": float(data["glucose"]),
|
| 63 |
+
"BloodPressure": float(data["blood_pressure"]),
|
| 64 |
+
"SkinThickness": float(data["skin_thickness"]),
|
| 65 |
+
"Insulin": float(data["insulin"]),
|
| 66 |
+
"BMI": float(data["bmi"]),
|
| 67 |
+
"DiabetesPedigreeFunction": float(data["dpf"]),
|
| 68 |
+
"Age": float(data["age"])
|
| 69 |
+
}])
|
| 70 |
+
|
| 71 |
+
arr = df
|
| 72 |
+
if scalers.get("diabetes"):
|
| 73 |
+
arr = scalers["diabetes"].transform(arr)
|
| 74 |
+
|
| 75 |
+
prediction = models["diabetes"].predict(arr)[0]
|
| 76 |
+
label = "Diabetic" if prediction == 1 else "Not Diabetic"
|
| 77 |
+
return {"prediction": int(prediction), "label": label, "confidence": None}
|
| 78 |
+
except Exception as e:
|
| 79 |
+
import traceback
|
| 80 |
+
traceback.print_exc()
|
| 81 |
+
return JSONResponse(status_code=400, content={"error": str(e)})
|
| 82 |
+
|
| 83 |
+
@app.post("/predict/stroke")
|
| 84 |
+
async def predict_stroke(data: dict):
|
| 85 |
+
if not models.get("stroke"):
|
| 86 |
+
return JSONResponse(status_code=500, content={"error": "Stroke risk model not loaded yet. Coming soon!"})
|
| 87 |
+
try:
|
| 88 |
+
df = pd.DataFrame([{
|
| 89 |
+
"gender": str(data["gender"]),
|
| 90 |
+
"age": float(data["age"]),
|
| 91 |
+
"hypertension": int(data["hypertension"]),
|
| 92 |
+
"heart_disease": int(data["heart_disease"]),
|
| 93 |
+
"ever_married": str(data["ever_married"]),
|
| 94 |
+
"work_type": str(data["work_type"]),
|
| 95 |
+
"Residence_type": str(data["residence_type"]),
|
| 96 |
+
"avg_glucose_level": float(data["avg_glucose_level"]),
|
| 97 |
+
"bmi": float(data["bmi"]),
|
| 98 |
+
"smoking_status": str(data["smoking_status"])
|
| 99 |
+
}])
|
| 100 |
+
|
| 101 |
+
prediction = models["stroke"].predict(df)[0]
|
| 102 |
+
label = "High Stroke Risk" if prediction == 1 else "Low Stroke Risk"
|
| 103 |
+
return {"prediction": int(prediction), "label": label, "confidence": None}
|
| 104 |
+
except Exception as e:
|
| 105 |
+
import traceback
|
| 106 |
+
traceback.print_exc()
|
| 107 |
+
return JSONResponse(status_code=400, content={"error": str(e)})
|
| 108 |
+
|
| 109 |
+
@app.post("/predict/heart")
|
| 110 |
+
async def predict_heart(data: dict):
|
| 111 |
+
if not models.get("heart"):
|
| 112 |
+
return JSONResponse(status_code=500, content={"error": "Heart disease model not loaded yet. Coming soon!"})
|
| 113 |
+
try:
|
| 114 |
+
features = [
|
| 115 |
+
float(data["age"]),
|
| 116 |
+
int(data["sex"]),
|
| 117 |
+
int(data["cp"]),
|
| 118 |
+
float(data["trestbps"]),
|
| 119 |
+
float(data["chol"]),
|
| 120 |
+
int(data["fbs"]),
|
| 121 |
+
int(data["restecg"]),
|
| 122 |
+
float(data["thalach"]),
|
| 123 |
+
int(data["exang"]),
|
| 124 |
+
float(data["oldpeak"]),
|
| 125 |
+
int(data["ca"]),
|
| 126 |
+
int(data["thal"])
|
| 127 |
+
]
|
| 128 |
+
|
| 129 |
+
arr = np.array(features).reshape(1, -1)
|
| 130 |
+
if scalers.get("heart"):
|
| 131 |
+
arr = scalers["heart"].transform(arr)
|
| 132 |
+
prediction = models["heart"].predict(arr)[0]
|
| 133 |
+
label = "Disease Detected" if prediction == 1 else "No Disease Detected"
|
| 134 |
+
return {"prediction": int(prediction), "label": label}
|
| 135 |
+
except Exception as e:
|
| 136 |
+
import traceback
|
| 137 |
+
traceback.print_exc()
|
| 138 |
+
return JSONResponse(status_code=400, content={"error": str(e)})
|
| 139 |
+
|
| 140 |
+
# Groq Context Data Setup
|
| 141 |
+
class ChatContext(BaseModel):
|
| 142 |
+
current_page: str
|
| 143 |
+
inputs: Optional[dict] = {}
|
| 144 |
+
prediction: Optional[Any] = None
|
| 145 |
+
|
| 146 |
+
class ChatPayload(BaseModel):
|
| 147 |
+
message: str
|
| 148 |
+
context: ChatContext
|
| 149 |
+
|
| 150 |
+
# Route: Chatbot
|
| 151 |
+
@app.post("/chat")
|
| 152 |
+
async def chat(payload: ChatPayload):
|
| 153 |
+
if groq_client is None:
|
| 154 |
+
return JSONResponse(status_code=500, content={"error": "Groq client not configured or missing API key."})
|
| 155 |
+
|
| 156 |
+
try:
|
| 157 |
+
system_prompt = f"""You are an intelligent assistant embedded in a machine learning portfolio app. You help users understand their prediction results.
|
| 158 |
+
|
| 159 |
+
Current context:
|
| 160 |
+
- Page: {payload.context.current_page}
|
| 161 |
+
- User inputs: {payload.context.inputs}
|
| 162 |
+
- Model prediction: {payload.context.prediction}
|
| 163 |
+
|
| 164 |
+
Your responsibilities:
|
| 165 |
+
1. Explain the prediction result in plain, simple language
|
| 166 |
+
2. Answer questions about the medical condition or real estate topic
|
| 167 |
+
3. Clarify what each input field means and how it affects the result
|
| 168 |
+
4. Provide helpful, accurate domain information
|
| 169 |
+
5. Always recommend consulting a licensed professional for medical/financial decisions
|
| 170 |
+
6. Be warm, concise, and non-alarmist
|
| 171 |
+
|
| 172 |
+
Keep all responses under 130 words unless the user asks for more detail.
|
| 173 |
+
Never provide a definitive medical diagnosis. Always encourage professional consultation."""
|
| 174 |
+
|
| 175 |
+
response = groq_client.chat.completions.create(
|
| 176 |
+
model="llama-3.1-8b-instant",
|
| 177 |
+
messages=[
|
| 178 |
+
{"role": "system", "content": system_prompt},
|
| 179 |
+
{"role": "user", "content": payload.message}
|
| 180 |
+
],
|
| 181 |
+
max_tokens=300,
|
| 182 |
+
temperature=0.7
|
| 183 |
+
)
|
| 184 |
+
return {"reply": response.choices[0].message.content}
|
| 185 |
+
except Exception as e:
|
| 186 |
+
return JSONResponse(status_code=500, content={"error": str(e)})
|
models/diabetes_model.sav
ADDED
|
Binary file (27.6 kB). View file
|
|
|
models/diabetes_scaler.sav
ADDED
|
Binary file (800 Bytes). View file
|
|
|
models/heart_model.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2500e3086a76226ca87e10e7da1479c76fa5e09e2a3a924a1817fcbf2926d095
|
| 3 |
+
size 979
|
models/loader.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pickle
|
| 2 |
+
import joblib
|
| 3 |
+
import os
|
| 4 |
+
import logging
|
| 5 |
+
|
| 6 |
+
BASE = os.path.dirname(__file__)
|
| 7 |
+
|
| 8 |
+
def load_models():
|
| 9 |
+
models, scalers = {}, {}
|
| 10 |
+
files = {
|
| 11 |
+
"diabetes": ("diabetes_model.sav", "diabetes_scaler.sav"),
|
| 12 |
+
"stroke": ("stroke_model.pkl", None),
|
| 13 |
+
"heart": ("heart_model.pkl", None),
|
| 14 |
+
}
|
| 15 |
+
for name, (model_file, scaler_file) in files.items():
|
| 16 |
+
try:
|
| 17 |
+
filepath = os.path.join(BASE, model_file)
|
| 18 |
+
try:
|
| 19 |
+
models[name] = joblib.load(filepath)
|
| 20 |
+
except Exception:
|
| 21 |
+
with open(filepath, "rb") as f:
|
| 22 |
+
models[name] = pickle.load(f)
|
| 23 |
+
logging.info(f"✅ Loaded {name} model")
|
| 24 |
+
except FileNotFoundError:
|
| 25 |
+
logging.warning(f"⚠️ {name} model not found — skipping")
|
| 26 |
+
models[name] = None
|
| 27 |
+
except Exception as e:
|
| 28 |
+
logging.error(f"❌ Error loading {name} model: {e}")
|
| 29 |
+
models[name] = None
|
| 30 |
+
|
| 31 |
+
if scaler_file:
|
| 32 |
+
try:
|
| 33 |
+
scalerpath = os.path.join(BASE, scaler_file)
|
| 34 |
+
try:
|
| 35 |
+
scalers[name] = joblib.load(scalerpath)
|
| 36 |
+
except Exception:
|
| 37 |
+
with open(scalerpath, "rb") as f:
|
| 38 |
+
scalers[name] = pickle.load(f)
|
| 39 |
+
except FileNotFoundError:
|
| 40 |
+
scalers[name] = None
|
| 41 |
+
except Exception as e:
|
| 42 |
+
logging.error(f"❌ Error loading {name} scaler: {e}")
|
| 43 |
+
scalers[name] = None
|
| 44 |
+
|
| 45 |
+
return models, scalers
|
models/stroke_model.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:025203c947fec195bf1769feffc2c6ecd891c80ced68bda799e35799491d042b
|
| 3 |
+
size 273440
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
scikit-learn
|
| 4 |
+
pandas
|
| 5 |
+
numpy
|
| 6 |
+
joblib
|
| 7 |
+
jinja2
|
| 8 |
+
python-multipart
|
| 9 |
+
groq
|
| 10 |
+
python-dotenv
|
static/style.css
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Premium Dark Tech Theme */
|
| 2 |
+
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:wght@400;500;700&family=JetBrains+Mono:wght@400;700&display=swap');
|
| 3 |
+
|
| 4 |
+
:root {
|
| 5 |
+
--bg-dark: #0a0f1e;
|
| 6 |
+
--surface-dark: #111827;
|
| 7 |
+
--border-dark: #1f2937;
|
| 8 |
+
--primary-cyan: #06b6d4;
|
| 9 |
+
--success-emerald: #10b981;
|
| 10 |
+
--danger-rose: #f43f5e;
|
| 11 |
+
--text-primary: #f1f5f9;
|
| 12 |
+
--text-muted: #94a3b8;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
body {
|
| 16 |
+
font-family: 'DM Sans', sans-serif;
|
| 17 |
+
background-color: var(--bg-dark);
|
| 18 |
+
background-image:
|
| 19 |
+
radial-gradient(circle at 15% 50%, rgba(6, 182, 212, 0.08), transparent 35%),
|
| 20 |
+
radial-gradient(circle at 85% 30%, rgba(168, 85, 247, 0.08), transparent 35%),
|
| 21 |
+
radial-gradient(circle at 50% 80%, rgba(16, 185, 129, 0.05), transparent 40%);
|
| 22 |
+
background-attachment: fixed;
|
| 23 |
+
color: var(--text-primary);
|
| 24 |
+
margin: 0;
|
| 25 |
+
padding: 0;
|
| 26 |
+
-webkit-font-smoothing: antialiased;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
h1, h2, h3, h4, h5, h6, .display-font {
|
| 30 |
+
font-family: 'Syne', sans-serif;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
.mono-font {
|
| 34 |
+
font-family: 'JetBrains Mono', monospace;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/* Custom Gradients & Animations */
|
| 38 |
+
.hero-gradient-text {
|
| 39 |
+
background: linear-gradient(to right, #ffffff, var(--primary-cyan));
|
| 40 |
+
-webkit-background-clip: text;
|
| 41 |
+
background-clip: text;
|
| 42 |
+
-webkit-text-fill-color: transparent;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.card-hover {
|
| 46 |
+
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.4s ease, border-color 0.4s ease, background 0.4s ease;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.card-hover:hover {
|
| 50 |
+
transform: translateY(-8px) scale(1.02);
|
| 51 |
+
box-shadow: 0 20px 40px -10px rgba(6, 182, 212, 0.3), 0 0 20px rgba(168, 85, 247, 0.15) inset;
|
| 52 |
+
border-color: rgba(6, 182, 212, 0.6);
|
| 53 |
+
background: rgba(17, 24, 39, 0.85);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.glass-card {
|
| 57 |
+
background: rgba(17, 24, 39, 0.55);
|
| 58 |
+
backdrop-filter: blur(20px);
|
| 59 |
+
-webkit-backdrop-filter: blur(20px);
|
| 60 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 61 |
+
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
|
| 62 |
+
position: relative;
|
| 63 |
+
overflow: hidden;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.glass-card::before {
|
| 67 |
+
content: '';
|
| 68 |
+
position: absolute;
|
| 69 |
+
top: 0; left: 0; right: 0;
|
| 70 |
+
height: 2px;
|
| 71 |
+
background: linear-gradient(90deg, transparent, rgba(6, 182, 212, 0.3), transparent);
|
| 72 |
+
opacity: 0;
|
| 73 |
+
transition: opacity 0.4s;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.glass-card:hover::before {
|
| 77 |
+
opacity: 1;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
/* Inputs */
|
| 81 |
+
.floating-input {
|
| 82 |
+
background-color: var(--surface-dark);
|
| 83 |
+
border: 1px solid var(--border-dark);
|
| 84 |
+
color: var(--text-primary);
|
| 85 |
+
transition: all 0.3s ease;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.floating-input:focus {
|
| 89 |
+
border-color: var(--primary-cyan);
|
| 90 |
+
box-shadow: 0 0 0 2px rgba(6, 182, 212, 0.2);
|
| 91 |
+
outline: none;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* Buttons */
|
| 95 |
+
.btn-primary {
|
| 96 |
+
background: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%);
|
| 97 |
+
color: white;
|
| 98 |
+
transition: all 0.3s ease;
|
| 99 |
+
position: relative;
|
| 100 |
+
overflow: hidden;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.btn-primary:hover {
|
| 104 |
+
box-shadow: 0 0 15px rgba(6, 182, 212, 0.5);
|
| 105 |
+
transform: translateY(-1px);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
/* Base HTML & Chatbot */
|
| 109 |
+
.backdrop-blur-nav {
|
| 110 |
+
backdrop-filter: blur(12px);
|
| 111 |
+
background-color: rgba(10, 15, 30, 0.8);
|
| 112 |
+
border-bottom: 1px solid var(--border-dark);
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
/* Chatbot Styles */
|
| 116 |
+
#chat-widget {
|
| 117 |
+
position: fixed;
|
| 118 |
+
bottom: 2rem;
|
| 119 |
+
right: 2rem;
|
| 120 |
+
z-index: 50;
|
| 121 |
+
font-family: 'DM Sans', sans-serif;
|
| 122 |
+
perspective: 1000px;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
#chat-toggle {
|
| 126 |
+
width: 65px;
|
| 127 |
+
height: 65px;
|
| 128 |
+
border-radius: 50%;
|
| 129 |
+
background: linear-gradient(135deg, var(--primary-cyan), #0056b3, #0891b2, var(--primary-cyan));
|
| 130 |
+
background-size: 300% 300%;
|
| 131 |
+
color: white;
|
| 132 |
+
display: flex;
|
| 133 |
+
align-items: center;
|
| 134 |
+
justify-content: center;
|
| 135 |
+
box-shadow: 0 0 25px rgba(6, 182, 212, 0.6), 0 0 50px rgba(6, 182, 212, 0.2);
|
| 136 |
+
cursor: pointer;
|
| 137 |
+
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 138 |
+
border: 2px solid rgba(255, 255, 255, 0.2);
|
| 139 |
+
animation: gradientShift 5s ease infinite, pulseGlow 2s infinite;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
@keyframes gradientShift { 0% { background-position: 0% 50% } 50% { background-position: 100% 50% } 100% { background-position: 0% 50% } }
|
| 143 |
+
@keyframes pulseGlow { 0% { box-shadow: 0 0 15px rgba(6, 182, 212, 0.4); } 50% { box-shadow: 0 0 35px rgba(6, 182, 212, 0.8), 0 0 55px rgba(6, 182, 212, 0.4); } 100% { box-shadow: 0 0 15px rgba(6, 182, 212, 0.4); } }
|
| 144 |
+
|
| 145 |
+
#chat-toggle:hover {
|
| 146 |
+
transform: scale(1.1) rotate(5deg);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
#chat-panel {
|
| 150 |
+
position: absolute;
|
| 151 |
+
bottom: 90px;
|
| 152 |
+
right: 0;
|
| 153 |
+
width: calc(100vw - 4rem);
|
| 154 |
+
max-width: 400px;
|
| 155 |
+
height: 70vh;
|
| 156 |
+
max-height: 580px;
|
| 157 |
+
background: rgba(17, 24, 39, 0.65);
|
| 158 |
+
backdrop-filter: blur(20px);
|
| 159 |
+
-webkit-backdrop-filter: blur(20px);
|
| 160 |
+
border: 1px solid rgba(6, 182, 212, 0.3);
|
| 161 |
+
border-radius: 1.5rem;
|
| 162 |
+
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.8), 0 0 20px rgba(6, 182, 212, 0.1) inset;
|
| 163 |
+
display: flex;
|
| 164 |
+
flex-direction: column;
|
| 165 |
+
overflow: hidden;
|
| 166 |
+
transition: opacity 0.4s cubic-bezier(0.19, 1, 0.22, 1), transform 0.4s cubic-bezier(0.19, 1, 0.22, 1);
|
| 167 |
+
opacity: 0;
|
| 168 |
+
transform: translateY(40px) scale(0.9) rotateX(-5deg);
|
| 169 |
+
pointer-events: none;
|
| 170 |
+
transform-origin: bottom right;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
@media (max-width: 640px) {
|
| 174 |
+
#chat-widget {
|
| 175 |
+
bottom: 1rem;
|
| 176 |
+
right: 1rem;
|
| 177 |
+
}
|
| 178 |
+
#chat-panel {
|
| 179 |
+
bottom: 80px;
|
| 180 |
+
width: calc(100vw - 2rem);
|
| 181 |
+
height: 75vh;
|
| 182 |
+
}
|
| 183 |
+
#chat-toggle {
|
| 184 |
+
width: 55px;
|
| 185 |
+
height: 55px;
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
#chat-panel.open {
|
| 190 |
+
opacity: 1;
|
| 191 |
+
transform: translateY(0) scale(1) rotateX(0deg);
|
| 192 |
+
pointer-events: auto;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
#chat-header {
|
| 196 |
+
padding: 1.25rem;
|
| 197 |
+
border-bottom: 1px solid rgba(6, 182, 212, 0.2);
|
| 198 |
+
display: flex;
|
| 199 |
+
justify-content: space-between;
|
| 200 |
+
align-items: center;
|
| 201 |
+
background: linear-gradient(90deg, rgba(6, 182, 212, 0.15) 0%, rgba(17, 24, 39, 0) 100%);
|
| 202 |
+
position: relative;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
#chat-header::before {
|
| 206 |
+
content: '';
|
| 207 |
+
position: absolute;
|
| 208 |
+
top: 0;
|
| 209 |
+
left: 0;
|
| 210 |
+
width: 100%;
|
| 211 |
+
height: 2px;
|
| 212 |
+
background: linear-gradient(90deg, transparent, var(--primary-cyan), transparent);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
#chat-messages {
|
| 216 |
+
flex: 1;
|
| 217 |
+
padding: 1.5rem;
|
| 218 |
+
overflow-y: auto;
|
| 219 |
+
display: flex;
|
| 220 |
+
flex-direction: column;
|
| 221 |
+
gap: 1.25rem;
|
| 222 |
+
scrollbar-width: thin;
|
| 223 |
+
scrollbar-color: rgba(6, 182, 212, 0.5) transparent;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
#chat-messages::-webkit-scrollbar {
|
| 227 |
+
width: 6px;
|
| 228 |
+
}
|
| 229 |
+
#chat-messages::-webkit-scrollbar-track {
|
| 230 |
+
background: transparent;
|
| 231 |
+
}
|
| 232 |
+
#chat-messages::-webkit-scrollbar-thumb {
|
| 233 |
+
background-color: rgba(6, 182, 212, 0.5);
|
| 234 |
+
border-radius: 20px;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.chat-bubble {
|
| 238 |
+
max-width: 85%;
|
| 239 |
+
padding: 0.9rem 1.2rem;
|
| 240 |
+
border-radius: 1.2rem;
|
| 241 |
+
font-size: 0.95rem;
|
| 242 |
+
line-height: 1.5;
|
| 243 |
+
word-wrap: break-word;
|
| 244 |
+
animation: messageSlideIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
|
| 245 |
+
opacity: 0;
|
| 246 |
+
transform: translateY(10px);
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
@keyframes messageSlideIn {
|
| 250 |
+
to { opacity: 1; transform: translateY(0); }
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.chat-user {
|
| 254 |
+
align-self: flex-end;
|
| 255 |
+
background: linear-gradient(135deg, var(--primary-cyan), #0056b3);
|
| 256 |
+
color: #fff;
|
| 257 |
+
border-bottom-right-radius: 0.25rem;
|
| 258 |
+
box-shadow: 0 5px 15px rgba(6, 182, 212, 0.3);
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
.chat-bot {
|
| 262 |
+
align-self: flex-start;
|
| 263 |
+
background: rgba(255, 255, 255, 0.05);
|
| 264 |
+
color: var(--text-primary);
|
| 265 |
+
border-bottom-left-radius: 0.25rem;
|
| 266 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 267 |
+
backdrop-filter: blur(10px);
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
#chat-input-area {
|
| 271 |
+
padding: 1.25rem;
|
| 272 |
+
border-top: 1px solid rgba(6, 182, 212, 0.2);
|
| 273 |
+
display: flex;
|
| 274 |
+
gap: 0.75rem;
|
| 275 |
+
background: rgba(0, 0, 0, 0.2);
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
#chat-input {
|
| 279 |
+
flex: 1;
|
| 280 |
+
background-color: rgba(0, 0, 0, 0.4);
|
| 281 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 282 |
+
color: white;
|
| 283 |
+
padding: 0.75rem 1.25rem;
|
| 284 |
+
border-radius: 2rem;
|
| 285 |
+
outline: none;
|
| 286 |
+
transition: all 0.3s ease;
|
| 287 |
+
font-size: 0.95rem;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
#chat-input:focus {
|
| 291 |
+
border-color: var(--primary-cyan);
|
| 292 |
+
background-color: rgba(6, 182, 212, 0.05);
|
| 293 |
+
box-shadow: 0 0 0 2px rgba(6, 182, 212, 0.2);
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
#chat-send {
|
| 297 |
+
background: linear-gradient(135deg, var(--primary-cyan), #0891b2);
|
| 298 |
+
color: white;
|
| 299 |
+
border: none;
|
| 300 |
+
border-radius: 50%;
|
| 301 |
+
width: 46px;
|
| 302 |
+
height: 46px;
|
| 303 |
+
display: flex;
|
| 304 |
+
justify-content: center;
|
| 305 |
+
align-items: center;
|
| 306 |
+
cursor: pointer;
|
| 307 |
+
transition: all 0.2s;
|
| 308 |
+
box-shadow: 0 4px 10px rgba(6, 182, 212, 0.4);
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
#chat-send:active {
|
| 312 |
+
transform: scale(0.95);
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
/* Online indicator for bot */
|
| 316 |
+
.online-dot {
|
| 317 |
+
width: 8px;
|
| 318 |
+
height: 8px;
|
| 319 |
+
background-color: var(--success-emerald);
|
| 320 |
+
border-radius: 50%;
|
| 321 |
+
box-shadow: 0 0 8px var(--success-emerald);
|
| 322 |
+
animation: blink 2s infinite;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
| 326 |
+
|
| 327 |
+
/* Typing indicator */
|
| 328 |
+
.typing-indicator {
|
| 329 |
+
display: none;
|
| 330 |
+
align-self: flex-start;
|
| 331 |
+
background-color: #1f2937;
|
| 332 |
+
padding: 0.5rem 1rem;
|
| 333 |
+
border-radius: 1rem;
|
| 334 |
+
border-bottom-left-radius: 0.25rem;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
.typing-indicator span {
|
| 338 |
+
width: 6px;
|
| 339 |
+
height: 6px;
|
| 340 |
+
background-color: var(--text-muted);
|
| 341 |
+
border-radius: 50%;
|
| 342 |
+
display: inline-block;
|
| 343 |
+
margin: 0 2px;
|
| 344 |
+
animation: typing 1.4s infinite cubic-bezier(0.2, 0.8, 0.2, 1);
|
| 345 |
+
}
|
| 346 |
+
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
| 347 |
+
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
| 348 |
+
|
| 349 |
+
@keyframes typing {
|
| 350 |
+
0%, 100% { transform: scale(1); opacity: 0.5; }
|
| 351 |
+
50% { transform: scale(1.5); opacity: 1; }
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
/* Results */
|
| 355 |
+
.result-card {
|
| 356 |
+
animation: slideIn 0.5s ease forwards;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
@keyframes slideIn {
|
| 360 |
+
from { opacity: 0; transform: translateY(20px); }
|
| 361 |
+
to { opacity: 1; transform: translateY(0); }
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
/* Spinner */
|
| 365 |
+
.loader-spinner {
|
| 366 |
+
border: 3px solid rgba(255,255,255,0.1);
|
| 367 |
+
border-top-color: white;
|
| 368 |
+
border-radius: 50%;
|
| 369 |
+
width: 20px;
|
| 370 |
+
height: 20px;
|
| 371 |
+
animation: spin 1s linear infinite;
|
| 372 |
+
display: none;
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
| 376 |
+
|
| 377 |
+
/* New Hero Title Styles */
|
| 378 |
+
.hero-gradient-text-new {
|
| 379 |
+
background: linear-gradient(90deg, #06b6d4, #a855f7, #ffffff, #06b6d4);
|
| 380 |
+
background-size: 300% 100%;
|
| 381 |
+
-webkit-background-clip: text;
|
| 382 |
+
background-clip: text;
|
| 383 |
+
-webkit-text-fill-color: transparent;
|
| 384 |
+
animation: gradientSweep 6s linear infinite;
|
| 385 |
+
text-shadow: 0 0 20px rgba(6, 182, 212, 0.4), 0 0 40px rgba(6, 182, 212, 0.2);
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
@keyframes gradientSweep {
|
| 389 |
+
0% { background-position: 0% 50%; }
|
| 390 |
+
100% { background-position: 100% 50%; }
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
/* Typewriter Effect Container */
|
| 394 |
+
.typewriter-text {
|
| 395 |
+
overflow: hidden;
|
| 396 |
+
white-space: nowrap;
|
| 397 |
+
display: inline-block;
|
| 398 |
+
border-right: 3px solid rgba(255, 255, 255, 0.8);
|
| 399 |
+
animation: typingEffect 3s steps(40, end), blinkCursor 0.75s step-end infinite;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
@keyframes typingEffect {
|
| 403 |
+
from { width: 0; }
|
| 404 |
+
to { width: 100%; }
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
@keyframes blinkCursor {
|
| 408 |
+
from, to { border-color: transparent; }
|
| 409 |
+
50% { border-color: rgba(255, 255, 255, 0.8); }
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
@media (max-width: 1024px) {
|
| 413 |
+
.typewriter-text {
|
| 414 |
+
white-space: normal;
|
| 415 |
+
animation: none;
|
| 416 |
+
border-right: none;
|
| 417 |
+
width: 100%;
|
| 418 |
+
}
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
/* Line 2 Animation */
|
| 422 |
+
.delayed-fade-in {
|
| 423 |
+
opacity: 0;
|
| 424 |
+
transform: translateY(10px);
|
| 425 |
+
animation: fadeUpIn 1s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
|
| 426 |
+
animation-delay: 3s; /* Wait for typewriter to finish */
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
@keyframes fadeUpIn {
|
| 430 |
+
to { opacity: 1; transform: translateY(0); }
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.highlight-cyan-glow {
|
| 434 |
+
position: relative;
|
| 435 |
+
display: inline-block;
|
| 436 |
+
}
|
| 437 |
+
.highlight-cyan-glow::after {
|
| 438 |
+
content: '';
|
| 439 |
+
position: absolute;
|
| 440 |
+
bottom: 0px;
|
| 441 |
+
left: 0;
|
| 442 |
+
width: 100%;
|
| 443 |
+
height: 2px;
|
| 444 |
+
background-color: var(--primary-cyan);
|
| 445 |
+
box-shadow: 0 0 8px var(--primary-cyan), 0 0 12px var(--primary-cyan);
|
| 446 |
+
border-radius: 2px;
|
| 447 |
+
}
|
templates/base.html
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>{% block title %}PredictLab{% endblock %}</title>
|
| 7 |
+
<!-- Tailwind CSS CDN -->
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<!-- Custom CSS -->
|
| 10 |
+
<link rel="stylesheet" href="/static/style.css" />
|
| 11 |
+
<!-- Font Awesome -->
|
| 12 |
+
<link
|
| 13 |
+
rel="stylesheet"
|
| 14 |
+
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
| 15 |
+
/>
|
| 16 |
+
</head>
|
| 17 |
+
<body data-page="{% block page_name %}home{% endblock %}">
|
| 18 |
+
<!-- Sticky Navbar -->
|
| 19 |
+
<nav class="fixed top-0 w-full z-40 backdrop-blur-nav">
|
| 20 |
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 21 |
+
<div class="flex items-center justify-between h-16">
|
| 22 |
+
<div class="flex-shrink-0">
|
| 23 |
+
<a
|
| 24 |
+
href="/"
|
| 25 |
+
class="text-2xl font-bold display-font text-white flex items-center gap-2"
|
| 26 |
+
>
|
| 27 |
+
<i class="fa-solid fa-flask text-cyan-400"></i> PredictLab
|
| 28 |
+
</a>
|
| 29 |
+
</div>
|
| 30 |
+
<div class="hidden md:block">
|
| 31 |
+
<div class="ml-10 flex items-baseline space-x-4">
|
| 32 |
+
<a
|
| 33 |
+
href="/"
|
| 34 |
+
class="text-gray-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors"
|
| 35 |
+
>Home</a
|
| 36 |
+
>
|
| 37 |
+
<a
|
| 38 |
+
href="/diabetes"
|
| 39 |
+
class="text-gray-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors"
|
| 40 |
+
>Diabetes</a
|
| 41 |
+
>
|
| 42 |
+
<a
|
| 43 |
+
href="/stroke"
|
| 44 |
+
class="text-gray-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors"
|
| 45 |
+
>Stroke</a
|
| 46 |
+
>
|
| 47 |
+
<a
|
| 48 |
+
href="/heart"
|
| 49 |
+
class="text-gray-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors"
|
| 50 |
+
>Heart Disease</a
|
| 51 |
+
>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
</nav>
|
| 57 |
+
|
| 58 |
+
<!-- Main Content -->
|
| 59 |
+
<main class="pt-24 pb-12 min-h-screen flex flex-col items-center">
|
| 60 |
+
{% block content %}{% endblock %}
|
| 61 |
+
</main>
|
| 62 |
+
|
| 63 |
+
<!-- Footer -->
|
| 64 |
+
<footer
|
| 65 |
+
class="border-t border-gray-800 py-8 text-center text-gray-500 text-sm mt-auto w-full"
|
| 66 |
+
>
|
| 67 |
+
Built with FastAPI + Groq Llama 3 + scikit-learn
|
| 68 |
+
</footer>
|
| 69 |
+
|
| 70 |
+
<!-- Chatbot Widget -->
|
| 71 |
+
<div id="chat-widget" class="z-50">
|
| 72 |
+
<!-- Toggle Button -->
|
| 73 |
+
<button
|
| 74 |
+
id="chat-toggle"
|
| 75 |
+
class="fixed bottom-4 right-4 sm:bottom-6 sm:right-6 w-16 h-16 rounded-full bg-gradient-to-br from-cyan-400 to-blue-600 flex items-center justify-center text-white shadow-lg transition-transform hover:scale-110"
|
| 76 |
+
>
|
| 77 |
+
<i class="fa-solid fa-robot text-2xl"></i>
|
| 78 |
+
</button>
|
| 79 |
+
|
| 80 |
+
<!-- Chat Panel -->
|
| 81 |
+
<div
|
| 82 |
+
id="chat-panel"
|
| 83 |
+
class="fixed bottom-20 right-4 sm:bottom-24 sm:right-6 w-[90vw] max-w-md h-[70vh] sm:h-[80vh] bg-[rgba(17,24,39,0.65)] backdrop-blur-lg rounded-xl shadow-2xl flex flex-col opacity-0 pointer-events-none transition-all transform translate-y-10 sm:translate-y-0"
|
| 84 |
+
>
|
| 85 |
+
<div
|
| 86 |
+
id="chat-header"
|
| 87 |
+
class="flex justify-between items-center p-4 border-b border-cyan-500/20 bg-gradient-to-r from-cyan-500/20 to-transparent rounded-t-xl"
|
| 88 |
+
>
|
| 89 |
+
<div
|
| 90 |
+
class="font-bold display-font text-white flex items-center gap-3"
|
| 91 |
+
>
|
| 92 |
+
<div class="relative">
|
| 93 |
+
<i class="fa-solid fa-robot text-cyan-400 text-xl"></i>
|
| 94 |
+
<div class="absolute -bottom-1 -right-1 online-dot"></div>
|
| 95 |
+
</div>
|
| 96 |
+
<span>Llama 3 AI Assistant</span>
|
| 97 |
+
</div>
|
| 98 |
+
<button
|
| 99 |
+
id="close-chat"
|
| 100 |
+
class="text-gray-400 hover:text-white transition-colors"
|
| 101 |
+
>
|
| 102 |
+
<i class="fa-solid fa-xmark"></i>
|
| 103 |
+
</button>
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<div
|
| 107 |
+
id="chat-messages"
|
| 108 |
+
class="flex-1 overflow-y-auto p-3 flex flex-col gap-2"
|
| 109 |
+
>
|
| 110 |
+
<div class="chat-bubble chat-bot">
|
| 111 |
+
Hello! I'm here to help you understand your machine learning
|
| 112 |
+
predictions.
|
| 113 |
+
</div>
|
| 114 |
+
<div class="typing-indicator" id="typing-indicator">
|
| 115 |
+
<span></span><span></span><span></span>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
|
| 119 |
+
<div
|
| 120 |
+
id="chat-input-area"
|
| 121 |
+
class="flex gap-2 p-3 border-t border-cyan-500/20"
|
| 122 |
+
>
|
| 123 |
+
<input
|
| 124 |
+
type="text"
|
| 125 |
+
id="chat-input"
|
| 126 |
+
placeholder="Ask a question..."
|
| 127 |
+
autocomplete="off"
|
| 128 |
+
class="flex-1 rounded-full p-2 bg-[rgba(0,0,0,0.4)] text-white outline-none border border-gray-700 focus:border-cyan-400 focus:ring-1 focus:ring-cyan-400 transition-all"
|
| 129 |
+
/>
|
| 130 |
+
<button
|
| 131 |
+
id="chat-send"
|
| 132 |
+
class="w-12 h-12 rounded-full bg-gradient-to-br from-cyan-400 to-blue-600 flex items-center justify-center text-white shadow hover:scale-95 transition-transform"
|
| 133 |
+
>
|
| 134 |
+
<i class="fa-solid fa-paper-plane"></i>
|
| 135 |
+
</button>
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
|
| 140 |
+
<!-- Chatbot Logic -->
|
| 141 |
+
<script>
|
| 142 |
+
window.chatContext = {
|
| 143 |
+
current_page: document.body.dataset.page || "home",
|
| 144 |
+
inputs: {},
|
| 145 |
+
prediction: null,
|
| 146 |
+
};
|
| 147 |
+
|
| 148 |
+
const chatToggle = document.getElementById("chat-toggle");
|
| 149 |
+
const chatPanel = document.getElementById("chat-panel");
|
| 150 |
+
const closeChat = document.getElementById("close-chat");
|
| 151 |
+
const chatMessages = document.getElementById("chat-messages");
|
| 152 |
+
const chatInput = document.getElementById("chat-input");
|
| 153 |
+
const chatSend = document.getElementById("chat-send");
|
| 154 |
+
const typingIndicator = document.getElementById("typing-indicator");
|
| 155 |
+
|
| 156 |
+
function toggleChat() {
|
| 157 |
+
chatPanel.classList.toggle("open");
|
| 158 |
+
if (chatPanel.classList.contains("open")) chatInput.focus();
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
chatToggle.addEventListener("click", toggleChat);
|
| 162 |
+
closeChat.addEventListener("click", () =>
|
| 163 |
+
chatPanel.classList.remove("open"),
|
| 164 |
+
);
|
| 165 |
+
|
| 166 |
+
function appendMessage(sender, text) {
|
| 167 |
+
const div = document.createElement("div");
|
| 168 |
+
div.className = `chat-bubble ${sender === "user" ? "chat-user" : "chat-bot"}`;
|
| 169 |
+
div.textContent = text;
|
| 170 |
+
chatMessages.insertBefore(div, typingIndicator);
|
| 171 |
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
function showTypingIndicator() {
|
| 175 |
+
typingIndicator.style.display = "flex";
|
| 176 |
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
| 177 |
+
}
|
| 178 |
+
function hideTypingIndicator() {
|
| 179 |
+
typingIndicator.style.display = "none";
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
async function sendChatMessage(userMessage) {
|
| 183 |
+
if (!userMessage.trim()) return;
|
| 184 |
+
appendMessage("user", userMessage);
|
| 185 |
+
chatInput.value = "";
|
| 186 |
+
showTypingIndicator();
|
| 187 |
+
try {
|
| 188 |
+
const res = await fetch("/chat", {
|
| 189 |
+
method: "POST",
|
| 190 |
+
headers: { "Content-Type": "application/json" },
|
| 191 |
+
body: JSON.stringify({
|
| 192 |
+
message: userMessage,
|
| 193 |
+
context: window.chatContext,
|
| 194 |
+
}),
|
| 195 |
+
});
|
| 196 |
+
if (res.ok) {
|
| 197 |
+
const data = await res.json();
|
| 198 |
+
hideTypingIndicator();
|
| 199 |
+
appendMessage("bot", data.reply);
|
| 200 |
+
} else {
|
| 201 |
+
hideTypingIndicator();
|
| 202 |
+
appendMessage(
|
| 203 |
+
"bot",
|
| 204 |
+
"Sorry, I had trouble connecting to the network.",
|
| 205 |
+
);
|
| 206 |
+
}
|
| 207 |
+
} catch (err) {
|
| 208 |
+
hideTypingIndicator();
|
| 209 |
+
appendMessage("bot", "Error connecting to AI service.");
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
chatSend.addEventListener("click", () =>
|
| 214 |
+
sendChatMessage(chatInput.value),
|
| 215 |
+
);
|
| 216 |
+
chatInput.addEventListener("keypress", (e) => {
|
| 217 |
+
if (e.key === "Enter") sendChatMessage(chatInput.value);
|
| 218 |
+
});
|
| 219 |
+
|
| 220 |
+
window.onPredictionComplete = function (page, inputs, prediction) {
|
| 221 |
+
window.chatContext = { current_page: page, inputs, prediction };
|
| 222 |
+
sendChatMessage(
|
| 223 |
+
"I just got a prediction result. Can you explain what this means?",
|
| 224 |
+
);
|
| 225 |
+
};
|
| 226 |
+
</script>
|
| 227 |
+
|
| 228 |
+
{% block scripts %}{% endblock %}
|
| 229 |
+
</body>
|
| 230 |
+
</html>
|
templates/diabetes.html
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %} {% block title %}Diabetes Predictor - PredictLab{%
|
| 2 |
+
endblock %} {% block page_name %}diabetes{% endblock %} {% block content %}
|
| 3 |
+
<div class="w-full max-w-4xl mx-auto px-4 sm:px-6">
|
| 4 |
+
<div class="mb-10">
|
| 5 |
+
<h1 class="text-4xl font-bold mb-4 flex items-center gap-3">
|
| 6 |
+
<div
|
| 7 |
+
class="w-12 h-12 rounded-lg bg-cyan-900/30 text-cyan-400 flex items-center justify-center text-2xl"
|
| 8 |
+
>
|
| 9 |
+
<i class="fa-solid fa-stethoscope"></i>
|
| 10 |
+
</div>
|
| 11 |
+
Pima Diabetes Predictor
|
| 12 |
+
</h1>
|
| 13 |
+
<p class="text-gray-400">
|
| 14 |
+
Predicts the likelihood of diabetes in females of Pima Indian heritage
|
| 15 |
+
using a Support Vector Machine (Linear Kernel).
|
| 16 |
+
</p>
|
| 17 |
+
</div>
|
| 18 |
+
|
| 19 |
+
<!-- Form & Result Grid -->
|
| 20 |
+
<div class="grid grid-cols-1 lg:grid-cols-5 gap-8">
|
| 21 |
+
<!-- Form Section -->
|
| 22 |
+
<div
|
| 23 |
+
class="lg:col-span-3 bg-[#111827] border border-[#1f2937] rounded-2xl p-6 sm:p-8 relative"
|
| 24 |
+
>
|
| 25 |
+
<form id="prediction-form" class="space-y-6">
|
| 26 |
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
| 27 |
+
{% for field in [ {'label':'Pregnancies
|
| 28 |
+
(0-17)','id':'pregnancies','min':0,'max':25,'step':1,'placeholder':'2'},
|
| 29 |
+
{'label':'Glucose
|
| 30 |
+
(0-200)','id':'glucose','min':0,'max':300,'placeholder':'120'},
|
| 31 |
+
{'label':'Blood Pressure
|
| 32 |
+
(0-122)','id':'blood_pressure','min':0,'max':200,'placeholder':'70'},
|
| 33 |
+
{'label':'Skin Thickness
|
| 34 |
+
(0-99)','id':'skin_thickness','min':0,'max':150,'placeholder':'20'},
|
| 35 |
+
{'label':'Insulin
|
| 36 |
+
(0-846)','id':'insulin','min':0,'max':1000,'placeholder':'80'},
|
| 37 |
+
{'label':'BMI
|
| 38 |
+
(0.0-67.1)','id':'bmi','min':0,'max':100,'step':0.1,'placeholder':'32.0'},
|
| 39 |
+
{'label':'Diabetes Pedigree
|
| 40 |
+
(0.0-2.5)','id':'dpf','min':0,'max':5,'step':0.01,'placeholder':'0.5'},
|
| 41 |
+
{'label':'Age
|
| 42 |
+
(21-81)','id':'age','min':1,'max':120,'placeholder':'45'} ] %}
|
| 43 |
+
<div>
|
| 44 |
+
<label class="block text-sm font-medium text-gray-400 mb-2"
|
| 45 |
+
>{{ field.label }}</label
|
| 46 |
+
>
|
| 47 |
+
<input
|
| 48 |
+
type="number"
|
| 49 |
+
id="{{ field.id }}"
|
| 50 |
+
required
|
| 51 |
+
min="{{ field.min }}"
|
| 52 |
+
max="{{ field.max }}"
|
| 53 |
+
{%
|
| 54 |
+
if
|
| 55 |
+
field.step
|
| 56 |
+
%}
|
| 57 |
+
step="{{ field.step }}"
|
| 58 |
+
{%
|
| 59 |
+
endif
|
| 60 |
+
%}
|
| 61 |
+
class="w-full floating-input rounded-xl p-3"
|
| 62 |
+
placeholder="{{ field.placeholder }}"
|
| 63 |
+
/>
|
| 64 |
+
</div>
|
| 65 |
+
{% endfor %}
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
<div class="pt-4">
|
| 69 |
+
<button
|
| 70 |
+
type="submit"
|
| 71 |
+
class="w-full btn-primary py-4 rounded-xl font-bold flex items-center justify-center gap-3 text-lg"
|
| 72 |
+
>
|
| 73 |
+
<span id="btn-text">Run Prediction</span>
|
| 74 |
+
<div id="btn-spinner" class="loader-spinner"></div>
|
| 75 |
+
</button>
|
| 76 |
+
</div>
|
| 77 |
+
</form>
|
| 78 |
+
</div>
|
| 79 |
+
|
| 80 |
+
<!-- Result Section -->
|
| 81 |
+
<div class="lg:col-span-2 flex flex-col gap-4">
|
| 82 |
+
<div
|
| 83 |
+
id="result-container"
|
| 84 |
+
class="hidden flex-1 flex flex-col items-center justify-center bg-[#111827] border border-[#1f2937] rounded-2xl p-8 result-card text-center"
|
| 85 |
+
>
|
| 86 |
+
<!-- JS Injected -->
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
<div
|
| 90 |
+
id="placeholder-container"
|
| 91 |
+
class="flex-1 flex flex-col items-center justify-center bg-[#111827]/50 border border-dashed border-[#1f2937] rounded-2xl p-8 text-center"
|
| 92 |
+
>
|
| 93 |
+
<i class="fa-solid fa-chart-bar text-4xl text-gray-600 mb-4"></i>
|
| 94 |
+
<h3 class="text-xl font-bold text-gray-500 mb-2">Awaiting Data</h3>
|
| 95 |
+
<p class="text-sm text-gray-600">
|
| 96 |
+
Fill out the form to generate a prediction.
|
| 97 |
+
</p>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
{% endblock %} {% block scripts %}
|
| 103 |
+
<script>
|
| 104 |
+
document
|
| 105 |
+
.getElementById("prediction-form")
|
| 106 |
+
.addEventListener("submit", async (e) => {
|
| 107 |
+
e.preventDefault();
|
| 108 |
+
|
| 109 |
+
const btnText = document.getElementById("btn-text");
|
| 110 |
+
const btnSpinner = document.getElementById("btn-spinner");
|
| 111 |
+
const submitBtn = e.target.querySelector('button[type="submit"]');
|
| 112 |
+
const resContainer = document.getElementById("result-container");
|
| 113 |
+
const placeholder = document.getElementById("placeholder-container");
|
| 114 |
+
|
| 115 |
+
// Collect inputs dynamically
|
| 116 |
+
const inputs = [
|
| 117 |
+
"pregnancies",
|
| 118 |
+
"glucose",
|
| 119 |
+
"blood_pressure",
|
| 120 |
+
"skin_thickness",
|
| 121 |
+
"insulin",
|
| 122 |
+
"bmi",
|
| 123 |
+
"dpf",
|
| 124 |
+
"age",
|
| 125 |
+
];
|
| 126 |
+
const data = {};
|
| 127 |
+
inputs.forEach((id) => (data[id] = document.getElementById(id).value));
|
| 128 |
+
|
| 129 |
+
// UI Loading
|
| 130 |
+
btnText.textContent = "Processing...";
|
| 131 |
+
btnSpinner.style.display = "block";
|
| 132 |
+
submitBtn.disabled = true;
|
| 133 |
+
resContainer.classList.add("hidden");
|
| 134 |
+
placeholder.classList.remove("hidden");
|
| 135 |
+
|
| 136 |
+
try {
|
| 137 |
+
const response = await fetch("/predict/diabetes", {
|
| 138 |
+
method: "POST",
|
| 139 |
+
headers: { "Content-Type": "application/json" },
|
| 140 |
+
body: JSON.stringify(data),
|
| 141 |
+
});
|
| 142 |
+
const result = await response.json();
|
| 143 |
+
|
| 144 |
+
placeholder.classList.add("hidden");
|
| 145 |
+
resContainer.classList.remove("hidden");
|
| 146 |
+
|
| 147 |
+
if (response.ok) {
|
| 148 |
+
const isRisk = result.label === "Diabetic";
|
| 149 |
+
const colorClass = isRisk ? "text-rose-500" : "text-emerald-500";
|
| 150 |
+
const bgClass = isRisk ? "bg-rose-500/10" : "bg-emerald-500/10";
|
| 151 |
+
const borderClass = isRisk
|
| 152 |
+
? "border-rose-500/30"
|
| 153 |
+
: "border-emerald-500/30";
|
| 154 |
+
const icon = isRisk ? "fa-triangle-exclamation" : "fa-shield-heart";
|
| 155 |
+
|
| 156 |
+
resContainer.className = `flex-1 flex flex-col items-center justify-center border-2 rounded-2xl p-8 result-card text-center ${bgClass} ${borderClass}`;
|
| 157 |
+
resContainer.innerHTML = `
|
| 158 |
+
<div class="w-20 h-20 rounded-full flex items-center justify-center mb-6 bg-[#111827] border ${borderClass}">
|
| 159 |
+
<i class="fa-solid ${icon} text-4xl ${colorClass}"></i>
|
| 160 |
+
</div>
|
| 161 |
+
<div class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-2">Prediction Result</div>
|
| 162 |
+
<h2 class="text-3xl font-extrabold ${colorClass} mb-4">${result.label}</h2>
|
| 163 |
+
<p class="text-sm text-gray-400">Our AI assistant will explain what this indicates.</p>
|
| 164 |
+
`;
|
| 165 |
+
|
| 166 |
+
if (window.onPredictionComplete) {
|
| 167 |
+
window.onPredictionComplete("diabetes", data, result.label);
|
| 168 |
+
}
|
| 169 |
+
} else {
|
| 170 |
+
resContainer.className =
|
| 171 |
+
"flex-1 flex flex-col items-center justify-center bg-gray-900 border-2 border-gray-700 rounded-2xl p-8 result-card text-center";
|
| 172 |
+
resContainer.innerHTML = `
|
| 173 |
+
<div class="text-gray-400 mb-4"><i class="fa-solid fa-circle-exclamation text-4xl"></i></div>
|
| 174 |
+
<h2 class="text-xl font-bold text-white mb-2">Prediction Unavailable</h2>
|
| 175 |
+
<p class="text-gray-400 text-sm">${result.error || "An error occurred."}</p>
|
| 176 |
+
`;
|
| 177 |
+
}
|
| 178 |
+
} catch (error) {
|
| 179 |
+
placeholder.classList.add("hidden");
|
| 180 |
+
resContainer.classList.remove("hidden");
|
| 181 |
+
resContainer.className =
|
| 182 |
+
"flex-1 flex flex-col items-center justify-center bg-rose-900/10 border-2 border-rose-900/30 rounded-2xl p-8 result-card text-center";
|
| 183 |
+
resContainer.innerHTML = `<h2 class="text-xl font-bold text-rose-500 mb-2">Network Error</h2><p class="text-gray-400 text-sm">Could not connect to the server.</p>`;
|
| 184 |
+
} finally {
|
| 185 |
+
btnText.textContent = "Run Prediction";
|
| 186 |
+
btnSpinner.style.display = "none";
|
| 187 |
+
submitBtn.disabled = false;
|
| 188 |
+
}
|
| 189 |
+
});
|
| 190 |
+
</script>
|
| 191 |
+
{% endblock %}
|
templates/heart.html
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %} {% block title %}Heart Disease Predictor -
|
| 2 |
+
PredictLab{% endblock %} {% block page_name %}heart{% endblock %} {% block
|
| 3 |
+
content %}
|
| 4 |
+
<div class="w-full max-w-5xl mx-auto px-4 sm:px-6">
|
| 5 |
+
<div class="mb-10 text-center">
|
| 6 |
+
<h1 class="text-4xl font-bold mb-4 flex items-center justify-center gap-3">
|
| 7 |
+
<div
|
| 8 |
+
class="w-12 h-12 rounded-lg bg-pink-900/30 text-pink-500 flex items-center justify-center text-2xl"
|
| 9 |
+
>
|
| 10 |
+
<i class="fa-solid fa-heart-pulse"></i>
|
| 11 |
+
</div>
|
| 12 |
+
Heart Disease Predictor
|
| 13 |
+
</h1>
|
| 14 |
+
<p class="text-gray-400 max-w-2xl mx-auto">
|
| 15 |
+
Calculates the likelihood of heart disease using a logistic regression
|
| 16 |
+
model trained on comprehensive biometrics.
|
| 17 |
+
</p>
|
| 18 |
+
</div>
|
| 19 |
+
|
| 20 |
+
<div
|
| 21 |
+
class="bg-[#111827] border border-[#1f2937] rounded-2xl p-6 sm:p-10 mb-8 relative shadow-2xl"
|
| 22 |
+
>
|
| 23 |
+
<form id="prediction-form" class="space-y-6">
|
| 24 |
+
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
| 25 |
+
{% for field in [
|
| 26 |
+
{'label':'Age','id':'age','type':'number','min':1,'max':120,'step':1,'placeholder':'52'},
|
| 27 |
+
{'label':'Sex','id':'sex','type':'select','options':[{'label':'Male','value':'1'},{'label':'Female','value':'0'}]},
|
| 28 |
+
{'label':'Chest Pain Type
|
| 29 |
+
(0-3)','id':'cp','type':'select','options':[{'label':'0: Typical
|
| 30 |
+
Angina','value':'0'},{'label':'1: Atypical
|
| 31 |
+
Angina','value':'1'},{'label':'2: Non-anginal
|
| 32 |
+
pain','value':'2'},{'label':'3: Asymptomatic','value':'3'}]},
|
| 33 |
+
{'label':'Resting Blood
|
| 34 |
+
Pressure','id':'trestbps','type':'number','min':50,'max':250,'placeholder':'125'},
|
| 35 |
+
{'label':'Cholesterol
|
| 36 |
+
(mg/dl)','id':'chol','type':'number','min':100,'max':600,'placeholder':'212'},
|
| 37 |
+
{'label':'Fasting Blood Sugar >
|
| 38 |
+
120','id':'fbs','type':'select','options':[{'label':'False','value':'0'},{'label':'True','value':'1'}]},
|
| 39 |
+
{'label':'Resting ECG
|
| 40 |
+
(0-2)','id':'restecg','type':'select','options':[{'label':'0:
|
| 41 |
+
Normal','value':'0'},{'label':'1: ST-T wave
|
| 42 |
+
alt','value':'1'},{'label':'2: LV Hypertrophy','value':'2'}]},
|
| 43 |
+
{'label':'Maximum Heart
|
| 44 |
+
Rate','id':'thalach','type':'number','min':50,'max':250,'placeholder':'168'},
|
| 45 |
+
{'label':'Exercise Induced
|
| 46 |
+
Angina','id':'exang','type':'select','options':[{'label':'No','value':'0'},{'label':'Yes','value':'1'}]},
|
| 47 |
+
{'label':'ST Depression
|
| 48 |
+
(Oldpeak)','id':'oldpeak','type':'number','min':0,'max':10,'step':0.1,'placeholder':'1.0'},
|
| 49 |
+
{'label':'Major Vessels
|
| 50 |
+
(0-4)','id':'ca','type':'number','min':0,'max':4,'step':1,'placeholder':'2'},
|
| 51 |
+
{'label':'Thalassemia
|
| 52 |
+
(0-3)','id':'thal','type':'select','options':[{'label':'1:
|
| 53 |
+
Normal','value':'1'},{'label':'2: Fixed
|
| 54 |
+
defect','value':'2'},{'label':'3: Reversable
|
| 55 |
+
defect','value':'3'},{'label':'0: Other','value':'0'}]} ] %}
|
| 56 |
+
<div>
|
| 57 |
+
<label class="block text-sm font-medium text-gray-400 mb-2"
|
| 58 |
+
>{{ field.label }}</label
|
| 59 |
+
>
|
| 60 |
+
{% if field.type == 'number' %}
|
| 61 |
+
<input
|
| 62 |
+
type="number"
|
| 63 |
+
id="{{ field.id }}"
|
| 64 |
+
required
|
| 65 |
+
min="{{ field.min | default('') }}"
|
| 66 |
+
max="{{ field.max | default('') }}"
|
| 67 |
+
{%
|
| 68 |
+
if
|
| 69 |
+
field.step
|
| 70 |
+
%}
|
| 71 |
+
step="{{ field.step }}"
|
| 72 |
+
{%
|
| 73 |
+
endif
|
| 74 |
+
%}
|
| 75 |
+
class="w-full floating-input rounded-xl p-3"
|
| 76 |
+
placeholder="{{ field.placeholder | default('') }}"
|
| 77 |
+
/>
|
| 78 |
+
{% elif field.type == 'select' %}
|
| 79 |
+
<select
|
| 80 |
+
id="{{ field.id }}"
|
| 81 |
+
required
|
| 82 |
+
class="w-full floating-input rounded-xl p-3 appearance-none"
|
| 83 |
+
>
|
| 84 |
+
{% for option in field.options %}
|
| 85 |
+
<option value="{{ option.value }}">{{ option.label }}</option>
|
| 86 |
+
{% endfor %}
|
| 87 |
+
</select>
|
| 88 |
+
{% endif %}
|
| 89 |
+
</div>
|
| 90 |
+
{% endfor %}
|
| 91 |
+
</div>
|
| 92 |
+
|
| 93 |
+
<div class="pt-8">
|
| 94 |
+
<button
|
| 95 |
+
type="submit"
|
| 96 |
+
class="w-full md:w-auto md:px-16 btn-primary py-4 rounded-xl font-bold flex items-center justify-center gap-3 text-lg mx-auto"
|
| 97 |
+
>
|
| 98 |
+
<span id="btn-text">Detect Anomalies</span>
|
| 99 |
+
<div id="btn-spinner" class="loader-spinner"></div>
|
| 100 |
+
</button>
|
| 101 |
+
</div>
|
| 102 |
+
</form>
|
| 103 |
+
</div>
|
| 104 |
+
|
| 105 |
+
<!-- Result -->
|
| 106 |
+
<div id="result-container" class="hidden flex flex-col gap-4">
|
| 107 |
+
<!-- Injected via JS -->
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
{% endblock %} {% block scripts %}
|
| 111 |
+
<script>
|
| 112 |
+
document
|
| 113 |
+
.getElementById("prediction-form")
|
| 114 |
+
.addEventListener("submit", async (e) => {
|
| 115 |
+
e.preventDefault();
|
| 116 |
+
|
| 117 |
+
const btnText = document.getElementById("btn-text");
|
| 118 |
+
const btnSpinner = document.getElementById("btn-spinner");
|
| 119 |
+
const submitBtn = e.target.querySelector('button[type="submit"]');
|
| 120 |
+
const resContainer = document.getElementById("result-container");
|
| 121 |
+
|
| 122 |
+
// Collect all inputs dynamically
|
| 123 |
+
const inputs = [
|
| 124 |
+
"age",
|
| 125 |
+
"sex",
|
| 126 |
+
"cp",
|
| 127 |
+
"trestbps",
|
| 128 |
+
"chol",
|
| 129 |
+
"fbs",
|
| 130 |
+
"restecg",
|
| 131 |
+
"thalach",
|
| 132 |
+
"exang",
|
| 133 |
+
"oldpeak",
|
| 134 |
+
"ca",
|
| 135 |
+
"thal",
|
| 136 |
+
];
|
| 137 |
+
const data = {};
|
| 138 |
+
inputs.forEach((id) => (data[id] = document.getElementById(id).value));
|
| 139 |
+
|
| 140 |
+
// UI Loading
|
| 141 |
+
btnText.textContent = "Processing...";
|
| 142 |
+
btnSpinner.style.display = "block";
|
| 143 |
+
submitBtn.disabled = true;
|
| 144 |
+
resContainer.classList.add("hidden");
|
| 145 |
+
|
| 146 |
+
try {
|
| 147 |
+
const response = await fetch("/predict/heart", {
|
| 148 |
+
method: "POST",
|
| 149 |
+
headers: { "Content-Type": "application/json" },
|
| 150 |
+
body: JSON.stringify(data),
|
| 151 |
+
});
|
| 152 |
+
const result = await response.json();
|
| 153 |
+
|
| 154 |
+
resContainer.classList.remove("hidden");
|
| 155 |
+
|
| 156 |
+
if (response.ok) {
|
| 157 |
+
const isRisk = result.prediction === 1;
|
| 158 |
+
const colorClass = isRisk ? "text-pink-500" : "text-emerald-500";
|
| 159 |
+
const bgClass = isRisk ? "bg-pink-500/10" : "bg-emerald-500/10";
|
| 160 |
+
const borderClass = isRisk
|
| 161 |
+
? "border-pink-500/30"
|
| 162 |
+
: "border-emerald-500/30";
|
| 163 |
+
const icon = isRisk ? "fa-heart-crack" : "fa-heart-circle-check";
|
| 164 |
+
|
| 165 |
+
resContainer.innerHTML = `
|
| 166 |
+
<div class="w-full rounded-2xl p-8 result-card border-2 flex items-center justify-between ${bgClass} ${borderClass} shadow-xl backdrop-blur-sm">
|
| 167 |
+
<div class="flex items-center gap-6">
|
| 168 |
+
<div class="w-16 h-16 rounded-full flex items-center justify-center bg-[#0a0f1e] border ${borderClass}">
|
| 169 |
+
<i class="fa-solid ${icon} text-3xl ${colorClass}"></i>
|
| 170 |
+
</div>
|
| 171 |
+
<div>
|
| 172 |
+
<div class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-1">Diagnostic Complete</div>
|
| 173 |
+
<h2 class="text-3xl font-extrabold ${colorClass}">${result.label}</h2>
|
| 174 |
+
</div>
|
| 175 |
+
</div>
|
| 176 |
+
<div class="text-right hidden sm:block">
|
| 177 |
+
<p class="text-sm text-gray-400 max-w-xs">Analysis generated by Logistic Regression parameters. Open Chatbot for breakdown.</p>
|
| 178 |
+
</div>
|
| 179 |
+
</div>
|
| 180 |
+
`;
|
| 181 |
+
|
| 182 |
+
if (window.onPredictionComplete) {
|
| 183 |
+
window.onPredictionComplete("heart", data, result.label);
|
| 184 |
+
}
|
| 185 |
+
} else {
|
| 186 |
+
resContainer.innerHTML = `
|
| 187 |
+
<div class="w-full rounded-2xl p-6 bg-[#0a0f1e] border border-gray-700 result-card text-center shadow-lg">
|
| 188 |
+
<h2 class="text-lg font-bold text-white mb-2">Model Offline</h2>
|
| 189 |
+
<p class="text-gray-400 text-sm">${result.error || "An error occurred."}</p>
|
| 190 |
+
</div>
|
| 191 |
+
`;
|
| 192 |
+
}
|
| 193 |
+
} catch (error) {
|
| 194 |
+
resContainer.classList.remove("hidden");
|
| 195 |
+
resContainer.innerHTML = `
|
| 196 |
+
<div class="w-full rounded-2xl p-6 bg-rose-900/10 border border-rose-900/30 result-card text-center">
|
| 197 |
+
<h2 class="text-lg font-bold text-pink-500">Error</h2>
|
| 198 |
+
<p class="text-gray-400">Network connection failed.</p>
|
| 199 |
+
</div>
|
| 200 |
+
`;
|
| 201 |
+
} finally {
|
| 202 |
+
btnText.textContent = "Detect Anomalies";
|
| 203 |
+
btnSpinner.style.display = "none";
|
| 204 |
+
submitBtn.disabled = false;
|
| 205 |
+
resContainer.scrollIntoView({ behavior: "smooth", block: "end" });
|
| 206 |
+
}
|
| 207 |
+
});
|
| 208 |
+
</script>
|
| 209 |
+
{% endblock %}
|
templates/index.html
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %} {% block title %}PredictLab - ML Portfolio{% endblock
|
| 2 |
+
%} {% block page_name %}home{% endblock %} {% block content %}
|
| 3 |
+
<div class="max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8">
|
| 4 |
+
<!-- Hero Section -->
|
| 5 |
+
<div class="text-center py-16 mb-8 flex flex-col items-center">
|
| 6 |
+
<h1
|
| 7 |
+
class="text-3xl sm:text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-extrabold hero-gradient-text-new mb-6 text-center mx-auto break-words"
|
| 8 |
+
>
|
| 9 |
+
From raw data to real insights.
|
| 10 |
+
</h1>
|
| 11 |
+
<p
|
| 12 |
+
class="text-lg sm:text-xl text-gray-400 w-full sm:max-w-3xl mx-auto mt-2"
|
| 13 |
+
>
|
| 14 |
+
Clinical-grade health risk predictions — enhanced with
|
| 15 |
+
<span class="highlight-cyan-glow font-semibold text-white"
|
| 16 |
+
>Llama 3 AI intelligence</span
|
| 17 |
+
>.
|
| 18 |
+
</p>
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
<!-- Models Grid -->
|
| 22 |
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
| 23 |
+
<!-- Diabetes Card -->
|
| 24 |
+
<div
|
| 25 |
+
class="glass-card card-hover flex flex-col h-full rounded-2xl p-6 shadow-lg"
|
| 26 |
+
>
|
| 27 |
+
<div class="flex items-center justify-between mb-4">
|
| 28 |
+
<div
|
| 29 |
+
class="w-12 h-12 rounded-lg bg-cyan-900/30 text-cyan-400 flex items-center justify-center text-2xl"
|
| 30 |
+
>
|
| 31 |
+
<i class="fa-solid fa-stethoscope"></i>
|
| 32 |
+
</div>
|
| 33 |
+
<span
|
| 34 |
+
class="bg-emerald-900/30 text-emerald-400 text-xs font-bold px-3 py-1 rounded-full border border-emerald-800/50"
|
| 35 |
+
>
|
| 36 |
+
89% Accuracy
|
| 37 |
+
</span>
|
| 38 |
+
</div>
|
| 39 |
+
<h3 class="text-2xl font-bold mb-2 text-white">Diabetes Scan</h3>
|
| 40 |
+
<p class="text-sm text-cyan-400 font-mono mb-4">
|
| 41 |
+
<span class="text-amber-400">⚡</span> SVM · Linear Kernel
|
| 42 |
+
</p>
|
| 43 |
+
<p class="text-gray-400 text-sm flex-grow mb-6">
|
| 44 |
+
Enter your health metrics and instantly know your diabetes risk —
|
| 45 |
+
powered by an SVM trained on real clinical data.
|
| 46 |
+
</p>
|
| 47 |
+
<a
|
| 48 |
+
href="/diabetes"
|
| 49 |
+
class="mt-auto w-full btn-primary text-center py-3 rounded-xl font-bold"
|
| 50 |
+
>Try Model →</a
|
| 51 |
+
>
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<!-- Stroke Risk Card -->
|
| 55 |
+
<div
|
| 56 |
+
class="bg-[#111827] border border-[#1f2937] rounded-2xl p-6 card-hover flex flex-col h-full shadow-2xl"
|
| 57 |
+
>
|
| 58 |
+
<div class="flex items-center justify-between mb-4">
|
| 59 |
+
<div
|
| 60 |
+
class="w-12 h-12 rounded-lg bg-purple-900/30 text-purple-500 flex items-center justify-center text-2xl"
|
| 61 |
+
>
|
| 62 |
+
<i class="fa-solid fa-brain"></i>
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
<h3 class="text-2xl font-bold mb-2">Stroke Risk</h3>
|
| 66 |
+
<p class="text-sm text-cyan-400 font-mono mb-4">
|
| 67 |
+
🌲 Random Forest Ensemble • 88.3% Accuracy
|
| 68 |
+
</p>
|
| 69 |
+
<p class="text-gray-400 text-sm flex-grow mb-6">
|
| 70 |
+
Assess your stroke risk in seconds. Trained on real clinical data across
|
| 71 |
+
demographics, lifestyle, and medical history.
|
| 72 |
+
</p>
|
| 73 |
+
<a
|
| 74 |
+
href="/stroke"
|
| 75 |
+
class="mt-auto w-full btn-primary text-center py-3 rounded-xl font-bold"
|
| 76 |
+
>Try Model →</a
|
| 77 |
+
>
|
| 78 |
+
</div>
|
| 79 |
+
|
| 80 |
+
<!-- Heart Disease Card -->
|
| 81 |
+
<div
|
| 82 |
+
class="bg-[#111827] border border-[#1f2937] rounded-2xl p-6 card-hover flex flex-col h-full shadow-2xl"
|
| 83 |
+
>
|
| 84 |
+
<div class="flex items-center justify-between mb-4">
|
| 85 |
+
<div
|
| 86 |
+
class="w-12 h-12 rounded-lg bg-pink-900/30 text-pink-500 flex items-center justify-center text-2xl"
|
| 87 |
+
>
|
| 88 |
+
<i class="fa-solid fa-heart-pulse"></i>
|
| 89 |
+
</div>
|
| 90 |
+
</div>
|
| 91 |
+
<h3 class="text-2xl font-bold mb-2">CardioScan</h3>
|
| 92 |
+
<p class="text-sm text-cyan-400 font-mono mb-4">
|
| 93 |
+
📈 Logistic Regression • 91% Accuracy
|
| 94 |
+
</p>
|
| 95 |
+
<p class="text-gray-400 text-sm flex-grow mb-6">
|
| 96 |
+
Enter your cardiac test results for an instant heart disease risk
|
| 97 |
+
assessment — powered by Logistic Regression trained on real clinical
|
| 98 |
+
data across 13 key markers.
|
| 99 |
+
</p>
|
| 100 |
+
<a
|
| 101 |
+
href="/heart"
|
| 102 |
+
class="mt-auto w-full btn-primary text-center py-3 rounded-xl font-bold"
|
| 103 |
+
>Try Model →</a
|
| 104 |
+
>
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
{% endblock %}
|
templates/stroke.html
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %} {% block title %}Stroke Risk Predictor - PredictLab{%
|
| 2 |
+
endblock %} {% block page_name %}stroke{% endblock %} {% block content %}
|
| 3 |
+
<div class="w-full max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 4 |
+
<div class="mb-10 text-center">
|
| 5 |
+
<h1
|
| 6 |
+
class="text-4xl sm:text-5xl md:text-6xl font-bold mb-4 flex items-center justify-center gap-3 flex-wrap"
|
| 7 |
+
>
|
| 8 |
+
<div
|
| 9 |
+
class="w-12 h-12 rounded-lg bg-red-900/30 text-red-500 flex items-center justify-center text-2xl"
|
| 10 |
+
>
|
| 11 |
+
<i class="fa-solid fa-brain"></i>
|
| 12 |
+
</div>
|
| 13 |
+
Stroke Risk Predictor
|
| 14 |
+
</h1>
|
| 15 |
+
<p class="text-gray-400 max-w-full sm:max-w-2xl mx-auto">
|
| 16 |
+
Classifies stroke probability based on demographics and clinical features
|
| 17 |
+
using a Random Forest model.
|
| 18 |
+
</p>
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
<!-- Form Section -->
|
| 22 |
+
<div
|
| 23 |
+
class="bg-[#111827] border border-[#1f2937] rounded-2xl p-6 sm:p-10 mb-8 relative shadow-xl"
|
| 24 |
+
>
|
| 25 |
+
<form
|
| 26 |
+
id="prediction-form"
|
| 27 |
+
class="space-y-6 max-w-full lg:max-w-3xl mx-auto"
|
| 28 |
+
>
|
| 29 |
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 30 |
+
<div>
|
| 31 |
+
<label class="block text-sm font-medium text-gray-400 mb-2"
|
| 32 |
+
>Age</label
|
| 33 |
+
>
|
| 34 |
+
<input
|
| 35 |
+
type="number"
|
| 36 |
+
id="age"
|
| 37 |
+
required
|
| 38 |
+
min="0"
|
| 39 |
+
max="120"
|
| 40 |
+
step="0.1"
|
| 41 |
+
class="w-full floating-input rounded-xl p-3"
|
| 42 |
+
placeholder="e.g. 55"
|
| 43 |
+
/>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<div>
|
| 47 |
+
<label class="block text-sm font-medium text-gray-400 mb-2"
|
| 48 |
+
>Average Glucose Level</label
|
| 49 |
+
>
|
| 50 |
+
<input
|
| 51 |
+
type="number"
|
| 52 |
+
id="avg_glucose_level"
|
| 53 |
+
required
|
| 54 |
+
min="0"
|
| 55 |
+
max="300"
|
| 56 |
+
step="0.1"
|
| 57 |
+
class="w-full floating-input rounded-xl p-3"
|
| 58 |
+
placeholder="e.g. 105.2"
|
| 59 |
+
/>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
<div>
|
| 63 |
+
<label class="block text-sm font-medium text-gray-400 mb-2"
|
| 64 |
+
>BMI</label
|
| 65 |
+
>
|
| 66 |
+
<input
|
| 67 |
+
type="number"
|
| 68 |
+
id="bmi"
|
| 69 |
+
required
|
| 70 |
+
min="0"
|
| 71 |
+
max="100"
|
| 72 |
+
step="0.1"
|
| 73 |
+
class="w-full floating-input rounded-xl p-3"
|
| 74 |
+
placeholder="e.g. 28.5"
|
| 75 |
+
/>
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
<div>
|
| 79 |
+
<label class="block text-sm font-medium text-gray-400 mb-2"
|
| 80 |
+
>Gender</label
|
| 81 |
+
>
|
| 82 |
+
<select
|
| 83 |
+
id="gender"
|
| 84 |
+
required
|
| 85 |
+
class="w-full floating-input rounded-xl p-3 appearance-none"
|
| 86 |
+
>
|
| 87 |
+
<option value="Female">Female</option>
|
| 88 |
+
<option value="Male">Male</option>
|
| 89 |
+
<option value="Other">Other</option>
|
| 90 |
+
</select>
|
| 91 |
+
</div>
|
| 92 |
+
|
| 93 |
+
<div>
|
| 94 |
+
<label class="block text-sm font-medium text-gray-400 mb-2"
|
| 95 |
+
>Hypertension</label
|
| 96 |
+
>
|
| 97 |
+
<select
|
| 98 |
+
id="hypertension"
|
| 99 |
+
required
|
| 100 |
+
class="w-full floating-input rounded-xl p-3 appearance-none"
|
| 101 |
+
>
|
| 102 |
+
<option value="0">No</option>
|
| 103 |
+
<option value="1">Yes</option>
|
| 104 |
+
</select>
|
| 105 |
+
</div>
|
| 106 |
+
|
| 107 |
+
<div>
|
| 108 |
+
<label class="block text-sm font-medium text-gray-400 mb-2"
|
| 109 |
+
>Heart Disease</label
|
| 110 |
+
>
|
| 111 |
+
<select
|
| 112 |
+
id="heart_disease"
|
| 113 |
+
required
|
| 114 |
+
class="w-full floating-input rounded-xl p-3 appearance-none"
|
| 115 |
+
>
|
| 116 |
+
<option value="0">No</option>
|
| 117 |
+
<option value="1">Yes</option>
|
| 118 |
+
</select>
|
| 119 |
+
</div>
|
| 120 |
+
|
| 121 |
+
<div>
|
| 122 |
+
<label class="block text-sm font-medium text-gray-400 mb-2"
|
| 123 |
+
>Ever Married</label
|
| 124 |
+
>
|
| 125 |
+
<select
|
| 126 |
+
id="ever_married"
|
| 127 |
+
required
|
| 128 |
+
class="w-full floating-input rounded-xl p-3 appearance-none"
|
| 129 |
+
>
|
| 130 |
+
<option value="No">No</option>
|
| 131 |
+
<option value="Yes">Yes</option>
|
| 132 |
+
</select>
|
| 133 |
+
</div>
|
| 134 |
+
|
| 135 |
+
<div>
|
| 136 |
+
<label class="block text-sm font-medium text-gray-400 mb-2"
|
| 137 |
+
>Work Type</label
|
| 138 |
+
>
|
| 139 |
+
<select
|
| 140 |
+
id="work_type"
|
| 141 |
+
required
|
| 142 |
+
class="w-full floating-input rounded-xl p-3 appearance-none"
|
| 143 |
+
>
|
| 144 |
+
<option value="Private">Private</option>
|
| 145 |
+
<option value="Self-employed">Self-employed</option>
|
| 146 |
+
<option value="Govt_job">Govt_job</option>
|
| 147 |
+
<option value="children">Children</option>
|
| 148 |
+
<option value="Never_worked">Never_worked</option>
|
| 149 |
+
</select>
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
<div>
|
| 153 |
+
<label class="block text-sm font-medium text-gray-400 mb-2"
|
| 154 |
+
>Residence Type</label
|
| 155 |
+
>
|
| 156 |
+
<select
|
| 157 |
+
id="residence_type"
|
| 158 |
+
required
|
| 159 |
+
class="w-full floating-input rounded-xl p-3 appearance-none"
|
| 160 |
+
>
|
| 161 |
+
<option value="Rural">Rural</option>
|
| 162 |
+
<option value="Urban">Urban</option>
|
| 163 |
+
</select>
|
| 164 |
+
</div>
|
| 165 |
+
|
| 166 |
+
<div class="md:col-span-2 lg:col-span-3">
|
| 167 |
+
<label class="block text-sm font-medium text-gray-400 mb-2"
|
| 168 |
+
>Smoking Status</label
|
| 169 |
+
>
|
| 170 |
+
<select
|
| 171 |
+
id="smoking_status"
|
| 172 |
+
required
|
| 173 |
+
class="w-full floating-input rounded-xl p-3 appearance-none"
|
| 174 |
+
>
|
| 175 |
+
<option value="Unknown">Unknown</option>
|
| 176 |
+
<option value="formerly smoked">Formerly smoked</option>
|
| 177 |
+
<option value="never smoked">Never smoked</option>
|
| 178 |
+
<option value="smokes">Smokes</option>
|
| 179 |
+
</select>
|
| 180 |
+
</div>
|
| 181 |
+
</div>
|
| 182 |
+
|
| 183 |
+
<div class="pt-6 text-center">
|
| 184 |
+
<button
|
| 185 |
+
type="submit"
|
| 186 |
+
class="w-full md:w-auto md:px-12 btn-primary py-4 rounded-xl font-bold flex items-center justify-center gap-3 mx-auto"
|
| 187 |
+
>
|
| 188 |
+
<span id="btn-text">Assess Risk Level</span>
|
| 189 |
+
<div id="btn-spinner" class="loader-spinner"></div>
|
| 190 |
+
</button>
|
| 191 |
+
</div>
|
| 192 |
+
</form>
|
| 193 |
+
</div>
|
| 194 |
+
|
| 195 |
+
<!-- Result Banner -->
|
| 196 |
+
<div
|
| 197 |
+
id="result-container"
|
| 198 |
+
class="hidden mb-12 max-w-full lg:max-w-3xl mx-auto"
|
| 199 |
+
>
|
| 200 |
+
<!-- Injected via JS -->
|
| 201 |
+
</div>
|
| 202 |
+
</div>
|
| 203 |
+
{% endblock %}
|
| 204 |
+
|
| 205 |
+
{% block scripts %}
|
| 206 |
+
<script>
|
| 207 |
+
document
|
| 208 |
+
.getElementById("prediction-form")
|
| 209 |
+
.addEventListener("submit", async (e) => {
|
| 210 |
+
e.preventDefault();
|
| 211 |
+
|
| 212 |
+
const btnText = document.getElementById("btn-text");
|
| 213 |
+
const btnSpinner = document.getElementById("btn-spinner");
|
| 214 |
+
const submitBtn = e.target.querySelector('button[type="submit"]');
|
| 215 |
+
const resContainer = document.getElementById("result-container");
|
| 216 |
+
|
| 217 |
+
// Collect inputs dynamically
|
| 218 |
+
const inputs = [
|
| 219 |
+
"age",
|
| 220 |
+
"avg_glucose_level",
|
| 221 |
+
"bmi",
|
| 222 |
+
"gender",
|
| 223 |
+
"hypertension",
|
| 224 |
+
"heart_disease",
|
| 225 |
+
"ever_married",
|
| 226 |
+
"work_type",
|
| 227 |
+
"residence_type",
|
| 228 |
+
"smoking_status"
|
| 229 |
+
];
|
| 230 |
+
const data = {};
|
| 231 |
+
inputs.forEach((id) => (data[id] = document.getElementById(id).value));
|
| 232 |
+
|
| 233 |
+
// UI Loading
|
| 234 |
+
btnText.textContent = "Processing...";
|
| 235 |
+
btnSpinner.style.display = "block";
|
| 236 |
+
submitBtn.disabled = true;
|
| 237 |
+
resContainer.classList.add("hidden");
|
| 238 |
+
|
| 239 |
+
try {
|
| 240 |
+
const response = await fetch("/predict/stroke", {
|
| 241 |
+
method: "POST",
|
| 242 |
+
headers: { "Content-Type": "application/json" },
|
| 243 |
+
body: JSON.stringify(data),
|
| 244 |
+
});
|
| 245 |
+
const result = await response.json();
|
| 246 |
+
|
| 247 |
+
resContainer.classList.remove("hidden");
|
| 248 |
+
|
| 249 |
+
if (response.ok) {
|
| 250 |
+
const isRisk = result.label === "High Stroke Risk";
|
| 251 |
+
const colorClass = isRisk ? "text-rose-500" : "text-emerald-500";
|
| 252 |
+
const bgClass = isRisk ? "bg-rose-500/10" : "bg-emerald-500/10";
|
| 253 |
+
const borderClass = isRisk
|
| 254 |
+
? "border-rose-500/30"
|
| 255 |
+
: "border-emerald-500/30";
|
| 256 |
+
const icon = isRisk ? "fa-brain" : "fa-shield-heart";
|
| 257 |
+
|
| 258 |
+
resContainer.className = `flex-1 flex flex-col items-center justify-center border-2 rounded-2xl p-8 result-card text-center ${bgClass} ${borderClass} mb-12 max-w-full lg:max-w-3xl mx-auto`;
|
| 259 |
+
resContainer.innerHTML = `
|
| 260 |
+
<div class="w-20 h-20 rounded-full flex items-center justify-center mb-6 bg-[#111827] border ${borderClass}">
|
| 261 |
+
<i class="fa-solid ${icon} text-4xl ${colorClass}"></i>
|
| 262 |
+
</div>
|
| 263 |
+
<div class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-2">Prediction Result</div>
|
| 264 |
+
<h2 class="text-3xl font-extrabold ${colorClass} mb-4">${result.label}</h2>
|
| 265 |
+
<p class="text-sm text-gray-400">Our AI assistant will explain what this indicates.</p>
|
| 266 |
+
`;
|
| 267 |
+
|
| 268 |
+
if (window.onPredictionComplete) {
|
| 269 |
+
window.onPredictionComplete("stroke", data, result.label);
|
| 270 |
+
}
|
| 271 |
+
} else {
|
| 272 |
+
resContainer.className =
|
| 273 |
+
"flex-1 flex flex-col items-center justify-center bg-gray-900 border-2 border-gray-700 rounded-2xl p-8 result-card text-center mb-12 max-w-full lg:max-w-3xl mx-auto";
|
| 274 |
+
resContainer.innerHTML = `
|
| 275 |
+
<div class="text-gray-400 mb-4"><i class="fa-solid fa-circle-exclamation text-4xl"></i></div>
|
| 276 |
+
<h2 class="text-xl font-bold text-white mb-2">Prediction Unavailable</h2>
|
| 277 |
+
<p class="text-gray-400 text-sm">${result.error || "An error occurred."}</p>
|
| 278 |
+
`;
|
| 279 |
+
}
|
| 280 |
+
} catch (error) {
|
| 281 |
+
resContainer.classList.remove("hidden");
|
| 282 |
+
resContainer.className =
|
| 283 |
+
"flex-1 flex flex-col items-center justify-center bg-rose-900/10 border-2 border-rose-900/30 rounded-2xl p-8 result-card text-center mb-12 max-w-full lg:max-w-3xl mx-auto";
|
| 284 |
+
resContainer.innerHTML = `<h2 class="text-xl font-bold text-rose-500 mb-2">Network Error</h2><p class="text-gray-400 text-sm">Could not connect to the server.</p>`;
|
| 285 |
+
} finally {
|
| 286 |
+
btnText.textContent = "Assess Risk Level";
|
| 287 |
+
btnSpinner.style.display = "none";
|
| 288 |
+
submitBtn.disabled = false;
|
| 289 |
+
}
|
| 290 |
+
});
|
| 291 |
+
</script>
|
| 292 |
+
{% endblock %}
|