Raspberry Pi: Now It Has a Window - A Desktop AI App on Raspberry Pi OS

Parts 1 and 2 lived in the terminal. Part 3 gets a proper window — file picker, results display, save button — built with Tkinter, which ships with Python on every Raspberry Pi OS install. Zero additional dependencies.

 

Now It Has a Window - A Desktop AI App on Raspberry Pi OS

Series: Building AI on the Raspberry Pi — Part 3 of 4 | Tkinter Desktop App

Parts 1 and 2 lived in the terminal. Part 3 gets a proper window — file picker, results display, save button — built with Tkinter, which ships with Python on every Raspberry Pi OS install. Zero additional dependencies.


At this point in the series, we have proven the concept twice. A bash script can send a CSV to Groq and get JSON back. A Python script does it more cleanly and saves the output to a file. Both run in a terminal window on the Raspberry Pi OS desktop.

Part 3 is where it starts to feel like a real application.

Tkinter is Python's built-in GUI toolkit. It is not the most beautiful framework ever built. But it is capable, it is fast on low-power hardware, and it produces a real native window that looks at home on the Raspberry Pi desktop. No browser. No web server. A window that opens when you run the script and closes when you are done.


How We Built This — With a Little Help

Before writing the Tkinter layout, we pulled up Claude at claude.ai — the free plan — and described what we wanted. Not to write the code for us, but to think through the structure.

We described the app: a file picker, a text display area, two buttons. We asked whether to use a grid layout or pack layout for Tkinter, and whether to run the API call on a background thread so the UI does not freeze while waiting for the response.

The answer was direct: use grid for anything with more than two or three elements because it gives you alignment control, and yes — absolutely use threading, because a Tkinter window that freezes for two seconds while an API call completes looks broken even if it is not. That saved us from finding out the hard way.

This is a workflow worth keeping. Claude on the free web plan is a capable thinking partner when you are mid-build and need to reason through a structural decision. You are not outsourcing the work. You are asking a smart collaborator a specific question and incorporating the answer.

A window that freezes for two seconds while an API call runs looks broken even when it isn't. Threading fixes that before users notice it.


The Complete App

Create a file called ai_csv_app.py:

import tkinter as tk
from tkinter import filedialog, scrolledtext, messagebox
import threading
import requests
import json

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

