diff --git a/front-flask/Dockerfile b/front-flask/Dockerfile new file mode 100644 index 0000000..9049640 --- /dev/null +++ b/front-flask/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 5000 + +CMD ["python", "app.py"] diff --git a/front-flask/app.py b/front-flask/app.py new file mode 100644 index 0000000..2c6acc3 --- /dev/null +++ b/front-flask/app.py @@ -0,0 +1,75 @@ +import os + +import requests +from flask import Flask, render_template, request, url_for + +app = Flask(__name__) + +API_HOST = os.environ.get("API_HOST", "http://localhost:8888").rstrip("/") + +CUT_OPTIONS = ["Ideal", "Premium", "Good", "Very Good", "Fair"] +COLOR_OPTIONS = ["E", "I", "J", "H", "F", "G", "D"] +CLARITY_OPTIONS = ["SI2", "SI1", "VS1", "VS2", "VVS2", "VVS1", "I1", "IF"] + + +@app.route("/") +def home(): + return render_template("home.html") + + +@app.route("/predict", methods=["GET", "POST"]) +def predict(): + prediction = None + error = None + form_data = {} + + if request.method == "POST": + form_data = request.form.to_dict() + try: + payload = { + "carat": float(form_data.get("carat", 0.5)), + "cut": form_data.get("cut", "Ideal"), + "color": form_data.get("color", "E"), + "clarity": form_data.get("clarity", "SI1"), + "depth": float(form_data.get("depth", 61.0)), + "table": float(form_data.get("table", 55.0)), + "x": float(form_data.get("x", 0.0)), + "y": float(form_data.get("y", 0.0)), + "z": float(form_data.get("z", 0.0)), + } + response = requests.post( + f"{API_HOST}/predict_one", json=payload, timeout=10 + ) + response.raise_for_status() + result = response.json() + prediction = result.get("price") + if prediction is None: + error = f"API response is missing 'price'. Response: {result}" + except requests.exceptions.ConnectionError: + error = "Prediction API is unreachable. Please make sure the API is running." + except requests.exceptions.Timeout: + error = "Prediction API timed out. Please try again." + except requests.exceptions.HTTPError as exc: + error = f"API returned an error: {exc}" + except (ValueError, KeyError) as exc: + error = f"Failed to parse API response: {exc}" + + return render_template( + "predict.html", + prediction=prediction, + error=error, + form_data=form_data, + cut_options=CUT_OPTIONS, + color_options=COLOR_OPTIONS, + clarity_options=CLARITY_OPTIONS, + ) + + +@app.route("/visualize") +def visualize(): + return render_template("visualize.html", api_host=API_HOST) + + +if __name__ == "__main__": + debug = os.environ.get("FLASK_DEBUG", "false").lower() == "true" + app.run(debug=debug, host="0.0.0.0", port=5000) diff --git a/front-flask/requirements.txt b/front-flask/requirements.txt new file mode 100644 index 0000000..0c2e19b --- /dev/null +++ b/front-flask/requirements.txt @@ -0,0 +1,2 @@ +flask==3.1.1 +requests==2.32.3 diff --git a/front-flask/static/style.css b/front-flask/static/style.css new file mode 100644 index 0000000..314b989 --- /dev/null +++ b/front-flask/static/style.css @@ -0,0 +1,200 @@ +/* ─── Reset & base ─────────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; } + +body { + margin: 0; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: #f8f9fc; + color: #333; +} + +/* ─── Navbar ────────────────────────────────────────────────────── */ +.navbar { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + padding: 0 2rem; + display: flex; + align-items: center; + justify-content: space-between; + height: 60px; + box-shadow: 0 2px 8px rgba(0,0,0,.35); +} + +.navbar-brand { + color: #e0e0ff; + font-size: 1.3rem; + font-weight: 700; + text-decoration: none; + letter-spacing: .5px; +} + +.navbar-links { + list-style: none; + margin: 0; + padding: 0; + display: flex; + gap: 1.5rem; +} + +.navbar-links a { + color: #b0b8e0; + text-decoration: none; + font-size: .95rem; + transition: color .2s; +} + +.navbar-links a:hover, +.navbar-links a.active { + color: #ffffff; +} + +/* ─── Main container ────────────────────────────────────────────── */ +.container { + max-width: 860px; + margin: 2.5rem auto; + padding: 0 1.5rem; +} + +/* ─── Cards ─────────────────────────────────────────────────────── */ +.card { + background: #ffffff; + border-radius: 12px; + padding: 2rem; + box-shadow: 0 2px 12px rgba(0,0,0,.08); + margin-bottom: 1.5rem; +} + +/* ─── Form elements ─────────────────────────────────────────────── */ +.form-group { + margin-bottom: 1.1rem; +} + +.form-group label { + display: block; + font-weight: 600; + margin-bottom: .35rem; + font-size: .9rem; + color: #444; +} + +.form-control { + width: 100%; + padding: .5rem .75rem; + border: 1px solid #ced4da; + border-radius: 6px; + font-size: .95rem; + transition: border-color .2s, box-shadow .2s; + background: #fff; +} + +.form-control:focus { + outline: none; + border-color: #6c63ff; + box-shadow: 0 0 0 3px rgba(108,99,255,.15); +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 1rem; +} + +/* ─── Buttons ───────────────────────────────────────────────────── */ +.btn { + display: inline-block; + padding: .55rem 1.4rem; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: transform .1s, opacity .2s; +} + +.btn:active { transform: scale(.97); } + +.btn-primary { + background: linear-gradient(135deg, #6c63ff, #48dbfb); + color: #fff; +} + +.btn-primary:hover { opacity: .9; } + +/* ─── Alerts ────────────────────────────────────────────────────── */ +.alert { + border-radius: 8px; + padding: .85rem 1.2rem; + margin-bottom: 1.2rem; + font-size: .95rem; +} + +.alert-success { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.alert-error { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +/* ─── Prediction result ─────────────────────────────────────────── */ +.price-tag { + font-size: 2rem; + font-weight: 700; + color: #6c63ff; +} + +/* ─── Logo on home page ─────────────────────────────────────────── */ +.home-logo { + width: 120px; + margin: 1.5rem 0; +} + +/* ─── Rotating cats ─────────────────────────────────────────────── */ +/* + Clockwise rotation (sens horaire): + transform: rotate(360deg) — positive degrees = clockwise in CSS +*/ +@keyframes spin-cw { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.cat-container { + display: flex; + gap: 1.5rem; + justify-content: center; + align-items: center; + margin: 1.5rem 0; + flex-wrap: wrap; +} + +.cat { + font-size: 2.5rem; + display: inline-block; + animation: spin-cw linear infinite; + transform-origin: center; + user-select: none; +} + +/* Slightly different durations for variety */ +.cat:nth-child(1) { animation-duration: 3s; } +.cat:nth-child(2) { animation-duration: 4s; } +.cat:nth-child(3) { animation-duration: 2.5s; } +.cat:nth-child(4) { animation-duration: 5s; } +.cat:nth-child(5) { animation-duration: 3.5s; } + +/* ─── Visualize page ─────────────────────────────────────────────── */ +.plot-container { + width: 100%; + min-height: 420px; +} + +/* ─── Slider display ─────────────────────────────────────────────── */ +.slider-value { + font-weight: 700; + color: #6c63ff; + margin-left: .4rem; +} diff --git a/front-flask/templates/base.html b/front-flask/templates/base.html new file mode 100644 index 0000000..5257bf2 --- /dev/null +++ b/front-flask/templates/base.html @@ -0,0 +1,28 @@ + + + + + + {% block title %}Diamonds Price Prediction{% endblock %} + + + + + + +
+ {% block content %}{% endblock %} +
+ + + diff --git a/front-flask/templates/home.html b/front-flask/templates/home.html new file mode 100644 index 0000000..803b7bc --- /dev/null +++ b/front-flask/templates/home.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% block title %}Home – Diamonds Price Prediction{% endblock %} + +{% block content %} +
+

