From zero to a deployed, working AI chatbot in two hours. You will write real code, call real APIs, and share a live URL with the world.
By the end of this two-hour lab, you will have built and deployed a fully functional AI chatbot that runs entirely in the browser. No server, no backend, no frameworks.
A polished dark-theme chat UI with message bubbles, typing indicators, and smooth scrolling
Connected to Google's Gemini API for real-time, intelligent responses about Earth Observation
Custom system prompt making the chatbot an expert assistant for satellite data analysis
Deployed on GitHub Pages with a real URL you can share with anyone in the world
Our chatbot uses a simple client-side architecture. The browser does all the work: capturing user input, calling the AI API, and rendering the response.
Before we start coding, let's figure out what we are building. Here are three suggested project tracks to get you inspired, but you are absolutely welcome to bring your own idea. The only requirement is that your project combines satellite/Earth observation data with AI.
A lean, automated insurance platform for farmers. If satellite data (NDVI, soil moisture) detects a drought or flood event triggering a specific threshold, a smart contract automatically issues a payout.
An AI dashboard tailored for an ISU student-built CubeSat. It augments existing global satellite data with localized low-orbit sensors tuned for regional needs like Alsace wine production or targeted air pollution monitoring.
View the Alsace CubeSat Concept Brochure βAn automated ESG monitoring dashboard designed for Paddock Academy's industrial partners. The AI analyzes Sentinel-2/5P emissions against EU regulations to automatically generate environmental audit reports.
Have a different problem you want to solve with satellite data and AI? Wildfire monitoring, coastal erosion tracking, urban heat islands, ocean pollution? Pitch it! Any project that combines EO data with an intelligent web application is fair game.
Google's Gemini API is free for prototyping. The free tier gives you 15 requests per minute, which is more than enough for development and learning.1
Navigate to https://aistudio.google.com/apikey
Sign in with your Google account (the same one you use for Gmail).
Click "Create API Key". You may be asked to create a Google Cloud project. Accept the defaults.
Your key will look something like: AIzaSyB...xyz
Copy it and save it somewhere safe (a text file, a note app). You will need it shortly.
Groq provides extremely fast inference on open-source models like Llama 3 and Mixtral. We will use it as a backup provider and to demonstrate multi-model support.
Navigate to https://console.groq.com/keys
Sign up or log in (you can use your Google or GitHub account).
Click "Create API Key". Name it something like isu-lab.
Your key will look like: gsk_aBcDeFgH...
Copy and store it alongside your Gemini key.
Before writing any app code, let's verify your keys work. Open your browser's Developer Tools (F12) and go to the Console tab.
// Paste this into your browser console (F12 > Console)
// Replace YOUR_KEY with your actual Gemini API key
const response = await fetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=YOUR_KEY', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: 'Hello! What is NDVI in one sentence?' }] }]
})
});
const data = await response.json();
console.log(data.candidates[0].content.parts[0].text);
NDVI (Normalized Difference Vegetation Index) is a measure of
vegetation health derived from satellite imagery by comparing
near-infrared and red light reflectance.
In production applications, API keys must never be exposed in client-side code. However, for this learning exercise, we will use client-side keys with appropriate precautions.
| Approach | Use Case | Security Level |
|---|---|---|
| Key in JS file (what we do today) | Prototyping, learning labs | β οΈ Low |
| .env file + build tool | Local development | β οΈ Medium |
| Backend proxy server | Production web apps | β High |
| Puter.js (no key needed) | Quick prototyping | β High |
sessionStorage:
let apiKey = sessionStorage.getItem('gemini_api_key');
if (!apiKey) {
apiKey = prompt('Please enter your Gemini API Key:');
sessionStorage.setItem('gemini_api_key', apiKey);
}
Let's break down every part of the API call you just ran. Understanding this structure is essential for building the chatbot.
// The API endpoint: model name + action + API key
const API_URL =
'https://generativelanguage.googleapis.com/v1beta/' // Base URL
+ 'models/gemini-2.0-flash' // Model
+ ':generateContent' // Action
+ '?key=' + API_KEY; // Auth
// The request body: an array of conversation turns
const requestBody = {
contents: [
{
role: 'user', // Who sent this message
parts: [
{ text: 'What is NDVI?' } // The message content
]
}
]
};
// The response structure
// data.candidates[0].content.parts[0].text = the AI's reply
Every chat interface has three core components: a message container, an input field, and a send button. Let's build the HTML skeleton.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0">
<title>EO Chat Assistant</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Header -->
<header>
<h1>π°οΈ EO Chat Assistant</h1>
<p>Ask me anything about Earth Observation</p>
</header>
<!-- Messages container: all chat bubbles go here -->
<main id="chat-messages">
<!-- Messages will be inserted by JavaScript -->
</main>
<!-- Input area: fixed at the bottom -->
<footer>
<form id="chat-form">
<input type="text"
id="user-input"
placeholder="Ask about NDVI, Sentinel-2, land cover..."
autocomplete="off"
required>
<button type="submit">Send</button>
</form>
</footer>
<script src="app.js"></script>
</body>
</html>
We will style our chat with a dark theme that matches the course aesthetic. The key is styling user bubbles (right-aligned, colored) differently from AI bubbles (left-aligned, neutral).
/* Reset and base */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: #050a18;
color: #f9fafb;
height: 100vh;
display: flex;
flex-direction: column;
}
header {
text-align: center;
padding: 1.5rem;
border-bottom: 1px solid rgba(255,255,255,0.08);
}
header h1 { font-size: 1.5rem; }
header p { color: #9ca3af; font-size: 0.9rem; }
/* Messages area */
#chat-messages {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
/* Message bubbles */
.message {
max-width: 75%;
padding: 0.9rem 1.2rem;
border-radius: 16px;
font-size: 0.95rem;
line-height: 1.6;
}
.message.user {
background: #10b981;
color: white;
align-self: flex-end;
border-bottom-right-radius: 4px;
}
.message.ai {
background: rgba(255,255,255,0.06);
color: #e5e7eb;
align-self: flex-start;
border-bottom-left-radius: 4px;
}
The full CSS continues on the next slide with input styling and animations.
The JavaScript handles three tasks: capturing input, rendering messages, and auto-scrolling to the latest message.
// ---- DOM Elements ----
const chatMessages = document.getElementById('chat-messages');
const chatForm = document.getElementById('chat-form');
const userInput = document.getElementById('user-input');
// ---- Render a message bubble ----
function addMessage(text, sender) {
const div = document.createElement('div');
div.className = `message ${sender}`; // 'user' or 'ai'
div.textContent = text;
chatMessages.appendChild(div);
// Auto-scroll to the bottom
chatMessages.scrollTop = chatMessages.scrollHeight;
return div; // Return so we can update it later
}
// ---- Handle form submission ----
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const message = userInput.value.trim();
if (!message) return;
// 1. Show user message
addMessage(message, 'user');
userInput.value = '';
// 2. Show typing indicator
const typingDiv = addMessage('Thinking...', 'ai');
// 3. Call the AI (we'll implement this next!)
const reply = await getAIResponse(message);
typingDiv.textContent = reply;
});
A polished chat app shows visual feedback while the AI is "thinking." Let's add an animated typing indicator and proper input disabling.
/* Typing indicator animation */
.typing-indicator {
display: flex;
gap: 4px;
padding: 0.8rem 1.2rem;
}
.typing-indicator span {
width: 8px;
height: 8px;
background: #9ca3af;
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out;
}
.typing-indicator span:nth-child(1) {
animation-delay: 0s;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1.0); }
}
/* Disable input while AI is responding */
input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
function showTypingIndicator() {
const div = document.createElement('div');
div.className = 'message ai typing-indicator';
div.id = 'typing';
div.innerHTML = '<span></span><span></span><span></span>';
chatMessages.appendChild(div);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function removeTypingIndicator() {
const el = document.getElementById('typing');
if (el) el.remove();
}
Here is the complete, runnable app.js file. This includes everything: DOM handling, API calls, conversation history, and the typing indicator. Copy this into your project.
// =============================================
// EO Chat Assistant - Complete Application
// =============================================
// ---- Configuration ----
const API_KEY = prompt('Enter your Gemini API key:');
const API_URL = 'https://generativelanguage.googleapis.com/v1beta/'
+ 'models/gemini-2.0-flash:generateContent?key=' + API_KEY;
// ---- System prompt: makes the AI an EO expert ----
const SYSTEM_PROMPT = `You are an Earth Observation assistant
for university students. You help with satellite data,
remote sensing concepts (NDVI, SAR, spectral bands),
and tools like Google Earth Engine. Be concise and
include practical examples when possible.`;
// ---- Conversation history ----
const history = [];
// ---- DOM Elements ----
const chatMessages = document.getElementById('chat-messages');
const chatForm = document.getElementById('chat-form');
const userInput = document.getElementById('user-input');
// ---- Add a message to the chat ----
function addMessage(text, sender) {
const div = document.createElement('div');
div.className = `message ${sender}`;
div.textContent = text;
chatMessages.appendChild(div);
chatMessages.scrollTop = chatMessages.scrollHeight;
return div;
}
// ---- Call the Gemini API ----
async function getAIResponse(userMessage) {
// Add user message to history
history.push({
role: 'user',
parts: [{ text: userMessage }]
});
try {
const res = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
system_instruction: {
parts: [{ text: SYSTEM_PROMPT }]
},
contents: history
})
});
const data = await res.json();
const reply = data.candidates[0]
.content.parts[0].text;
// Add AI reply to history
history.push({
role: 'model',
parts: [{ text: reply }]
});
return reply;
} catch (err) {
return 'Sorry, something went wrong: ' + err.message;
}
}
// ---- Form submission handler ----
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const msg = userInput.value.trim();
if (!msg) return;
addMessage(msg, 'user');
userInput.value = '';
userInput.disabled = true;
const placeholder = addMessage('Thinking...', 'ai');
const reply = await getAIResponse(msg);
placeholder.textContent = reply;
userInput.disabled = false;
userInput.focus();
});
// ---- Welcome message ----
addMessage(
'Hello! π°οΈ I am your Earth Observation assistant. '
+ 'Ask me about NDVI, Sentinel-2, land cover '
+ 'classification, or any remote sensing topic!',
'ai'
);
The getAIResponse() function is the heart of our application. Let's examine each step of the API request lifecycle.
async function getAIResponse(userMessage) {
// STEP 1: Add the user's message to the conversation history.
// This array grows with each exchange, giving the AI
// context about previous turns in the conversation.
history.push({
role: 'user',
parts: [{ text: userMessage }]
});
// STEP 2: Send the ENTIRE conversation history to the API.
// The model has no memory between calls, so we must
// send all previous messages every time.
const res = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
system_instruction: {
parts: [{ text: SYSTEM_PROMPT }]
},
contents: history // All messages so far
})
});
// STEP 3: Parse the JSON response and extract the text.
const data = await res.json();
const reply = data.candidates[0].content.parts[0].text;
// STEP 4: Add the AI's reply to history so the next
// request includes it as context.
history.push({
role: 'model',
parts: [{ text: reply }]
});
return reply;
}
The Gemini API supports two response modes. Understanding the difference helps you build better user experiences.3
| Feature | Non-Streaming (what we use) | Streaming |
|---|---|---|
| Endpoint | :generateContent | :streamGenerateContent |
| Response | Full text arrives at once | Text arrives chunk by chunk |
| User Experience | Wait, then see full answer | See text appear word by word |
| Time to First Token | Longer (waits for full generation) | Shorter (starts immediately) |
| Complexity | Simple: one fetch() call | Requires reading a stream |
// Streaming: change the endpoint and read chunks
const streamURL = API_URL.replace(
'generateContent',
'streamGenerateContent'
) + '&alt=sse'; // append with & since ?key= is already present
const res = await fetch(streamURL, { /* same options */ });
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
// Parse and append each chunk to the message div
}
The history array is what gives our chatbot the ability to have multi-turn conversations. Without it, every message would be treated as an isolated question.
// After 2 exchanges, history looks like this:
[
{ "role": "user", "parts": [{ "text": "What is NDVI?" }] },
{ "role": "model", "parts": [{ "text": "NDVI stands for..." }] },
{ "role": "user", "parts": [{ "text": "How do I calculate it?" }] },
{ "role": "model", "parts": [{ "text": "NDVI = (NIR - Red)..." }] }
]
// Keep only the last 20 messages to control token usage
const MAX_HISTORY = 20;
if (history.length > MAX_HISTORY) {
history.splice(0, history.length - MAX_HISTORY);
}
The system prompt (also called "system instruction") tells the AI how to behave. It is sent with every request but is invisible to the user. This is how you turn a general-purpose LLM into a specialized assistant.5
const SYSTEM_PROMPT = `You are an Earth Observation assistant
for university students at the International Space University.
Your expertise includes:
- Satellite remote sensing (Sentinel-2, Landsat, MODIS)
- Vegetation indices (NDVI, EVI, SAVI)
- Land cover classification and change detection
- Google Earth Engine and geospatial Python libraries
- SAR (Synthetic Aperture Radar) fundamentals
Rules:
1. Be concise but thorough
2. Include formulas when discussing indices
3. Suggest practical code examples when relevant
4. If unsure, say so honestly
5. Always cite the satellite or sensor being discussed`;
GitHub Pages gives you free hosting for static websites. Since our chat app is pure HTML/CSS/JS (no server needed), it deploys perfectly.
Open a terminal in your project folder and run:
# Initialize a git repository
git init
git add .
git commit -m "feat: initial EO chat assistant"
# Create the repository on GitHub (use GitHub CLI or web UI)
# Then link and push:
git remote add origin https://github.com/YOUR_USER/eo-chat.git
git push -u origin main
Go to your repository on GitHub → Settings → Pages. Under "Source," select main branch and / (root) folder. Click Save.
After a minute or two, your app will be live at:
https://YOUR_USER.github.io/eo-chat/
Puter.js is an open-source library that gives you free, unlimited AI access with no API keys required. It handles authentication and billing for you. Here is how simple it is:6
<!DOCTYPE html>
<html>
<head>
<title>EO Chat (Puter.js)</title>
<script src="https://js.puter.com/v2/"></script>
</head>
<body>
<h2>π°οΈ EO Chat (Puter.js)</h2>
<div id="chat"></div>
<input id="input" placeholder="Ask about EO...">
<button onclick="ask()">Send</button>
<script>
async function ask() {
const input = document.getElementById('input');
const chat = document.getElementById('chat');
chat.innerHTML += `<p><b>You:</b> ${input.value}</p>`;
// That's it! No API key needed!
const response = await puter.ai.chat(
input.value
);
chat.innerHTML += `<p><b>AI:</b> ${response.message.content}</p>`;
input.value = '';
}
</script>
</body>
</html>
| Approach | Best For | Trade-offs |
|---|---|---|
| Direct API (Gemini/Groq) | Full control, model selection, streaming | Requires API key management |
| Puter.js | Quick prototyping, demos, hackathons | Less control over model choice |
Calling an AI model is the same as calling any web API: send JSON via fetch(), receive JSON back. No magic, just HTTP.
The model has no memory between calls. You must send the entire conversation history with every request to maintain context.
A well-crafted system prompt transforms a generic model into a domain-specific assistant. Prompt engineering is a core AI skill.
Never expose API keys in production code. Use backend proxies, environment variables, or key-free services like Puter.js.
A pure HTML/CSS/JS application with client-side API calls can be deployed for free on GitHub Pages with zero infrastructure.
Godfather of AI
His pioneering work on artificial neural networks and deep learning laid the foundation for modern LLMs.
Earth Observation provides a macroscopic view of environmental trends, but its true power lies in downscaling this data to affect local policy and design, such as urban planning and sustainable workplaces.
Your team needs to process thousands of unstructured reports on workplace well-being and map them to physical office locations.
What was your biggest takeaway from this session, and how does it apply to the TERRA project? Write your response below. Your instructor will review this to track your progress.