Utkarshres32 commited on
Commit
7a5bb5d
·
0 Parent(s):

Initial commit: AI-powered Oil Spill Detection and Monitoring System

Browse files
.gitignore ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Node / Frontend
2
+ node_modules/
3
+ dist/
4
+ build/
5
+ .env
6
+ .env.*
7
+ !.env.example
8
+
9
+ # Python / Backend
10
+ __pycache__/
11
+ *.py[cod]
12
+ *$py.class
13
+ .venv/
14
+ env/
15
+ venv/
16
+ ENV/
17
+ .env
18
+
19
+ # Models / Large Files
20
+ *.keras
21
+ *.h5
22
+ *.pkl
23
+ model/saved_models/*.keras
24
+ data/images/
25
+ data/masks/
26
+
27
+ # OS
28
+ .DS_Store
29
+ Thumbs.db
Colab_Training_Notebook.ipynb ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "nbformat": 4,
3
+ "nbformat_minor": 4,
4
+ "metadata": {},
5
+ "cells": [
6
+ {
7
+ "cell_type": "markdown",
8
+ "metadata": {},
9
+ "source": [
10
+ "# DeepOceans - Satellite Semantic Segmentation Training\nMake sure to go to **Runtime > Change runtime type > T4 GPU** before running."
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "code",
15
+ "execution_count": null,
16
+ "metadata": {},
17
+ "outputs": [],
18
+ "source": [
19
+ "# 1. Mount Google Drive\nfrom google.colab import drive\ndrive.mount('/content/drive')\n"
20
+ ]
21
+ },
22
+ {
23
+ "cell_type": "code",
24
+ "execution_count": null,
25
+ "metadata": {},
26
+ "outputs": [],
27
+ "source": [
28
+ "# 2. Imports and Setup\nimport os\nimport glob\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models, backend as K\nfrom tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau\nfrom sklearn.model_selection import train_test_split\n\nprint(\"TensorFlow Version:\", tf.__version__)\nprint(\"GPU Available:\", tf.config.list_physical_devices('GPU'))\n"
29
+ ]
30
+ },
31
+ {
32
+ "cell_type": "code",
33
+ "execution_count": null,
34
+ "metadata": {},
35
+ "outputs": [],
36
+ "source": [
37
+ "# 3. U-Net Architecture Construction\ndef conv_block(input_tensor, num_filters):\n x = layers.Conv2D(num_filters, (3, 3), padding=\"same\")(input_tensor)\n x = layers.BatchNormalization()(x)\n x = layers.Activation(\"relu\")(x)\n x = layers.Conv2D(num_filters, (3, 3), padding=\"same\")(x)\n x = layers.BatchNormalization()(x)\n x = layers.Activation(\"relu\")(x)\n return x\n\ndef encoder_block(input_tensor, num_filters):\n x = conv_block(input_tensor, num_filters)\n p = layers.MaxPooling2D((2, 2))(x)\n return x, p\n\ndef decoder_block(input_tensor, concat_tensor, num_filters):\n x = layers.Conv2DTranspose(num_filters, (2, 2), strides=(2, 2), padding=\"same\")(input_tensor)\n x = layers.concatenate([x, concat_tensor])\n x = conv_block(x, num_filters)\n return x\n\ndef build_unet(input_shape=(256, 256, 3)):\n inputs = layers.Input(shape=input_shape)\n e1, p1 = encoder_block(inputs, 64)\n e2, p2 = encoder_block(p1, 128)\n e3, p3 = encoder_block(p2, 256)\n e4, p4 = encoder_block(p3, 512)\n b = conv_block(p4, 1024)\n d1 = decoder_block(b, e4, 512)\n d2 = decoder_block(d1, e3, 256)\n d3 = decoder_block(d2, e2, 128)\n d4 = decoder_block(d3, e1, 64)\n outputs = layers.Conv2D(1, (1, 1), padding=\"same\", activation=\"sigmoid\")(d4)\n model = models.Model(inputs, outputs, name=\"U-Net\")\n return model\n"
38
+ ]
39
+ },
40
+ {
41
+ "cell_type": "code",
42
+ "execution_count": null,
43
+ "metadata": {},
44
+ "outputs": [],
45
+ "source": [
46
+ "# 4. Custom Loss Functions & Metrics (Dice + BCE)\ndef dice_coef(y_true, y_pred, smooth=1e-6):\n y_true_f = K.flatten(y_true)\n y_pred_f = K.flatten(y_pred)\n intersection = K.sum(y_true_f * y_pred_f)\n return (2. * intersection + smooth) / (K.sum(y_true_f) + K.sum(y_pred_f) + smooth)\n\ndef dice_loss(y_true, y_pred):\n return 1 - dice_coef(y_true, y_pred)\n\ndef bce_dice_loss(y_true, y_pred):\n bce = tf.keras.losses.binary_crossentropy(y_true, y_pred)\n bce = K.mean(bce) \n return bce + dice_loss(y_true, y_pred)\n\ndef iou_metric(y_true, y_pred, smooth=1e-6):\n y_true_f = K.flatten(y_true)\n y_pred_f = K.flatten(y_pred)\n intersection = K.sum(y_true_f * y_pred_f)\n union = K.sum(y_true_f) + K.sum(y_pred_f) - intersection\n return (intersection + smooth) / (union + smooth)\n\ndef get_metrics():\n return [\n dice_coef,\n iou_metric,\n tf.keras.metrics.Precision(name='precision'),\n tf.keras.metrics.Recall(name='recall')\n ]\n"
47
+ ]
48
+ },
49
+ {
50
+ "cell_type": "code",
51
+ "execution_count": null,
52
+ "metadata": {},
53
+ "outputs": [],
54
+ "source": [
55
+ "# 5. Data Pipeline & Augmentation\nIMG_SIZE = (256, 256)\n\ndef decode_image(image_file):\n image = tf.io.read_file(image_file)\n image = tf.image.decode_image(image, channels=3, expand_animations=False)\n image = tf.image.resize(image, IMG_SIZE)\n image = tf.cast(image, tf.float32) / 255.0\n return image\n\ndef decode_mask(mask_file):\n mask = tf.io.read_file(mask_file)\n mask = tf.image.decode_image(mask, channels=1, expand_animations=False)\n mask = tf.image.resize(mask, IMG_SIZE)\n mask = tf.cast(mask, tf.float32) / 255.0\n mask = tf.math.round(mask) \n return mask\n\ndef process_path(image_path, mask_path):\n image = decode_image(image_path)\n mask = decode_mask(mask_path)\n return image, mask\n\ndef augment(image, mask):\n if tf.random.uniform(()) > 0.5:\n image = tf.image.flip_left_right(image)\n mask = tf.image.flip_left_right(mask)\n if tf.random.uniform(()) > 0.5:\n image = tf.image.flip_up_down(image)\n mask = tf.image.flip_up_down(mask)\n image = tf.image.random_brightness(image, max_delta=0.2)\n image = tf.clip_by_value(image, 0.0, 1.0)\n return image, mask\n\ndef get_dataset(image_paths, mask_paths, batch_size=16, is_train=True):\n dataset = tf.data.Dataset.from_tensor_slices((image_paths, mask_paths))\n if is_train:\n dataset = dataset.shuffle(buffer_size=1000)\n dataset = dataset.map(process_path, num_parallel_calls=tf.data.AUTOTUNE)\n if is_train:\n dataset = dataset.map(augment, num_parallel_calls=tf.data.AUTOTUNE)\n dataset = dataset.batch(batch_size)\n dataset = dataset.prefetch(tf.data.AUTOTUNE)\n return dataset\n"
56
+ ]
57
+ },
58
+ {
59
+ "cell_type": "code",
60
+ "execution_count": null,
61
+ "metadata": {},
62
+ "outputs": [],
63
+ "source": [
64
+ "# 6. Execute Training\n\n# ==============================================================================\n# \u26a0\ufe0f ACTION REQUIRED: Verify this points to the folder containing your dataset\n# (Ensure there is an 'images/' and 'masks/' folder inside this directory)\n# ==============================================================================\nDATA_DIR = '/content/drive/MyDrive/1j35RM-5uZbTOfDv0mBTeRRmOUPvvg28t'\n# ==============================================================================\n\nEPOCHS = 50\nBATCH_SIZE = 16\nLEARNING_RATE = 1e-4\n\ndef get_file_paths(data_dir):\n image_dir = os.path.join(data_dir, 'images')\n mask_dir = os.path.join(data_dir, 'masks')\n \n image_paths = glob.glob(os.path.join(image_dir, '*.*'))\n image_paths.sort()\n \n final_img_paths = []\n final_mask_paths = []\n for img in image_paths:\n base = os.path.splitext(os.path.basename(img))[0]\n masks = glob.glob(os.path.join(mask_dir, f\"{base}.*\"))\n if len(masks) > 0:\n final_img_paths.append(img)\n final_mask_paths.append(masks[0])\n\n return final_img_paths, final_mask_paths\n\nprint(\"Hunting for dataset at folder:\", DATA_DIR)\nimg_paths, mask_paths = get_file_paths(DATA_DIR)\nprint(f\"Found {len(img_paths)} valid image/mask pairs.\\n\")\n\nif len(img_paths) == 0:\n print(\"\u274c ERROR: No images found. Check that DATA_DIR contains your 'images' and 'masks' folders.\")\nelse:\n # 80/20 train/validation split\n train_x, val_x, train_y, val_y = train_test_split(img_paths, mask_paths, test_size=0.2, random_state=42)\n \n train_dataset = get_dataset(train_x, train_y, batch_size=BATCH_SIZE, is_train=True)\n val_dataset = get_dataset(val_x, val_y, batch_size=BATCH_SIZE, is_train=False)\n \n model = build_unet(input_shape=(256, 256, 3))\n optimizer = tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE)\n \n model.compile(optimizer=optimizer, loss=bce_dice_loss, metrics=get_metrics())\n \n # Save directly to the root of your Google Drive so it's easy to download\n model_save_path = '/content/drive/MyDrive/oil_spill_unet_best.keras'\n \n callbacks = [\n ModelCheckpoint(model_save_path, verbose=1, save_best_only=True, monitor='val_loss'),\n EarlyStopping(monitor='val_loss', patience=15, restore_best_weights=True),\n ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=5, verbose=1, min_lr=1e-6)\n ]\n \n print(\"\ud83d\ude80 Initializing Training Pipeline...\")\n history = model.fit(\n train_dataset,\n validation_data=val_dataset,\n epochs=EPOCHS,\n callbacks=callbacks\n )\n \n print(\"\\n\u2705 Training Complete!\")\n print(f\"Your trained model file has been successfully saved to: {model_save_path}\")\n print(\"\u2b07\ufe0f You can now download this file and move it to your local 'model/saved_models/' folder.\")\n"
65
+ ]
66
+ }
67
+ ]
68
+ }
README.md ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Oil Spill Detection & Monitoring System
2
+
3
+ This is a full-stack AI-driven application that detects oil spills from satellite imagery using a deep learning segmentation model (U-Net).
4
+
5
+ ## System Architecture
6
+ * **AI/ML**: TensorFlow / Keras (U-Net)
7
+ * **Backend**: FastAPI (REST API with Python)
8
+ * **Frontend**: React + Vite + TypeScript (Modern UI)
9
+ * **Containerization**: Docker & Docker Compose
10
+
11
+ ## Repository Directory Structure
12
+ * `backend/` - FastAPI predict/health endpoints and Inference Engine
13
+ * `frontend/` - React Vite App for the UI
14
+ * `model/` - U-Net architecture, Custom losses, and Training logic
15
+ * `utils/` - Data loading and augmentation pipelines utilizing `tf.data.Dataset`
16
+ * `data/` - Holds your images and masks
17
+ * `notebooks/` - Place to run exploratory code
18
+
19
+ ## Getting Data (Google Drive)
20
+ Download the dataset using the following Google Drive link provided:
21
+ `https://drive.google.com/drive/folders/1j35RM-5uZbTOfDv0mBTeRRmOUPvvg28t?usp=sharing`
22
+
23
+ 1. Download the images and masks folders inside it.
24
+ 2. Place them inside the `./data/` folder in the root directory like so:
25
+ ```
26
+ d:/Oill_SPLIT/data/images/
27
+ d:/Oill_SPLIT/data/masks/
28
+ ```
29
+
30
+ ## Local Setup (Without Docker)
31
+
32
+ ### Backend
33
+ 1. `cd backend`
34
+ 2. `pip install -r requirements.txt`
35
+ 3. `uvicorn main:app --reload`
36
+ The API will be available at `http://localhost:8000`
37
+
38
+ ### Frontend
39
+ 1. `cd frontend`
40
+ 2. `npm install`
41
+ 3. `npm run dev`
42
+ The React UI will be available at `http://localhost:5173`
43
+
44
+ ## Setup (With Docker)
45
+ To run the entire suite using Docker:
46
+ ```bash
47
+ docker-compose up --build
48
+ ```
49
+
50
+ ## Model Training Instructions (Google Colab / Kaggle)
51
+ You can train the model on Google Colab leveraging the free T4 GPU.
52
+
53
+ 1. Zip the `model/`, `utils/`, and `data/` directories and upload them to Google Drive or directly to Colab/Kaggle.
54
+ 2. Open a Colab Notebook.
55
+ 3. Install dependencies: `!pip install tensorflow scikit-learn pillow numpy`
56
+ 4. Run the training script:
57
+ ```bash
58
+ !python model/train.py --data_dir ./data --save_dir ./model/saved_models --epochs 50 --batch_size 16
59
+ ```
60
+ 5. Once trained, download `oil_spill_unet_best.keras` from `./model/saved_models` and place it in your local folder structure.
61
+
62
+ *(Note: During initial development, if `oil_spill_unet_best.keras` isn't found, the backend will auto-initialize an untrained "stub" model so you can immediately test the UI and API offline).*
backend/Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies required for OpenCV/Pillow if needed
6
+ RUN apt-get update && apt-get install -y libglib2.0-0 libsm6 libxext6 libxrender-dev && rm -rf /var/lib/apt/lists/*
7
+
8
+ COPY backend/requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ # Copy the entire project for path resolution, or correctly scope it
12
+ COPY backend/ ./backend/
13
+ COPY model/ ./model/
14
+ COPY utils/ ./utils/
15
+
16
+ EXPOSE 8000
17
+
18
+ CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]
backend/inference.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tensorflow as tf
3
+ import numpy as np
4
+ from PIL import Image
5
+ import io
6
+ import sys
7
+
8
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
9
+ from model.unet import build_unet
10
+ from model.loss_metrics import get_metrics, bce_dice_loss
11
+
12
+ # Path to the saved model weights
13
+ MODEL_PATH = os.path.join(os.path.dirname(__file__), "../model/saved_models/oil_spill_unet_best.keras")
14
+ IMG_SIZE = (256, 256)
15
+
16
+ class OilSpillModel:
17
+ def __init__(self):
18
+ self.model = None
19
+ self.load_model()
20
+
21
+ def load_model(self):
22
+ print(f"Attempting to load model from: {os.path.abspath(MODEL_PATH)}")
23
+ if os.path.exists(MODEL_PATH):
24
+ try:
25
+ # Provide custom objects if model was saved with custom metrics/loss
26
+ custom_objects = {
27
+ 'bce_dice_loss': bce_dice_loss,
28
+ 'dice_coef': get_metrics()[0],
29
+ 'iou_metric': get_metrics()[1]
30
+ }
31
+ self.model = tf.keras.models.load_model(MODEL_PATH, custom_objects=custom_objects)
32
+ print("Model loaded successfully.")
33
+ except Exception as e:
34
+ print(f"Failed to load model from file: {e}")
35
+ self._build_stub_model()
36
+ else:
37
+ print("Trained model weights not found. Building an untrained stub model for development.")
38
+ self._build_stub_model()
39
+
40
+ def _build_stub_model(self):
41
+ """Used for development when trained weights aren't available yet."""
42
+ # Builds architecture with random weights
43
+ self.model = build_unet(input_shape=(256, 256, 3))
44
+
45
+ def predict(self, image_bytes):
46
+ """
47
+ Takes raw image bytes, preprocesses, predicts, and returns the binary mask array and confidence score.
48
+ """
49
+ # Load image
50
+ img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
51
+
52
+ # Resize to network input shape
53
+ img = img.resize(IMG_SIZE)
54
+
55
+ # Convert to numpy array and normalize to [0, 1]
56
+ img_array = np.array(img, dtype=np.float32) / 255.0
57
+
58
+ # Expand dimension to create a batch size of 1
59
+ img_array = np.expand_dims(img_array, axis=0)
60
+
61
+ # Inference
62
+ pred_mask = self.model.predict(img_array)[0] # Shape is (256, 256, 1)
63
+
64
+ # Calculate confidence metric:
65
+ # We average the probability of pixels that the network thinks are part of the spill (>0.5 probability)
66
+ oil_pixels = pred_mask[pred_mask > 0.5]
67
+ confidence = float(np.mean(oil_pixels)) if len(oil_pixels) > 0 else 0.0
68
+
69
+ # Threshold to create binary mask (255 for oil, 0 for background)
70
+ binary_mask = (pred_mask > 0.5).astype(np.uint8) * 255
71
+
72
+ return binary_mask, confidence
73
+
74
+ # Singleton prediction engine
75
+ prediction_engine = OilSpillModel()
backend/main.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException
2
+ from fastapi.responses import Response, JSONResponse
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from PIL import Image
5
+ import io
6
+ import numpy as np
7
+ import time
8
+
9
+ import sys
10
+ import os
11
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
12
+ from backend.inference import prediction_engine
13
+
14
+ app = FastAPI(title="Oil Spill Detection API", version="1.0.0", description="API to detect oil spills from satellite images using U-Net")
15
+
16
+ # Setup CORS for the React frontend
17
+ app.add_middleware(
18
+ CORSMiddleware,
19
+ allow_origins=["*"], # Allow all for local dev
20
+ allow_credentials=True,
21
+ allow_methods=["*"],
22
+ allow_headers=["*"],
23
+ )
24
+
25
+ @app.get("/health")
26
+ def health_check():
27
+ """Health check endpoint to ensure API and Model are ready."""
28
+ return {"status": "healthy", "model_loaded": prediction_engine.model is not None}
29
+
30
+ @app.post("/predict")
31
+ async def predict_spill(file: UploadFile = File(...)):
32
+ """Receives an image, performs inference, and returns a PNG mask segmenting the oil spill."""
33
+ if not file.content_type.startswith("image/"):
34
+ raise HTTPException(status_code=400, detail="Invalid file format. Upload an image.")
35
+
36
+ try:
37
+ start_time = time.time()
38
+
39
+ contents = await file.read()
40
+
41
+ # Perform inference
42
+ mask_array, confidence = prediction_engine.predict(contents)
43
+
44
+ # Convert mask array (256, 256, 1) -> (256, 256) for Pillow
45
+ if len(mask_array.shape) == 3 and mask_array.shape[-1] == 1:
46
+ mask_array = np.squeeze(mask_array, axis=-1)
47
+
48
+ mask_image = Image.fromarray(mask_array.astype(np.uint8), mode="L")
49
+
50
+ # Save Image to bytes
51
+ img_byte_arr = io.BytesIO()
52
+ mask_image.save(img_byte_arr, format='PNG')
53
+ img_byte_arr = img_byte_arr.getvalue()
54
+
55
+ latency_ms = int((time.time() - start_time) * 1000)
56
+
57
+ # Include metadata in headers so frontend can read confidence and latency
58
+ headers = {
59
+ "Access-Control-Expose-Headers": "X-Confidence-Score, X-Inference-Latency-Ms",
60
+ "X-Confidence-Score": str(round(confidence * 100, 2)),
61
+ "X-Inference-Latency-Ms": str(latency_ms)
62
+ }
63
+
64
+ return Response(content=img_byte_arr, media_type="image/png", headers=headers)
65
+
66
+ except Exception as e:
67
+ print(f"Error during prediction: {e}")
68
+ raise HTTPException(status_code=500, detail=str(e))
69
+
70
+ if __name__ == "__main__":
71
+ import uvicorn
72
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
backend/requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ python-multipart
4
+ tensorflow
5
+ numpy
6
+ Pillow
7
+ scikit-learn
docker-compose.yml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ backend:
5
+ build:
6
+ context: .
7
+ dockerfile: backend/Dockerfile
8
+ ports:
9
+ - "8000:8000"
10
+ volumes:
11
+ - ./backend:/app/backend
12
+ - ./model:/app/model
13
+ environment:
14
+ - PYTHONUNBUFFERED=1
15
+
16
+ frontend:
17
+ build:
18
+ context: .
19
+ dockerfile: frontend/Dockerfile
20
+ ports:
21
+ - "5173:5173"
22
+ volumes:
23
+ - ./frontend:/app/frontend
24
+ - /app/frontend/node_modules
25
+ depends_on:
26
+ - backend
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ COPY frontend/package.json ./
6
+ # If package-lock.json exists, copy it (using wildcard prevents failure if missing initially)
7
+ COPY frontend/package-lock.json* ./
8
+ RUN npm install
9
+
10
+ COPY frontend/ .
11
+
12
+ EXPOSE 5173
13
+
14
+ CMD ["npm", "run", "dev", "--", "--host"]
frontend/README.md ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + TypeScript + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17
+
18
+ ```js
19
+ export default defineConfig([
20
+ globalIgnores(['dist']),
21
+ {
22
+ files: ['**/*.{ts,tsx}'],
23
+ extends: [
24
+ // Other configs...
25
+
26
+ // Remove tseslint.configs.recommended and replace with this
27
+ tseslint.configs.recommendedTypeChecked,
28
+ // Alternatively, use this for stricter rules
29
+ tseslint.configs.strictTypeChecked,
30
+ // Optionally, add this for stylistic rules
31
+ tseslint.configs.stylisticTypeChecked,
32
+
33
+ // Other configs...
34
+ ],
35
+ languageOptions: {
36
+ parserOptions: {
37
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
38
+ tsconfigRootDir: import.meta.dirname,
39
+ },
40
+ // other options...
41
+ },
42
+ },
43
+ ])
44
+ ```
45
+
46
+ You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
47
+
48
+ ```js
49
+ // eslint.config.js
50
+ import reactX from 'eslint-plugin-react-x'
51
+ import reactDom from 'eslint-plugin-react-dom'
52
+
53
+ export default defineConfig([
54
+ globalIgnores(['dist']),
55
+ {
56
+ files: ['**/*.{ts,tsx}'],
57
+ extends: [
58
+ // Other configs...
59
+ // Enable lint rules for React
60
+ reactX.configs['recommended-typescript'],
61
+ // Enable lint rules for React DOM
62
+ reactDom.configs.recommended,
63
+ ],
64
+ languageOptions: {
65
+ parserOptions: {
66
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
67
+ tsconfigRootDir: import.meta.dirname,
68
+ },
69
+ // other options...
70
+ },
71
+ },
72
+ ])
73
+ ```
frontend/eslint.config.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { defineConfig, globalIgnores } from 'eslint/config'
7
+
8
+ export default defineConfig([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs.flat.recommended,
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ ecmaVersion: 2020,
20
+ globals: globals.browser,
21
+ },
22
+ },
23
+ ])
frontend/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>frontend</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "lucide-react": "^1.8.0",
14
+ "react": "^19.2.4",
15
+ "react-dom": "^19.2.4"
16
+ },
17
+ "devDependencies": {
18
+ "@eslint/js": "^9.39.4",
19
+ "@types/node": "^24.12.2",
20
+ "@types/react": "^19.2.14",
21
+ "@types/react-dom": "^19.2.3",
22
+ "@vitejs/plugin-react": "^6.0.1",
23
+ "eslint": "^9.39.4",
24
+ "eslint-plugin-react-hooks": "^7.0.1",
25
+ "eslint-plugin-react-refresh": "^0.5.2",
26
+ "globals": "^17.4.0",
27
+ "typescript": "~6.0.2",
28
+ "typescript-eslint": "^8.58.0",
29
+ "vite": "^8.0.4"
30
+ }
31
+ }
frontend/public/favicon.svg ADDED
frontend/public/icons.svg ADDED
frontend/src/App.css ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
2
+
3
+ :root {
4
+ /* Color Palette */
5
+ --bg-app: #F8FAFC;
6
+ --bg-surface: #FFFFFF;
7
+ --bg-surface-hover: #F1F5F9;
8
+
9
+ --text-primary: #0F172A;
10
+ --text-secondary: #475569;
11
+ --text-tertiary: #94A3B8;
12
+
13
+ --accent: #0F766E;
14
+ --accent-hover: #0D9488;
15
+ --accent-light: #F0FDFA;
16
+
17
+ --alert: #D97706;
18
+ --alert-light: #FEF3C7;
19
+
20
+ --border-light: #E2E8F0;
21
+ --border-medium: #CBD5E1;
22
+
23
+ /* Shadows - Premium Soft */
24
+ --shadow-sm: 0 1px 2px 0 rgba(15, 23, 42, 0.04);
25
+ --shadow-md: 0 4px 6px -1px rgba(15, 23, 42, 0.05), 0 2px 4px -2px rgba(15, 23, 42, 0.05);
26
+ --shadow-lg: 0 10px 15px -3px rgba(15, 23, 42, 0.08), 0 4px 6px -4px rgba(15, 23, 42, 0.04);
27
+ --shadow-hover: 0 20px 25px -5px rgba(15, 23, 42, 0.1), 0 8px 10px -6px rgba(15, 23, 42, 0.04);
28
+
29
+ /* Geometry */
30
+ --radius-sm: 6px;
31
+ --radius-md: 12px;
32
+ --radius-lg: 20px;
33
+ --radius-full: 9999px;
34
+
35
+ /* Fonts */
36
+ --font-ui: 'Inter', system-ui, sans-serif;
37
+ --font-mono: 'JetBrains Mono', monospace;
38
+ }
39
+
40
+ body {
41
+ margin: 0;
42
+ font-family: var(--font-ui);
43
+ background-color: var(--bg-app);
44
+ color: var(--text-primary);
45
+ -webkit-font-smoothing: antialiased;
46
+ -moz-osx-font-smoothing: grayscale;
47
+ }
48
+
49
+ * { box-sizing: border-box; }
50
+ h1, h2, h3, h4, p { margin: 0; }
51
+
52
+ /* SHARED ANIMATIONS & UTILS */
53
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
54
+ .fade-in { animation: fadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
55
+
56
+ @keyframes spin { 100% { transform: rotate(360deg); } }
57
+ .loader { width: 18px; height: 18px; border: 2px solid #FFF; border-bottom-color: transparent; border-radius: 50%; display: inline-block; animation: spin 1s linear infinite; }
58
+
59
+ .badge-pill {
60
+ display: inline-flex;
61
+ align-items: center;
62
+ padding: 4px 12px;
63
+ background: var(--bg-app);
64
+ color: var(--text-secondary);
65
+ border-radius: var(--radius-full);
66
+ font-size: 0.75rem;
67
+ font-weight: 600;
68
+ letter-spacing: 0.05em;
69
+ text-transform: uppercase;
70
+ }
71
+
72
+
73
+ /* ========================================================================= */
74
+ /* LANDING PAGE */
75
+ /* ========================================================================= */
76
+
77
+ .landing-container { background: #FFFFFF; display: flex; flex-direction: column; }
78
+
79
+ /* Nav */
80
+ .nav-bar { display: flex; justify-content: space-between; align-items: center; padding: 20px 6%; position: absolute; width: 100%; top: 0; z-index: 100; }
81
+ .logo { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 1.25rem; color: var(--text-primary); letter-spacing: -0.02em; }
82
+ .logo-icon { width: 32px; height: 32px; background: var(--text-primary); border-radius: 8px; display: flex; align-items: center; justify-content: center; }
83
+
84
+ .nav-actions { display: flex; align-items: center; gap: 24px; }
85
+ .btn-text { background: none; border: none; font-family: var(--font-ui); font-weight: 500; font-size: 0.95rem; color: var(--text-secondary); cursor: pointer; transition: color 0.2s; }
86
+ .btn-text:hover { color: var(--text-primary); }
87
+
88
+ .btn-primary { background: var(--text-primary); color: #FFF; border: none; padding: 10px 20px; border-radius: var(--radius-md); font-weight: 500; font-family: var(--font-ui); cursor: pointer; transition: all 0.2s; }
89
+ .btn-primary:hover { background: var(--accent); box-shadow: var(--shadow-md); transform: translateY(-1px); }
90
+
91
+ /* Hero */
92
+ .hero-section { padding: 160px 6% 120px; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 32px; position: relative; overflow: hidden; }
93
+ .hero-badge { display: inline-block; padding: 6px 16px; background: var(--accent-light); color: var(--accent); border-radius: var(--radius-full); font-weight: 600; font-size: 0.85rem; border: 1px solid rgba(15,118,110,0.1); }
94
+ .hero-title { font-size: 4.5rem; font-weight: 800; line-height: 1.1; letter-spacing: -0.03em; max-width: 900px; color: var(--text-primary); z-index: 10; }
95
+ .hero-description { font-size: 1.25rem; line-height: 1.6; color: var(--text-secondary); max-width: 650px; z-index: 10; }
96
+
97
+ .hero-buttons { display: flex; gap: 16px; z-index: 10; }
98
+ .btn-primary-lg { background: var(--accent); color: #FFF; border: none; padding: 16px 32px; border-radius: var(--radius-full); font-size: 1.1rem; font-weight: 600; display: flex; align-items: center; gap: 8px; cursor: pointer; transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); }
99
+ .btn-primary-lg:hover { background: var(--accent-hover); transform: translateY(-2px); box-shadow: 0 12px 24px -4px rgba(15,118,110,0.2); }
100
+ .btn-secondary-lg { background: #FFF; color: var(--text-primary); border: 1px solid var(--border-medium); padding: 16px 32px; border-radius: var(--radius-full); font-size: 1.1rem; font-weight: 600; cursor: pointer; transition: all 0.3s ease; }
101
+ .btn-secondary-lg:hover { background: var(--bg-app); border-color: var(--text-primary); }
102
+
103
+ .hero-background-glow {
104
+ position: absolute; top: 10%; left: 50%; transform: translateX(-50%); width: 80%; height: 800px;
105
+ background: radial-gradient(circle, rgba(15,118,110,0.06) 0%, rgba(255,255,255,0) 70%); z-index: 0; pointer-events: none;
106
+ }
107
+
108
+ /* Features */
109
+ .features-section { padding: 80px 6%; background: var(--bg-surface); }
110
+ .feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 40px; max-width: 1400px; margin: 0 auto; }
111
+ .feature-card { display: flex; flex-direction: column; gap: 16px; padding: 16px; }
112
+ .feature-icon-wrapper { width: 56px; height: 56px; background: var(--accent-light); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; color: var(--accent); }
113
+ .feature-card h3 { font-size: 1.25rem; font-weight: 600; color: var(--text-primary); }
114
+ .feature-card p { font-size: 1rem; color: var(--text-secondary); line-height: 1.6; }
115
+
116
+ /* How it Works */
117
+ .how-it-works-section { padding: 120px 6%; background: var(--bg-app); }
118
+ .section-header { text-align: center; margin-bottom: 80px; }
119
+ .section-header h2 { font-size: 2.5rem; font-weight: 700; margin-bottom: 16px; letter-spacing: -0.02em; }
120
+ .section-header p { font-size: 1.1rem; color: var(--text-secondary); }
121
+
122
+ .steps-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px; max-width: 1400px; margin: 0 auto; }
123
+ .step-card { background: var(--bg-surface); padding: 48px 32px; border-radius: var(--radius-lg); box-shadow: var(--shadow-md); border: 1px solid var(--border-light); transition: all 0.3s ease; position: relative; overflow: hidden; }
124
+ .step-card:hover { transform: translateY(-5px); box-shadow: var(--shadow-hover); }
125
+ .step-numeral { position: absolute; top: 16px; right: 24px; font-size: 5rem; font-weight: 800; color: var(--bg-app); opacity: 0.5; font-family: var(--font-mono); }
126
+ .step-card h4 { font-size: 1.25rem; font-weight: 600; margin-bottom: 16px; position: relative; z-index: 10; }
127
+ .step-card p { color: var(--text-secondary); line-height: 1.6; position: relative; z-index: 10; }
128
+
129
+ /* Preview & CTA */
130
+ .preview-section { padding: 120px 6% 40px; background: var(--bg-surface); display: flex; justify-content: center; }
131
+ .preview-app-window { max-width: 1000px; width: 100%; box-shadow: var(--shadow-lg); border-radius: 12px; border: 1px solid var(--border-medium); overflow: hidden; background: #fff; }
132
+ .app-window-header { height: 38px; background: #F1F5F9; border-bottom: 1px solid var(--border-light); display: flex; align-items: center; padding: 0 16px; position: relative; }
133
+ .mac-dots { display: flex; gap: 8px; }
134
+ .mac-dot { width: 12px; height: 12px; border-radius: 50%; }
135
+ .mac-dot.red { background: #FF5F56; } .mac-dot.yellow { background: #FFBD2E; } .mac-dot.green { background: #27C93F; }
136
+ .app-title { position: absolute; left: 50%; transform: translateX(-50%); font-family: var(--font-ui); font-size: 0.75rem; color: var(--text-tertiary); font-weight: 500; letter-spacing: 0.05em; }
137
+ .app-window-body { height: 500px; padding: 16px; background: var(--bg-app); }
138
+ .app-mockup-skeleton { width: 100%; height: 100%; display: flex; gap: 16px; }
139
+ .mock-nav { width: 200px; background: #FFF; border-radius: 8px; border: 1px solid var(--border-light); }
140
+ .mock-layout { flex: 1; display: flex; flex-direction: column; gap: 16px; }
141
+ .mock-top { display: flex; gap: 16px; height: 100px; }
142
+ .mock-stat { flex: 1; background: #FFF; border-radius: 8px; border: 1px solid var(--border-light); }
143
+ .mock-main { flex: 1; display: flex; gap: 16px; }
144
+ .mock-side { width: 250px; background: #FFF; border-radius: 8px; border: 1px solid var(--border-light); }
145
+ .mock-center { flex: 1; background: #FFF; border-radius: 8px; border: 1px solid var(--border-light); }
146
+
147
+ .cta-section { padding: 120px 6%; text-align: center; background: var(--bg-surface); display: flex; flex-direction: column; align-items: center; gap: 24px; }
148
+ .cta-section h2 { font-size: 3rem; font-weight: 800; letter-spacing: -0.02em; }
149
+ .cta-section p { font-size: 1.25rem; color: var(--text-secondary); margin-bottom: 24px; }
150
+
151
+ .landing-footer { background: var(--bg-app); padding: 40px 6%; text-align: center; border-top: 1px solid var(--border-light); }
152
+ .footer-content { display: flex; justify-content: space-between; align-items: center; max-width: 1400px; margin: 0 auto; color: var(--text-tertiary); font-size: 0.9rem; }
153
+ @media (max-width: 600px) { .footer-content { flex-direction: column; gap: 16px; } }
154
+
155
+
156
+ /* ========================================================================= */
157
+ /* DASHBOARD APPLICATION */
158
+ /* ========================================================================= */
159
+
160
+ .app-layout {
161
+ min-height: 100vh;
162
+ display: flex;
163
+ flex-direction: column;
164
+ }
165
+
166
+ .app-header {
167
+ height: 64px;
168
+ background: var(--bg-surface);
169
+ border-bottom: 1px solid var(--border-light);
170
+ display: flex;
171
+ justify-content: space-between;
172
+ align-items: center;
173
+ padding: 0 32px;
174
+ position: sticky;
175
+ top: 0;
176
+ z-index: 50;
177
+ }
178
+
179
+ .app-logo { display: flex; align-items: center; gap: 12px; font-weight: 700; font-size: 1rem; color: var(--text-primary); }
180
+ .app-badge { font-weight: 500; color: var(--text-tertiary); background: var(--bg-app); padding: 4px 8px; border-radius: 6px; font-size: 0.75rem; letter-spacing: 0.05em; text-transform: uppercase; }
181
+
182
+ .header-actions { display: flex; align-items: center; gap: 24px; }
183
+ .connection-status { display: flex; align-items: center; gap: 8px; color: var(--text-secondary); font-size: 0.8rem; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; }
184
+ .status-orb { width: 8px; height: 8px; background: var(--accent); border-radius: 50%; box-shadow: 0 0 8px var(--accent); }
185
+
186
+ .icon-btn, .profile-btn { background: none; border: none; color: var(--text-secondary); cursor: pointer; width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: all 0.2s; }
187
+ .icon-btn:hover { background: var(--bg-app); color: var(--text-primary); }
188
+ .profile-btn { background: var(--text-primary); color: #FFF; }
189
+ .profile-btn:hover { background: var(--accent); }
190
+
191
+ .dashboard-content {
192
+ flex: 1;
193
+ padding: 32px;
194
+ width: 100%;
195
+ max-width: 1600px;
196
+ margin: 0 auto;
197
+ }
198
+
199
+ .dashboard-grid {
200
+ display: flex;
201
+ flex-direction: column;
202
+ gap: 32px;
203
+ }
204
+
205
+ /* Base Panel Styles */
206
+ .panel {
207
+ background: var(--bg-surface);
208
+ border-radius: var(--radius-lg);
209
+ border: 1px solid var(--border-light);
210
+ box-shadow: var(--shadow-sm);
211
+ padding: 24px;
212
+ }
213
+
214
+ .panel-header { display: flex; justify-content: space-between; align-items: center; }
215
+ .panel-title { font-size: 1rem; font-weight: 600; color: var(--text-primary); }
216
+
217
+ /* Metrics Row (Top) */
218
+ .metrics-row {
219
+ display: grid;
220
+ grid-template-columns: repeat(4, 1fr);
221
+ gap: 24px;
222
+ }
223
+ @media (max-width: 1200px) { .metrics-row { grid-template-columns: repeat(2, 1fr); } }
224
+
225
+ .stat-card {
226
+ background: var(--bg-surface);
227
+ border: 1px solid var(--border-light);
228
+ border-radius: var(--radius-md);
229
+ padding: 24px;
230
+ display: flex;
231
+ align-items: center;
232
+ gap: 16px;
233
+ transition: all 0.25s ease;
234
+ box-shadow: var(--shadow-sm);
235
+ }
236
+ .stat-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); border-color: var(--border-medium); }
237
+ .stat-icon-wrapper { width: 48px; height: 48px; background: var(--bg-app); border-radius: 12px; display: flex; align-items: center; justify-content: center; color: var(--text-secondary); }
238
+ .stat-content { display: flex; flex-direction: column; gap: 4px; flex: 1; }
239
+ .stat-label { font-size: 0.8rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; }
240
+ .stat-value { font-size: 1.5rem; font-weight: 700; font-family: var(--font-mono); color: var(--text-primary); }
241
+
242
+ .stat-control { display: flex; align-items: center; gap: 12px; margin-top: 4px; }
243
+ .stat-control input[type="range"] { flex: 1; accent-color: var(--accent); }
244
+ .range-val { font-family: var(--font-mono); font-size: 0.9rem; font-weight: 600; }
245
+
246
+ /* Main Section: Upload + Log */
247
+ .main-interaction-row {
248
+ display: grid;
249
+ grid-template-columns: 380px 1fr;
250
+ gap: 32px;
251
+ }
252
+ @media (max-width: 1024px) { .main-interaction-row { grid-template-columns: 1fr; } }
253
+
254
+ /* Upload Component */
255
+ .upload-container { display: flex; flex-direction: column; gap: 24px; }
256
+ .dropzone {
257
+ flex: 1;
258
+ border: 2px dashed var(--border-medium);
259
+ border-radius: var(--radius-md);
260
+ background: var(--bg-app);
261
+ display: flex;
262
+ flex-direction: column;
263
+ align-items: center;
264
+ justify-content: center;
265
+ padding: 40px 24px;
266
+ text-align: center;
267
+ cursor: pointer;
268
+ transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
269
+ position: relative;
270
+ }
271
+ .dropzone input[type="file"] { position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
272
+ .dropzone:hover, .dropzone.drag-active { border-color: var(--accent); background: var(--accent-light); }
273
+ .dropzone-icon { color: var(--text-tertiary); margin-bottom: 16px; transition: transform 0.2s; }
274
+ .dropzone:hover .dropzone-icon { transform: translateY(-4px); color: var(--accent); }
275
+ .dropzone-text { font-size: 1rem; color: var(--text-primary); margin-bottom: 8px; font-weight: 500; }
276
+ .dropzone-primary { color: var(--accent); font-weight: 600; }
277
+ .dropzone-secondary { font-size: 0.85rem; color: var(--text-secondary); }
278
+
279
+ .selected-file-chip { margin-top: 16px; font-family: var(--font-mono); font-size: 0.85rem; background: #FFF; padding: 6px 16px; border-radius: 20px; box-shadow: var(--shadow-sm); border: 1px solid var(--border-light); z-index: 10; font-weight: 500; }
280
+
281
+ .btn-action-lg {
282
+ width: 100%;
283
+ padding: 16px;
284
+ border-radius: var(--radius-md);
285
+ background: var(--text-primary);
286
+ color: #FFF;
287
+ font-weight: 600;
288
+ font-size: 1rem;
289
+ border: none;
290
+ cursor: pointer;
291
+ display: flex;
292
+ align-items: center;
293
+ justify-content: center;
294
+ transition: all 0.3s;
295
+ }
296
+ .btn-action-lg:not(:disabled):hover { background: var(--accent); box-shadow: var(--shadow-md); transform: translateY(-2px); }
297
+ .btn-action-lg:disabled { opacity: 0.6; cursor: not-allowed; }
298
+
299
+ /* History Component */
300
+ .history-container { display: flex; flex-direction: column; gap: 16px; min-height: 400px; }
301
+ .record-count { background: var(--bg-app); padding: 4px 12px; border-radius: var(--radius-full); font-size: 0.8rem; font-weight: 600; color: var(--text-secondary); }
302
+ .history-list { display: flex; flex-direction: column; gap: 12px; overflow-y: auto; max-height: 340px; padding-right: 8px; }
303
+ .history-row { display: flex; align-items: center; gap: 16px; padding: 12px; border: 1px solid var(--border-light); border-radius: var(--radius-md); transition: all 0.2s; cursor: default; }
304
+ .history-row:hover { background: var(--bg-app); border-color: var(--border-medium); }
305
+ .history-thumb { width: 56px; height: 56px; border-radius: 8px; object-fit: cover; background: var(--border-light); }
306
+ .history-details { flex: 1; display: flex; flex-direction: column; gap: 6px; overflow: hidden; }
307
+ .history-filename { font-size: 0.95rem; font-weight: 600; font-family: var(--font-mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text-primary); }
308
+ .history-time { font-size: 0.8rem; color: var(--text-secondary); }
309
+ .history-status { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
310
+ .status-badge { padding: 4px 12px; border-radius: var(--radius-full); font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
311
+ .status-badge.alert { background: var(--alert-light); color: var(--alert); }
312
+ .status-badge.clear { background: var(--bg-app); color: var(--text-secondary); }
313
+ .history-score { font-family: var(--font-mono); font-size: 1rem; font-weight: 700; color: var(--text-primary); }
314
+ .empty-state { padding: 48px; text-align: center; color: var(--text-tertiary); font-weight: 500; border: 2px dashed var(--border-light); border-radius: var(--radius-md); margin-top: 24px; }
315
+
316
+
317
+ /* Bottom Component: Visualizations */
318
+ .visualization-container { padding: 32px; }
319
+ .frames-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
320
+ @media (max-width: 900px) { .frames-grid { grid-template-columns: 1fr; } }
321
+ .frame-card { display: flex; flex-direction: column; gap: 12px; }
322
+ .frame-header { font-size: 0.85rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; text-align: center; }
323
+ .frame-content { aspect-ratio: 1; background: var(--bg-app); border-radius: var(--radius-md); border: 1px solid var(--border-light); box-shadow: inset 0 2px 4px rgba(0,0,0,0.02); position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center; }
324
+ .frame-content.dark-mode { background: #0F172A; border-color: #1E293B; }
325
+ .frame-content img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain; }
326
+ .invert-render { filter: invert(1); }
327
+ .feature-overlay { mix-blend-mode: multiply; opacity: 0.8; filter: sepia(1) hue-rotate(130deg) saturate(500%); }
frontend/src/App.tsx ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { UploadCloud, Activity, CheckCircle, Layers, BarChart2, ArrowRight, ShieldCheck, Zap, Globe, Cpu, Settings, User } from 'lucide-react';
3
+ import './App.css';
4
+
5
+ interface HistoryRecord {
6
+ id: number;
7
+ timestamp: string;
8
+ filename: string;
9
+ confidence: number;
10
+ thumbnail: string;
11
+ }
12
+
13
+ function LandingPage({ onEnter }: { onEnter: () => void }) {
14
+ return (
15
+ <div className="landing-container">
16
+ <nav className="nav-bar">
17
+ <div className="logo">
18
+ <div className="logo-icon"><Activity size={20} color="#fff" /></div>
19
+ DeepOceans
20
+ </div>
21
+ <div className="nav-actions">
22
+ <button className="btn-text">Documentation</button>
23
+ <button className="btn-text">API Features</button>
24
+ <button className="btn-primary" onClick={onEnter}>View Dashboard</button>
25
+ </div>
26
+ </nav>
27
+
28
+ <main>
29
+ <section className="hero-section">
30
+ <div className="hero-badge">DeepOceans v1.2 Engine Live</div>
31
+ <h1 className="hero-title">Protecting Marine Life with Precision AI.</h1>
32
+ <p className="hero-description">
33
+ Enterprise-grade anomaly detection for satellite telemetry. Monitor coastal regions and detect oceanic oil spills automatically using our state-of-the-art segmentation network.
34
+ </p>
35
+ <div className="hero-buttons">
36
+ <button className="btn-primary-lg" onClick={onEnter}>Start Monitoring <ArrowRight size={20} /></button>
37
+ <button className="btn-secondary-lg">Talk to Sales</button>
38
+ </div>
39
+ <div className="hero-background-glow"></div>
40
+ </section>
41
+
42
+ <section className="features-section">
43
+ <div className="feature-grid">
44
+ <div className="feature-card">
45
+ <div className="feature-icon-wrapper"><Zap size={24} className="feature-icon" /></div>
46
+ <h3>Real-Time Inference</h3>
47
+ <p>Achieve sub-200ms latency on 256x256 image segmentation masks utilizing optimized T4 compute instances.</p>
48
+ </div>
49
+ <div className="feature-card">
50
+ <div className="feature-icon-wrapper"><Globe size={24} className="feature-icon" /></div>
51
+ <h3>Satellite Agnostic</h3>
52
+ <p>Process telemetry from SAR, Sentinel-1, or multi-spectral sources effortlessly through our robust API.</p>
53
+ </div>
54
+ <div className="feature-card">
55
+ <div className="feature-icon-wrapper"><ShieldCheck size={24} className="feature-icon" /></div>
56
+ <h3>Validated Accuracy</h3>
57
+ <p>Averaging 0.84 IoU against validation datasets, ensuring you capture maximum spill events with minimal noise.</p>
58
+ </div>
59
+ <div className="feature-card">
60
+ <div className="feature-icon-wrapper"><Cpu size={24} className="feature-icon" /></div>
61
+ <h3>Seamless Integration</h3>
62
+ <p>Plug predictions directly into your existing command center via standard REST protocols.</p>
63
+ </div>
64
+ </div>
65
+ </section>
66
+
67
+ <section className="how-it-works-section">
68
+ <div className="section-header">
69
+ <h2>Streamlined Operations</h2>
70
+ <p>From raw signal to actionable intelligence in three steps.</p>
71
+ </div>
72
+ <div className="steps-container">
73
+ <div className="step-card">
74
+ <span className="step-numeral">01</span>
75
+ <h4>Data Ingestion</h4>
76
+ <p>Upload your optical or synthetic aperture radar imagery directly into our secure pipeline.</p>
77
+ </div>
78
+ <div className="step-card">
79
+ <span className="step-numeral">02</span>
80
+ <h4>Neural Processing</h4>
81
+ <p>Our proprietary U-Net architecture isolates hydrocarbon signatures instantly.</p>
82
+ </div>
83
+ <div className="step-card">
84
+ <span className="step-numeral">03</span>
85
+ <h4>Spatial Analytics</h4>
86
+ <p>Generate highly accurate masks and probability scores to dispatch cleanup crews faster.</p>
87
+ </div>
88
+ </div>
89
+ </section>
90
+
91
+ <section className="preview-section">
92
+ <div className="preview-app-window">
93
+ <div className="app-window-header">
94
+ <div className="mac-dots"><div className="mac-dot red"></div><div className="mac-dot yellow"></div><div className="mac-dot green"></div></div>
95
+ <div className="app-title">app.deepoceans.ai/workspace</div>
96
+ </div>
97
+ <div className="app-window-body">
98
+ <div className="app-mockup-skeleton">
99
+ <div className="mock-nav"></div>
100
+ <div className="mock-layout">
101
+ <div className="mock-top">
102
+ <div className="mock-stat"></div>
103
+ <div className="mock-stat"></div>
104
+ <div className="mock-stat"></div>
105
+ <div className="mock-stat"></div>
106
+ </div>
107
+ <div className="mock-main">
108
+ <div className="mock-side"></div>
109
+ <div className="mock-center"></div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </section>
116
+
117
+ <section className="cta-section">
118
+ <h2>Ready to revolutionize your monitoring?</h2>
119
+ <p>Join the marine conservation operations running on DeepOceans.</p>
120
+ <button className="btn-primary-lg" onClick={onEnter}>Launch Product <ArrowRight size={20} /></button>
121
+ </section>
122
+ </main>
123
+
124
+ <footer className="landing-footer">
125
+ <div className="footer-content">
126
+ <div className="logo"><Activity size={20} color="var(--accent)" /> DeepOceans</div>
127
+ <p>© 2026 DeepOceans AI. Securing marine futures.</p>
128
+ </div>
129
+ </footer>
130
+ </div>
131
+ );
132
+ }
133
+
134
+ function Dashboard() {
135
+ const [selectedFile, setSelectedFile] = useState<File | null>(null);
136
+ const [preview, setPreview] = useState<string | null>(null);
137
+ const [maskSrc, setMaskSrc] = useState<string | null>(null);
138
+ const [isLoading, setIsLoading] = useState(false);
139
+ const [confidence, setConfidence] = useState<number | null>(null);
140
+ const [latency, setLatency] = useState<number | null>(null);
141
+ const [threshold, setThreshold] = useState<number>(0.5);
142
+
143
+ const [history, setHistory] = useState<HistoryRecord[]>([
144
+ {
145
+ id: 1,
146
+ timestamp: new Date(Date.now() - 14400000).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}),
147
+ filename: 'sentinel_scan_alpha.png',
148
+ confidence: 84.2,
149
+ thumbnail: 'https://images.unsplash.com/photo-1621213038477-9dfaf942dcde?auto=format&fit=crop&w=48&h=48'
150
+ }
151
+ ]);
152
+
153
+ const [isDragActive, setIsDragActive] = useState(false);
154
+
155
+ const handleFile = (file: File) => {
156
+ setSelectedFile(file);
157
+ const url = URL.createObjectURL(file);
158
+ setPreview(url);
159
+ setMaskSrc(null);
160
+ setConfidence(null);
161
+ setLatency(null);
162
+ };
163
+
164
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
165
+ if (e.target.files && e.target.files.length > 0) handleFile(e.target.files[0]);
166
+ };
167
+
168
+ const onDragOver = (e: React.DragEvent) => { e.preventDefault(); setIsDragActive(true); };
169
+ const onDragLeave = () => { setIsDragActive(false); };
170
+ const onDrop = (e: React.DragEvent) => {
171
+ e.preventDefault();
172
+ setIsDragActive(false);
173
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) handleFile(e.dataTransfer.files[0]);
174
+ };
175
+
176
+ const handleUpload = async () => {
177
+ if (!selectedFile || !preview) return;
178
+
179
+ setIsLoading(true);
180
+ const formData = new FormData();
181
+ formData.append("file", selectedFile);
182
+
183
+ try {
184
+ const response = await fetch("http://localhost:8000/predict", {
185
+ method: "POST",
186
+ body: formData,
187
+ });
188
+
189
+ if (response.ok) {
190
+ const confHeader = response.headers.get("X-Confidence-Score");
191
+ const latHeader = response.headers.get("X-Inference-Latency-Ms");
192
+
193
+ let confValue = 0;
194
+ if (confHeader) {
195
+ confValue = parseFloat(confHeader);
196
+ setConfidence(confValue);
197
+ }
198
+ if (latHeader) setLatency(parseInt(latHeader));
199
+
200
+ const blob = await response.blob();
201
+ const maskUrl = URL.createObjectURL(blob);
202
+ setMaskSrc(maskUrl);
203
+
204
+ const newRecord: HistoryRecord = {
205
+ id: Date.now(),
206
+ timestamp: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}),
207
+ filename: selectedFile.name,
208
+ confidence: confValue,
209
+ thumbnail: preview
210
+ };
211
+ setHistory(prev => [newRecord, ...prev].slice(0, 10));
212
+ } else {
213
+ alert("Inference server returned an error. Ensure backend is running.");
214
+ }
215
+ } catch (error) {
216
+ console.error(error);
217
+ alert("Connection failure to inference node.");
218
+ } finally {
219
+ setIsLoading(false);
220
+ }
221
+ };
222
+
223
+ return (
224
+ <div className="app-layout">
225
+ {/* SaaS App Header */}
226
+ <header className="app-header">
227
+ <div className="app-logo">
228
+ <div className="logo-icon"><Activity size={18} color="#fff" /></div>
229
+ DeepOceans <span className="app-badge">Workspace</span>
230
+ </div>
231
+ <div className="header-actions">
232
+ <div className="connection-status">
233
+ <div className="status-orb"></div> Node Operational
234
+ </div>
235
+ <button className="icon-btn"><Settings size={18} /></button>
236
+ <button className="profile-btn"><User size={18} /></button>
237
+ </div>
238
+ </header>
239
+
240
+ <main className="dashboard-content">
241
+ <div className="dashboard-grid">
242
+
243
+ {/* Top Row: Metrics Grid */}
244
+ <div className="metrics-row">
245
+ <div className="stat-card">
246
+ <div className="stat-icon-wrapper"><BarChart2 size={20} className="stat-icon" /></div>
247
+ <div className="stat-content">
248
+ <div className="stat-label">Spill Probability</div>
249
+ <div className="stat-value" style={{ color: confidence && confidence > 50 ? 'var(--alert)' : 'var(--text-primary)' }}>
250
+ {confidence !== null ? `${confidence.toFixed(1)}%` : '--'}
251
+ </div>
252
+ </div>
253
+ </div>
254
+ <div className="stat-card">
255
+ <div className="stat-icon-wrapper"><Activity size={20} className="stat-icon" /></div>
256
+ <div className="stat-content">
257
+ <div className="stat-label">Inference Latency</div>
258
+ <div className="stat-value">{latency !== null ? `${latency}ms` : '--'}</div>
259
+ </div>
260
+ </div>
261
+ <div className="stat-card">
262
+ <div className="stat-icon-wrapper"><CheckCircle size={20} className="stat-icon" /></div>
263
+ <div className="stat-content">
264
+ <div className="stat-label">Model IoU (Validation)</div>
265
+ <div className="stat-value">0.842</div>
266
+ </div>
267
+ </div>
268
+ <div className="stat-card">
269
+ <div className="stat-icon-wrapper"><Layers size={20} className="stat-icon" /></div>
270
+ <div className="stat-content">
271
+ <div className="stat-label">Detection Threshold</div>
272
+ <div className="stat-control">
273
+ <input type="range" min="0.1" max="0.9" step="0.1" value={threshold} onChange={(e) => setThreshold(parseFloat(e.target.value))} />
274
+ <span className="range-val">{threshold.toFixed(2)}</span>
275
+ </div>
276
+ </div>
277
+ </div>
278
+ </div>
279
+
280
+ <div className="main-interaction-row">
281
+ {/* Left: Uploader */}
282
+ <div className="upload-container panel">
283
+ <h3 className="panel-title">Data Ingestion</h3>
284
+
285
+ <div
286
+ className={`dropzone ${isDragActive ? 'drag-active' : ''} ${selectedFile ? 'has-file' : ''}`}
287
+ onDragOver={onDragOver}
288
+ onDragLeave={onDragLeave}
289
+ onDrop={onDrop}
290
+ >
291
+ <input type="file" id="file-upload" accept="image/*" onChange={handleFileChange} />
292
+ <UploadCloud size={48} className="dropzone-icon" />
293
+ <div className="dropzone-text">
294
+ <span className="dropzone-primary">Click to upload</span> or drag and drop
295
+ </div>
296
+ <div className="dropzone-secondary">JPG or PNG (Optimal size: 256x256)</div>
297
+ {selectedFile && <div className="selected-file-chip">{selectedFile.name}</div>}
298
+ </div>
299
+
300
+ <button
301
+ className="btn-action-lg"
302
+ onClick={handleUpload}
303
+ disabled={!selectedFile || isLoading}
304
+ >
305
+ {isLoading ? <span className="loader" style={{marginRight: '8px'}}></span> : null}
306
+ {isLoading ? "Running Prediction Pipeline..." : "Execute Detection"}
307
+ </button>
308
+ </div>
309
+
310
+ {/* Right: History Log */}
311
+ <div className="history-container panel">
312
+ <div className="panel-header">
313
+ <h3 className="panel-title">Analysis History</h3>
314
+ <span className="record-count">{history.length} Records</span>
315
+ </div>
316
+ <div className="history-list">
317
+ {history.length === 0 ? (
318
+ <div className="empty-state">No telemetry analyzed yet.</div>
319
+ ) : (
320
+ history.map(record => (
321
+ <div key={record.id} className="history-row fade-in">
322
+ <img src={record.thumbnail} alt="snapshot" className="history-thumb" />
323
+ <div className="history-details">
324
+ <div className="history-filename" title={record.filename}>{record.filename}</div>
325
+ <div className="history-time">{record.timestamp}</div>
326
+ </div>
327
+ <div className="history-status">
328
+ <div className={`status-badge ${record.confidence > 50 ? 'alert' : 'clear'}`}>
329
+ {record.confidence > 50 ? 'Anomaly Detected' : 'All Clear'}
330
+ </div>
331
+ <div className="history-score">{record.confidence.toFixed(1)}%</div>
332
+ </div>
333
+ </div>
334
+ ))
335
+ )}
336
+ </div>
337
+ </div>
338
+ </div>
339
+
340
+ {/* Bottom Row: Visualizations */}
341
+ <div className="visualization-container panel">
342
+ <div className="panel-header" style={{ marginBottom: '24px' }}>
343
+ <h3 className="panel-title">Spatial Rendering Analysis</h3>
344
+ <span className="badge-pill">U-Net Feature Map</span>
345
+ </div>
346
+
347
+ <div className="frames-grid">
348
+ <div className="frame-card">
349
+ <div className="frame-header">Source Imagery</div>
350
+ <div className="frame-content">
351
+ {preview ? <img src={preview} alt="Raw" className="fade-in" /> : <div className="empty-state">Awaiting Feed</div>}
352
+ </div>
353
+ </div>
354
+
355
+ <div className="frame-card">
356
+ <div className="frame-header">Segmentation Mask</div>
357
+ <div className="frame-content dark-mode">
358
+ {maskSrc ? <img src={maskSrc} alt="Mask" className="fade-in invert-render" /> : <div className="empty-state">No Output</div>}
359
+ </div>
360
+ </div>
361
+
362
+ <div className="frame-card">
363
+ <div className="frame-header">Anomaly Overlay</div>
364
+ <div className="frame-content">
365
+ {preview && maskSrc ? (
366
+ <>
367
+ <img src={preview} alt="Base" />
368
+ <img src={maskSrc} alt="Overlay" className="fade-in feature-overlay" />
369
+ </>
370
+ ) : <div className="empty-state">No Output</div>}
371
+ </div>
372
+ </div>
373
+ </div>
374
+ </div>
375
+
376
+ </div>
377
+ </main>
378
+ </div>
379
+ );
380
+ }
381
+
382
+ function App() {
383
+ const [view, setView] = useState<'landing' | 'dashboard'>('landing');
384
+
385
+ return (
386
+ <>
387
+ {view === 'landing' ? <LandingPage onEnter={() => setView('dashboard')} /> : <Dashboard />}
388
+ </>
389
+ );
390
+ }
391
+
392
+ export default App;
frontend/src/assets/hero.png ADDED
frontend/src/assets/react.svg ADDED
frontend/src/assets/vite.svg ADDED
frontend/src/index.css ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --text: #6b6375;
3
+ --text-h: #08060d;
4
+ --bg: #fff;
5
+ --border: #e5e4e7;
6
+ --code-bg: #f4f3ec;
7
+ --accent: #aa3bff;
8
+ --accent-bg: rgba(170, 59, 255, 0.1);
9
+ --accent-border: rgba(170, 59, 255, 0.5);
10
+ --social-bg: rgba(244, 243, 236, 0.5);
11
+ --shadow:
12
+ rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
13
+
14
+ --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
15
+ --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
16
+ --mono: ui-monospace, Consolas, monospace;
17
+
18
+ font: 18px/145% var(--sans);
19
+ letter-spacing: 0.18px;
20
+ color-scheme: light dark;
21
+ color: var(--text);
22
+ background: var(--bg);
23
+ font-synthesis: none;
24
+ text-rendering: optimizeLegibility;
25
+ -webkit-font-smoothing: antialiased;
26
+ -moz-osx-font-smoothing: grayscale;
27
+
28
+ @media (max-width: 1024px) {
29
+ font-size: 16px;
30
+ }
31
+ }
32
+
33
+ @media (prefers-color-scheme: dark) {
34
+ :root {
35
+ --text: #9ca3af;
36
+ --text-h: #f3f4f6;
37
+ --bg: #16171d;
38
+ --border: #2e303a;
39
+ --code-bg: #1f2028;
40
+ --accent: #c084fc;
41
+ --accent-bg: rgba(192, 132, 252, 0.15);
42
+ --accent-border: rgba(192, 132, 252, 0.5);
43
+ --social-bg: rgba(47, 48, 58, 0.5);
44
+ --shadow:
45
+ rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
46
+ }
47
+
48
+ #social .button-icon {
49
+ filter: invert(1) brightness(2);
50
+ }
51
+ }
52
+
53
+ #root {
54
+ width: 1126px;
55
+ max-width: 100%;
56
+ margin: 0 auto;
57
+ text-align: center;
58
+ border-inline: 1px solid var(--border);
59
+ min-height: 100svh;
60
+ display: flex;
61
+ flex-direction: column;
62
+ box-sizing: border-box;
63
+ }
64
+
65
+ body {
66
+ margin: 0;
67
+ }
68
+
69
+ h1,
70
+ h2 {
71
+ font-family: var(--heading);
72
+ font-weight: 500;
73
+ color: var(--text-h);
74
+ }
75
+
76
+ h1 {
77
+ font-size: 56px;
78
+ letter-spacing: -1.68px;
79
+ margin: 32px 0;
80
+ @media (max-width: 1024px) {
81
+ font-size: 36px;
82
+ margin: 20px 0;
83
+ }
84
+ }
85
+ h2 {
86
+ font-size: 24px;
87
+ line-height: 118%;
88
+ letter-spacing: -0.24px;
89
+ margin: 0 0 8px;
90
+ @media (max-width: 1024px) {
91
+ font-size: 20px;
92
+ }
93
+ }
94
+ p {
95
+ margin: 0;
96
+ }
97
+
98
+ code,
99
+ .counter {
100
+ font-family: var(--mono);
101
+ display: inline-flex;
102
+ border-radius: 4px;
103
+ color: var(--text-h);
104
+ }
105
+
106
+ code {
107
+ font-size: 15px;
108
+ line-height: 135%;
109
+ padding: 4px 8px;
110
+ background: var(--code-bg);
111
+ }
frontend/src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
frontend/tsconfig.app.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "es2023",
5
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
6
+ "module": "esnext",
7
+ "types": ["vite/client"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+
18
+ /* Linting */
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true
23
+ },
24
+ "include": ["src"]
25
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
frontend/tsconfig.node.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "es2023",
5
+ "lib": ["ES2023"],
6
+ "module": "esnext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "erasableSyntaxOnly": true,
21
+ "noFallthroughCasesInSwitch": true
22
+ },
23
+ "include": ["vite.config.ts"]
24
+ }
frontend/vite.config.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })
model/loss_metrics.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tensorflow as tf
2
+ from tensorflow.keras import backend as K
3
+
4
+ def dice_coef(y_true, y_pred, smooth=1e-6):
5
+ """Computes the Dice Coefficient."""
6
+ y_true_f = K.flatten(y_true)
7
+ y_pred_f = K.flatten(y_pred)
8
+ intersection = K.sum(y_true_f * y_pred_f)
9
+ return (2. * intersection + smooth) / (K.sum(y_true_f) + K.sum(y_pred_f) + smooth)
10
+
11
+ def dice_loss(y_true, y_pred):
12
+ """Computes the Dice Loss."""
13
+ return 1 - dice_coef(y_true, y_pred)
14
+
15
+ def bce_dice_loss(y_true, y_pred):
16
+ """Computes the Combined BCE + Dice Loss."""
17
+ bce = tf.keras.losses.binary_crossentropy(y_true, y_pred)
18
+ # Ensure dimensions match before adding
19
+ bce = K.mean(bce)
20
+ return bce + dice_loss(y_true, y_pred)
21
+
22
+ def iou_metric(y_true, y_pred, smooth=1e-6):
23
+ """Computes Intersection over Union (IoU)."""
24
+ y_true_f = K.flatten(y_true)
25
+ y_pred_f = K.flatten(y_pred)
26
+ intersection = K.sum(y_true_f * y_pred_f)
27
+ union = K.sum(y_true_f) + K.sum(y_pred_f) - intersection
28
+ return (intersection + smooth) / (union + smooth)
29
+
30
+ def get_metrics():
31
+ """Returns a list of metrics for model compilation."""
32
+ return [
33
+ dice_coef,
34
+ iou_metric,
35
+ tf.keras.metrics.Precision(name='precision'),
36
+ tf.keras.metrics.Recall(name='recall')
37
+ ]
model/train.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import argparse
3
+ import tensorflow as tf
4
+ from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau
5
+ import glob
6
+ from sklearn.model_selection import train_test_split
7
+
8
+ from unet import build_unet
9
+ from loss_metrics import bce_dice_loss, get_metrics
10
+ import sys
11
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
12
+ from utils.data_loader import get_dataset
13
+
14
+ def get_file_paths(data_dir):
15
+ """Retrieves image and mask paths."""
16
+ image_dir = os.path.join(data_dir, 'images')
17
+ mask_dir = os.path.join(data_dir, 'masks')
18
+
19
+ # Supported formats
20
+ image_paths = glob.glob(os.path.join(image_dir, '*.*'))
21
+ # Sorting ensures that image and mask align if they have identical names
22
+ image_paths.sort()
23
+
24
+ # Map image paths to corresponding mask paths
25
+ # Assumes masks have the exact same base name
26
+ mask_paths = []
27
+ for img_path in image_paths:
28
+ base_name = os.path.basename(img_path)
29
+ name, _ = os.path.splitext(base_name)
30
+ # Search for corresponding mask (usually png)
31
+ mask_search = glob.glob(os.path.join(mask_dir, f"{name}.*"))
32
+ if mask_search:
33
+ mask_paths.append(mask_search[0])
34
+ else:
35
+ print(f"Warning: No mask found for {img_path}")
36
+ # If no mask, we should ideally remove the image too, but for now just skip adding
37
+
38
+ # Filter image paths to only those with masks
39
+ valid_image_paths = [p for p in image_paths if glob.glob(os.path.join(mask_dir, f"{os.path.splitext(os.path.basename(p))[0]}.*"))]
40
+ valid_mask_paths = valid_image_paths.copy() # Placeholder - actually they match 1:1 if sorted and filtered
41
+
42
+ # Let's do it safely
43
+ final_img_paths = []
44
+ final_mask_paths = []
45
+ for img in image_paths:
46
+ base = os.path.splitext(os.path.basename(img))[0]
47
+ masks = glob.glob(os.path.join(mask_dir, f"{base}.*"))
48
+ if len(masks) > 0:
49
+ final_img_paths.append(img)
50
+ final_mask_paths.append(masks[0])
51
+
52
+ return final_img_paths, final_mask_paths
53
+
54
+ def train(args):
55
+ print(f"Starting training process. Using data dir: {args.data_dir}")
56
+
57
+ img_paths, mask_paths = get_file_paths(args.data_dir)
58
+ print(f"Found {len(img_paths)} image/mask pairs.")
59
+
60
+ if len(img_paths) == 0:
61
+ print("Error: No data found. Please check data_dir structure.")
62
+ return
63
+
64
+ # Split: 80% Train, 20% Val
65
+ train_x, val_x, train_y, val_y = train_test_split(img_paths, mask_paths, test_size=0.2, random_state=42)
66
+
67
+ train_dataset = get_dataset(train_x, train_y, batch_size=args.batch_size, is_train=True)
68
+ val_dataset = get_dataset(val_x, val_y, batch_size=args.batch_size, is_train=False)
69
+
70
+ model = build_unet(input_shape=(256, 256, 3))
71
+
72
+ optimizer = tf.keras.optimizers.Adam(learning_rate=args.learning_rate)
73
+
74
+ model.compile(optimizer=optimizer, loss=bce_dice_loss, metrics=get_metrics())
75
+
76
+ os.makedirs(args.save_dir, exist_ok=True)
77
+ model_path = os.path.join(args.save_dir, "oil_spill_unet_best.keras")
78
+
79
+ callbacks = [
80
+ ModelCheckpoint(model_path, verbose=1, save_best_only=True, monitor='val_loss'),
81
+ EarlyStopping(monitor='val_loss', patience=15, restore_best_weights=True),
82
+ ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=5, verbose=1, min_lr=1e-6)
83
+ ]
84
+
85
+ history = model.fit(
86
+ train_dataset,
87
+ validation_data=val_dataset,
88
+ epochs=args.epochs,
89
+ callbacks=callbacks
90
+ )
91
+
92
+ print("Training complete. Best model saved at:", model_path)
93
+
94
+ if __name__ == "__main__":
95
+ parser = argparse.ArgumentParser("Train Oil Spill U-Net")
96
+ parser.add_argument("--data_dir", type=str, default="../data", help="Directory containing images/ and masks/ folders.")
97
+ parser.add_argument("--save_dir", type=str, default="../model/saved_models", help="Directory to save the trained model.")
98
+ parser.add_argument("--epochs", type=int, default=50, help="Number of training epochs.")
99
+ parser.add_argument("--batch_size", type=int, default=16, help="Batch size.")
100
+ parser.add_argument("--learning_rate", type=float, default=1e-4, help="Learning rate.")
101
+
102
+ args = parser.parse_args()
103
+ train(args)
model/unet.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tensorflow as tf
2
+ from tensorflow.keras import layers, models
3
+
4
+ def conv_block(input_tensor, num_filters):
5
+ x = layers.Conv2D(num_filters, (3, 3), padding="same")(input_tensor)
6
+ x = layers.BatchNormalization()(x)
7
+ x = layers.Activation("relu")(x)
8
+
9
+ x = layers.Conv2D(num_filters, (3, 3), padding="same")(x)
10
+ x = layers.BatchNormalization()(x)
11
+ x = layers.Activation("relu")(x)
12
+ return x
13
+
14
+ def encoder_block(input_tensor, num_filters):
15
+ x = conv_block(input_tensor, num_filters)
16
+ p = layers.MaxPooling2D((2, 2))(x)
17
+ return x, p
18
+
19
+ def decoder_block(input_tensor, concat_tensor, num_filters):
20
+ x = layers.Conv2DTranspose(num_filters, (2, 2), strides=(2, 2), padding="same")(input_tensor)
21
+ x = layers.concatenate([x, concat_tensor])
22
+ x = conv_block(x, num_filters)
23
+ return x
24
+
25
+ def build_unet(input_shape=(256, 256, 3)):
26
+ """Builds a U-Net architecture for image segmentation."""
27
+ inputs = layers.Input(shape=input_shape)
28
+
29
+ # Encoder
30
+ e1, p1 = encoder_block(inputs, 64)
31
+ e2, p2 = encoder_block(p1, 128)
32
+ e3, p3 = encoder_block(p2, 256)
33
+ e4, p4 = encoder_block(p3, 512)
34
+
35
+ # Bridge
36
+ b = conv_block(p4, 1024)
37
+
38
+ # Decoder
39
+ d1 = decoder_block(b, e4, 512)
40
+ d2 = decoder_block(d1, e3, 256)
41
+ d3 = decoder_block(d2, e2, 128)
42
+ d4 = decoder_block(d3, e1, 64)
43
+
44
+ # Output (1 class for Oil Spill vs Background)
45
+ outputs = layers.Conv2D(1, (1, 1), padding="same", activation="sigmoid")(d4)
46
+
47
+ model = models.Model(inputs, outputs, name="U-Net")
48
+ return model
49
+
50
+ if __name__ == "__main__":
51
+ model = build_unet()
52
+ model.summary()
utils/data_loader.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tensorflow as tf
2
+ import os
3
+
4
+ IMG_SIZE = (256, 256)
5
+
6
+ def decode_image(image_file):
7
+ """Reads and decode an image to float32 [0, 1]."""
8
+ image = tf.io.read_file(image_file)
9
+ # Most general images dataset will be JPEG, but you can change to png if needed.
10
+ image = tf.image.decode_image(image, channels=3, expand_animations=False)
11
+ image = tf.image.resize(image, IMG_SIZE)
12
+ # Normalize between 0 and 1
13
+ image = tf.cast(image, tf.float32) / 255.0
14
+ return image
15
+
16
+ def decode_mask(mask_file):
17
+ """Reads and decode a segmentation mask to binary [0, 1]."""
18
+ mask = tf.io.read_file(mask_file)
19
+ # Masks are typically PNG format
20
+ mask = tf.image.decode_image(mask, channels=1, expand_animations=False)
21
+ mask = tf.image.resize(mask, IMG_SIZE)
22
+
23
+ # Normalize and convert mask to binary (0 and 1)
24
+ mask = tf.cast(mask, tf.float32) / 255.0
25
+ mask = tf.math.round(mask)
26
+ return mask
27
+
28
+ def process_path(image_path, mask_path):
29
+ """Loads image and mask from paths."""
30
+ image = decode_image(image_path)
31
+ mask = decode_mask(mask_path)
32
+ return image, mask
33
+
34
+ def augment(image, mask):
35
+ """Applies random transformations for data augmentation."""
36
+ # Random flip left-right
37
+ if tf.random.uniform(()) > 0.5:
38
+ image = tf.image.flip_left_right(image)
39
+ mask = tf.image.flip_left_right(mask)
40
+
41
+ # Random flip up-down
42
+ if tf.random.uniform(()) > 0.5:
43
+ image = tf.image.flip_up_down(image)
44
+ mask = tf.image.flip_up_down(mask)
45
+
46
+ # Random brightness (only to the image, not the mask)
47
+ image = tf.image.random_brightness(image, max_delta=0.2)
48
+
49
+ # Clip values to be in [0, 1] after brightness modifications
50
+ image = tf.clip_by_value(image, 0.0, 1.0)
51
+
52
+ return image, mask
53
+
54
+ def get_dataset(image_paths, mask_paths, batch_size=16, is_train=True):
55
+ """
56
+ Creates a tf.data.Dataset pipeline.
57
+
58
+ Args:
59
+ image_paths (list): List of file paths to images.
60
+ mask_paths (list): List of file paths to corresponding masks.
61
+ batch_size (int): Size of batches.
62
+ is_train (bool): If true, applies shuffling and augmentations.
63
+ """
64
+ dataset = tf.data.Dataset.from_tensor_slices((image_paths, mask_paths))
65
+
66
+ if is_train:
67
+ # Shuffle heavily for training
68
+ dataset = dataset.shuffle(buffer_size=1000)
69
+
70
+ # Map the image reading function across the dataset
71
+ dataset = dataset.map(process_path, num_parallel_calls=tf.data.AUTOTUNE)
72
+
73
+ if is_train:
74
+ # Apply data augmentations
75
+ dataset = dataset.map(augment, num_parallel_calls=tf.data.AUTOTUNE)
76
+
77
+ # Batch and prefetch for performance
78
+ dataset = dataset.batch(batch_size)
79
+ dataset = dataset.prefetch(tf.data.AUTOTUNE)
80
+ return dataset