Module 13: AI Chatbot
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
- Build a chat layout with a message list and input box.
- Style messages to distinguish user vs. bot.
- Use CSS Flexbox for a sticky input bar.
- Append messages to the DOM dynamically.
- Create a clean, responsive mobile-friendly design.
Instructor notes
- Start with static HTML: a
#messages container and a <form> with input + button.
- Use a
.user / .bot class with different background colors and alignment.
- Show
scrollIntoView() so new messages stay visible.
- Keep the UI framework-free first, then optionally add Bootstrap.
- Remind students the UI works before any AI is connected.
Presentation outline
- Chat UI anatomy (5 min)
- HTML structure: messages + form (10 min)
- CSS for bubbles and layout (15 min)
- Appending messages with JS (10 min)
- Mobile responsiveness (5 min)
Demonstration
- Create
index.html with #messages and a form.
- Add CSS for
.message, .user, .bot, and a fixed input bar.
- Write
addMessage(text, sender) that creates a div and appends it.
- On form submit, add the user's message and a placeholder bot reply.
- Resize the window to confirm responsive behavior.
AI prompts
"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."
"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
- Change colors and add rounded avatars for the bot.
- Add a timestamp to each message.
- Make the input bar stay at the bottom on small screens (test with device emulation).
Homework
- Style the chatbot to match your personal taste and ensure it looks good on phone width.
- Add a "clear chat" button that empties the
#messages container.
Quiz
- 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
- 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()
- 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
- How do you scroll to the newest message?
- a)
window.scroll() - b) Set
messages.scrollTop = messages.scrollHeight ✓ - c)
scrollToBottom() - d)
body.scroll()
- 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
- Understand how a chat-completion API works.
- Send a user message and receive a bot reply with
fetch.
- Use
async/await to handle the asynchronous response.
- Parse the API's JSON response and extract the text.
- Show a loading state while waiting for the reply.
Instructor notes
- Use a free API such as OpenRouter, Hugging Face, or a mock endpoint for the demo.
- For a safe classroom demo, start with a mock
setTimeout reply, then swap in the real fetch.
- Explain the request body shape:
messages: [{role, content}].
- Show how to extract the reply from nested JSON (varies by provider — verify!).
- Reinforce: the network is slow; always show a loading indicator.
Presentation outline
- How chat APIs represent conversation (10 min)
- Building the request with fetch (15 min)
- async/await and error handling (15 min)
- Parsing and displaying the reply (10 min)
- Loading states (10 min)
Demonstration
- Replace the placeholder bot reply with an
async getBotReply(text) function.
- Use
fetch('/api/chat', { method: 'POST', body: JSON.stringify({message}) }).
await response.json() and pull out the reply text.
- Replace the
"..." placeholder bubble with the real reply.
- Simulate a network failure and show the error message.
AI prompts
"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."
"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
- Log the full API response to the console and confirm the JSON path to the reply text.
- Add a
typing... animated indicator instead of static "...".
- Disable the Send button while a request is in flight.
Homework
- Connect your chatbot to a real free AI API (e.g., OpenRouter or Hugging Face) and verify it returns replies.
- Handle the case where the API rate-limits you (429) with a friendly message.
Quiz
- 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
- 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
- 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
- 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
- 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
- Explain why API keys must stay secret.
- Use a server-side proxy to hide keys from the browser.
- Store keys in environment variables, not in code.
- Recognize the danger of committing keys to GitHub.
- Call your own backend endpoint instead of the AI API directly.
Instructor notes
- Critical safety lesson: never put the real key in client JS — "View Source" reveals it.
- Build a tiny PHP proxy
api/chat.php that holds the key and forwards the request.
- Show a
.env / getenv() pattern (or a config file outside web root).
- Demonstrate what a leaked key looks like in the browser network tab.
- Tie to the rubric: security awareness is graded.
Presentation outline
- Why keys leak in the browser (10 min)
- The proxy pattern (10 min)
- Storing keys server-side (PHP/getenv) (15 min)
- Calling your own endpoint from JS (5 min)
- Never committing secrets (5 min)
Demonstration
- Show a key exposed in browser DevTools when called directly (anti-pattern).
- Create
api/chat.php that reads the key via getenv('AI_KEY').
- Forward the user message to the AI provider server-side.
- Return only the reply JSON to the browser.
- Add
.env to a mental "never commit" list and show a .gitignore example.
AI prompts
"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."
"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
- Add input validation in
chat.php to reject empty messages with a 400.
- Add a simple rate limit (max 10 requests per minute per IP).
- Write a
.gitignore entry that would prevent committing a .env file.
Homework
- Set up your own proxy endpoint and confirm the key is never visible in the browser.
- Write a one-paragraph explanation of how the proxy protects the key.
Quiz
- 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
- 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
- What does "committing a key to GitHub" risk?
- a) Nothing
- b) Anyone can steal and abuse your key ✓
- c) Faster builds
- d) Better SEO
- How do you read an env variable in PHP?
- a)
$_ENV['KEY'] or getenv('KEY') ✓ - b)
env('KEY') - c)
KEY.value - d)
php.env
- 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
- Keep conversation history so the bot has context.
- Display bot replies with basic Markdown/line breaks.
- Add a welcome message and empty-state handling.
- Save chat to
localStorage so it survives refresh.
- Polish UX: auto-resize input, Enter to send.
Instructor notes
- Maintain a
messages array sent to the API each turn for context.
- Use
textContent for safety, or a small Markdown renderer; never innerHTML with untrusted text.
localStorage is great for a demo but not private — mention that.
- Show how Enter sends and Shift+Enter adds a newline.
- Encourage students to verify the history is actually sent (check Network tab).
Presentation outline
- Conversation context with a messages array (10 min)
- Rendering multi-line replies safely (10 min)
- Persisting chat with localStorage (10 min)
- UX polish: Enter to send (10 min)
- Review and test (5 min)
Demonstration
- Build a
history array: ['role','content'] pairs.
- Send the full history each request so the bot remembers.
- Load/save
history to localStorage on each message.
- Add a welcome message on first load.
- Make Enter submit the form; verify history persists after refresh.
AI prompts
"Show how to keep chat history in a JavaScript array and send it with each API call so the bot remembers the conversation."
"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
- Add a "New chat" button that clears
history and localStorage.
- Render line breaks in bot replies using
white-space: pre-wrap CSS.
- Show a character counter or message count.
Homework
- Add conversation memory to your chatbot and verify it remembers context across turns.
- Write a short reflection: what would you add next to make the bot more useful?
Quiz
- 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
- 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
- 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
- What should Enter do in the input?
- a) Nothing
- b) Submit the message ✓
- c) Close the tab
- d) Reload the page
- 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.
الوحدة 13: روبوت الدردشة بالذكاء الاصطناعي
المبدأ الأساسي: تحقّق دائمًا من الكود الذي ينشئه الذكاء الاصطناعي. مفاتيح API أسرار — لا تكشفها أبدًا في كود جهة العميل ولا ترفعها إلى GitHub.
جدول المحتويات
الدرس 13.1: واجهة روبوت الدردشة باستخدام HTML وCSS
المدة: 45 دقيقة
أهداف التعلّم
- بناء تخطيط دردشة يتضمن قائمة رسائل ومربع إدخال.
- تنسيق الرسائل للتمييز بين المستخدم والروبوت.
- استخدام CSS Flexbox لشريط إدخال ثابت.
- إضافة الرسائل إلى DOM ديناميكيًا.
- إنشاء تصميم نظيف ومتجاوب ومناسب للهواتف.
ملاحظات المدرّب
- ابدأ بـ HTML ثابت: حاوية
#messages ونموذج <form> يحتوي على إدخال + زر.
- استخدم فئتَي
.user / .bot بألوان خلفية ومحاذاة مختلفة.
- اعرض
scrollIntoView() لتبقى الرسائل الجديدة مرئية.
- ابقِ الواجهة بلا إطار عمل أولًا، ثم أضِف Bootstrap اختياريًا.
- ذكّر الطلاب بأن الواجهة تعمل قبل توصيل أي ذكاء اصطناعي.
مخطط العرض التقديمي
- تشريح واجهة الدردشة (5 دقائق)
- بنية HTML: الرسائل + النموذج (10 دقائق)
- CSS للفقاعات والتخطيط (15 دقيقة)
- إضافة الرسائل باستخدام JS (10 دقائق)
- التجاوب مع الهواتف (5 دقائق)
العرض العملي
- أنشئ
index.html مع #messages ونموذج.
- أضِف CSS لـ
.message و.user و.bot وشريط إدخال ثابت.
- اكتب
addMessage(text, sender) التي تنشئ عنصر div وتضيفه.
- عند إرسال النموذج، أضِف رسالة المستخدم وردًا نائبًا من الروبوت.
- غيّر حجم النافذة للتأكد من السلوك المتجاوب.
موجّهات الذكاء الاصطناعي
"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."
"Show a JavaScript function addMessage(text, sender) that appends a styled message div to a #messages container and scrolls to the bottom."
الكود المبدئي
<!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>
الكود النهائي
<!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 = '';
});
تمارين
- غيّر الألوان وأضِف صورًا رمزية دائرية للروبوت.
- أضِف طابعًا زمنيًا لكل رسالة.
- اجعل شريط الإدخال يبقى في الأسفل على الشاشات الصغيرة (اختبر بمحاكاة الأجهزة).
الواجب المنزلي
- نسّق روبوت الدردشة ليناسب ذوقك الشخصي وتأكد أنه يبدو جيدًا بعرض الهاتف.
- أضِف زر "مسح الدردشة" يُفرّغ حاوية
#messages.
اختبار قصير
- أي خاصية CSS تُبقي شريط الإدخال في الأسفل؟
- أ)
position: top - ب) عمود Flexbox مع
flex: 1 على الرسائل ونموذج بتدفق عادي ✓ - ج)
float: bottom - د)
display: hidden
- كيف تضيف عنصرًا جديدًا إلى الصفحة في JavaScript؟
- أ)
page.add() - ب)
parent.appendChild(element) ✓ - ج)
element.push() - د)
document.put()
- ماذا يفعل
e.preventDefault() في معالج إرسال النموذج؟
- أ) يمنع الصفحة من إعادة التحميل ✓
- ب) يحذف الإدخال
- ج) يرسل بريدًا إلكترونيًا
- د) لا شيء
- كيف تُمرّر التمرير إلى أحدث رسالة؟
- أ)
window.scroll() - ب) اضبط
messages.scrollTop = messages.scrollHeight ✓ - ج)
scrollToBottom() - د)
body.scroll()
- أي فرق في الفئات يساعد المستخدمين على التمييز بين الرسائل؟
- أ) حجم الخط فقط
- ب) خلفية/محاذاة مختلفة لـ
.user مقابل .bot ✓ - ج) نص بأحرف كبيرة
- د) الرموز التعبيرية فقط
الدرس 13.2: استدعاء واجهة API مجانية للذكاء الاصطناعي باستخدام async/await
المدة: 60 دقيقة
أهداف التعلّم
- فهم كيفية عمل واجهة API لإكمال الدردشة.
- إرسال رسالة المستخدم واستقبال رد الروبوت باستخدام
fetch.
- استخدام
async/await للتعامل مع الاستجابة غير المتزامنة.
- تحليل استجابة JSON من الـ API واستخراج النص.
- إظهار حالة تحميل أثناء انتظار الرد.
ملاحظات المدرّب
- استخدم واجهة API مجانية مثل OpenRouter أو Hugging Face أو نقطة نهاية وهمية للعرض.
- لعرض آمن في الصف، ابدأ برد وهمي عبر
setTimeout، ثم استبدله بـ fetch الحقيقي.
- اشرح شكل جسم الطلب:
messages: [{role, content}].
- اعرض كيفية استخراج الرد من JSON متداخل (يختلف حسب المزوّد — تحقّق!).
- عزّز الفكرة: الشبكة بطيئة؛ اعرض دائمًا مؤشر تحميل.
مخطط العرض التقديمي
- كيف تمثّل واجهات الدردشة المحادثة (10 دقائق)
- بناء الطلب باستخدام fetch (15 دقيقة)
- async/await ومعالجة الأخطاء (15 دقيقة)
- تحليل الرد وعرضه (10 دقائق)
- حالات التحميل (10 دقائق)
العرض العملي
- استبدل رد الروبوت النائب بدالة
async باسم getBotReply(text).
- استخدم
fetch('/api/chat', { method: 'POST', body: JSON.stringify({message}) }).
await response.json() واستخرج نص الرد.
- استبدل فقاعة
"..." النائبة بالرد الحقيقي.
- حاكِ فشلًا في الشبكة واعرض رسالة الخطأ.
موجّهات الذكاء الاصطناعي
"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."
"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."
الكود المبدئي
async function getBotReply(message) {
// TODO: call the AI API and return the reply text
return 'Mock reply';
}
الكود النهائي
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 = '';
});
تمارين
- سجّل استجابة الـ API الكاملة في وحدة التحكم وتأكد من مسار JSON لنص الرد.
- أضِف مؤشر
typing... متحركًا بدلًا من "..." الثابت.
- عطّل زر الإرسال أثناء تنفيذ الطلب.
الواجب المنزلي
- اربط روبوت الدردشة بواجهة API حقيقية مجانية (مثل OpenRouter أو Hugging Face) وتأكد أنها تُرجع ردودًا.
- عالج الحالة التي تفرض فيها الـ API حدًا للمعدل (429) برسالة ودّية.
اختبار قصير
- لماذا نستخدم
async/await مع fetch؟
- أ) لجعل الكود أسرع
- ب) للتعامل مع استجابة الشبكة غير المتزامنة بأناقة ✓
- ج) لأنه مطلوب في HTML
- د) لتجنّب JavaScript
- ماذا يفعل
await response.json()؟
- أ) يرسل الطلب
- ب) يحلّل جسم الاستجابة إلى كائن JavaScript ✓
- ج) يحذف الاستجابة
- د) يفتح المتصفح
- من أين ينبغي استخراج نص الرد؟
- أ) أي حقل عشوائي
- ب) مسار JSON المحدد الذي يُرجعه مزوّدك (تحقّق منه!) ✓
- ج) عنوان URL
- د) الترويسات فقط
- ما تجربة المستخدم الجيدة أثناء انتظار الـ API؟
- أ) تجميد الصفحة
- ب) إظهار مؤشر تحميل/كتابة ✓
- ج) إغلاق التبويب
- د) عدم إظهار شيء
- ماذا يحدث إذا لم تعالج أخطاء الشبكة؟
- أ) يتعطّل التطبيق أو يعرض undefined ✓
- ب) يعيد المحاولة إلى الأبد
- ج) لا شيء سيّئ
- د) تصلحها الـ API
الدرس 13.3: التعامل مع مفاتيح API والأمان
المدة: 45 دقيقة
أهداف التعلّم
- شرح لماذا يجب أن تبقى مفاتيح API سرية.
- استخدام وكيل (proxy) من جهة الخادم لإخفاء المفاتيح عن المتصفح.
- تخزين المفاتيح في متغيرات البيئة، لا في الكود.
- إدراك خطر رفع المفاتيح إلى GitHub.
- استدعاء نقطة نهاية خادمك الخاص بدلًا من واجهة API للذكاء الاصطناعي مباشرة.
ملاحظات المدرّب
- درس أمان حاسم: لا تضع المفتاح الحقيقي أبدًا في JS العميل — "عرض المصدر" يكشفه.
- ابنِ وكيل PHP صغيرًا
api/chat.php يحتفظ بالمفتاح ويمرّر الطلب.
- اعرض نمط
.env / getenv() (أو ملف إعداد خارج جذر الويب).
- أظهر كيف يبدو المفتاح المُسرّب في تبويب الشبكة بالمتصفح.
- اربطه بمعايير التقييم: الوعي الأمني يُقيَّم.
مخطط العرض التقديمي
- لماذا تتسرّب المفاتيح في المتصفح (10 دقائق)
- نمط الوكيل (10 دقائق)
- تخزين المفاتيح من جهة الخادم (PHP/getenv) (15 دقيقة)
- استدعاء نقطة نهايتك الخاصة من JS (5 دقائق)
- عدم رفع الأسرار أبدًا (5 دقائق)
العرض العملي
- أظهر مفتاحًا مكشوفًا في أدوات المطوّر عند استدعائه مباشرة (نمط خاطئ).
- أنشئ
api/chat.php يقرأ المفتاح عبر getenv('AI_KEY').
- مرّر رسالة المستخدم إلى مزوّد الذكاء الاصطناعي من جهة الخادم.
- أعِد فقط JSON الرد إلى المتصفح.
- أضِف
.env إلى قائمة "لا تُرفع أبدًا" واعرض مثال .gitignore.
موجّهات الذكاء الاصطناعي
"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."
"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."
الكود المبدئي
<?php
// TODO: read API key from env, call AI provider, return reply JSON
الكود النهائي
<?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']);
تمارين
- أضِف تحققًا من المدخلات في
chat.php لرفض الرسائل الفارغة برمز 400.
- أضِف حدًا بسيطًا للمعدل (10 طلبات كحد أقصى في الدقيقة لكل عنوان IP).
- اكتب مدخلًا في
.gitignore يمنع رفع ملف .env.
الواجب المنزلي
- أنشئ نقطة نهاية وكيل خاصة بك وتأكد أن المفتاح لا يظهر أبدًا في المتصفح.
- اكتب فقرة واحدة تشرح كيف يحمي الوكيل المفتاح.
اختبار قصير
- أين ينبغي أن يوجد مفتاح API للذكاء الاصطناعي؟
- أ) في JavaScript بالمتصفح
- ب) من جهة الخادم، في متغيّر بيئة ✓
- ج) في مستودع عام
- د) في HTML
- ما الغرض من وكيل جهة الخادم؟
- أ) إبطاء الطلبات
- ب) إخفاء مفتاح API عن المتصفح ✓
- ج) تنسيق الصفحة
- د) استبدال قاعدة البيانات
- ماذا يخاطر به "رفع مفتاح إلى GitHub"؟
- أ) لا شيء
- ب) يمكن لأي شخص سرقة مفتاحك وإساءة استخدامه ✓
- ج) بناء أسرع
- د) تحسين SEO
- كيف تقرأ متغيّر بيئة في PHP؟
- أ)
$_ENV['KEY'] أو getenv('KEY') ✓ - ب)
env('KEY') - ج)
KEY.value - د)
php.env
- ماذا ينبغي أن يستقبل المتصفح من الوكيل؟
- أ) مفتاح API
- ب) فقط JSON رد الروبوت ✓
- ج) استجابة المزوّد الكاملة مع المفتاح
- د) الكود المصدري
الدرس 13.4: تحسين تجربة الدردشة
المدة: 45 دقيقة
أهداف التعلّم
- الاحتفاظ بسجل المحادثة ليكون للروبوت سياق.
- عرض ردود الروبوت مع Markdown أساسي/فواصل أسطر.
- إضافة رسالة ترحيب ومعالجة الحالة الفارغة.
- حفظ الدردشة في
localStorage لتبقى بعد التحديث.
- تحسين التجربة: تغيير حجم الإدخال تلقائيًا، والإرسال بمفتاح Enter.
ملاحظات المدرّب
- احتفظ بمصفوفة
messages تُرسل إلى الـ API في كل دور للسياق.
- استخدم
textContent للأمان، أو مُصيّر Markdown صغير؛ لا تستخدم أبدًا innerHTML مع نص غير موثوق.
localStorage رائع للعرض لكنه ليس خاصًا — أشِر إلى ذلك.
- اعرض كيف يرسل Enter وكيف يضيف Shift+Enter سطرًا جديدًا.
- شجّع الطلاب على التحقق من أن السجل يُرسل فعلًا (افحص تبويب الشبكة).
مخطط العرض التقديمي
- سياق المحادثة باستخدام مصفوفة messages (10 دقائق)
- عرض الردود متعددة الأسطر بأمان (10 دقائق)
- حفظ الدردشة باستخدام localStorage (10 دقائق)
- تحسين التجربة: الإرسال بمفتاح Enter (10 دقائق)
- المراجعة والاختبار (5 دقائق)
العرض العملي
- ابنِ مصفوفة
history: أزواج ['role','content'].
- أرسل السجل الكامل مع كل طلب ليتذكّر الروبوت.
- حمّل/احفظ
history في localStorage عند كل رسالة.
- أضِف رسالة ترحيب عند التحميل الأول.
- اجعل Enter يرسل النموذج؛ تأكد من بقاء السجل بعد التحديث.
موجّهات الذكاء الاصطناعي
"Show how to keep chat history in a JavaScript array and send it with each API call so the bot remembers the conversation."
"Write code to save and load a chat array from localStorage and render it on page load, safely escaping text."
الكود المبدئي
let history = [];
// TODO: push user/bot messages and send history to the API
الكود النهائي
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();
}
تمارين
- أضِف زر "دردشة جديدة" يمسح
history وlocalStorage.
- اعرض فواصل الأسطر في ردود الروبوت باستخدام CSS
white-space: pre-wrap.
- اعرض عدّاد أحرف أو عدد الرسائل.
الواجب المنزلي
- أضِف ذاكرة محادثة لروبوتك وتأكد أنه يتذكّر السياق عبر الأدوار.
- اكتب تأمّلًا قصيرًا: ما الذي ستضيفه لاحقًا لجعل الروبوت أكثر فائدة؟
اختبار قصير
- لماذا نرسل السجل الكامل إلى الـ API في كل دور؟
- أ) لإهدار عرض النطاق
- ب) ليكون للروبوت سياق المحادثة ✓
- ج) مطلوب في CSS
- د) لمسح الذاكرة
- ماذا يتيح لك
localStorage؟
- أ) تشغيل PHP
- ب) الاحتفاظ بالبيانات في المتصفح عبر عمليات التحديث ✓
- ج) إخفاء مفاتيح API بأمان
- د) الاتصال بـ MySQL
- لماذا نتجنّب
innerHTML مع نص الروبوت؟
- أ) لأنه أبطأ
- ب) قد يحقن النص غير الموثوق سكربتات (XSS) ✓
- ج) يكسر flexbox
- د) يفرضه القانون
- ماذا ينبغي أن يفعل Enter في الإدخال؟
- أ) لا شيء
- ب) إرسال الرسالة ✓
- ج) إغلاق التبويب
- د) إعادة تحميل الصفحة
- أي طريقة آمنة لعرض نص الروبوت مع فواصل الأسطر؟
- أ)
innerHTML = reply - ب)
textContent + CSS white-space: pre-wrap ✓ - ج)
eval(reply) - د)
document.write
نهاية الوحدة 13. لا ترسل أبدًا مفتاح API حقيقيًا إلى المتصفح — تحقّق في أدوات المطوّر قبل التسليم.