← All courses

Tutorial · Intermediate

Add a web UI to your chatbot

By Ian Freitz de la Cernaianfreitz.com

Wrap your Python Gemini chatbot in a Django web app so it runs in the browser — views, templates, and a live chat page.

IntermediateDjangoPythonGemini~12 minFree tier

What you’ll become

A developer who can take a Python AI chatbot and put it on the web.

  • Scaffold a Django project and wire a chat app
  • Turn a Python script into a reusable Gemini client
  • Render chat turns with views, URLs, and templates
  • Ship a live chat page running in the browser

Recommended first

Take Create a chatbot first — this course builds on what you made there.

Step 01

Before you start

This course picks up exactly where Create a chatbot ended. You’ll reuse the same project folder, the same .venv, and the same .envfile with your Gemini key. We’ll keep the original chatbot.py intact and wrap its logic in a Django site so users can chat in a browser.

  • A working gemini-chatbot/ folder from the previous course.
  • The .venv virtual environment still present, and GEMINI_API_KEY in .env.
  • Comfort with running python manage.py commands from a terminal.

What you'll build

A single-page Django site at http://127.0.0.1:8000/ with a chat log, an input box, and a Send button. Each message is sent to Gemini and the reply streams back into the log. Conversation history is kept in the Django session so the bot remembers earlier turns.

Step 02

Install Django

Django goes into the same virtualenv you already have. The google-generativeai and python-dotenv packages from the previous course stay exactly as they were.

terminal

# Activate the virtualenv from the previous course
source .venv/bin/activate   # macOS / Linux
# .venv\Scripts\Activate.ps1   # Windows PowerShell

# Install Django alongside your existing packages
pip install django

Step 03

Create the project and app

From inside the gemini-chatbot/ folder, create a Django project called chatsite and an app called chat. The trailing dot on startproject tells Django to use the current directory rather than nesting another folder.

terminal

# From inside gemini-chatbot/
django-admin startproject chatsite .
python manage.py startapp chat

Then register the new chat app in the project settings so Django can find its templates and views.

chatsite/settings.py

# chatsite/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "chat",
]

The folder should now look like this:

project structure

gemini-chatbot/
  .venv/
  .env
  chatbot.py              original terminal bot (left untouched)
  manage.py               Django entrypoint
  chatsite/               project config (settings, urls, wsgi)
  chat/                   the new app we'll fill in

Step 04

Extract the Gemini logic

The terminal loop from chatbot.pydoesn’t fit a web request — each HTTP call is independent. Move the reusable pieces (load the key, configure the client, call the model) into a small module and expose a single get_reply() function.

chat/gemini_client.py

# chat/gemini_client.py
import os
from dotenv import load_dotenv
import google.generativeai as genai

load_dotenv()
_api_key = os.getenv("GEMINI_API_KEY")
if not _api_key:
    raise RuntimeError("GEMINI_API_KEY not found. Check your .env file.")

genai.configure(api_key=_api_key)

_model = genai.GenerativeModel("gemini-2.0-flash")


def get_reply(message: str, history: list[dict]) -> str:
    """Send a message with prior history and return Gemini's reply."""
    chat = _model.start_chat(history=history)
    response = chat.send_message(message)
    return response.text

The model and API key are loaded once when the module is imported. Each call builds a fresh chat session from the passed-in history, which is how we’ll carry conversation state across requests.

Step 05

Add views and URLs

Two views are enough: one to render the chat page and one to accept a message over POST and return Gemini’s reply as JSON. History lives in request.session, which Django persists in an encrypted cookie by default.

chat/views.py

# chat/views.py
import json
from django.http import JsonResponse
from django.shortcuts import render
from django.views.decorators.http import require_http_methods

from . import gemini_client


def index(request):
    return render(request, "chat/index.html")