class AICSVApp:
    def __init__(self, root):
        self.root = root
        self.root.title("AI CSV Processor")
        self.root.geometry("700x520")
        self.root.configure(bg="#f0f0f0")
        self.csv_path = None
        self.result_json = None
        self.build_ui()

    def build_ui(self):
        title = tk.Label(self.root, text="AI CSV Processor",
            font=("DejaVu Sans", 16, "bold"), bg="#f0f0f0", fg="#1a1a1a")
        title.grid(row=0, column=0, columnspan=3, pady=(20, 4))

        subtitle = tk.Label(self.root, text="Powered by Groq + Llama 3.3",
            font=("DejaVu Sans", 10), bg="#f0f0f0", fg="#666")
        subtitle.grid(row=1, column=0, columnspan=3, pady=(0, 16))

        self.file_label = tk.Label(self.root, text="No file selected",
            font=("DejaVu Sans", 10), bg="#f0f0f0", fg="#444", anchor="w")
        self.file_label.grid(row=2, column=0, padx=(20, 8), sticky="ew")

        pick_btn = tk.Button(self.root, text="Choose CSV",
            command=self.pick_file, bg="#1a5c8a", fg="white",
            font=("DejaVu Sans", 10, "bold"), relief="flat", padx=12, pady=6)
        pick_btn.grid(row=2, column=1, padx=4)

        self.process_btn = tk.Button(self.root, text="Send to AI",
            command=self.process, bg="#2e7d32", fg="white",
            font=("DejaVu Sans", 10, "bold"), relief="flat", padx=12, pady=6,
            state="disabled")
        self.process_btn.grid(row=2, column=2, padx=(4, 20))

        self.status_var = tk.StringVar(value="Ready.")
        status = tk.Label(self.root, textvariable=self.status_var,
            font=("DejaVu Sans", 9), bg="#f0f0f0", fg="#888")
        status.grid(row=3, column=0, columnspan=3, pady=(8, 4))

        self.output = scrolledtext.ScrolledText(self.root,
            font=("DejaVu Mono", 11), bg="#0f1923", fg="#a8d8f0",
            insertbackground="white", relief="flat", padx=12, pady=12, wrap=tk.NONE)
        self.output.grid(row=4, column=0, columnspan=3,
            padx=20, pady=8, sticky="nsew")

        self.save_btn = tk.Button(self.root, text="Save JSON",
            command=self.save_output, bg="#555", fg="white",
            font=("DejaVu Sans", 10), relief="flat", padx=12, pady=6, state="disabled")
        self.save_btn.grid(row=5, column=2, padx=(4, 20), pady=(4, 20))

        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(4, weight=1)

    def pick_file(self):
        path = filedialog.askopenfilename(title="Select a CSV file",
            filetypes=[("CSV files", "*.csv"), ("All files", "*.*")])
        if path:
            self.csv_path = path
            self.file_label.config(text=path.split("/")[-1])
            self.process_btn.config(state="normal")
            self.status_var.set("File loaded. Click 'Send to AI' when ready.")

    def process(self):
        self.process_btn.config(state="disabled")
        self.save_btn.config(state="disabled")
        self.status_var.set("Sending to Groq API...")
        self.output.delete("1.0", tk.END)
        thread = threading.Thread(target=self.run_api_call)
        thread.daemon = True
        thread.start()

    def run_api_call(self):
        try:
            with open(self.csv_path, "r") as f:
                csv_data = f.read()

            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}

            response = requests.post(GROQ_URL, headers=headers, json=payload)
            response.raise_for_status()
            content = response.json()["choices"][0]["message"]["content"]
            self.result_json = json.loads(content)
            pretty = json.dumps(self.result_json, indent=2)
            self.root.after(0, self.show_result, pretty)

        except Exception as e:
            self.root.after(0, self.show_error, str(e))

    def show_result(self, text):
        self.output.insert("1.0", text)
        self.status_var.set("Done. Results displayed below.")
        self.process_btn.config(state="normal")
        self.save_btn.config(state="normal")

    def show_error(self, message):
        self.status_var.set(f"Error: {message}")
        self.process_btn.config(state="normal")
        messagebox.showerror("API Error", message)

    def save_output(self):
        if not self.result_json:
            return
        path = filedialog.asksaveasfilename(defaultextension=".json",
            filetypes=[("JSON files", "*.json")])
        if path:
            with open(path, "w") as f:
                json.dump(self.result_json, f, indent=2)
            self.status_var.set(f"Saved to {path.split('/')[-1]}")

if __name__ == "__main__":
    root = tk.Tk()
    app = AICSVApp(root)
    root.mainloop()

Run it from the terminal:

python3 ai_csv_app.py

A window opens. Click Choose CSV, select your inventory file, click Send to AI, and watch the JSON appear in the dark text area. Click Save JSON when you are ready to write it to disk.

Additional packages to install: zero. Tkinter ships with Python on Raspberry Pi OS.


The Threading Detail

Notice the threading.Thread in the process method. The API call runs on a background thread so the window stays responsive while it waits for Groq. Without that, the window would freeze for the duration of the network call — not broken, but it looks broken, which is the same problem from a user's perspective.

The self.root.after(0, ...) calls bring results back to the main UI thread. Tkinter is not thread-safe, so you never update widgets directly from a background thread. You schedule the update instead.

This pattern — background thread for the work, after() for the UI update — is the right structure for any Tkinter app that makes network calls. Use it every time.


Next in the series — Part 4: The Full Stack: A Browser-Based AI Dashboard on a Pi 3. Flask in the background, Chromium up front, results rendered as a table in the browser. Two files. Zero cost.


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