Diamonds Price Prediction App

+ +

+ This is a simple Flask application that allows you to predict the price of a diamond + based on its characteristics. You can input the features of the diamond and get an + estimated price using a machine learning model deployed as an API. +

+ + + + + +
+ 🐱 + 🐈 + 😺 + 🐱 + 🐈‍⬛ +
+ + Try a Prediction → +
+{% endblock %} diff --git a/front-flask/templates/predict.html b/front-flask/templates/predict.html new file mode 100644 index 0000000..f954f6d --- /dev/null +++ b/front-flask/templates/predict.html @@ -0,0 +1,150 @@ +{% extends "base.html" %} +{% block title %}Predict – Diamonds Price Prediction{% endblock %} + +{% block content %} +
+

Predict Diamond Price

+ + +
+ 🐱 + 🐈 + 😸 +
+ + {% if error %} +
{{ error }}
+ {% endif %} + + {% if prediction is not none %} +
+ Predicted Price: ${{ "%.2f"|format(prediction) }} +
+ {% endif %} + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+
+{% endblock %} diff --git a/front-flask/templates/visualize.html b/front-flask/templates/visualize.html new file mode 100644 index 0000000..2d27fbf --- /dev/null +++ b/front-flask/templates/visualize.html @@ -0,0 +1,92 @@ +{% extends "base.html" %} +{% block title %}Visualize – Diamonds Price Prediction{% endblock %} + +{% block content %} +
+

Visualize Diamond Data

+ + +
+ 🐱 + 🐈‍⬛ + 😺 + 🐈 +
+ +

+ The charts below are generated client-side from the diamonds dataset using + Plotly.js. Data is fetched directly from the backend. +

+ +
+
+
+ + + + +{% endblock %}