From 5b9229ed4c0668a3847e2b3ddecbdc50327cefe3 Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Fri, 10 Apr 2026 16:45:53 +0600 Subject: [PATCH 01/10] Remove `pyarrow` and add `python-multipart` in `requirements.txt` --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 094571c..ef69195 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,13 +35,13 @@ pandas==2.3.3 parso==0.8.6 pillow==12.1.0 psutil==7.2.2 -pyarrow==23.0.1 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-multipart==0.0.24 pytz==2025.2 PyYAML==6.0.3 pyzmq==27.1.0 From e883563d7fe5d51d8f067306cf6797d98fb8853a Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Fri, 10 Apr 2026 16:46:38 +0600 Subject: [PATCH 02/10] Add batch prediction route in `app.py` --- app/app.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/app/app.py b/app/app.py index b3efa14..4d71380 100644 --- a/app/app.py +++ b/app/app.py @@ -1,9 +1,13 @@ from .schema.validation import UserInput -from fastapi import FastAPI, Request , Depends +from fastapi import FastAPI, Request , Depends, UploadFile from fastapi.responses import JSONResponse, FileResponse +from fastapi.exceptions import HTTPException from fastapi.staticfiles import StaticFiles from typing import List, Tuple import numpy as np +import pandas as pd +import shutil +import uuid from sklearn.pipeline import Pipeline import joblib from contextlib import asynccontextmanager @@ -13,8 +17,11 @@ # paths to the pickle files (relative to project root) MODEL_PATH = Path(__file__).parent.parent / "models" / "estimator.pkl" NAMES_PATH = Path(__file__).parent.parent / "models" / "names.pkl" +# Directory for user-uploaded files +UPLOAD_DIR = Path("app","uploads") +UPLOAD_DIR.mkdir(exist_ok=True) -# if pickle files are not found, this function will train the model +# if pickle files are not found, this helper will train the model def ensure_models(): print("🔍 Checking for existing model files...") if not MODEL_PATH.is_file() or not NAMES_PATH.is_file(): @@ -30,6 +37,24 @@ def ensure_models(): print("✅ Model and feature names loaded successfully.") return model, names +# helper for validating the user-uploaded .csv file +def validate_csv(payload:pd.DataFrame, expected_columns:List)-> pd.DataFrame: + expected_columns = [r for r in expected_columns if r != "alert"] + if payload.columns.tolist() != expected_columns: + raise HTTPException( + status_code=422, + detail="Uploaded csv file does not match the expected column configuration" + ) + + try: + df = payload.astype(float) + except Exception: + raise HTTPException( + status_code=422, + detail="Value in the uploaded csv file must be numeric (float-compatible)" + ) + return df + @asynccontextmanager async def lifespan(app:FastAPI): pipe,feat_names = ensure_models() @@ -92,3 +117,38 @@ def predict_things(value:UserInput,dep:Tuple[Pipeline,np.ndarray] = Depends(get_ return JSONResponse( status_code=201, content=msg ) + +@app.post("/predict/batch",status_code=201) +def predict_things_in_batch(payload:UploadFile,dep:Tuple[Pipeline,np.ndarray] = Depends(get_things)): + pipe,feat_names = dep + feat_names:List[str] = feat_names.tolist() + + # Receive and validate incoming upload + valid_exts = [".csv"] + extension = Path(payload.filename).suffix + if extension not in valid_exts: + raise HTTPException( + status_code=422, detail=f"Only `.csv` files are accepted as input, got {extension} instead" + ) + df_prev = pd.read_csv(payload.file) + df: pd.DataFrame = validate_csv(df_prev,feat_names) + + # Run Predictions + user_inp = df.to_numpy() + pred_label:List[float] = pipe.predict(user_inp).tolist() + pred_proba:List[List[float]] = pipe.predict_proba(user_inp).tolist() + + # Postprocessing + label_map = {0: "green", 1: "orange", 2: "red", 3: "yellow"} + pred_label:List[str] = [label_map.get(r) for r in pred_label] + pred_proba = [[round(proba,3) for proba in sample] for sample in pred_proba] + + # Final Output + msg = { + "message": "batch prediction successful", + "prediction": pred_label, + "probabilities": pred_proba + } + return JSONResponse( + status_code=201, content=msg + ) \ No newline at end of file From 843d13898515ba89f3faaad5e11578448bae50b7 Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Fri, 10 Apr 2026 16:47:37 +0600 Subject: [PATCH 03/10] Add sample csv file generator script in `/dataset` --- dataset/__init__.py | 0 dataset/generate_sampled_data.py | 49 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 dataset/__init__.py create mode 100644 dataset/generate_sampled_data.py diff --git a/dataset/__init__.py b/dataset/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dataset/generate_sampled_data.py b/dataset/generate_sampled_data.py new file mode 100644 index 0000000..85c66cc --- /dev/null +++ b/dataset/generate_sampled_data.py @@ -0,0 +1,49 @@ +import pandas as pd +import numpy as np +from pathlib import Path + +np.random.seed(42) + +original_path = Path("dataset","earthquake_data.csv") +output_path = Path("dataset","sample.csv") + +df = pd.read_csv(original_path) + +class_ratios = df['alert'].value_counts(normalize=True) +print(f"Original class distribution:\n{class_ratios}\n") + +sample_fraction = 0.05 + +sampled_dfs = [] +for alert_class in df['alert'].unique(): + class_df = df[df['alert'] == alert_class] + n_samples = max(1, int(len(class_df) * sample_fraction)) + sampled = class_df.sample(n=n_samples, random_state=42) + sampled_dfs.append(sampled) + +sampled_df = pd.concat(sampled_dfs, ignore_index=True) + +numeric_cols = ['magnitude', 'depth', 'cdi', 'mmi', 'sig'] +perturbation_factors = { + 'magnitude': 0.05, + 'depth': 0.1, + 'cdi': 0.15, + 'mmi': 0.15, + 'sig': 0.2 +} + +for col in numeric_cols: + noise = sampled_df[col] * np.random.uniform(-perturbation_factors[col], perturbation_factors[col], size=len(sampled_df)) + sampled_df[col] = sampled_df[col] + noise + if col in ['magnitude', 'depth', 'cdi', 'mmi']: + sampled_df[col] = sampled_df[col].round(1) + else: + sampled_df[col] = sampled_df[col].round(0) + +sampled_df_out = sampled_df.drop(columns=["alert"]) +sampled_df_out.to_csv(output_path, index=False) + +new_class_ratios = sampled_df['alert'].value_counts(normalize=True) +print(f"New dataset class distribution:\n{new_class_ratios}\n") +print(f"Original samples: {len(df)}, New samples: {len(sampled_df)}") +print(f"Saved to: {output_path}") \ No newline at end of file From 59092aa7d6dca2d7c995059d63df84dbc9f1404b Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Fri, 10 Apr 2026 16:49:54 +0600 Subject: [PATCH 04/10] Rename sample generator script and add docstring --- .../{generate_sampled_data.py => sample_generator.py} | 11 +++++++++++ 1 file changed, 11 insertions(+) rename dataset/{generate_sampled_data.py => sample_generator.py} (85%) diff --git a/dataset/generate_sampled_data.py b/dataset/sample_generator.py similarity index 85% rename from dataset/generate_sampled_data.py rename to dataset/sample_generator.py index 85c66cc..55bed07 100644 --- a/dataset/generate_sampled_data.py +++ b/dataset/sample_generator.py @@ -1,3 +1,14 @@ +""" +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 datasets.sample_generator.py +``` +""" + import pandas as pd import numpy as np from pathlib import Path From bb319fd6a3c5b0e40ae1a8147371fc8ce7f0806b Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Fri, 10 Apr 2026 16:56:51 +0600 Subject: [PATCH 05/10] Update .gitignore to ignore dataset/sample.csv --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ef1c844..eb9a66d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ scaler.pkl instance/ shit/ *.db -.vscode/ \ No newline at end of file +.vscode/ +dataset/sample.csv \ No newline at end of file From 202e1ddc9a00e685443d0fcf9efe0a9bb443c7b5 Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Fri, 10 Apr 2026 17:21:55 +0600 Subject: [PATCH 06/10] Add `pytest` in `requirements.txt` --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index ef69195..a30c5b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,6 +16,7 @@ httpcore==1.0.9 httpx==0.28.1 idna==3.11 imbalanced-learn==0.14.1 +iniconfig==2.3.0 itsdangerous==2.2.0 jedi==0.19.2 Jinja2==3.1.6 @@ -34,12 +35,14 @@ packaging==25.0 pandas==2.3.3 parso==0.8.6 pillow==12.1.0 +pluggy==1.6.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 +pytest==9.0.3 python-dateutil==2.9.0.post0 python-multipart==0.0.24 pytz==2025.2 From 51a3e0bcdc8040a36647ba11c7ccda421d578ffe Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Fri, 10 Apr 2026 17:45:28 +0600 Subject: [PATCH 07/10] Implement batch prediction feature with CSV file upload and results display --- app/static/script.js | 165 +++++++++++++++++++++++++++++++++++++++ app/static/style.css | 110 ++++++++++++++++++++++++++ app/templates/index.html | 61 ++++++++++++++- 3 files changed, 335 insertions(+), 1 deletion(-) diff --git a/app/static/script.js b/app/static/script.js index e82fe9e..ce7e701 100644 --- a/app/static/script.js +++ b/app/static/script.js @@ -161,3 +161,168 @@ if (clearBtn && form) { } }); } + +// File input display +const csvFileInput = document.getElementById('csvfile'); +const fileNameDisplay = document.getElementById('fileName'); +if (csvFileInput && fileNameDisplay) { + csvFileInput.addEventListener('change', (e) => { + if (e.target.files.length > 0) { + fileNameDisplay.textContent = e.target.files[0].name; + } + }); +} + +// Batch Prediction Form +const batchForm = document.getElementById('batchForm'); +const batchBtn = document.getElementById('batchPredictBtn'); +const batchLoader = document.getElementById('batchLoader'); + +if (batchForm && batchBtn) { + batchForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + const fileInput = document.getElementById('csvfile'); + if (!fileInput || !fileInput.files.length) { + showToast('Please select a CSV file', 'error'); + return; + } + + const formData = new FormData(); + formData.append('payload', fileInput.files[0]); + + const btnText = batchBtn.querySelector('.btn-text'); + batchBtn.disabled = true; + if (btnText) { + btnText.innerHTML = ' Processing...'; + } + if (batchLoader) { + batchLoader.style.display = 'inline-block'; + } + + try { + const response = await fetch('/predict/batch', { + method: 'POST', + body: formData + }); + + const data = await response.json(); + + if (response.ok) { + showToast('Batch prediction successful!', 'success'); + displayBatchResults(data); + } else if (response.status === 422) { + if (data.detail) { + showToast(data.detail, 'error'); + } else if (Array.isArray(data.detail)) { + data.detail.forEach(err => { + const field = err.loc ? err.loc[err.loc.length - 1] : 'validation'; + showToast(`Validation Error (${field}): ${err.msg}`, 'error'); + }); + } else { + showToast('Validation Error', 'error'); + } + } else { + showToast(data.message || 'An error occurred', 'error'); + } + } catch (error) { + showToast('Network error or server down', 'error'); + } finally { + batchBtn.disabled = false; + if (btnText) { + btnText.innerHTML = ' Run Batch Prediction'; + } + if (batchLoader) { + batchLoader.style.display = 'none'; + } + } + }); +} + +function displayBatchResults(data) { + const container = document.getElementById('batch-result-container'); + if (!container) return; + + const predictions = data.prediction; + const probabilities = data.probabilities; + const count = predictions.length; + + const counts = { green: 0, orange: 0, red: 0, yellow: 0 }; + predictions.forEach(p => { + if (counts[p] !== undefined) counts[p]++; + }); + + let tableRows = ''; + predictions.forEach((pred, i) => { + const probs = probabilities[i]; + const maxProb = Math.max(...probs) * 100; + tableRows += ` + + ${i + 1} + ${pred.toUpperCase()} + ${maxProb.toFixed(1)}% + + `; + }); + + container.innerHTML = ` +
+
+
${count}
+
Total Predictions
+
+
+
${counts.green}
+
Green
+
+
+
${counts.orange}
+
Orange
+
+
+
${counts.red}
+
Red
+
+
+
${counts.yellow}
+
Yellow
+
+
+ + + + + + + + + + ${tableRows} + +
#AlertConfidence
+ `; +} + +// Clear batch button +const clearBatchBtn = document.getElementById('clearBatchBtn'); +if (clearBatchBtn && batchForm) { + clearBatchBtn.addEventListener('click', () => { + const fileInput = document.getElementById('csvfile'); + if (fileInput) fileInput.value = ''; + + const fileNameDisplay = document.getElementById('fileName'); + if (fileNameDisplay) fileNameDisplay.textContent = 'Choose CSV file...'; + + const batchContainer = document.getElementById('batch-result-container'); + if (batchContainer) { + batchContainer.innerHTML = ` +
+
+ +
+

