العربية ← Back to Module README

Module 13: AI Chatbot

Course: Programming with Free AI Tools — Build Real Websites and Apps
Module: 13 of 17
Audience: Ages 15–25, beginners
Focus: Build a simple AI chatbot UI with HTML, CSS, JavaScript, and a free AI API. Cover API keys, async/await, and displaying responses.

Core principle: Always verify AI-generated code. API keys are secrets — never expose them in client-side code or commit them to GitHub.

Table of Contents


Lesson 13.1: Chatbot UI with HTML and CSS

Duration: 45 minutes

Learning objectives

Instructor notes

Presentation outline

  1. Chat UI anatomy (5 min)
  2. HTML structure: messages + form (10 min)
  3. CSS for bubbles and layout (15 min)
  4. Appending messages with JS (10 min)
  5. Mobile responsiveness (5 min)

Demonstration

  1. Create index.html with #messages and a form.
  2. Add CSS for .message, .user, .bot, and a fixed input bar.
  3. Write addMessage(text, sender) that creates a div and appends it.
  4. On form submit, add the user's message and a placeholder bot reply.
  5. Resize the window to confirm responsive behavior.

AI prompts

  1. "Write HTML and CSS for a simple chatbot UI: a scrollable message area and a fixed input bar at the bottom, with user and bot message bubbles styled differently."
  2. "Show a JavaScript function addMessage(text, sender) that appends a styled message div to a #messages container and scrolls to the bottom."

Starter code

<!DOCTYPE html>
<html>
<head><title>Chatbot</title></head>
<body>
  <div id="messages"></div>
  <form id="chat-form">
    <input id="user-input" placeholder="Type a message..." />
    <button type="submit">Send</button>
  </form>
  <script src="app.js"></script>
</body>
</html>

Final code

<!DOCTYPE html>
<html lang="en">
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Chatbot</title>
  <style>
    body { font-family: sans-serif; margin: 0; display: flex; flex-direction: column; height: 100vh; }
    #messages { flex: 1; overflow-y: auto; padding: 16px; }
    .message { max-width: 70%; padding: 10px 14px; margin: 6px 0; border-radius: 12px; }
    .user { background: #2563eb; color: #fff; margin-left: auto; }
    .bot { background: #e5e7eb; color: #111; }
    #chat-form { display: flex; padding: 12px; border-top: 1px solid #ccc; }
    #user-input { flex: 1; padding: 10px; border: 1px solid #ccc; border-radius: 8px; }
    button { margin-left: 8px; padding: 10px 16px; border: 0; border-radius: 8px; background: #2563eb; color: #fff; }
  </style>
</head>
<body>
  <div id="messages"></div>
  <form id="chat-form">
    <input id="user-input" placeholder="Type a message..." autocomplete="off" />
    <button type="submit">Send</button>
  </form>
  <script src="app.js"></script>
</body>
</html>
// app.js
const messages = document.getElementById('messages');
const form = document.getElementById('chat-form');
const input = document.getElementById('user-input');

function addMessage(text, sender) {
  const div = document.createElement('div');
  div.className = 'message ' + sender;
  div.textContent = text;
  messages.appendChild(div);
  messages.scrollTop = messages.scrollHeight;
}

form.addEventListener('submit', (e) => {
  e.preventDefault();
  const text = input.value.trim();
  if (!text) return;
  addMessage(text, 'user');
  addMessage('...', 'bot');
  input.value = '';
});

Exercises

  1. Change colors and add rounded avatars for the bot.
  2. Add a timestamp to each message.
  3. Make the input bar stay at the bottom on small screens (test with device emulation).

Homework

  1. Style the chatbot to match your personal taste and ensure it looks good on phone width.
  2. Add a "clear chat" button that empties the #messages container.

Quiz

  1. Which CSS property keeps the input bar at the bottom?
    • a) position: top
    • b) Flexbox column with flex: 1 on messages and a normal flow form ✓
    • c) float: bottom
    • d) display: hidden
  2. How do you append a new element to the page in JavaScript?
    • a) page.add()
    • b) parent.appendChild(element) ✓
    • c) element.push()
    • d) document.put()
  3. What does e.preventDefault() do in a form submit handler?
    • a) Prevents the page from reloading ✓
    • b) Deletes the input
    • c) Sends an email
    • d) Nothing
  4. How do you scroll to the newest message?
    • a) window.scroll()
    • b) Set messages.scrollTop = messages.scrollHeight ✓
    • c) scrollToBottom()
    • d) body.scroll()
  5. What class difference helps users tell messages apart?
    • a) Font size only
    • b) Different background/alignment for .user vs .bot ✓
    • c) Uppercase text
    • d) Emojis only