@require_http_methods(["POST"])
def send_message(request):
    data = json.loads(request.body)
    message = data.get("message", "").strip()
    history = request.session.get("history", [])

    if not message:
        return JsonResponse({"error": "empty message"}, status=400)

    reply = gemini_client.get_reply(message, history)

    history.append({"role": "user", "parts": [message]})
    history.append({"role": "model", "parts": [reply]})
    request.session["history"] = history

    return JsonResponse({"reply": reply})

Wire the views to URL paths for the app:

chat/urls.py

# chat/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("", views.index, name="index"),
    path("send/", views.send_message, name="send_message"),
]

Then include them from the project’s root URLs:

chatsite/urls.py

# chatsite/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("chat.urls")),
]

Step 06

Build the chat page

Put the template at chat/templates/chat/index.html — the nested chat/ folder is a Django convention that avoids template name collisions across apps. The page is plain HTML with a little CSS and a small script that POSTs to /send/.

chat/templates/chat/index.html

{# chat/templates/chat/index.html #}
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Gemini Chat</title>
    <style>
      body { font-family: system-ui, sans-serif; max-width: 640px;
             margin: 2rem auto; padding: 0 1rem; }
      #log { border: 1px solid #ddd; border-radius: 8px; padding: 1rem;
             height: 420px; overflow-y: auto; background: #fafafa; }
      .msg { margin: 0.5rem 0; line-height: 1.4; }
      .user { color: #333; }
      .bot  { color: #2c694e; }
      form  { display: flex; gap: 0.5rem; margin-top: 1rem; }
      input { flex: 1; padding: 0.6rem; border: 1px solid #ccc;
              border-radius: 6px; }
      button { padding: 0.6rem 1rem; border: 0; border-radius: 6px;
               background: #2c694e; color: white; cursor: pointer; }
    </style>
  </head>
  <body>
    <h1>Gemini Chat</h1>
    <div id="log"></div>
    <form id="f">
      <input id="m" placeholder="Say something..." autofocus />
      <button>Send</button>
    </form>

    <script>
      const log = document.getElementById("log");
      const form = document.getElementById("f");
      const input = document.getElementById("m");
      const csrf = document.cookie.match(/csrftoken=([^;]+)/)?.[1];

      function append(role, text) {
        const p = document.createElement("p");
        p.className = "msg " + role;
        p.textContent = (role === "user" ? "You: " : "Gemini: ") + text;
        log.appendChild(p);
        log.scrollTop = log.scrollHeight;
      }

      form.addEventListener("submit", async (e) => {
        e.preventDefault();
        const message = input.value.trim();
        if (!message) return;
        append("user", message);
        input.value = "";

        const res = await fetch("/send/", {
          method: "POST",
          headers: { "Content-Type": "application/json", "X-CSRFToken": csrf },
          body: JSON.stringify({ message }),
        });
        const data = await res.json();
        append("bot", data.reply ?? ("[error] " + (data.error ?? "unknown")));
      });
    </script>
  </body>
</html>

CSRF

Django protects POST requests with a CSRF token. We read it from the csrftoken cookie and send it back in the X-CSRFToken header — no template tag or form needed.

Step 07

Run it

Run the initial migrations (Django needs them for the session table), then start the dev server.

terminal

(.venv) $ python manage.py migrate
(.venv) $ python manage.py runserver
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Open http://127.0.0.1:8000/ in a browser and try a conversation. Refresh the page — thanks to the session, the bot still remembers what you said before.

Step 08

Where to take it next

  • Persist history in a Messagemodel instead of the session, so conversations survive cookie clears and server restarts — there’s a full follow-up course for this.
  • Stream the replywith Server-Sent Events so tokens appear as they’re generated, not all at once.
  • Add accounts with django.contrib.auth so each user has their own chat history.
  • Deploy to Vercel — a full follow-up course covers moving DEBUG to False, setting ALLOWED_HOSTS, and going live on a shareable URL.

Discussion

0
Sign in to join the conversation.

No comments yet. Start the conversation.