From 9982c5dcb0c448fd587f9a4407ff277fd27e6709 Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Mon, 13 Apr 2026 09:31:09 +0600 Subject: [PATCH 01/16] Refactor project structure and move app-specific files to the newly created `/app` subdirectory --- app/__init__.py | 0 app.py => app/app.py | 0 {static => app/static}/css/style.css | 0 {static => app/static}/js/main.js | 0 .../materials/32247a32f95f2504404c78a8df9ed849.png | Bin .../d821657540b6765c2d915b547bfce9c5 (1).jpg | Bin {static => app/static}/materials/next.png | Bin {static => app/static}/script.js | 0 {templates => app/templates}/about.html | 0 {templates => app/templates}/base.html | 0 {templates => app/templates}/index.html | 0 fit.py => models/fit.py | 0 12 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/__init__.py rename app.py => app/app.py (100%) rename {static => app/static}/css/style.css (100%) rename {static => app/static}/js/main.js (100%) rename {static => app/static}/materials/32247a32f95f2504404c78a8df9ed849.png (100%) rename {static => app/static}/materials/d821657540b6765c2d915b547bfce9c5 (1).jpg (100%) rename {static => app/static}/materials/next.png (100%) rename {static => app/static}/script.js (100%) rename {templates => app/templates}/about.html (100%) rename {templates => app/templates}/base.html (100%) rename {templates => app/templates}/index.html (100%) rename fit.py => models/fit.py (100%) diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app.py b/app/app.py similarity index 100% rename from app.py rename to app/app.py diff --git a/static/css/style.css b/app/static/css/style.css similarity index 100% rename from static/css/style.css rename to app/static/css/style.css diff --git a/static/js/main.js b/app/static/js/main.js similarity index 100% rename from static/js/main.js rename to app/static/js/main.js diff --git a/static/materials/32247a32f95f2504404c78a8df9ed849.png b/app/static/materials/32247a32f95f2504404c78a8df9ed849.png similarity index 100% rename from static/materials/32247a32f95f2504404c78a8df9ed849.png rename to app/static/materials/32247a32f95f2504404c78a8df9ed849.png diff --git a/static/materials/d821657540b6765c2d915b547bfce9c5 (1).jpg b/app/static/materials/d821657540b6765c2d915b547bfce9c5 (1).jpg similarity index 100% rename from static/materials/d821657540b6765c2d915b547bfce9c5 (1).jpg rename to app/static/materials/d821657540b6765c2d915b547bfce9c5 (1).jpg diff --git a/static/materials/next.png b/app/static/materials/next.png similarity index 100% rename from static/materials/next.png rename to app/static/materials/next.png diff --git a/static/script.js b/app/static/script.js similarity index 100% rename from static/script.js rename to app/static/script.js diff --git a/templates/about.html b/app/templates/about.html similarity index 100% rename from templates/about.html rename to app/templates/about.html diff --git a/templates/base.html b/app/templates/base.html similarity index 100% rename from templates/base.html rename to app/templates/base.html diff --git a/templates/index.html b/app/templates/index.html similarity index 100% rename from templates/index.html rename to app/templates/index.html diff --git a/fit.py b/models/fit.py similarity index 100% rename from fit.py rename to models/fit.py From 902f40dbf321077bcd8416c08b34b7b91142f6e7 Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Mon, 13 Apr 2026 09:32:49 +0600 Subject: [PATCH 02/16] Rename older flask-powered app script and create new one for fastapi --- app/app.py | 118 ------------------------------------------------- app/prv_app.py | 118 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 118 deletions(-) create mode 100644 app/prv_app.py diff --git a/app/app.py b/app/app.py index 2fe595e..e69de29 100644 --- a/app/app.py +++ b/app/app.py @@ -1,118 +0,0 @@ -import os -import joblib -import pandas as pd -import numpy as np -from flask import Flask, render_template, request, jsonify -from models.download_from_hf import download - -# --- Configuration --- -MODEL_DIR = "models" -PIPE_PATH = os.path.join(MODEL_DIR, "pipe.pkl") -COLUMNS_PATH = os.path.join(MODEL_DIR, "column_names.pkl") -reverse_mapping = {0: "FALSE POSITIVE", 1: "CANDIDATE", 2: "CONFIRMED"} - -# --- Self-Heal Function --- -def initialize_artifacts(): - """ - Checks if model artifacts exist. If not, runs the training script. - """ - # 1. Ensure the model directory exists - os.makedirs(MODEL_DIR, exist_ok=True) - - # 2. Check for missing files - pipe_exists = os.path.exists(PIPE_PATH) - columns_exists = os.path.exists(COLUMNS_PATH) - - if not pipe_exists or not columns_exists: - print("--- MODEL ARTIFACTS MISSING ---") - if not pipe_exists: - print(f"Missing: {PIPE_PATH}") - if not columns_exists: - print(f"Missing: {COLUMNS_PATH}") - - print("Downloading the saved models from Hugging Face... This may take a moment.") - try: - # Run the `download` function from `models/download_from_hf.py` - download() - print("Download complete. Artifacts generated successfully.") - print("---------------------------------") - except Exception as e: - print(f"\nFATAL: Error during self-heal downloading: {e}") - print("Application cannot start without model artifacts. Exitting......") - exit(1) # Exit if training fails - else: - print("Model artifacts found. Loading...") - -# --- Application Startup --- - -# Run the self-heal check *before* loading models -initialize_artifacts() - -# Load models -try: - pipe = joblib.load(PIPE_PATH) - column_names = joblib.load(COLUMNS_PATH) - print("Models loaded successfully.") -except Exception as e: - print(f"\nFATAL: Error loading model artifacts: {e}") - print("Files might be corrupt. Try deleting the 'models' directory and restarting.") - exit(1) # Exit if loading fails - -# Initialize Flask App -app = Flask(__name__) - -@app.route("/") -def home(): - return render_template("index.html") - -@app.route("/about") -def about(): - return render_template("about.html") - -@app.route("/predict", methods=["POST"]) -def predict(): - try: - # Extract features from the JSON request - raw_features = [ - request.json["orbital-period"], - request.json["transit-epoch"], - request.json["transit-depth"], - request.json["planet-radius"], - request.json["semi-major-axis"], - request.json["inclination"], - request.json["equilibrium-temp"], - request.json["insolation-flux"], - request.json["impact-parameter"], - request.json["radius-ratio"], - request.json["stellar-density"], - request.json["star-distance"], - request.json["num-transits"], - ] - - # Create DataFrame with correct column names - df = pd.DataFrame([raw_features], columns=column_names) - - # Get prediction and probabilities - pred = int(pipe.predict(df)[0]) - proba = pipe.predict_proba(df)[0] - - # Format probabilities for the response - proba_dict = { - reverse_mapping[i]: round(p, 3) for i, p in enumerate(proba) - } - - # Send response - return jsonify( - {"prediction": reverse_mapping[pred], "probabilities": proba_dict} - ) - - except KeyError as e: - print(f"Prediction Error: Missing key in request {e}") - return jsonify({"error": f"Missing feature in request: {e}"}), 400 - except Exception as e: - print(f"Prediction Error: {e}") - return jsonify({"error": str(e)}), 400 - - -if __name__ == "__main__": - app.run(debug=True) \ No newline at end of file diff --git a/app/prv_app.py b/app/prv_app.py new file mode 100644 index 0000000..2fe595e --- /dev/null +++ b/app/prv_app.py @@ -0,0 +1,118 @@ +import os +import joblib +import pandas as pd +import numpy as np +from flask import Flask, render_template, request, jsonify +from models.download_from_hf import download + +# --- Configuration --- +MODEL_DIR = "models" +PIPE_PATH = os.path.join(MODEL_DIR, "pipe.pkl") +COLUMNS_PATH = os.path.join(MODEL_DIR, "column_names.pkl") +reverse_mapping = {0: "FALSE POSITIVE", 1: "CANDIDATE", 2: "CONFIRMED"} + +# --- Self-Heal Function --- +def initialize_artifacts(): + """ + Checks if model artifacts exist. If not, runs the training script. + """ + # 1. Ensure the model directory exists + os.makedirs(MODEL_DIR, exist_ok=True) + + # 2. Check for missing files + pipe_exists = os.path.exists(PIPE_PATH) + columns_exists = os.path.exists(COLUMNS_PATH) + + if not pipe_exists or not columns_exists: + print("--- MODEL ARTIFACTS MISSING ---") + if not pipe_exists: + print(f"Missing: {PIPE_PATH}") + if not columns_exists: + print(f"Missing: {COLUMNS_PATH}") + + print("Downloading the saved models from Hugging Face... This may take a moment.") + try: + # Run the `download` function from `models/download_from_hf.py` + download() + print("Download complete. Artifacts generated successfully.") + print("---------------------------------") + except Exception as e: + print(f"\nFATAL: Error during self-heal downloading: {e}") + print("Application cannot start without model artifacts. Exitting......") + exit(1) # Exit if training fails + else: + print("Model artifacts found. Loading...") + +# --- Application Startup --- + +# Run the self-heal check *before* loading models +initialize_artifacts() + +# Load models +try: + pipe = joblib.load(PIPE_PATH) + column_names = joblib.load(COLUMNS_PATH) + print("Models loaded successfully.") +except Exception as e: + print(f"\nFATAL: Error loading model artifacts: {e}") + print("Files might be corrupt. Try deleting the 'models' directory and restarting.") + exit(1) # Exit if loading fails + +# Initialize Flask App +app = Flask(__name__) + +@app.route("/") +def home(): + return render_template("index.html") + +@app.route("/about") +def about(): + return render_template("about.html") + +@app.route("/predict", methods=["POST"]) +def predict(): + try: + # Extract features from the JSON request + raw_features = [ + request.json["orbital-period"], + request.json["transit-epoch"], + request.json["transit-depth"], + request.json["planet-radius"], + request.json["semi-major-axis"], + request.json["inclination"], + request.json["equilibrium-temp"], + request.json["insolation-flux"], + request.json["impact-parameter"], + request.json["radius-ratio"], + request.json["stellar-density"], + request.json["star-distance"], + request.json["num-transits"], + ] + + # Create DataFrame with correct column names + df = pd.DataFrame([raw_features], columns=column_names) + + # Get prediction and probabilities + pred = int(pipe.predict(df)[0]) + proba = pipe.predict_proba(df)[0] + + # Format probabilities for the response + proba_dict = { + reverse_mapping[i]: round(p, 3) for i, p in enumerate(proba) + } + + # Send response + return jsonify( + {"prediction": reverse_mapping[pred], "probabilities": proba_dict} + ) + + except KeyError as e: + print(f"Prediction Error: Missing key in request {e}") + return jsonify({"error": f"Missing feature in request: {e}"}), 400 + except Exception as e: + print(f"Prediction Error: {e}") + return jsonify({"error": str(e)}), 400 + + +if __name__ == "__main__": + app.run(debug=True) \ No newline at end of file From 8f2e121a66f0101a110a560e61be255ceb5c84d5 Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Mon, 13 Apr 2026 09:38:59 +0600 Subject: [PATCH 03/16] Add primary self-heal function and configuration in `app.py` and add `BaseModel` class in schema for validation --- app/app.py | 45 ++++++++++++++++++++++++++++++++++++++++++ app/schema/__init__.py | 0 app/schema/validate.py | 4 ++++ 3 files changed, 49 insertions(+) create mode 100644 app/schema/__init__.py create mode 100644 app/schema/validate.py diff --git a/app/app.py b/app/app.py index e69de29..b2096a5 100644 --- a/app/app.py +++ b/app/app.py @@ -0,0 +1,45 @@ +from fastapi import FastAPI +from .schema.validate import UserInput +from models.download_from_hf import download + +import pandas as pd +import os +from pathlib import Path + +# --- Configuration --- +MODEL_DIR = "models" +PIPE_PATH = os.path.join(MODEL_DIR, "pipe.pkl") +COLUMNS_PATH = os.path.join(MODEL_DIR, "column_names.pkl") +reverse_mapping = {0: "FALSE POSITIVE", 1: "CANDIDATE", 2: "CONFIRMED"} + +# --- Self-Heal Function --- +def initialize_artifacts(): + """ + Checks if model artifacts exist. If not, runs the training script. + """ + # 1. Ensure the model directory exists + os.makedirs(MODEL_DIR, exist_ok=True) + + # 2. Check for missing files + pipe_exists = os.path.exists(PIPE_PATH) + columns_exists = os.path.exists(COLUMNS_PATH) + + if not pipe_exists or not columns_exists: + print("--- MODEL ARTIFACTS MISSING ---") + if not pipe_exists: + print(f"Missing: {PIPE_PATH}") + if not columns_exists: + print(f"Missing: {COLUMNS_PATH}") + + print("Downloading the saved models from Hugging Face... This may take a moment.") + try: + # Run the `download` function from `models/download_from_hf.py` + download() + print("Download complete. Artifacts generated successfully.") + print("---------------------------------") + except Exception as e: + print(f"\nFATAL: Error during self-heal downloading: {e}") + print("Application cannot start without model artifacts. Exitting......") + exit(1) # Exit if training fails + else: + print("Model artifacts found. Loading...") \ No newline at end of file diff --git a/app/schema/__init__.py b/app/schema/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/schema/validate.py b/app/schema/validate.py new file mode 100644 index 0000000..c65a656 --- /dev/null +++ b/app/schema/validate.py @@ -0,0 +1,4 @@ +from pydantic import BaseModel + +class UserInput(BaseModel): + pass \ No newline at end of file From 5974eeeb4d5bc2a0c42fe495d579c1dbc12b5ef1 Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Mon, 13 Apr 2026 13:37:01 +0600 Subject: [PATCH 04/16] Add fully configured pydantic `BaseModel` for input validation on `/predict` route --- app/schema/validate.py | 59 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/app/schema/validate.py b/app/schema/validate.py index c65a656..bb0fa59 100644 --- a/app/schema/validate.py +++ b/app/schema/validate.py @@ -1,4 +1,59 @@ -from pydantic import BaseModel +from pydantic import BaseModel, Field +from typing import Annotated class UserInput(BaseModel): - pass \ No newline at end of file + koi_period : Annotated[float, Field( + ..., gt=0, description="Orbital Period (days)", + examples= [0.837, 2.154, 9.88, 54.3, 365.2] + )] + koi_time0bk : Annotated[float, Field( + ..., gt=2_450_000, description="Transit Epoch (BJD)", + examples=[2454833.0, 2455002.5] + )] + koi_depth : Annotated[float, Field( + ..., gt = 0, le = 1_000_000, + description="Transit Depth (ppm)", + examples=[150.2, 892.5, 3400.0] + )] + koi_prad : Annotated[float, Field( + ..., description="Planet Radius (Earth radii)", + gt=0, examples=[0.84, 1.2, 2.5, 6.8, 14.3] + )] + koi_sma : Annotated[float, Field( + ..., examples=[0.021, 0.085, 0.234], + gt = 0, description="Semi-Major Axis (AU)" + )] + koi_incl : Annotated[float, Field( + ..., description="Inclination (deg)", + gt=0, le=90, examples=[74.5,20.1,86.4] + )] + koi_teq : Annotated[float, Field( + ..., description= "Equilibrium Temperature (K)", + gt = 0, examples=[312.5, 542.0, 876.3] + )] + koi_insol : Annotated[float, Field( + ..., examples=[0.32, 1.02, 4.75, 28.6, 310.0], + description= "Insolation Flux (Earth flux)", gt =0 + )] + koi_impact : Annotated[float, Field( + ..., description="Impact Parameter", + examples=[0.02, 0.18, 0.45, 0.72, 0.95], + ge = 0, lt = 1 + )] + koi_ror : Annotated[float, Field( + ..., description="Planet/Star Radius Ratio", + examples=[0.011, 0.028, 0.065, 0.112, 0.198], + gt = 0, lt = 1 + )] + koi_srho : Annotated[float, Field( + ..., description="Stellar Density (g/cmΒ³)", + gt = 0, examples=[0.18, 0.85, 1.41, 3.72, 18.6] + )] + koi_dor : Annotated[float, Field( + ..., examples=[2.3, 8.7, 21.4, 56.8, 134.2], + gt = 1, description="Planet-Star Distance (Rβ˜…)" + )] + koi_num_transits : Annotated[int, Field( + ..., description="Number of Transits", + ge=1, examples= [1, 3, 7, 15, 42] + )] \ No newline at end of file From ddbc8d51017434993486d0ac39848493aab58e61 Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Mon, 13 Apr 2026 15:54:26 +0600 Subject: [PATCH 05/16] Update `requirements.txt` to add `fastapi` and other dependencies --- requirements.txt | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/requirements.txt b/requirements.txt index 0aaa4fe..4dbfaad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,14 @@ annotated-doc==0.0.4 +annotated-types==0.7.0 anyio==4.13.0 blinker==1.9.0 certifi==2026.2.25 click==8.3.1 contourpy==1.3.3 cycler==0.12.1 +docutils==0.22.4 dotenv==0.9.9 +fastapi==0.135.3 filelock==3.25.2 Flask==3.1.2 fonttools==4.61.1 @@ -19,24 +22,37 @@ huggingface_hub==1.10.1 idna==3.11 imbalanced-learn==0.14.1 itsdangerous==2.2.0 +jedi==0.19.2 Jinja2==3.1.6 joblib==1.5.3 kiwisolver==1.4.9 lightgbm==4.6.0 +loro==1.10.3 +marimo==0.23.1 +Markdown==3.10.2 markdown-it-py==4.0.0 MarkupSafe==3.0.3 matplotlib==3.10.8 mdurl==0.1.2 +msgspec==0.21.1 +narwhals==2.19.0 numpy==2.4.1 packaging==25.0 pandas==2.3.3 +parso==0.8.6 pillow==12.1.0 +psutil==7.2.2 +pydantic==2.12.5 +pydantic_core==2.41.5 Pygments==2.20.0 +pymdown-extensions==10.21.2 pyparsing==3.3.1 python-dateutil==2.9.0.post0 python-dotenv==1.2.1 +python-multipart==0.0.26 pytz==2025.2 PyYAML==6.0.3 +pyzmq==27.1.0 rich==15.0.0 scikit-learn==1.8.0 scipy==1.17.0 @@ -44,10 +60,15 @@ seaborn==0.13.2 shellingham==1.5.4 six==1.17.0 sklearn-compat==0.1.5 +starlette==1.0.0 threadpoolctl==3.6.0 +tomlkit==0.14.0 tqdm==4.67.3 typer==0.24.1 +typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2025.3 +uvicorn==0.44.0 +websockets==16.0 Werkzeug==3.1.5 xgboost==3.1.3 From d77f204686fe4ca2d5d33a703e954d41ef366ec1 Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Mon, 13 Apr 2026 15:57:51 +0600 Subject: [PATCH 06/16] Add `/about`,`/predict` and `/predict/batch` endpoints for both regular and batch prediction to `app.py` --- app/app.py | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 148 insertions(+), 3 deletions(-) diff --git a/app/app.py b/app/app.py index b2096a5..4aaa33f 100644 --- a/app/app.py +++ b/app/app.py @@ -1,8 +1,17 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Depends, UploadFile +from fastapi.requests import Request +from fastapi.responses import JSONResponse, FileResponse +from fastapi.exceptions import HTTPException +from fastapi.staticfiles import StaticFiles from .schema.validate import UserInput from models.download_from_hf import download +from sklearn.pipeline import Pipeline +from typing import Tuple, List +import joblib import pandas as pd +import numpy as np +from contextlib import asynccontextmanager import os from pathlib import Path @@ -10,10 +19,12 @@ MODEL_DIR = "models" PIPE_PATH = os.path.join(MODEL_DIR, "pipe.pkl") COLUMNS_PATH = os.path.join(MODEL_DIR, "column_names.pkl") +INDEX_PATH = Path("app","templates","index.html") +ABOUT_PATH = Path("app","templates","about.html") reverse_mapping = {0: "FALSE POSITIVE", 1: "CANDIDATE", 2: "CONFIRMED"} # --- Self-Heal Function --- -def initialize_artifacts(): +def initialize_artifacts() -> Tuple[Pipeline,np.ndarray]: """ Checks if model artifacts exist. If not, runs the training script. """ @@ -42,4 +53,138 @@ def initialize_artifacts(): print("Application cannot start without model artifacts. Exitting......") exit(1) # Exit if training fails else: - print("Model artifacts found. Loading...") \ No newline at end of file + print("Model artifacts found. Loading...") + pipe = joblib.load(PIPE_PATH) + column_names = joblib.load(COLUMNS_PATH) + print("Model artifacts are loaded. Ready for prediction πŸš€") + return pipe,column_names + +@asynccontextmanager +async def lifespan(app:FastAPI): + """ + Loads the models at start + """ + pipe,column_names = initialize_artifacts() + + app.state.pipe = pipe + app.state.column_names = column_names + + yield + +app = FastAPI(title="TransitIQ",version="3.0 (ByteBard58_Fork-FastAPI)",lifespan=lifespan) + +# Mount static files +app.mount(name="static",path="/static",app=StaticFiles( + directory=Path("app","static") +)) + +async def get_artifacts(request:Request) -> Tuple[Pipeline,np.ndarray]: + """ + Helper to serve the artifacts in a route + """ + return request.app.state.pipe, request.app.state.column_names + +def validate_csv(target:pd.DataFrame,expected_columns:List) -> pd.DataFrame: + """ + Helper for validating user-uploaded `.csv` files during batch prediction + """ + if target.columns != expected_columns: + raise HTTPException( + status_code=422, + detail="The columns of the uploaded `.csv` files do not match with the expected list of columns or the order of them" + + ) + try: + target.astype(float) + except Exception: + raise HTTPException( + status_code=422, + detail = "Provided values must be numeric (float-compatible)" + ) + + return target + +@app.get("/health") +def health(): + msg = { + "title":"TransitIQ", + "version":"3.0(ByteBard58_Fork-FastAPI)", + "status":"All systems operational" + } + return JSONResponse(content=msg,status_code=200) + +@app.get("/") +def home(): + msg = "Welcome to TransitIQ !" + return JSONResponse(content=msg,status_code=200) + +@app.get("/about") +def about(): + return FileResponse(ABOUT_PATH,status_code=200) + +reverse_mapping = {0: "FALSE POSITIVE", 1: "CANDIDATE", 2: "CONFIRMED"} + +@app.post("/predict") +def predict_with_manual_inputs( + payload:UserInput, + dep:Tuple[Pipeline,np.ndarray] = Depends(get_artifacts) +): + pipe, column_names = dep + column_names:List = column_names.tolist() + payload:dict = payload.model_dump(mode="json") + + sample = [] + for i,(key,val) in enumerate(payload.items()): + if column_names[i] == key: + sample.append(val) + else: + raise ValueError("The payload does not match the original order defined by the BaseModel") + sample = np.array(sample).reshape(1,-1) + + label = int(pipe.predict(sample)[0]) + proba:List = pipe.predict_proba(sample)[0].tolist() + + label:str = reverse_mapping.get(label) + proba:dict = {cls:round(proba,3) for cls,proba in zip(reverse_mapping.values(),proba)} + msg = { + "status":"prediction successful", + "predicted_label":label, + "predction_probability":proba + } + + return JSONResponse(status_code=201,content=msg) + +@app.post("/predict/batch") +def predict_with_batch_input( + file:UploadFile, + dep:Tuple[Pipeline,np.ndarray] = Depends(get_artifacts) +): + pipe, column_names = dep + column_names:List = column_names.tolist() + + ext = Path(file.filename).suffix + if ext != ".csv": + raise HTTPException( + status_code=422, detail=f"Only `.csv` file is allowed as an upload, got {ext} instead" + ) + + try: + df = pd.read_csv(file) + except Exception as e: + raise HTTPException(status_code=422, detail=f"Failed to parse CSV file tracking: {str(e)}") + df:np.ndarray = validate_csv(df).to_numpy() + + sample = df + label:List[float] = pipe.predict(sample).tolist() + proba:List[List[float]] = pipe.predict_proba(sample).tolist() + + label:List[str] = [reverse_mapping.get(l) for l in label] + proba = [[round(value,3) for value in prob] for prob in proba] + proba + msg = { + "status":"batch prediction successful", + "predicted_labels":label, + "predction_probability":proba + } + + return JSONResponse(status_code=201,content=msg) \ No newline at end of file From 0386234b5348d717a03d142cd19cac6e6e44aca0 Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Mon, 13 Apr 2026 16:08:06 +0600 Subject: [PATCH 07/16] Add sample generator script and modify `.gitignore` to ignore generated sample CSV files --- .gitignore | 3 +- data/__init__.py | 0 data/sample_generator.py | 106 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 data/__init__.py create mode 100644 data/sample_generator.py diff --git a/.gitignore b/.gitignore index 6a62caa..4d69ff6 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ instance/ shit/ *.db .vscode/ -*.pkl \ No newline at end of file +*.pkl +data/test_sample.csv \ No newline at end of file diff --git a/data/__init__.py b/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data/sample_generator.py b/data/sample_generator.py new file mode 100644 index 0000000..6dcbdbc --- /dev/null +++ b/data/sample_generator.py @@ -0,0 +1,106 @@ +""" +sample_generator.py + +This script is used to generate samples directly from the main dataset. +These samples are used to test the `/predict/batch` route. +To run it, enter this in your command line: +``` +python -m data.sample_generator.py +``` +""" + + +import pandas as pd +import numpy as np + + +def get_window(camps, campaign_dates): + if pd.isna(camps) or not camps: + return np.nan, np.nan + + camps = str(camps).split(',') if isinstance(camps, str) else camps + starts, ends = [], [] + + for c in camps: + try: + camp_num = int(c.strip()) + if camp_num in campaign_dates: + start, end = campaign_dates[camp_num] + starts.append(start) + ends.append(end) + except (ValueError, KeyError): + continue + + return (min(starts) if starts else np.nan, max(ends) if ends else np.nan) + + +def create_test_sample(num_samples=10, random_seed=42): + np.random.seed(random_seed) + + kepler_df = pd.read_csv("data/kepler_data.csv", comment="#") + k2_df = pd.read_csv("data/k2_data.csv", comment="#") + + feature_list = [ + "koi_period", "koi_time0bk", "koi_depth", "koi_prad", + "koi_sma", "koi_incl", "koi_teq", "koi_insol", "koi_impact", + "koi_ror", "koi_srho", "koi_dor", "koi_num_transits" + ] + + kepler_subset = kepler_df[feature_list] + + campaign_dates = { + 0: (2456725.0, 2456805.0), 1: (2456808.0, 2456891.0), 2: (2456893.0, 2456975.0), + 3: (2456976.0, 2457064.0), 4: (2457065.0, 2457159.0), 5: (2457159.0, 2457246.0), + 6: (2457250.0, 2457338.0), 7: (2457339.0, 2457420.0), 8: (2457421.0, 2457530.0), + 9: (2457504.0, 2457579.0), 10: (2457577.0, 2457653.0), 11: (2457657.0, 2457732.0), + 12: (2457731.0, 2457819.0), 13: (2457820.0, 2457900.0), 14: (2457898.0, 2457942.0), + 15: (2457941.0, 2458022.0), 16: (2458020.0, 2458074.0), 17: (2458074.0, 2458176.0), + 18: (2458151.0, 2458201.0), 19: (2458232.0, 2458348.0) + } + + k2_df['campaigns'] = k2_df['k2_campaigns'] + k2_df[['obs_start_bjd', 'obs_end_bjd']] = k2_df['campaigns'].apply( + lambda x: pd.Series(get_window(x, campaign_dates)) + ) + + k2_df['n_min'] = np.ceil((k2_df['obs_start_bjd'] - k2_df['pl_tranmid']) / k2_df['pl_orbper']) + k2_df['n_max'] = np.floor((k2_df['obs_end_bjd'] - k2_df['pl_tranmid']) / k2_df['pl_orbper']) + k2_df['num_transits'] = (k2_df['n_max'] - k2_df['n_min'] + 1).clip(lower=0) + + k2_mapping = { + "pl_orbper": "koi_period", "pl_tranmid": "koi_time0bk", + "pl_trandep": "koi_depth", "pl_rade": "koi_prad", "pl_orbsmax": "koi_sma", + "pl_orbincl": "koi_incl", "pl_eqt": "koi_teq", "pl_insol": "koi_insol", + "pl_imppar": "koi_impact", "pl_ratror": "koi_ror", "pl_dens": "koi_srho", + "pl_ratdor": "koi_dor", "num_transits": "koi_num_transits" + } + + k2_subset = k2_df[list(k2_mapping.keys())].rename(columns=k2_mapping) + + combined = pd.concat([kepler_subset, k2_subset], ignore_index=True) + + combined = combined.dropna(subset=feature_list) + + sample_indices = np.random.choice(len(combined), size=min(num_samples, len(combined)), replace=False) + sample = combined.iloc[sample_indices].copy() + + noise_factor = 0.15 + for col in feature_list: + col_std = sample[col].std() + if col_std > 0: + noise = np.random.normal(0, col_std * noise_factor, size=len(sample)) + sample[col] = sample[col] + noise + sample[col] = sample[col].clip(lower=0) if col in ["koi_depth", "koi_impact", "koi_ror", "koi_num_transits"] else sample[col] + + sample = sample[feature_list] + + output_path = "data/test_sample.csv" + sample.to_csv(output_path, index=False) + print(f"Created test sample with {len(sample)} rows at {output_path}") + print(f"Columns: {sample.columns.tolist()}") + print(f"\nSample preview:") + print(sample.head()) + + +if __name__ == "__main__": + create_test_sample(num_samples=20) From b9ec8afd89b77ad6bc62c0884225fff00d6c302a Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Mon, 13 Apr 2026 16:09:01 +0600 Subject: [PATCH 08/16] Fix typo in docstring of `sample_generator.py` --- data/sample_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/sample_generator.py b/data/sample_generator.py index 6dcbdbc..aad974c 100644 --- a/data/sample_generator.py +++ b/data/sample_generator.py @@ -5,7 +5,7 @@ These samples are used to test the `/predict/batch` route. To run it, enter this in your command line: ``` -python -m data.sample_generator.py +python -m data.sample_generator ``` """ From cfbb4a3b025e7952b704c899589ed7d719cf7d1b Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Mon, 13 Apr 2026 16:22:25 +0600 Subject: [PATCH 09/16] Debug some issues in `app.py`, --- app/app.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/app.py b/app/app.py index 4aaa33f..184bbde 100644 --- a/app/app.py +++ b/app/app.py @@ -88,10 +88,10 @@ def validate_csv(target:pd.DataFrame,expected_columns:List) -> pd.DataFrame: """ Helper for validating user-uploaded `.csv` files during batch prediction """ - if target.columns != expected_columns: + if target.columns.to_list() != expected_columns: raise HTTPException( status_code=422, - detail="The columns of the uploaded `.csv` files do not match with the expected list of columns or the order of them" + detail="The columns of the uploaded .csv file do not match with the expected list of columns or the order of them" ) try: @@ -155,7 +155,7 @@ def predict_with_manual_inputs( return JSONResponse(status_code=201,content=msg) @app.post("/predict/batch") -def predict_with_batch_input( +async def predict_with_batch_input( file:UploadFile, dep:Tuple[Pipeline,np.ndarray] = Depends(get_artifacts) ): @@ -165,14 +165,14 @@ def predict_with_batch_input( ext = Path(file.filename).suffix if ext != ".csv": raise HTTPException( - status_code=422, detail=f"Only `.csv` file is allowed as an upload, got {ext} instead" + status_code=422, detail=f"Only .csv file is allowed as an upload, got {ext} instead" ) try: - df = pd.read_csv(file) + df = pd.read_csv(file.file) except Exception as e: raise HTTPException(status_code=422, detail=f"Failed to parse CSV file tracking: {str(e)}") - df:np.ndarray = validate_csv(df).to_numpy() + df:np.ndarray = validate_csv(df,column_names).to_numpy() sample = df label:List[float] = pipe.predict(sample).tolist() From 205b5584a50794d1afae5d335be26fa551499033 Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Mon, 13 Apr 2026 16:26:29 +0600 Subject: [PATCH 10/16] Modify path filtering configuration in the CI scripts --- .github/workflows/docker.yml | 5 +---- .github/workflows/python-app.yml | 9 +++------ 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 24acda5..848ae4f 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -24,10 +24,7 @@ jobs: - "Dockerfile" - ".dockerignore" - "requirements.txt" - - "fit.py" - - "app.py" - - "templates/**" - - "static/**" + - "app/**" - "models/**" docker-build: diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 4b6c79c..12230d6 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -24,13 +24,10 @@ jobs: with: filters: | src: - - ".github/workflows/python-app.yml" - - "data/**" + - ".github/workflows/docker.yml" + - "requirements.txt" + - "app/**" - "models/**" - - "static/**" - - "templates/**" - - "app.py" - - "fit.py" build: needs: changes From 2eaa837c466935be932be0675c3e0423d9f36fc5 Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Mon, 13 Apr 2026 17:26:29 +0600 Subject: [PATCH 11/16] Replace older gunicorn setup with uvicorn for fastapi --- .github/workflows/python-app.yml | 10 +++++----- Dockerfile | 11 +++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 12230d6..57a5d79 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -51,9 +51,9 @@ jobs: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Run Flask app and test with Curl + - name: Run FastAPI app and test with Curl run: | - nohup python app.py & - sleep 95 - curl -I http://127.0.0.1:5000 - pkill -f "python app.py" + nohup uvicorn app.app:app --host 0.0.0.0 --port 8000 & + sleep 30 + curl -I http://127.0.0.1:8000/health + pkill -f "uvicorn" diff --git a/Dockerfile b/Dockerfile index 8e1ec5f..3f2108d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# Dockerfile (exoplanet_classifier) +# Dockerfile (TransitIQ) FROM python:3.11-slim # Avoid interactive prompts @@ -14,10 +14,9 @@ RUN pip install --upgrade pip && pip install -r requirements.txt # copy app source COPY . . -# example env and port -ENV FLASK_APP=app.py -EXPOSE 5000 +# expose port +EXPOSE 8000 -# run the flask app -CMD ["python", "-m", "gunicorn", "--bind", "0.0.0.0:5000", "app:app", "--workers", "2"] +# run the fastapi app +CMD ["uvicorn", "app.app:app", "--host", "0.0.0.0", "--port", "8000"] From 145c1501ec4767ea87b1e2e9b1ca25a5d4903f60 Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Mon, 13 Apr 2026 17:28:32 +0600 Subject: [PATCH 12/16] Update the dependency list by getting rid of no-longer required libraries --- requirements.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4dbfaad..8a8e2ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,10 +10,8 @@ docutils==0.22.4 dotenv==0.9.9 fastapi==0.135.3 filelock==3.25.2 -Flask==3.1.2 fonttools==4.61.1 fsspec==2026.3.0 -gunicorn==23.0.0 h11==0.16.0 hf-xet==1.4.3 httpcore==1.0.9 @@ -70,5 +68,4 @@ typing_extensions==4.15.0 tzdata==2025.3 uvicorn==0.44.0 websockets==16.0 -Werkzeug==3.1.5 xgboost==3.1.3 From 6bb216b578124bd7768d198ef9a01c34b09f4f74 Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Mon, 13 Apr 2026 17:28:46 +0600 Subject: [PATCH 13/16] Delete older flask-based app --- app/prv_app.py | 118 ------------------------------------------------- 1 file changed, 118 deletions(-) delete mode 100644 app/prv_app.py diff --git a/app/prv_app.py b/app/prv_app.py deleted file mode 100644 index 2fe595e..0000000 --- a/app/prv_app.py +++ /dev/null @@ -1,118 +0,0 @@ -import os -import joblib -import pandas as pd -import numpy as np -from flask import Flask, render_template, request, jsonify -from models.download_from_hf import download - -# --- Configuration --- -MODEL_DIR = "models" -PIPE_PATH = os.path.join(MODEL_DIR, "pipe.pkl") -COLUMNS_PATH = os.path.join(MODEL_DIR, "column_names.pkl") -reverse_mapping = {0: "FALSE POSITIVE", 1: "CANDIDATE", 2: "CONFIRMED"} - -# --- Self-Heal Function --- -def initialize_artifacts(): - """ - Checks if model artifacts exist. If not, runs the training script. - """ - # 1. Ensure the model directory exists - os.makedirs(MODEL_DIR, exist_ok=True) - - # 2. Check for missing files - pipe_exists = os.path.exists(PIPE_PATH) - columns_exists = os.path.exists(COLUMNS_PATH) - - if not pipe_exists or not columns_exists: - print("--- MODEL ARTIFACTS MISSING ---") - if not pipe_exists: - print(f"Missing: {PIPE_PATH}") - if not columns_exists: - print(f"Missing: {COLUMNS_PATH}") - - print("Downloading the saved models from Hugging Face... This may take a moment.") - try: - # Run the `download` function from `models/download_from_hf.py` - download() - print("Download complete. Artifacts generated successfully.") - print("---------------------------------") - except Exception as e: - print(f"\nFATAL: Error during self-heal downloading: {e}") - print("Application cannot start without model artifacts. Exitting......") - exit(1) # Exit if training fails - else: - print("Model artifacts found. Loading...") - -# --- Application Startup --- - -# Run the self-heal check *before* loading models -initialize_artifacts() - -# Load models -try: - pipe = joblib.load(PIPE_PATH) - column_names = joblib.load(COLUMNS_PATH) - print("Models loaded successfully.") -except Exception as e: - print(f"\nFATAL: Error loading model artifacts: {e}") - print("Files might be corrupt. Try deleting the 'models' directory and restarting.") - exit(1) # Exit if loading fails - -# Initialize Flask App -app = Flask(__name__) - -@app.route("/") -def home(): - return render_template("index.html") - -@app.route("/about") -def about(): - return render_template("about.html") - -@app.route("/predict", methods=["POST"]) -def predict(): - try: - # Extract features from the JSON request - raw_features = [ - request.json["orbital-period"], - request.json["transit-epoch"], - request.json["transit-depth"], - request.json["planet-radius"], - request.json["semi-major-axis"], - request.json["inclination"], - request.json["equilibrium-temp"], - request.json["insolation-flux"], - request.json["impact-parameter"], - request.json["radius-ratio"], - request.json["stellar-density"], - request.json["star-distance"], - request.json["num-transits"], - ] - - # Create DataFrame with correct column names - df = pd.DataFrame([raw_features], columns=column_names) - - # Get prediction and probabilities - pred = int(pipe.predict(df)[0]) - proba = pipe.predict_proba(df)[0] - - # Format probabilities for the response - proba_dict = { - reverse_mapping[i]: round(p, 3) for i, p in enumerate(proba) - } - - # Send response - return jsonify( - {"prediction": reverse_mapping[pred], "probabilities": proba_dict} - ) - - except KeyError as e: - print(f"Prediction Error: Missing key in request {e}") - return jsonify({"error": f"Missing feature in request: {e}"}), 400 - except Exception as e: - print(f"Prediction Error: {e}") - return jsonify({"error": str(e)}), 400 - - -if __name__ == "__main__": - app.run(debug=True) \ No newline at end of file From 38a08171edca753a02b67eb81505f37274751402 Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Mon, 13 Apr 2026 17:31:04 +0600 Subject: [PATCH 14/16] Link the existing vanilla frontend with the fastapi app, implement notification system and improve error handling for form submissions --- README.md | 41 +++-- app/app.py | 11 +- app/static/css/style.css | 66 ++++++++ app/static/js/main.js | 68 ++++++++- app/static/script.js | 87 ----------- app/templates/about.html | 320 ++++++++++++++++++++++----------------- app/templates/base.html | 43 ------ app/templates/index.html | 288 ++++++++++++++++++++--------------- 8 files changed, 499 insertions(+), 425 deletions(-) delete mode 100644 app/static/script.js delete mode 100644 app/templates/base.html diff --git a/README.md b/README.md index 18ce32a..f9a9757 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ This version blends the strengths of **ensemble learning** with extensive prepro NASA’s exoplanet survey missions (Kepler, K2, and others) have generated thousands of data points using **the transit method** β€” tracking dips in starlight caused by orbiting planets. These datasets contain both **confirmed exoplanets** and **false positives**, and the aim of this project is to build an AI classifier capable of making preliminary predictions on new candidates. -The classifier runs inside a **Flask-powered web interface**, allowing anyone β€” from students to researchers β€” to enter transit parameters and instantly receive a prediction. +The classifier runs inside a **FastAPI-powered web interface**, allowing anyone β€” from students to researchers β€” to enter transit parameters and instantly receive a prediction. The goal is to provide a *scientifically meaningful, intuitive, and educational experience* for users interested in exoplanet research. @@ -68,8 +68,8 @@ The goal is to provide a *scientifically meaningful, intuitive, and educational - **Scikit-learn** – Pipeline, scaling, imputation, model stacking, metrics - **XGBoost** – Gradient boosting-based sub-model for ensemble - **Imbalanced-learn (SMOTE)** – Class balancing for improved fairness -- **Flask** – Backend web framework -- **HTML/CSS/JavaScript** – Frontend for the interactive web UI +- **FastAPI** – Backend web framework +- **HTML/CSS/JavaScript (Vanilla)** – Frontend for the interactive web UI - **Jupyter Notebook** – Used as a sandbox (`research.ipynb`) to experiment with different model architectures, hyperparameters, and feature engineering before finalizing `fit.py`. --- @@ -93,14 +93,14 @@ cd "TransitIQ" pip install -r requirements.txt ``` -3. **Run the Flask app** +3. **Run the FastAPI app** ```bash -python app.py +uvicorn app.app:app --host 0.0.0.0 --port 8000 ``` -4. Open your browser and go to `http://127.0.0.1:5000` to access the web interface. +4. Open your browser and go to `http://127.0.0.1:8000` to access the web interface. -5. If you want to close the server, press `Ctrl + C` in the terminal where you have run `app.py` from. +5. If you want to close the server, press `Ctrl + C` in the terminal. --- @@ -114,14 +114,14 @@ The image is built on both ARM64 and AMD64 architectures, so that it can run on 2. Open Terminal and run: ```bash docker pull bytebard101/exoplanet_classifier -docker run --rm -p 5000:5000 bytebard101/exoplanet_classifier:latest +docker run --rm -p 8000:8000 bytebard101/exoplanet_classifier:latest ``` 3. If your machine faces a port conflict, you will need to assign another port. Try to run this: ```bash -docker run --rm -p 5001:5000 bytebard101/exoplanet_classifier:latest +docker run --rm -p 8001:8000 bytebard101/exoplanet_classifier:latest ``` > If you followed Step 2 and the command ran successfully, then **DO NOT** follow this step. -4. The app will be live at localhost:5000. Open your browser and navigate to [http://127.0.0.1:5000](http://127.0.0.1:5000/) (or [http://127.0.0.1:5001](http://127.0.0.1:5000/) if you followed Step 3). +4. The app will be live at localhost:8000. Open your browser and navigate to [http://127.0.0.1:8000](http://127.0.0.1:8000/) (or [http://127.0.0.1:8001](http://127.0.0.1:8001/) if you followed Step 3). Check [Docker Documentation](https://docs.docker.com/) to learn more about Docker and it's commands. @@ -146,31 +146,28 @@ Check [Docker Documentation](https://docs.docker.com/) to learn more about Docke TransitIQ/ β”œβ”€β”€ .github/ # Folder for GitHub actions β”‚ +β”œβ”€β”€ app/ # FastAPI Application +β”‚ β”œβ”€β”€ schema/ # Pydantic schemas +β”‚ β”œβ”€β”€ static/ # Static assets (CSS, JS, images) +β”‚ β”œβ”€β”€ templates/ # HTML templates (served as static) +β”‚ └── app.py # Main FastAPI entry point +β”‚ β”œβ”€β”€ data/ β”‚ β”œβ”€β”€ k2_data.csv β”‚ β”œβ”€β”€ kepler_data.csv β”‚ └── source.txt β”‚ β”œβ”€β”€ models/ -β”‚ β”œβ”€β”€ column_names.pkl # Not included in the repo, run fit.py to generate +β”‚ β”œβ”€β”€ column_names.pkl β”‚ β”œβ”€β”€ info.txt -β”‚ └── pipe.pkl # Not included in the repo, run fit.py to generate +β”‚ └── pipe.pkl β”‚ β”œβ”€β”€ screenshots/ β”‚ -β”œβ”€β”€ static/ -β”‚ β”œβ”€β”€ materials/ -β”‚ └── script.js -β”‚ -β”œβ”€β”€ templates/ -β”‚ β”œβ”€β”€ about.html -β”‚ └── index.html -β”‚ β”œβ”€β”€ .gitignore -β”œβ”€β”€ app.py β”œβ”€β”€ fit.py β”œβ”€β”€ LICENSE -β”œβ”€β”€ README.md # You're reading it now +β”œβ”€β”€ README.md β”œβ”€β”€ requirements.txt └── research.ipynb ``` diff --git a/app/app.py b/app/app.py index 184bbde..35548c0 100644 --- a/app/app.py +++ b/app/app.py @@ -115,8 +115,7 @@ def health(): @app.get("/") def home(): - msg = "Welcome to TransitIQ !" - return JSONResponse(content=msg,status_code=200) + return FileResponse(INDEX_PATH) @app.get("/about") def about(): @@ -138,7 +137,7 @@ def predict_with_manual_inputs( if column_names[i] == key: sample.append(val) else: - raise ValueError("The payload does not match the original order defined by the BaseModel") + raise ValueError(f"Payload key {key} does not match expected column {column_names[i]}") sample = np.array(sample).reshape(1,-1) label = int(pipe.predict(sample)[0]) @@ -147,9 +146,9 @@ def predict_with_manual_inputs( label:str = reverse_mapping.get(label) proba:dict = {cls:round(proba,3) for cls,proba in zip(reverse_mapping.values(),proba)} msg = { - "status":"prediction successful", - "predicted_label":label, - "predction_probability":proba + "status":"success", + "prediction":label, + "probabilities":proba } return JSONResponse(status_code=201,content=msg) diff --git a/app/static/css/style.css b/app/static/css/style.css index 8073bc7..16859ed 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -482,6 +482,72 @@ h1, h2, h3, h4, h5, h6 { background: rgba(255, 255, 255, 0.02); } +/* Notification System */ +#notification-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 3000; + display: flex; + flex-direction: column; + gap: 10px; +} + +.notification { + width: 320px; + padding: 12px 16px; + border-radius: 14px; + background: rgba(30, 30, 30, 0.7); + backdrop-filter: blur(25px) saturate(180%); + -webkit-backdrop-filter: blur(25px) saturate(180%); + border: 1px solid rgba(255, 255, 255, 0.1); + color: var(--text-main); + box-shadow: 0 15px 35px rgba(0, 0, 0, 0.35), 0 5px 15px rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + gap: 12px; + transform: translateX(calc(100% + 40px)); + transition: transform 0.5s cubic-bezier(0.19, 1, 0.22, 1); + font-weight: 500; + font-size: 0.85rem; +} + +.notification.active { + transform: translateX(0); +} + +.notification.success { + border-left: 4px solid #00ff88; +} + +.notification.error { + border-left: 4px solid #ff4b2b; +} + +.notification.info { + border-left: 4px solid var(--accent-primary); +} + +.notification-icon { + font-size: 1.2rem; +} + +.notification.success .notification-icon { color: #00ff88; } +.notification.error .notification-icon { color: #ff4b2b; } +.notification.info .notification-icon { color: var(--accent-primary); } + +/* Header Link Fix */ +.site-title-link { + display: flex; + align-items: center; + gap: 1rem; + transition: var(--transition-fast); +} + +.site-title-link:hover { + opacity: 0.8; +} + /* ========================================= Responsive Design ========================================= */ diff --git a/app/static/js/main.js b/app/static/js/main.js index de0f46e..b89752e 100644 --- a/app/static/js/main.js +++ b/app/static/js/main.js @@ -27,6 +27,54 @@ document.addEventListener('DOMContentLoaded', () => { }); }); + // Field Labels for Human-Readable Errors + const fieldLabels = { + 'koi_period': 'Orbital Period', + 'koi_time0bk': 'Transit Epoch', + 'koi_depth': 'Transit Depth', + 'koi_prad': 'Planet Radius', + 'koi_sma': 'Semi-Major Axis', + 'koi_incl': 'Inclination', + 'koi_teq': 'Equilibrium Temp', + 'koi_insol': 'Insolation Flux', + 'koi_impact': 'Impact Parameter', + 'koi_ror': 'Planet/Star Radius Ratio', + 'koi_srho': 'Stellar Density', + 'koi_dor': 'Planet-Star Distance', + 'koi_num_transits': 'Number of Transits' + }; + + // Notification System + function showNotification(message, type = 'info') { + const container = document.getElementById('notification-container'); + if (!container) return; + + const notification = document.createElement('div'); + notification.className = `notification ${type}`; + + const icons = { + success: 'fa-circle-check', + error: 'fa-circle-exclamation', + info: 'fa-circle-info' + }; + + notification.innerHTML = ` + + ${message} + `; + + container.appendChild(notification); + + // Animate in + setTimeout(() => notification.classList.add('active'), 10); + + // Remove after 5 seconds + setTimeout(() => { + notification.classList.remove('active'); + setTimeout(() => notification.remove(), 400); + }, 5000); + } + // Form Submission const form = document.getElementById('predictForm'); const modal = document.getElementById('resultModal'); @@ -61,17 +109,31 @@ document.addEventListener('DOMContentLoaded', () => { const data = await response.json(); - if (data.error) { - throw new Error(data.error); + if (!response.ok) { + let errorMsg = 'An error occurred during prediction.'; + if (data.detail) { + if (Array.isArray(data.detail)) { + errorMsg = data.detail.map(d => { + // Map technical field names to human labels + const field = d.loc[d.loc.length - 1]; + const label = fieldLabels[field] || field; + return `${label}: ${d.msg}`; + }).join('\n'); + } else { + errorMsg = data.detail; + } + } + throw new Error(errorMsg); } // Populate Results displayResults(data); openModal(); + showNotification('Analysis complete! Check the results.', 'success'); } catch (error) { console.error('Error:', error); - alert('An error occurred during prediction. Please check your inputs.'); + showNotification(error.message, 'error'); } finally { // Reset Button submitBtn.innerHTML = originalBtnText; diff --git a/app/static/script.js b/app/static/script.js deleted file mode 100644 index 3bdb2b8..0000000 --- a/app/static/script.js +++ /dev/null @@ -1,87 +0,0 @@ -function toPascalCase(str) { - return str - .split(" ") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) - .join(""); -} - - -document - .getElementById("predictForm") - .addEventListener("submit", async function (e) { - e.preventDefault(); - - let inputs = document.querySelectorAll("#predictForm input"); - let features = []; - inputs.forEach((input) => features.push(parseFloat(input.value))); - - let res = await fetch("/predict", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ features: features }), - }); - - let data = await res.json(); - const modal = document.getElementById("resultModal"); - const resultsBox = document.getElementById("resultsBox"); - - if (data.prediction) { - let probText = ""; - if (data.probabilities) { - probText = "