Lesson 13.2: Calling a Free AI API with async/await

Duration: 60 minutes

Learning objectives

Instructor notes

Presentation outline

  1. How chat APIs represent conversation (10 min)
  2. Building the request with fetch (15 min)
  3. async/await and error handling (15 min)
  4. Parsing and displaying the reply (10 min)
  5. Loading states (10 min)

Demonstration

  1. Replace the placeholder bot reply with an async getBotReply(text) function.
  2. Use fetch('/api/chat', { method: 'POST', body: JSON.stringify({message}) }).
  3. await response.json() and pull out the reply text.
  4. Replace the "..." placeholder bubble with the real reply.
  5. Simulate a network failure and show the error message.

AI prompts

  1. "Write a JavaScript async function getBotReply(message) that calls a chat API endpoint with fetch and async/await, sends the message, and returns the bot's text reply. Include error handling."
  2. "Show how to structure the request body for a chat-completion API using a messages array with role and content, and how to read the reply from the response JSON."

Starter code

async function getBotReply(message) {
  // TODO: call the AI API and return the reply text
  return 'Mock reply';
}

Final code

async function getBotReply(message) {
  try {
    const response = await fetch('/api/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ message: message })
    });
    if (!response.ok) {
      throw new Error('HTTP ' + response.status);
    }
    const data = await response.json();
    // Verify the actual shape of `data` from your provider!
    return data.reply || 'Sorry, I did not understand that.';
  } catch (error) {
    return 'Error: ' + error.message;
  }
}

form.addEventListener('submit', async (e) => {
  e.preventDefault();
  const text = input.value.trim();
  if (!text) return;
  addMessage(text, 'user');
  const loading = addMessage('...', 'bot');
  const reply = await getBotReply(text);
  loading.textContent = reply;
  input.value = '';
});

Exercises

  1. Log the full API response to the console and confirm the JSON path to the reply text.
  2. Add a typing... animated indicator instead of static "...".
  3. Disable the Send button while a request is in flight.

Homework

  1. Connect your chatbot to a real free AI API (e.g., OpenRouter or Hugging Face) and verify it returns replies.
  2. Handle the case where the API rate-limits you (429) with a friendly message.

Quiz

  1. Why use async/await with fetch?
    • a) To make code run faster
    • b) To handle the asynchronous network response cleanly ✓
    • c) It is required by HTML
    • d) To avoid JavaScript
  2. What does await response.json() do?
    • a) Sends the request
    • b) Parses the response body as a JavaScript object ✓
    • c) Deletes the response
    • d) Opens a browser
  3. Where should you extract the reply text from?
    • a) Any random field
    • b) The specific JSON path your provider returns (verify it!) ✓
    • c) The URL
    • d) The headers only
  4. What is a good UX while waiting for the API?
    • a) Freeze the page
    • b) Show a loading/typing indicator ✓
    • c) Close the tab
    • d) Show nothing
  5. What happens if you don't handle network errors?
    • a) The app crashes or shows undefined ✓
    • b) It auto-retries forever
    • c) Nothing bad
    • d) The API fixes it

Lesson 13.3: Handling API Keys and Security

Duration: 45 minutes

Learning objectives

Instructor notes

Presentation outline

  1. Why keys leak in the browser (10 min)
  2. The proxy pattern (10 min)
  3. Storing keys server-side (PHP/getenv) (15 min)
  4. Calling your own endpoint from JS (5 min)
  5. Never committing secrets (5 min)

Demonstration

  1. Show a key exposed in browser DevTools when called directly (anti-pattern).
  2. Create api/chat.php that reads the key via getenv('AI_KEY').
  3. Forward the user message to the AI provider server-side.
  4. Return only the reply JSON to the browser.
  5. Add .env to a mental "never commit" list and show a .gitignore example.

AI prompts

  1. "Explain why I should never put an AI API key in JavaScript that runs in the browser, and show a PHP proxy script that keeps the key server-side."
  2. "Write a PHP script that reads an API key from an environment variable and forwards a chat message to an AI API, returning only the reply to the client."

Starter code

<?php
// TODO: read API key from env, call AI provider, return reply JSON

Final code

<?php
// api/chat.php  (lives on your server, never shipped to the browser)
header('Content-Type: application/json');

$apiKey = getenv('AI_KEY');
if (!$apiKey) {
    http_response_code(500);
    echo json_encode(['reply' => 'Server missing API key']);
    exit;
}

$input = json_decode(file_get_contents('php://input'), true);
$message = $input['message'] ?? '';

// Example using a generic chat endpoint; adjust to your provider.
$payload = json_encode([
    'messages' => [['role' => 'user', 'content' => $message]]
]);