Upload a CSV file and click "Run Batch Prediction"

+
+ `; + } + }); +} diff --git a/app/static/style.css b/app/static/style.css index 4cbe0a9..5059ba7 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -832,3 +832,113 @@ body::before { max-width: 100%; } } + +/* File Upload Styles */ +.file-upload-wrapper { + position: relative; +} + +.file-input { + position: absolute; + width: 0.1px; + height: 0.1px; + opacity: 0; + overflow: hidden; + z-index: -1; +} + +.file-label { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 1rem 1.5rem; + background: var(--bg-input); + border: 2px dashed var(--border); + border-radius: 10px; + color: var(--text-secondary); + font-size: 0.9rem; + cursor: pointer; + transition: all 0.3s ease; +} + +.file-label:hover { + border-color: var(--accent); + color: var(--accent); +} + +.file-input:focus + .file-label { + border-color: var(--accent); + box-shadow: var(--shadow-glow); +} + +.file-label i { + font-size: 1.2rem; +} + +/* Batch Results Table */ +.batch-results-table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; + font-size: 0.85rem; +} + +.batch-results-table th, +.batch-results-table td { + padding: 0.75rem; + text-align: left; + border-bottom: 1px solid var(--border); +} + +.batch-results-table th { + color: var(--text-secondary); + font-weight: 600; + background: var(--bg-tertiary); +} + +.batch-results-table td { + color: var(--text-primary); +} + +.batch-results-table tr:hover td { + background: var(--bg-tertiary); +} + +.batch-results-table .alert-cell { + font-weight: 600; + text-transform: uppercase; +} + +.batch-results-table .alert-cell.green { color: var(--green); } +.batch-results-table .alert-cell.orange { color: var(--orange); } +.batch-results-table .alert-cell.red { color: var(--red); } +.batch-results-table .alert-cell.yellow { color: var(--yellow); } + +.batch-summary { + display: flex; + gap: 1rem; + margin-top: 1rem; + flex-wrap: wrap; +} + +.batch-stat { + flex: 1; + min-width: 80px; + padding: 0.75rem; + background: var(--bg-tertiary); + border-radius: 8px; + text-align: center; +} + +.batch-stat-value { + font-size: 1.25rem; + font-weight: 700; + color: var(--accent); +} + +.batch-stat-label { + font-size: 0.75rem; + color: var(--text-secondary); + margin-top: 0.25rem; +} diff --git a/app/templates/index.html b/app/templates/index.html index 1ed72f5..305535b 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -43,7 +43,66 @@
- + +
+
+

+ + Batch Prediction +

+
+ +
+
+ +
+ + +
+

CSV must contain columns: magnitude, depth, cdi, mmi, sig

+
+ +
+ + +
+
+
+ + +
+
+

+ + Batch Results +

+
+ +
+
+
+ +
+

Upload a CSV file and click "Run Batch Prediction"

+
+
+
+ +

From 416649c47b7eb6eda906e394aa13f67869898f3e Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Fri, 10 Apr 2026 17:45:50 +0600 Subject: [PATCH 08/10] Add new tests to test the batch prediction feature --- tests/test_app.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/test_app.py b/tests/test_app.py index 6f39a8b..2d21b4a 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,4 +1,5 @@ import pytest +import io from fastapi.testclient import TestClient from app.app import app @@ -22,3 +23,45 @@ def test_predict_endpoint_fail_on_missing_data(client): # But since we are in a 'with' block, lifespan will have run. response = client.post("/predict", json={}) assert response.status_code == 422 # Pydantic validation error + +def test_batch_predict_endpoint(client): + # Create a valid CSV file + csv_content = "magnitude,depth,cdi,mmi,sig\n5.5,25.0,6.0,5.5,500\n4.2,10.0,3.5,3.0,100" + csv_file = io.BytesIO(csv_content.encode()) + csv_file.name = "test_data.csv" + + response = client.post( + "/predict/batch", + files={"payload": ("test_data.csv", csv_file, "text/csv")} + ) + + assert response.status_code == 201 + data = response.json() + assert "prediction" in data + assert "probabilities" in data + assert len(data["prediction"]) == 2 + +def test_batch_predict_endpoint_invalid_file(client): + # Upload non-CSV file should fail + text_file = io.BytesIO(b"not a csv") + text_file.name = "test.txt" + + response = client.post( + "/predict/batch", + files={"payload": ("test.txt", text_file, "text/plain")} + ) + + assert response.status_code == 422 + +def test_batch_predict_endpoint_wrong_columns(client): + # CSV with wrong columns should fail validation + csv_content = "wrong_column1,wrong_column2\n1,2\n3,4" + csv_file = io.BytesIO(csv_content.encode()) + csv_file.name = "test_data.csv" + + response = client.post( + "/predict/batch", + files={"payload": ("test_data.csv", csv_file, "text/csv")} + ) + + assert response.status_code == 422 From 103089a77750c110a83bd09df1d645d08bbc7ce4 Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Fri, 10 Apr 2026 18:12:02 +0600 Subject: [PATCH 09/10] Add batch prediction tab with results display and pie chart visualization --- app/static/script.js | 53 ++++++++++++++++ app/static/style.css | 8 +++ app/templates/index.html | 132 ++++++++++++++++++++++----------------- 3 files changed, 134 insertions(+), 59 deletions(-) diff --git a/app/static/script.js b/app/static/script.js index ce7e701..8356281 100644 --- a/app/static/script.js +++ b/app/static/script.js @@ -253,9 +253,11 @@ function displayBatchResults(data) { }); let tableRows = ''; + const predictionDetails = { green: [], orange: [], red: [], yellow: [] }; predictions.forEach((pred, i) => { const probs = probabilities[i]; const maxProb = Math.max(...probs) * 100; + predictionDetails[pred].push(i + 1); tableRows += ` ${i + 1} @@ -266,6 +268,9 @@ function displayBatchResults(data) { }); container.innerHTML = ` +
+ +
${count}
@@ -301,6 +306,51 @@ function displayBatchResults(data) { `; + + const ctx = document.getElementById('batchPieChart'); + if (ctx) { + new Chart(ctx, { + type: 'pie', + data: { + labels: ['Green', 'Orange', 'Red', 'Yellow'], + datasets: [{ + data: [counts.green, counts.orange, counts.red, counts.yellow], + backgroundColor: ['#22c55e', '#f97316', '#ef4444', '#eab308'], + borderColor: ['#22c55e', '#f97316', '#ef4444', '#eab308'], + borderWidth: 1 + }] + }, + options: { + responsive: true, + plugins: { + legend: { + position: 'bottom', + labels: { + color: '#8b949e' + } + }, + tooltip: { + callbacks: { + label: function(context) { + const label = context.label || ''; + const value = context.raw || 0; + const predictionList = predictionDetails[label.toLowerCase()]; + return `${label}: ${value} predictions`; + }, + afterLabel: function(context) { + const label = context.label || ''; + const predictionList = predictionDetails[label.toLowerCase()]; + if (predictionList && predictionList.length > 0) { + return `Rows: ${predictionList.join(', ')}`; + } + return ''; + } + } + } + } + } + }); + } } // Clear batch button @@ -322,6 +372,9 @@ if (clearBatchBtn && batchForm) {