Class Probabilities:

"; - for (let key in data.probabilities) { - probText += ` -
- ${toPascalCase(key)}: ${( - data.probabilities[key] * 100 - ).toFixed(2)}% -
-
-
-
- `; - } - } - resultsBox.innerHTML = `

Prediction: ${data.prediction - .toLowerCase() - .split(" ") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join("")}

${probText}`; - } else { - resultsBox.innerHTML = `

Error: ${data.error}

`; - } - modal.classList.remove("hidden"); - }); -document.getElementById("closeModal").addEventListener("click", () => { - document.getElementById("resultModal").classList.add("hidden"); -}); - -resultsBox.classList.remove("show"); -void resultsBox.offsetWidth; -resultsBox.classList.add("show"); -const formLabel = document.querySelector(".form-label"); -const formInputs = document.querySelectorAll("#predictForm input"); - -formInputs.forEach((input) => { - input.addEventListener("input", () => { - const anyFilled = Array.from(formInputs).some((i) => i.value.trim() !== ""); - formLabel.textContent = anyFilled ? "Remove details" : "Enter the details"; - formLabel.style.cursor = anyFilled ? "pointer" : "default"; - }); -}); - -formLabel.addEventListener("click", () => { - if (formLabel.textContent === "Remove details") { - formInputs.forEach((input) => (input.value = "")); - formLabel.textContent = "Enter the details"; - } -}); - -document.querySelector("#animated-btn").addEventListener("click", (e) => { - e.preventDefault(); // prevent default if it's a link - document.querySelector("#form-section").scrollIntoView({ - behavior: "smooth", - }); -}); diff --git a/app/templates/about.html b/app/templates/about.html index 840f524..1b00186 100644 --- a/app/templates/about.html +++ b/app/templates/about.html @@ -1,147 +1,187 @@ -{% extends "base.html" %} + + + + + + About - TransitIQ + + + + + + + + + + + + + -{% block title %}About - TransitIQ{% endblock %} + + -{% block content %} -
-
-

About the Project

-

- The TransitIQ is an advanced machine learning tool developed for exoplanet classification research. - It leverages state-of-the-art ensemble algorithms to assist astronomers in identifying potential exoplanets. -

-
+
-
-

Objective

-

- The primary goal is to automate the classification of transit signals. By analyzing light curves and orbital parameters, - the model categorizes candidates into three distinct classes: -

-
    -
  • CONFIRMED (2): Verified exoplanets.
  • -
  • CANDIDATE (1): Potential planets requiring further observation.
  • -
  • FALSE POSITIVE (0): Signals caused by other astrophysical phenomena.
  • -
-
+ +
+
+
+

About the Project

+

+ The TransitIQ is an advanced machine learning tool developed for exoplanet classification research. + It leverages state-of-the-art ensemble algorithms to assist astronomers in identifying potential exoplanets. +

+
-
-

Technical Architecture

-

- Built on Scikit-learn, the system employs a Stacking Ensemble Classifier. - This combines the strengths of Random Forest and XGBoost, - orchestrated by a Logistic Regression meta-classifier. -

- The backend is powered by Flask, serving a robust API that processes user inputs - and returns real-time predictions with probability confidence scores. -

-
+
+

Objective

+

+ The primary goal is to automate the classification of transit signals. By analyzing light curves and orbital parameters, + the model categorizes candidates into three distinct classes: +

+
    +
  • CONFIRMED (2): Verified exoplanets.
  • +
  • CANDIDATE (1): Potential planets requiring further observation.
  • +
  • FALSE POSITIVE (0): Signals caused by other astrophysical phenomena.
  • +
+
-
-

Input Parameters

-

The model requires 13 specific orbital and transit features derived from NASA's Kepler mission data:

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FeatureUnitDescription
Orbital PeriodDaysTime taken to complete one full orbit.
Transit EpochBJDTime of the center of the first transit.
Transit DepthppmFraction of stellar flux lost during transit.
Planet RadiusEarth RadiiEstimated radius of the planet.
Semi-Major AxisAUAverage distance from the host star.
InclinationDegreesAngle of the orbital plane.
Equilibrium TempKelvinTheoretical surface temperature.
Insolation FluxEarth FluxIncident solar radiation.
Impact Parameter-Sky-projected distance at conjunction.
Radius Ratio-Ratio of planet radius to star radius.
Stellar Densityg/cmΒ³Density of the host star.
Planet-Star DistStellar RadiiDistance scaled by star size.
Num TransitsCountTotal number of transit events observed.
-
-
+
+

Technical Architecture

+

+ Built on Scikit-learn, the system employs a Stacking Ensemble Classifier. + This combines the strengths of Random Forest and XGBoost, + orchestrated by a Logistic Regression meta-classifier. +

+ The backend is powered by FastAPI, serving a robust API that processes user inputs + and returns real-time predictions with probability confidence scores. +

+
-
-

Disclaimer

-

- While this model achieves high accuracy on the validation set, no ML model is infallible. - Predictions should be used as a preliminary screening tool rather than absolute confirmation. - The model is optimized for Kepler-like data distributions. -

-
+
+

Input Parameters

+

The model requires 13 specific orbital and transit features derived from NASA's Kepler mission data:

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FeatureUnitDescription
Orbital PeriodDaysTime taken to complete one full orbit.
Transit EpochBJDTime of the center of the first transit.
Transit DepthppmFraction of stellar flux lost during transit.
Planet RadiusEarth RadiiEstimated radius of the planet.
Semi-Major AxisAUAverage distance from the host star.
InclinationDegreesAngle of the orbital plane.
Equilibrium TempKelvinTheoretical surface temperature.
Insolation FluxEarth FluxIncident solar radiation.
Impact Parameter-Sky-projected distance at conjunction.
Radius Ratio-Ratio of planet radius to star radius.
Stellar Densityg/cmΒ³Density of the host star.
Planet-Star DistStellar RadiiDistance scaled by star size.
Num TransitsCountTotal number of transit events observed.
+
+
-
-

- - View Source on GitHub - -

-

- © 2025 TransitIQ. Licensed under MIT. -

- - Back to TransitIQ - -
-
-{% endblock %} \ No newline at end of file +
+

Disclaimer

+

+ While this model achieves high accuracy on the validation set, no ML model is infallible. + Predictions should be used as a preliminary screening tool rather than absolute confirmation. + The model is optimized for Kepler-like data distributions. +

+
+ +
+

+ + View Source on GitHub + +

+

+ © 2025 TransitIQ. Licensed under MIT. +

+ + Back to TransitIQ + +
+
+ + + + + + \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html deleted file mode 100644 index b51aed6..0000000 --- a/app/templates/base.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - {% block title %}TransitIQ{% endblock %} - - - - - - - - - - - - - - - - - - -
- {% block content %}{% endblock %} -
- - - - - diff --git a/app/templates/index.html b/app/templates/index.html index 41a4c27..2542af8 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,132 +1,172 @@ -{% extends "base.html" %} - -{% block title %}Home - TransitIQ{% endblock %} - -{% block content %} - -
-
-

Welcome to the Future

-

- Discover New
- Worlds -

-

- Harness the power of Machine Learning to classify exoplanets. - Input transit data and let our advanced ensemble model determine if it's a - Confirmed Planet, Candidate, or False Positive. -

- -
+ + + + + + Home - TransitIQ -
-
- Exoplanet Art -
-
-
- - -
-
-
-

Input Parameters

-

Enter the transit details below to generate a prediction.

-
- -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+

Welcome to the Future

+

+ Discover New
+ Worlds +

+

+ Harness the power of Machine Learning to classify exoplanets. + Input transit data and let our advanced ensemble model determine if it's a + Confirmed Planet, Candidate, or False Positive. +

+
- -
- - + +
+
+ Exoplanet Art +
- -
- - +
+ + +
+
+
+

Input Parameters

+

Enter the transit details below to generate a prediction.

+
+ + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
- -
- +
+ + + -
- - - -{% endblock %} \ No newline at end of file + + + + \ No newline at end of file From be0b19a23cbd1ed04276f96915b006b21458df34 Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Mon, 13 Apr 2026 19:07:19 +0600 Subject: [PATCH 15/16] Enhance UI with tab navigation, batch prediction functionality, and improved button styles --- app/static/css/style.css | 345 ++++++++++++++++++++++++++++++++++++++- app/static/js/main.js | 272 +++++++++++++++++++++++++++--- app/templates/about.html | 4 +- app/templates/index.html | 300 +++++++++++++++++++++++----------- 4 files changed, 804 insertions(+), 117 deletions(-) diff --git a/app/static/css/style.css b/app/static/css/style.css index 16859ed..cfe57e6 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -180,6 +180,12 @@ h1, h2, h3, h4, h5, h6 { max-width: 600px; } +.hero-buttons { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + .hero-image { flex: 1; display: flex; @@ -258,9 +264,58 @@ h1, h2, h3, h4, h5, h6 { color: var(--text-main); } +.btn-secondary { + background: transparent; + color: var(--text-main); + border: 2px solid var(--accent-primary); + position: relative; + overflow: hidden; + z-index: 1; +} + +.btn-secondary::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 0%; + height: 100%; + background: var(--accent-primary); + transition: var(--transition-smooth); + z-index: -1; +} + +.btn-secondary:hover::before { + width: 100%; +} + +.btn-secondary:hover { + color: var(--text-main); +} + +.btn-secondary-small { + background: transparent; + color: var(--accent-primary); + border: 1px solid var(--accent-primary); + padding: 0.5rem 1rem; + border-radius: var(--radius-sm); + font-weight: 500; + font-size: 0.9rem; + text-decoration: none; + transition: var(--transition-smooth); + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.btn-secondary-small:hover { + background: var(--accent-primary); + color: var(--bg-dark); +} + /* ========================================= - Form Section - ========================================= */ + Form Section + ========================================= */ .form-section { padding: 6rem 0; display: flex; @@ -432,8 +487,290 @@ h1, h2, h3, h4, h5, h6 { } /* ========================================= - About Page Styles - ========================================= */ + Tab Navigation + ========================================= */ +.tab-nav { + display: flex; + gap: 0.5rem; +} + +.tab-link { + background: transparent; + border: none; + color: var(--text-muted); + font-family: 'Poppins', sans-serif; + font-size: 0.95rem; + font-weight: 500; + padding: 0.5rem 1rem; + cursor: pointer; + position: relative; + transition: var(--transition-fast); + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.tab-link::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 0; + height: 2px; + background: var(--accent-primary); + transition: var(--transition-smooth); +} + +.tab-link:hover { + color: var(--text-main); +} + +.tab-link.active { + color: var(--text-main); +} + +.tab-link.active::after { + width: 100%; +} + +.tab-link[href] { + color: var(--text-muted); + text-decoration: none; + font-family: 'Poppins', sans-serif; + font-size: 0.95rem; + font-weight: 500; + padding: 0.5rem 1rem; + position: relative; + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.tab-link[href]:hover { + color: var(--text-main); +} + +/* Tab Content */ +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +/* ========================================= + CSV Requirements + ========================================= */ +.csv-requirements { + background: rgba(0, 188, 255, 0.05); + border: 1px solid rgba(0, 188, 255, 0.2); + border-radius: var(--radius-md); + padding: 1.5rem; + margin-bottom: 2rem; +} + +.csv-requirements h3 { + color: var(--accent-primary); + margin-bottom: 1rem; + font-size: 1.2rem; +} + +.warning-text { + color: #ffab00; + margin-bottom: 1rem; +} + +.error-text { + color: #ff4b2b; + margin-top: 1rem; + font-size: 0.9rem; +} + +.column-list { + list-style: none; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 0.5rem; +} + +.column-list li { + color: var(--text-muted); + font-size: 0.9rem; +} + +.column-list code { + color: var(--accent-primary); + background: rgba(0, 188, 255, 0.1); + padding: 0.2rem 0.5rem; + border-radius: 4px; + font-family: 'Courier New', monospace; +} + +/* ========================================= + File Upload + ========================================= */ +.batch-form { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.file-upload-wrapper { + position: relative; +} + +.file-upload-label { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + padding: 3rem; + border: 2px dashed var(--border-color); + border-radius: var(--radius-md); + cursor: pointer; + transition: var(--transition-smooth); + background: rgba(255, 255, 255, 0.02); +} + +.file-upload-label:hover { + border-color: var(--accent-primary); + background: rgba(0, 188, 255, 0.05); +} + +.file-upload-label i { + font-size: 3rem; + color: var(--accent-primary); +} + +.file-upload-label span { + color: var(--text-muted); +} + +.file-upload-wrapper input[type="file"] { + position: absolute; + inset: 0; + opacity: 0; + cursor: pointer; +} + +.file-name { + margin-top: 1rem; + color: var(--text-main); + font-size: 0.9rem; +} + +/* ========================================= + Large Modal for Batch + ========================================= */ +.modal-large { + max-width: 800px; + max-height: 85vh; + overflow-y: auto; +} + +.batch-summary { + margin-bottom: 2rem; + text-align: center; +} + +.batch-summary h4 { + color: var(--text-muted); + margin-bottom: 1rem; + font-size: 1rem; + text-transform: uppercase; + letter-spacing: 1px; +} + +/* Pie Chart */ +.pie-chart-container { + display: flex; + justify-content: center; + align-items: center; + gap: 2rem; + flex-wrap: wrap; +} + +.pie-chart { + width: 180px; + height: 180px; + border-radius: 50%; + position: relative; +} + +.pie-legend { + display: flex; + flex-direction: column; + gap: 0.8rem; +} + +.pie-legend-item { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9rem; +} + +.pie-legend-color { + width: 16px; + height: 16px; + border-radius: 4px; +} + +/* Batch Result Table */ +.batch-table-wrapper { + margin-top: 1.5rem; +} + +.batch-table-wrapper h4 { + color: var(--text-muted); + margin-bottom: 1rem; + font-size: 1rem; + text-transform: uppercase; + letter-spacing: 1px; +} + +.batch-result-table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; +} + +.batch-result-table th, +.batch-result-table td { + padding: 0.8rem; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +.batch-result-table th { + background: rgba(0, 188, 255, 0.1); + color: var(--accent-primary); + font-weight: 600; + position: sticky; + top: 0; +} + +.batch-result-table tr:hover td { + background: rgba(255, 255, 255, 0.02); +} + +.batch-result-table .prediction-cell { + font-weight: 600; +} + +.class-confirmed { color: #00ff88; } +.class-candidate { color: #ffab00; } +.class-false { color: #ff4b2b; } + +.high-confidence { color: #00ff88; } +.medium-confidence { color: #ffab00; } +.low-confidence { color: #ff4b2b; } + +/* ========================================= + About Page Styles + ========================================= */ .about-content { background: var(--bg-card); border: 1px solid var(--glass-border); diff --git a/app/static/js/main.js b/app/static/js/main.js index b89752e..0380a12 100644 --- a/app/static/js/main.js +++ b/app/static/js/main.js @@ -1,19 +1,59 @@ document.addEventListener('DOMContentLoaded', () => { - // Smooth Scroll + // Tab Switching + const tabLinks = document.querySelectorAll('.tab-link:not([href])'); + const tabContents = document.querySelectorAll('.tab-content'); + + tabLinks.forEach(link => { + link.addEventListener('click', () => { + const targetTab = link.dataset.tab; + + tabLinks.forEach(l => l.classList.remove('active')); + tabContents.forEach(c => c.classList.remove('active')); + + link.classList.add('active'); + document.getElementById(`${targetTab}-tab`).classList.add('active'); + }); + }); + + // Smooth Scroll to Form (from Welcome tab) const scrollBtn = document.getElementById('scroll-to-form'); if (scrollBtn) { scrollBtn.addEventListener('click', (e) => { e.preventDefault(); - document.getElementById('form-section').scrollIntoView({ - behavior: 'smooth' - }); + // Switch to Predict tab first + tabLinks.forEach(l => l.classList.remove('active')); + tabContents.forEach(c => c.classList.remove('active')); + document.querySelector('[data-tab="home"]').classList.add('active'); + document.getElementById('home-tab').classList.add('active'); + // Then scroll to form + setTimeout(() => { + document.getElementById('form-section').scrollIntoView({ + behavior: 'smooth' + }); + }, 50); + }); + } + + // Go to Batch Tab (from Welcome tab) + const batchBtn = document.getElementById('go-to-batch'); + if (batchBtn) { + batchBtn.addEventListener('click', (e) => { + e.preventDefault(); + tabLinks.forEach(l => l.classList.remove('active')); + tabContents.forEach(c => c.classList.remove('active')); + document.querySelector('[data-tab="batch"]').classList.add('active'); + document.getElementById('batch-tab').classList.add('active'); + setTimeout(() => { + document.getElementById('batch-tab').scrollIntoView({ + behavior: 'smooth' + }); + }, 50); }); } // Input Animation & Label Handling const inputs = document.querySelectorAll('.input-field'); inputs.forEach(input => { - // Trigger label animation on load if value exists if (input.value) { input.classList.add('has-value'); } @@ -65,17 +105,15 @@ document.addEventListener('DOMContentLoaded', () => { container.appendChild(notification); - // Animate in setTimeout(() => notification.classList.add('active'), 10); - // Remove after 5 seconds setTimeout(() => { notification.classList.remove('active'); setTimeout(() => notification.remove(), 400); }, 5000); } - // Form Submission + // Form Submission (Single Prediction) const form = document.getElementById('predictForm'); const modal = document.getElementById('resultModal'); const closeModalBtn = document.getElementById('closeModal'); @@ -88,11 +126,9 @@ document.addEventListener('DOMContentLoaded', () => { const submitBtn = form.querySelector('button[type="submit"]'); const originalBtnText = submitBtn.innerHTML; - // Loading State submitBtn.innerHTML = ' Analyzing...'; submitBtn.disabled = true; - // Collect Data const formData = {}; inputs.forEach(input => { formData[input.id] = parseFloat(input.value); @@ -114,7 +150,6 @@ document.addEventListener('DOMContentLoaded', () => { if (data.detail) { if (Array.isArray(data.detail)) { errorMsg = data.detail.map(d => { - // Map technical field names to human labels const field = d.loc[d.loc.length - 1]; const label = fieldLabels[field] || field; return `${label}: ${d.msg}`; @@ -126,7 +161,6 @@ document.addEventListener('DOMContentLoaded', () => { throw new Error(errorMsg); } - // Populate Results displayResults(data); openModal(); showNotification('Analysis complete! Check the results.', 'success'); @@ -135,7 +169,6 @@ document.addEventListener('DOMContentLoaded', () => { console.error('Error:', error); showNotification(error.message, 'error'); } finally { - // Reset Button submitBtn.innerHTML = originalBtnText; submitBtn.disabled = false; } @@ -157,7 +190,6 @@ document.addEventListener('DOMContentLoaded', () => { closeModalBtn.addEventListener('click', closeModal); } - // Close on click outside if (modal) { modal.addEventListener('click', (e) => { if (e.target === modal) { @@ -171,17 +203,13 @@ document.addEventListener('DOMContentLoaded', () => { const predictionEl = document.getElementById('predictionResult'); const barsContainer = document.getElementById('probabilityBars'); - // Set Prediction Text predictionEl.textContent = data.prediction; - // Clear previous bars barsContainer.innerHTML = ''; - // Sort probabilities const sortedProbs = Object.entries(data.probabilities) .sort(([,a], [,b]) => b - a); - // Create Bars sortedProbs.forEach(([label, prob]) => { const percentage = (prob * 100).toFixed(1); @@ -200,10 +228,216 @@ document.addEventListener('DOMContentLoaded', () => { barsContainer.appendChild(item); - // Animate bar after a slight delay setTimeout(() => { item.querySelector('.prob-bar-fill').style.width = `${percentage}%`; }, 100); }); } + + // ========================================= + // Batch Prediction + // ========================================= + + // File input display + const csvFileInput = document.getElementById('csvFile'); + const fileNameDisplay = document.getElementById('fileName'); + + if (csvFileInput) { + csvFileInput.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (file) { + fileNameDisplay.textContent = `Selected: ${file.name}`; + fileNameDisplay.style.color = 'var(--accent-primary)'; + } else { + fileNameDisplay.textContent = ''; + } + }); + } + + // Batch Form Submission + const batchForm = document.getElementById('batchForm'); + const batchModal = document.getElementById('batchResultModal'); + const closeBatchModalBtn = document.getElementById('closeBatchModal'); + + if (batchForm) { + batchForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + const submitBtn = batchForm.querySelector('button[type="submit"]'); + const originalBtnText = submitBtn.innerHTML; + + submitBtn.innerHTML = ' Processing...'; + submitBtn.disabled = true; + + const fileInput = document.getElementById('csvFile'); + const file = fileInput.files[0]; + + if (!file) { + showNotification('Please select a CSV file', 'error'); + submitBtn.innerHTML = originalBtnText; + submitBtn.disabled = false; + return; + } + + const formData = new FormData(); + formData.append('file', file); + + try { + const response = await fetch('/predict/batch', { + method: 'POST', + body: formData + }); + + const data = await response.json(); + + if (!response.ok) { + let errorMsg = 'An error occurred during batch prediction.'; + if (data.detail) { + errorMsg = data.detail; + } + throw new Error(errorMsg); + } + + displayBatchResults(data); + openBatchModal(); + showNotification('Batch prediction complete!', 'success'); + + } catch (error) { + console.error('Error:', error); + showNotification(error.message, 'error'); + } finally { + submitBtn.innerHTML = originalBtnText; + submitBtn.disabled = false; + } + }); + } + + function openBatchModal() { + batchModal.classList.add('active'); + document.body.style.overflow = 'hidden'; + } + + function closeBatchModal() { + batchModal.classList.remove('active'); + document.body.style.overflow = ''; + } + + if (closeBatchModalBtn) { + closeBatchModalBtn.addEventListener('click', closeBatchModal); + } + + if (batchModal) { + batchModal.addEventListener('click', (e) => { + if (e.target === batchModal) { + closeBatchModal(); + } + }); + } + + function displayBatchResults(data) { + const pieChartContainer = document.getElementById('pieChartContainer'); + const tableBody = document.getElementById('batchResultsTableBody'); + + pieChartContainer.innerHTML = ''; + tableBody.innerHTML = ''; + + // Transform API response to results array + // Handle both "prediction_probability" and "predction_probability" (API typo) + const probabilities = data.prediction_probability || data.predction_probability || []; + + if (!data.predicted_labels || !Array.isArray(data.predicted_labels) || probabilities.length === 0) { + console.error('Invalid API response:', data); + showNotification('Invalid response from server', 'error'); + return; + } + + const results = data.predicted_labels.map((prediction, index) => { + const probs = probabilities[index] || []; + const maxProb = probs.length > 0 ? Math.max(...probs) : 0; + return { prediction, confidence: maxProb }; + }); + + // Count predictions by class + const classCounts = {}; + results.forEach(result => { + classCounts[result.prediction] = (classCounts[result.prediction] || 0) + 1; + }); + + const total = results.length; + + // Colors for each class + const classColors = { + 'CONFIRMED': '#00ff88', + 'CANDIDATE': '#ffab00', + 'FALSE POSITIVE': '#ff4b2b' + }; + + // Calculate angles for pie chart + let currentAngle = 0; + const conicGradientParts = []; + + Object.entries(classCounts).forEach(([className, count]) => { + const percentage = (count / total) * 100; + const angle = (count / total) * 360; + const startAngle = currentAngle; + const endAngle = currentAngle + angle; + const color = classColors[className] || '#888'; + + conicGradientParts.push(`${color} ${startAngle}deg ${endAngle}deg`); + currentAngle = endAngle; + }); + + // Create pie chart + const pieChart = document.createElement('div'); + pieChart.className = 'pie-chart'; + pieChart.style.background = `conic-gradient(${conicGradientParts.join(', ')})`; + pieChartContainer.appendChild(pieChart); + + // Create legend + const legend = document.createElement('div'); + legend.className = 'pie-legend'; + + Object.entries(classCounts).forEach(([className, count]) => { + const percentage = ((count / total) * 100).toFixed(1); + const color = classColors[className] || '#888'; + + const legendItem = document.createElement('div'); + legendItem.className = 'pie-legend-item'; + legendItem.innerHTML = ` +
+ ${className}: ${count} (${percentage}%) + `; + legend.appendChild(legendItem); + }); + + pieChartContainer.appendChild(legend); + + // Populate table + results.forEach((result, index) => { + const row = document.createElement('tr'); + + const confidenceClass = getConfidenceClass(result.confidence); + const predictionClass = getPredictionClass(result.prediction); + + row.innerHTML = ` + ${index + 1} + ${result.prediction} + ${(result.confidence * 100).toFixed(1)}% + `; + + tableBody.appendChild(row); + }); + } + + function getConfidenceClass(confidence) { + if (confidence >= 0.8) return 'high-confidence'; + if (confidence >= 0.5) return 'medium-confidence'; + return 'low-confidence'; + } + + function getPredictionClass(prediction) { + if (prediction === 'CONFIRMED') return 'class-confirmed'; + if (prediction === 'CANDIDATE') return 'class-candidate'; + return 'class-false'; + } }); diff --git a/app/templates/about.html b/app/templates/about.html index 1b00186..2120401 100644 --- a/app/templates/about.html +++ b/app/templates/about.html @@ -11,7 +11,7 @@ - + @@ -30,7 +30,7 @@

diff --git a/app/templates/index.html b/app/templates/index.html index 2542af8..7693b1d 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -11,7 +11,7 @@ - + @@ -29,8 +29,19 @@

-