Raspberry Pi: The Full Stack - A Browser-Based AI Dashboard on a Pi 3

A local Flask server, an HTML interface in Chromium, file upload, AI processing, and results rendered as a table. All of it running on a Raspberry Pi 3B+. Here is what we built.

 

The Full Stack - A Browser-Based AI Dashboard on a Pi 3

Series: Building AI on the Raspberry Pi — Part 4 of 4 | Chromium Dashboard

A local Flask server, an HTML interface in Chromium, file upload, AI processing, and results rendered as a table. All of it running on a Raspberry Pi 3B+. Here is what we built.


We started this series with twelve lines of bash and a curl command. We are finishing it with a web application.

Flask is a lightweight Python web framework. It starts a local HTTP server on port 5000 that Chromium connects to at http://localhost:5000. The HTML page sends the CSV file to Flask via a form upload. Flask reads the file, calls the Groq API, and sends the JSON result back. JavaScript in the page renders it as a table.

The whole thing is local except for that one outbound API call to Groq. Your CSV file never leaves the Pi except to travel to the AI model for processing. Everything else — serving the page, receiving the upload, rendering the results — happens on the board.

The browser is the UI. Flask is the bridge. Groq is the engine. The Pi is the stage. It looks like a web app because it is one.


Setup: Two Libraries

pip3 install flask requests

That is the complete dependency list for this installment.


File Structure

Create a folder called ai_dashboard in your home directory. Inside it, you need two files:

ai_dashboard/
├── app.py          ← the Flask server
└── templates/
    └── index.html  ← the dashboard page

The Flask Server

from flask import Flask, request, jsonify, render_template
import requests as http_requests
import json

app = Flask(__name__)

GROQ_API_KEY = "your_api_key_here"
GROQ_URL = "https://api.groq.com/openai/v1/chat/completions"

@app.route("/")
def index():
    return render_template("index.html")

@app.route("/process", methods=["POST"])
def process():
    if "csv_file" not in request.files:
        return jsonify({"error": "No file uploaded"}), 400

    file = request.files["csv_file"]
    csv_data = file.read().decode("utf-8")

    prompt = f"""Here is a CSV file with product inventory data:

{csv_data}

Return a JSON array where each item has:
- product (string)
- price (number)
- category (reasonable category based on product name)

Return only valid JSON. No explanation."""

    headers = {
        "Authorization": f"Bearer {GROQ_API_KEY}",
        "Content-Type": "application/json"
    }
    payload = {
        "model": "llama-3.3-70b-versatile",
        "messages": [{"role": "user", "content": prompt}],
        "temperature": 0.2
    }

    try:
        response = http_requests.post(GROQ_URL, headers=headers, json=payload)
        response.raise_for_status()
        content = response.json()["choices"][0]["message"]["content"]
        result = json.loads(content)
        return jsonify({"success": True, "data": result})
    except Exception as e:
        return jsonify({"error": str(e)}), 500

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000, debug=False)

The Dashboard Page