$ch = curl_init('https://api.example-ai.com/v1/chat');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $apiKey
    ],
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_RETURNTRANSFER => true
]);
$result = curl_exec($ch);
curl_close($ch);

$data = json_decode($result, true);
echo json_encode(['reply' => $data['choices'][0]['message']['content'] ?? 'No reply']);

Exercises

  1. Add input validation in chat.php to reject empty messages with a 400.
  2. Add a simple rate limit (max 10 requests per minute per IP).
  3. Write a .gitignore entry that would prevent committing a .env file.

Homework

  1. Set up your own proxy endpoint and confirm the key is never visible in the browser.
  2. Write a one-paragraph explanation of how the proxy protects the key.

Quiz

  1. Where should an AI API key live?
    • a) In the browser JavaScript
    • b) Server-side, in an environment variable ✓
    • c) In a public repo
    • d) In the HTML
  2. What is the purpose of a server-side proxy?
    • a) Slow down requests
    • b) Hide the API key from the browser ✓
    • c) Style the page
    • d) Replace the database
  3. What does "committing a key to GitHub" risk?
    • a) Nothing
    • b) Anyone can steal and abuse your key ✓
    • c) Faster builds
    • d) Better SEO
  4. How do you read an env variable in PHP?
    • a) $_ENV['KEY'] or getenv('KEY') ✓
    • b) env('KEY')
    • c) KEY.value
    • d) php.env
  5. What should the browser receive from the proxy?
    • a) The API key
    • b) Only the bot reply JSON ✓
    • c) The full provider response with the key
    • d) Source code

Lesson 13.4: Improving the Chat Experience

Duration: 45 minutes

Learning objectives

Instructor notes

Presentation outline

  1. Conversation context with a messages array (10 min)
  2. Rendering multi-line replies safely (10 min)
  3. Persisting chat with localStorage (10 min)
  4. UX polish: Enter to send (10 min)
  5. Review and test (5 min)

Demonstration

  1. Build a history array: ['role','content'] pairs.
  2. Send the full history each request so the bot remembers.
  3. Load/save history to localStorage on each message.
  4. Add a welcome message on first load.
  5. Make Enter submit the form; verify history persists after refresh.

AI prompts

  1. "Show how to keep chat history in a JavaScript array and send it with each API call so the bot remembers the conversation."
  2. "Write code to save and load a chat array from localStorage and render it on page load, safely escaping text."

Starter code

let history = [];
// TODO: push user/bot messages and send history to the API

Final code

let history = JSON.parse(localStorage.getItem('chat') || '[]');

function save() {
  localStorage.setItem('chat', JSON.stringify(history));
}

function renderHistory() {
  messages.innerHTML = '';
  history.forEach(m => addMessage(m.content, m.role === 'user' ? 'user' : 'bot'));
}

async function sendToApi() {
  const response = await fetch('/api/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ history })
  });
  const data = await response.json();
  return data.reply;
}

form.addEventListener('submit', async (e) => {
  e.preventDefault();
  const text = input.value.trim();
  if (!text) return;
  history.push({ role: 'user', content: text });
  addMessage(text, 'user');
  const loading = addMessage('...', 'bot');
  const reply = await sendToApi();
  history.push({ role: 'bot', content: reply });
  loading.textContent = reply;
  save();
  input.value = '';
});

if (history.length === 0) {
  addMessage('Hi! How can I help you today?', 'bot');
} else {
  renderHistory();
}

Exercises

  1. Add a "New chat" button that clears history and localStorage.
  2. Render line breaks in bot replies using white-space: pre-wrap CSS.
  3. Show a character counter or message count.

Homework

  1. Add conversation memory to your chatbot and verify it remembers context across turns.
  2. Write a short reflection: what would you add next to make the bot more useful?

Quiz

  1. Why send the full history to the API each turn?
    • a) To waste bandwidth
    • b) So the bot has conversation context ✓
    • c) Required by CSS
    • d) To clear memory
  2. What does localStorage let you do?
    • a) Run PHP
    • b) Persist data in the browser across refreshes ✓
    • c) Hide API keys safely
    • d) Connect to MySQL
  3. Why avoid innerHTML with bot text?
    • a) It is slower
    • b) Untrusted text could inject scripts (XSS) ✓
    • c) It breaks flexbox
    • d) Required by law
  4. What should Enter do in the input?
    • a) Nothing
    • b) Submit the message ✓
    • c) Close the tab
    • d) Reload the page
  5. Which is a safe way to show bot text with line breaks?
    • a) innerHTML = reply
    • b) textContent + CSS white-space: pre-wrap ✓
    • c) eval(reply)
    • d) document.write

End of Module 13. Never ship a real API key to the browser — verify in DevTools before submitting.