Upload a CSV file and click "Run Batch Prediction"

+ `; } }); diff --git a/app/static/style.css b/app/static/style.css index 5059ba7..5b241fa 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -166,6 +166,7 @@ body::before { .tab-panel { display: none; animation: fadeIn 0.3s ease; + min-height: calc(100vh - 200px); } .tab-panel.active { @@ -942,3 +943,10 @@ body::before { color: var(--text-secondary); margin-top: 0.25rem; } + +.chart-container { + margin: 1.5rem 0; + max-width: 400px; + margin-left: auto; + margin-right: auto; +} diff --git a/app/templates/index.html b/app/templates/index.html index 305535b..162e67f 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -7,6 +7,7 @@ + @@ -24,6 +25,10 @@ Predict + - -

- -
- - -
-
-

- - Batch Results -

-
- -
-
-
- -
-

Upload a CSV file and click "Run Batch Prediction"

-
-
-
-
@@ -248,6 +194,74 @@

+
+ + +
+
+

+ + Batch Prediction +

+
+ +
+
+ +
+ + +
+

CSV must contain columns: magnitude, depth, cdi, mmi, sig

+
+ +
+ + +
+
+
+ + +
+
+

+ + Batch Results +

+
+ +
+
+
+ +
+

Upload a CSV file and click "Run Batch Prediction"

+
+ +
+
+
+

+
From a280aa7117d4d23f3a67f8d495a8b7d8ce5e6b30 Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Fri, 10 Apr 2026 18:21:04 +0600 Subject: [PATCH 10/10] Add 5MB CSV size validation and CSV format guide --- app/static/script.js | 9 ++++++++- app/static/style.css | 33 +++++++++++++++++++++++++++++++++ app/templates/index.html | 7 +++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/app/static/script.js b/app/static/script.js index 8356281..e4ed256 100644 --- a/app/static/script.js +++ b/app/static/script.js @@ -168,7 +168,14 @@ const fileNameDisplay = document.getElementById('fileName'); if (csvFileInput && fileNameDisplay) { csvFileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) { - fileNameDisplay.textContent = e.target.files[0].name; + const file = e.target.files[0]; + if (file.size > 5 * 1024 * 1024) { + showToast('File size exceeds 5MB limit. Please upload a smaller file.', 'error'); + file.value = ''; + fileNameDisplay.textContent = 'Choose CSV file...'; + return; + } + fileNameDisplay.textContent = file.name; } }); } diff --git a/app/static/style.css b/app/static/style.css index 5b241fa..15d0d34 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -950,3 +950,36 @@ body::before { margin-left: auto; margin-right: auto; } + +.batch-help { + margin-top: 1.5rem; + padding: 1rem; + background: var(--bg-tertiary); + border-radius: 10px; + border-left: 3px solid var(--accent); +} + +.batch-help h4 { + font-size: 0.9rem; + font-weight: 600; + color: var(--accent); + margin-bottom: 0.5rem; +} + +.batch-help p { + font-size: 0.8rem; + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +.batch-columns { + padding: 0.5rem; + background: var(--bg-input); + border-radius: 5px; + text-align: center; +} + +.batch-columns code { + color: var(--accent); + font-family: 'JetBrains Mono', monospace; +} diff --git a/app/templates/index.html b/app/templates/index.html index 162e67f..af28527 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -235,6 +235,13 @@

Clear

+ +
+

CSV Format Guide

+

Your CSV file must contain exactly 5 columns in this order:

+

magnitude, depth, cdi, mmi, sig

+

Important: Incorrect column names or order will result in validation errors. Ensure all values are numeric. Files larger than 5MB are not accepted.

+