Save this as templates/index.html inside your ai_dashboard folder:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>AI CSV Dashboard</title>
<style>
  body { font-family: sans-serif; background: #0f1923; color: #e0e0e0;
         margin: 0; padding: 32px 24px; }
  h1 { font-size: 22px; color: #a8d8f0; margin-bottom: 4px; }
  .sub { font-size: 13px; color: #666; margin-bottom: 28px; }
  .card { background: #1a2535; border-radius: 6px; padding: 24px; margin-bottom: 20px; }
  input[type="file"] { color: #a8d8f0; margin-bottom: 16px; display: block; }
  button { background: #1a5c8a; color: white; border: none;
           padding: 10px 24px; border-radius: 4px; font-size: 14px; cursor: pointer; }
  button:hover { background: #2075b0; }
  button:disabled { background: #333; cursor: default; }
  #status { font-size: 13px; color: #888; margin-top: 12px; }
  table { width: 100%; border-collapse: collapse; margin-top: 8px; }
  th { text-align: left; padding: 10px 12px; background: #111e2b;
       color: #a8d8f0; font-size: 13px; }
  td { padding: 10px 12px; border-bottom: 1px solid #1e2f40; font-size: 14px; }
  tr:last-child td { border-bottom: none; }
  #download-btn { background: #2e5e2e; margin-top: 16px; }
  #download-btn:hover { background: #3a7a3a; }
</style>
</head>
<body>

<h1>AI CSV Dashboard</h1>
<p class="sub">Powered by Groq + Llama 3.3 — running on Raspberry Pi</p>

<div class="card">
  <input type="file" id="csv-input" accept=".csv">
  <button onclick="processCSV()">Send to AI</button>
  <div id="status">Choose a CSV file to get started.</div>
</div>

<div class="card" id="results-card" style="display:none">
  <table id="results-table"></table>
  <button id="download-btn" onclick="downloadJSON()">Download JSON</button>
</div>

<script>
  let lastResult = null;

  async function processCSV() {
    const input = document.getElementById("csv-input");
    const status = document.getElementById("status");
    if (!input.files[0]) { status.textContent = "Please select a CSV file first."; return; }
    status.textContent = "Sending to Groq API...";
    const formData = new FormData();
    formData.append("csv_file", input.files[0]);
    try {
      const response = await fetch("/process", { method: "POST", body: formData });
      const result = await response.json();
      if (result.error) {
        status.textContent = "Error: " + result.error;
      } else {
        lastResult = result.data;
        renderTable(result.data);
        status.textContent = "Done. " + result.data.length + " items processed.";
        document.getElementById("results-card").style.display = "block";
      }
    } catch (err) {
      status.textContent = "Network error: " + err.message;
    }
  }

  function renderTable(data) {
    const table = document.getElementById("results-table");
    const headers = Object.keys(data[0]);
    let html = "<tr>" + headers.map(h => `<th>${h.toUpperCase()}</th>`).join("") + "</tr>";
    data.forEach(row => {
      html += "<tr>" + headers.map(h => `<td>${row[h]}</td>`).join("") + "</tr>";
    });
    table.innerHTML = html;
  }

  function downloadJSON() {
    if (!lastResult) return;
    const blob = new Blob([JSON.stringify(lastResult, null, 2)], {type: "application/json"});
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = "output.json";
    a.click();
  }
</script>
</body>
</html>

Running It

Navigate to your ai_dashboard folder in the terminal and start the server:

cd ~/ai_dashboard
python3 app.py

Open Chromium and go to http://localhost:5000. The dashboard loads. Select your CSV, click Send to AI, and the results render as a table in the browser. Click Download JSON to save the output.

The complete application is two files — app.py and index.html. That is the entire stack.


What Just Happened

A Raspberry Pi 3B+ just served a web application to its own browser, processed a file through a frontier AI model, and rendered the results as a formatted data table. The Pi handled the HTTP routing, the file reading, and the UI. Groq handled the intelligence. The whole thing cost nothing to run except the electricity to power the board.

That is the thesis of this series proven at every level. Shell script. Python CLI. Desktop app. Web dashboard. The hardware constraint is real — you cannot run a meaningful AI model locally on a Pi 3. But you can build AI-powered applications on one, today, for free.

The Pi does not need to be the AI. It needs to be the builder. Those are different jobs. And the Pi 3 is very good at one of them.


Series complete. Four platforms. Four installments. One free API key. Any Pi that runs a desktop.


Tested on Raspberry Pi 3B+ and 3A+ running Raspberry Pi OS 64-bit. Also works on Pi 4 and Pi 5.





Aaron Rose is a software engineer and technology writer at tech-reader.blog. 

Catch up on the latest explainer videos, podcasts, and industry discussions below.


Popular posts from this blog

Insight: The Great Minimal OS Showdown—DietPi vs Raspberry Pi OS Lite

Running AI Models on Raspberry Pi 5 (8GB RAM): What Works and What Doesn't