BACK_TO_LOGS// STEP_8_OF_10
HANDBOOK_GUIDE2026-06-29

OptiCrop: Smart Agricultural Production Optimization Engine ML Guide

By Rayan Syed
45 min read

Part 8: Frontend User Interface Files

To create a clean interface, we split our frontend assets into three files: index.html, style.css, and script.js.

8.1 How the Frontend Connects with the Backend

Web applications operate on a Client-Server Architecture.

  • The Client (Frontend): This is the user interface running in the browser (index.html, style.css, script.js). Its job is to collect information from the user and display results.
  • The Server (Backend): This is our Flask script (app.py) running on our local machine. It listens for incoming HTTP requests, feeds features to our machine learning model, and returns the prediction.
  • The Communication (HTTP POST & JSON): When the user clicks the submit button, the frontend does not reload the page. Instead, JavaScript packages the form inputs into a JSON dictionary and sends it via an HTTP POST request to the /predict URL on the Flask server. Once the server responds with a success or rejection state, the webpage updates itself dynamically.
  • Why split into HTML, CSS, and JS?: We separate structure (HTML), style (CSS), and behavior (JavaScript) to maintain clean code. This follows the Separation of Concerns principle, making it easy to redesign the UI or tweak frontend validations without editing backend code.

Frontend Template (templates/index.html)

Save this layout configuration inside templates/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>OptiCrop Recommendation Engine</title>
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <div class="card">
        <h2 class="title">OptiCrop Recommendation Engine</h2>
        <p class="subtitle">Enter soil and climate variables to identify the optimal crop.</p>
        
        <form id="predictionForm">
            <div class="row">
                <div class="col">
                    <label for="N">Nitrogen (N) - mg/kg</label>
                    <input type="number" step="any" id="N" required placeholder="0 - 250">
                </div>
                <div class="col">
                    <label for="P">Phosphorus (P) - mg/kg</label>
                    <input type="number" step="any" id="P" required placeholder="0 - 250">
                </div>
                <div class="col">
                    <label for="K">Potassium (K) - mg/kg</label>
                    <input type="number" step="any" id="K" required placeholder="0 - 300">
                </div>
            </div>

            <div class="row">
                <div class="col">
                    <label for="temperature">Temperature (°C)</label>
                    <input type="number" step="any" id="temperature" required placeholder="e.g. 24.5">
                </div>
                <div class="col">
                    <label for="humidity">Humidity (%)</label>
                    <input type="number" step="any" id="humidity" required placeholder="e.g. 78">
                </div>
            </div>

            <div class="row">
                <div class="col">
                    <label for="ph">Soil pH Level</label>
                    <input type="number" step="any" id="ph" required placeholder="0.0 - 14.0">
                </div>
                <div class="col">
                    <label for="rainfall">Rainfall (mm)</label>
                    <input type="number" step="any" id="rainfall" required placeholder="e.g. 180">
                </div>
            </div>

            <button type="submit">Identify Best Crop</button>
        </form>

        <div id="result"></div>
    </div>
    <script src="/static/script.js"></script>
</body>
</html>

Style Sheet (static/style.css)

Save this CSS configuration inside static/style.css to build our form wrapper:

body {
    background-color: #0c0c0e;
    color: #f3f4f6;
    font-family: system-ui, -apple-system, sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    margin: 0;
    padding: 20px;
}
.card {
    background-color: #16161a;
    border: 1px solid #2e2e38;
    padding: 40px;
    border-radius: 16px;
    max-width: 500px;
    width: 100%;
}
.title {
    margin: 0 0 8px 0;
    font-size: 24px;
    font-weight: 700;
}
.subtitle {
    margin: 0 0 30px 0;
    color: #9ca3af;
    font-size: 14px;
}
.row {
    display: flex;
    gap: 16px;
    margin-bottom: 20px;
}
.col {
    display: flex;
    flex-direction: column;
    flex: 1;
}
label {
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin-bottom: 6px;
    color: #9ca3af;
}
input {
    background-color: #24242e;
    border: 1px solid #3a3a46;
    padding: 12px;
    border-radius: 8px;
    color: white;
    font-size: 14px;
    outline: none;
}
input:focus {
    border-color: #10b981;
}
button {
    background-color: #10b981;
    color: white;
    border: none;
    padding: 14px;
    border-radius: 8px;
    font-weight: 700;
    width: 100%;
    cursor: pointer;
    font-size: 14px;
    transition: background-color 0.2s;
}
button:hover {
    background-color: #059669;
}
#result {
    margin-top: 24px;
    padding: 16px;
    border-radius: 8px;
    text-align: center;
    font-weight: 700;
    font-size: 18px;
    display: none;
}
.success {
    background-color: rgba(16, 185, 129, 0.1);
    border: 1px solid #10b981;
    color: #10b981;
    display: block !important;
}
.error {
    background-color: rgba(239, 68, 68, 0.1);
    border: 1px solid #ef4444;
    color: #ef4444;
    display: block !important;
}

Application Logic Script (static/script.js)

Save this JS module inside static/script.js:

