VINAYAKKV commited on
Commit
5fdbe1b
·
verified ·
1 Parent(s): 85f8162

Upload folder using huggingface_hub

Browse files
.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
- title: Predict Ai
3
- emoji: 🏢
4
- colorFrom: blue
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: 'PredictLab is a full-stack Machine Learning web application '
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PredictLab: Machine Learning Portfolio Web App
2
+
3
+ ![FastAPI](https://img.shields.io/badge/FastAPI-005571?style=for-the-badge&logo=fastapi)
4
+ ![Groq](https://img.shields.io/badge/Groq-Llama%203-f15a24?style=for-the-badge)
5
+ ![scikit-learn](https://img.shields.io/badge/scikit--learn-%23F7931E.svg?style=for-the-badge&logo=scikit-learn&logoColor=white)
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 &middot; 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 &rarr;</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 &rarr;</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 &rarr;</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 %}