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
.venvvirtual environment still present, andGEMINI_API_KEYin.env. - Comfort with running
python manage.pycommands from a terminal.
What you'll build
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 inStep 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
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.authso each user has their own chat history. - Deploy to Vercel — a full follow-up course covers moving
DEBUGtoFalse, settingALLOWED_HOSTS, and going live on a shareable URL.