document.getElementById('predictionForm').addEventListener('submit', async (e) => {
    e.preventDefault();
    
    // Package form parameters
    const data = {
        N: parseFloat(document.getElementById('N').value),
        P: parseFloat(document.getElementById('P').value),
        K: parseFloat(document.getElementById('K').value),
        temperature: parseFloat(document.getElementById('temperature').value),
        humidity: parseFloat(document.getElementById('humidity').value),
        ph: parseFloat(document.getElementById('ph').value),
        rainfall: parseFloat(document.getElementById('rainfall').value)
    };

    const resultBox = document.getElementById('result');
    resultBox.style.display = 'block';
    resultBox.className = '';
    resultBox.innerText = 'Analyzing soil parameters...';

    try {
        const response = await fetch('/predict', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        });

        const res = await response.json();
        
        if (response.ok && res.success) {
            resultBox.className = 'success';
            resultBox.innerText = `RECOMMENDED CROP: ${res.crop}`;
        } else {
            resultBox.className = 'error';
            resultBox.innerText = `Prediction Error: ${res.error || 'Server rejection'}`;
        }
    } catch (err) {
        resultBox.className = 'error';
        resultBox.innerText = `Connection failed: ${err.message}`;
    }
});

8.2 Understanding the Script Execution

Let’s break down the logic of this script:

  • e.preventDefault(): Prevents the browser from reloading the page when you click the submit button. This allows us to handle the form processing silently in the background.
  • data: Gathers all values from form fields (e.g. N, P, K, temperature) and formats them into a single JavaScript object using parseFloat(). The keys in this object match the keys in our Python request parser inside app.py.
  • fetch('/predict', ...): Starts a secure asynchronous HTTP connection. It sends the payload to our backend server as a JSON string (JSON.stringify(data)) with headers identifying the content type.
  • response.json(): Waits for the server to reply and parses the returned JSON string back into a readable JavaScript dictionary.
  • Result Banner Update: Displays the #result container and applies the appropriate CSS styles. If the response succeeds, it displays the recommended crop name in a green success banner; if it fails, it displays a red warning banner.

8.3 Executing the Model Training Script

Before our server can process predictions, we must generate our serialized model and scaler weights:

  1. Open your terminal in the root of your opticrop directory.
  2. Execute the training pipeline:
    python train.py
    
  3. Inspect the Console Outputs: You will see step-by-step progress as Scikit-Learn cleans headers, splits train/test subsets, applies standard scaling, compares accuracies, selects the winner, and confirms serialization:
    Step 1: Loading agricultural dataset...
    Step 2: Resolving missing values...
    Step 3: Applying Standard Scaling to features...
    
    Step 4: Training and comparing classification models...
    -> Logistic Regression Test Accuracy: 96.59%
    -> K-Nearest Neighbors Test Accuracy: 97.95%
    -> Decision Tree Test Accuracy: 98.64%
    -> Random Forest Test Accuracy: 99.32%
    
    Unsupervised K-Means clustering completed on scaled features.
    
    Winner Selected: Random Forest with 99.32% accuracy.
    
    Step 5: Serializing and saving best model & scaler...
    ✓ Output model: models/crop_model.joblib
    ✓ Output scaler: models/scaler.joblib
    
  4. Verify Output Files: Confirm that crop_model.joblib and scaler.joblib have been successfully created inside your models/ directory.

8.4 Running the Flask Web Server

With the model weights and all frontend/backend files created, we can spin up our local development web server:

  1. Rerun the Flask server in your terminal:
    python app.py
    
  2. Confirm Server Initialization: The console will output:
    Crop model and scaler loaded successfully!
     * Serving Flask app 'app'
     * Debug mode: on
     * Running on http://127.0.0.1:5000
    
  3. Access the Portal: Open your web browser and navigate to:
    http://localhost:5000
    
    You should see your newly created Crop Recommendation form displayed.

8.5 Manual Verification & Test Scenarios

To test that your machine learning model and web interface are communicating correctly, try inputting this validation profile:

  • Test Scenario 1: Optimal Rice Soil Profile
    • Nitrogen (N): 90
    • Phosphorus (P): 42
    • Potassium (K): 43
    • Temperature: 21
    • Humidity: 82
    • pH: 6.5
    • Rainfall: 200
    • Expected Result: RECOMMENDED CROP: Rice displays in a green success box.
  • Test Scenario 2: High-Risk Out-of-Bounds Input
    • Enter a pH value of 15.0.
    • Click Identify Best Crop.
    • Expected Result: Prediction Error: Soil pH must be between 0.0 and 14.0. displays in a red warning box, proving that our Flask API validation checks are actively defending the machine learning pipeline from garbage inputs!

8.6 Critical Fallbacks & Common Run Issues

As a developer, you may encounter these common startup errors. Here is how to diagnose and resolve them:

  • Error: Address already in use (Port Conflict): If you see a crash saying OSError: [Errno 98] Address already in use, it means another service on your computer is already listening on port 5000 (such as macOS AirPlay Receiver, or an orphaned Python task running in the background).
    • The Fix: Open app.py, scroll to the very bottom, and change the port configuration to 5001 or 8080:
      app.run(port=5001, debug=True)
      
      Then, rerun python app.py and navigate to http://localhost:5001.
  • Error: TemplateNotFound: If you navigate to the page and see a Flask traceback error stating jinja2.exceptions.TemplateNotFound: index.html, it means your HTML file is in the wrong directory.
    • The Fix: Double check your workspace sidebar. Ensure index.html resides inside a folder named templates. If it is in the root or inside static, Flask’s engine will not find it.