Build an AI Chat Assistant with PHP
Build a PHP backend proxy that connects to OpenAI and powers a chat UI.
Introduction
30 Second Summary
Every time you ask ChatGPT a question, your message travels through an API to reach the AI model. Understanding how that connection works is the key to building your own AI-powered applications.
In this project, you will build a chat assistant that connects to OpenAI's ChatGPT API through a PHP backend. You will create a working chat interface on your local machine that sends messages, receives AI responses, and maintains conversation context.
What You'll Build
A functional AI chat application running on your local XAMPP server, where messages travel from your browser to a PHP backend, then to OpenAI's servers, and responses appear in your chat window.
By the end of this project, you'll have:
- A PHP API endpoint that securely relays messages to OpenAI using cURL, keeping your API key hidden from the browser.
- A modern chat interface built with HTML, CSS, and JavaScript that displays user and assistant messages in styled bubbles.
- A conversation history system stored in a JavaScript array that gives the AI context for multi-turn conversations.
- Secret Mission: Add a personality selector that lets you switch between different AI personas mid-conversation.
Are there any prerequisites?
You should have basic familiarity with HTML, CSS, JavaScript, and PHP. You also need XAMPP installed with Apache and PHP ready to go. If you have built a simple webpage before and understand how a local web server works, you are ready.
Set Up Your OpenAI Account and Project Structure
Your chat assistant needs a way to communicate with OpenAI's servers. Every request your PHP backend sends must include an API key that proves you have permission to use the service.
In this step, you'll create your OpenAI Platform account, generate an API key, and set up a secure project folder that keeps your key hidden from the browser.
In this step, get ready to:
- Create an OpenAI Platform account and generate an API key.
- Set up a project folder with secure configuration files.
- Verify that PHP's cURL extension is enabled.
Create your OpenAI Platform account and get an API key
The OpenAI Platform is the developer portal where you manage API keys, monitor usage, and access documentation. It is separate from ChatGPT (the consumer product).
- Open your browser and go to platform.openai.com.
- Click Sign up and create a free account using your email, Google, or Microsoft login.
- Complete the verification steps (email confirmation and phone number).
What about billing?
New OpenAI accounts receive $5 in free API credits. No credit card is required. These credits expire after 3 months, but they are more than enough for this entire project (you will spend well under $0.01).
- Once signed in, navigate to the API Keys page at platform.openai.com/api-keys.
- Click Create new secret key.
- Give your key a name (for example, ai-chat-project) and click Create secret key.
- Copy the key immediately and paste it somewhere safe (a temporary text file or password manager).
You can only see this key once
OpenAI will never show the full key again after you close this dialog. If you lose it, you will need to delete the key and create a new one. Your key starts with sk-proj- followed by a long string of characters.
Create your project folder and configuration files
Your project lives inside XAMPP's htdocs folder. This is the directory that Apache serves to your browser when you visit http://localhost/.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
Now open your XAMPP htdocs folder in VS Code.
macOS
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to /Applications/XAMPP/xamppfiles/htdocs/ and click Open.
Windows
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to C:\xampp\htdocs\ and click Select Folder.
- In the Explorer sidebar on the left, right-click in the empty space and select New Folder.
- Name the folder ai-chat and press Enter.
- Expand the ai-chat folder by clicking on it.
- Right-click inside ai-chat and select New File. Name it .env and press Enter.
- Paste the following content into your .env file:
OPENAI_API_KEY=your-api-key-here
- Replace your-api-key-here with the actual API key you copied from OpenAI (the one starting with sk-proj-).
- Create another new file inside ai-chat called .htaccess and paste the following content:
<Files .env>
Order allow,deny
Deny from all
</Files>
What do these files do?
The .env file stores your API key in a single place, separate from your code. Your PHP scripts will read this file to get the key.
The .htaccess file tells Apache to block any browser request for .env. Without this, anyone could visit http://localhost/ai-chat/.env and see your key.
- Save both files with Cmd+S (macOS) or Ctrl+S (Windows).
- Confirm both .env and .htaccess appear inside the ai-chat folder in the Explorer sidebar.
Don't see the files in the sidebar?
Files starting with a dot (like .env) are hidden by default on macOS. VS Code shows them regardless, but make sure you created them inside ai-chat and not in the parent htdocs folder.
If the file name shows as env without the leading dot, rename it by right-clicking the file and selecting Rename.
Help me troubleshoot my project files not showing up correctly.
Verify your PHP and cURL setup
Your PHP backend will use the cURL extension to make HTTP requests to OpenAI. XAMPP includes cURL by default, but you should confirm it is enabled before writing any code.
- First, start Apache in XAMPP. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type XAMPP and press Enter to open the XAMPP Control Panel.
- Click Start next to Apache. The status indicator turns green when Apache is running.
What is Apache?
Apache is the web server bundled with XAMPP. It listens for browser requests on http://localhost/ and serves the PHP files in your htdocs folder. Without Apache running, your browser cannot reach any of your project files.
- Back in VS Code, create a new file inside ai-chat called phpinfo.php and paste this content:
<?php
// Temporary file to verify PHP extensions
phpinfo();
- Save the file.
- Open your browser and navigate to http://localhost/ai-chat/phpinfo.php.
You should see a large purple table showing your PHP configuration.
- Press Cmd+F (macOS) or Ctrl+F (Windows) to open the browser's search bar.
- Type curl and press Enter.
- Look for a section titled curl with cURL support: enabled listed underneath.
✔️ I see cURL support: enabled
Your PHP installation has everything it needs to talk to OpenAI. Time to clean up.
- Go back to VS Code and delete phpinfo.php by right-clicking it in the Explorer sidebar and selecting Delete.
Why delete this file?
The phpinfo() function exposes your entire server configuration. Leaving it accessible is a security risk, even on localhost. Always remove test files once you have confirmed what you need.
ⓧ I don't see cURL listed
If cURL does not appear in the phpinfo output, it may be disabled in your PHP configuration.
- Open your XAMPP php.ini file. In the XAMPP Control Panel, click Config next to Apache, then select php.ini.
- Press Cmd+F (macOS) or Ctrl+F (Windows) and search for extension=curl.
- If the line starts with a semicolon (;extension=curl), remove the semicolon to enable it.
- Save php.ini and restart Apache by clicking Stop then Start in the XAMPP Control Panel.
- Refresh http://localhost/ai-chat/phpinfo.php and search for curl again.
Help me enable the PHP cURL extension in XAMPP.
✔️ Awesome, I've got everything!
Great. Make sure you have saved both files and deleted phpinfo.php.
ⓧ I'd like to double check the full code
Here is what each file should contain at this point.
OPENAI_API_KEY=your-api-key-here
<Files .env>
Order allow,deny
Deny from all
</Files>
Your project folder is secure and your server is ready. Next up, you'll build the PHP backend that actually talks to OpenAI.
Build the PHP Backend
Your project folder is ready with a secure .env file and Apache is serving files from ai-chat. Now it's time to build the PHP backend that connects your app to OpenAI.
Your backend acts as a secure bridge between the browser and OpenAI's servers. Without it, your API key would be exposed in the browser's source code for anyone to steal.
In this step, get ready to:
- Create config.php to load your API key from the .env file.
- Create chat.php as your API endpoint that talks to OpenAI.
- Test the endpoint to confirm it responds correctly.
Create config.php to load your API key
Your API key lives in the .env file, but PHP doesn't read .env files automatically. You need a helper function that parses the file and makes the key available to your code.
Why cURL instead of the OpenAI PHP SDK?
An official PHP SDK exists (openai-php/client), but it requires Composer, PHP 8.2+, and hides the HTTP request behind abstractions. Using cURL directly lets you see exactly how the REST API works at the protocol level.
This knowledge transfers to any API you integrate in the future, regardless of language or framework.
- Create a new file called config.php inside your ai-chat folder and paste the following code:
<?php
// Reads a .env file and sets each key-value pair as an environment variable
function loadEnv($path) {
// Throw an error if the .env file doesn't exist
if (!file_exists($path)) {
throw new Exception('.env file not found. Create a .env file with your OPENAI_API_KEY.');
}
// Read all lines, skipping blank ones
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$line = trim($line);
// Skip comment lines and lines without an equals sign
if (strpos($line, '#') === 0) continue;
if (strpos($line, '=') === false) continue;
// Split into name and value, then register as environment variable
list($name, $value) = explode('=', $line, 2);
$name = trim($name);
$value = trim($value);
putenv("$name=$value");
}
}
// Load the .env file from the same directory as this script
loadEnv(__DIR__ . '/.env');
What does this code do?
- loadEnv() opens your .env file and reads each line.
- It skips blank lines and comments (lines starting with #).
- For each valid line, it splits at the = sign and calls putenv() to make the value accessible via getenv() anywhere in your PHP code.
- The last line automatically runs the function when this file is included, so your API key is always loaded.
- Save config.php.
- You won't see a change yet. The next file (chat.php) loads this automatically to access your API key.
✔️ Awesome, I've got everything!
Great. Make sure you have saved config.php in your ai-chat folder.
ⓧ I'd like to double check the full code
<?php
function loadEnv($path) {
if (!file_exists($path)) {
throw new Exception('.env file not found. Create a .env file with your OPENAI_API_KEY.');
}
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$line = trim($line);
if (strpos($line, '#') === 0) continue;
if (strpos($line, '=') === false) continue;
list($name, $value) = explode('=', $line, 2);
$name = trim($name);
$value = trim($value);
putenv("$name=$value");
}
}
loadEnv(__DIR__ . '/.env');
Create chat.php as your API endpoint
This file is the core of your backend. It receives messages from your JavaScript frontend, validates them, sends them to OpenAI's /v1/chat/completions endpoint, and returns the AI's response as JSON.
You will build this file in two parts: first the request handling and validation, then the OpenAI connection.
- Create a new file called chat.php inside your ai-chat folder and paste the following code:
<?php
// Set response headers for JSON API with CORS support
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
// Handle preflight CORS requests from browsers
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
// Only allow POST requests to this endpoint
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Only POST requests are allowed']);
exit;
}
// Load the API key from .env via config.php
require_once 'config.php';
// Read and decode the JSON body sent by the frontend
$input = json_decode(file_get_contents('php://input'), true);
// Check that a messages array was provided
if (!$input || !isset($input['messages']) || empty($input['messages'])) {
http_response_code(400);
echo json_encode(['error' => 'No messages provided']);
exit;
}
// Validate and sanitize each message in the conversation
$messages = $input['messages'];
$maxMessageLength = 2000;
foreach ($messages as $key => $message) {
if (!isset($message['role']) || !isset($message['content'])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid message format']);
exit;
}
if (!in_array($message['role'], ['user', 'assistant'])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid message role']);
exit;
}
if (strlen($message['content']) > $maxMessageLength) {
http_response_code(400);
echo json_encode(['error' => 'Message too long (max 2000 characters)']);
exit;
}
// Strip control characters for security
$messages[$key]['content'] = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $message['content']);
}
What does this code do?
- The header() calls tell the browser this endpoint returns JSON and allow cross-origin requests (CORS).
- The method checks reject anything that isn't a POST request, returning a 405 error for GET requests (like when someone navigates to the URL directly).
- php://input reads the raw request body, which is how PHP receives JSON data sent from JavaScript.
- The foreach loop validates each message has a valid role and content, enforces a 2000-character limit, and strips dangerous control characters.
- Save chat.php.
- The endpoint isn't complete yet. The next chunk adds the OpenAI connection.
- Add the following code directly below the foreach loop you just pasted (at the bottom of chat.php):
// Define the system prompt that shapes the AI's personality
$systemPrompt = 'You are a helpful assistant. Keep responses concise and friendly.';
array_unshift($messages, [
'role' => 'system',
'content' => $systemPrompt
]);
// Retrieve the API key loaded by config.php
$apiKey = getenv('OPENAI_API_KEY');
// Make sure a real API key is configured
if (!$apiKey || $apiKey === 'your-api-key-here') {
http_response_code(500);
echo json_encode(['error' => 'API key not configured. Add your key to the .env file.']);
exit;
}
// Build the request payload for OpenAI
$data = [
'model' => 'gpt-4.1-nano',
'messages' => $messages,
'max_tokens' => 1000,
'temperature' => 0.7
];
// Send the request to OpenAI's Chat Completions endpoint via cURL
$ch = curl_init('https://api.openai.com/v1/chat/completions');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
$response = curl_exec($ch);
// Handle connection errors (network issues, DNS failure, etc.)
if (curl_errno($ch)) {
http_response_code(500);
echo json_encode(['error' => 'Failed to connect to OpenAI: ' . curl_error($ch)]);
curl_close($ch);
exit;
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$responseData = json_decode($response, true);
// Handle API-level errors (invalid key, rate limit, etc.)
if ($httpCode !== 200) {
$errorMessage = $responseData['error']['message'] ?? 'Unknown API error (HTTP ' . $httpCode . ')';
http_response_code($httpCode);
echo json_encode(['error' => $errorMessage]);
exit;
}
// Extract the assistant's reply and return it as JSON
$assistantMessage = $responseData['choices'][0]['message']['content'] ?? 'No response received';
echo json_encode([
'success' => true,
'message' => $assistantMessage
]);
What does this code do?
- array_unshift() inserts a system message at the front of the messages array. This tells the AI how to behave (its personality and tone).
- The API key check catches cases where the learner forgot to add their real key to .env.
- The $data array is the request payload. It uses the gpt-4.1-nano model, which costs $0.10 per million input tokens.
- curl_init() opens a connection to OpenAI's endpoint. The Authorization: Bearer header attaches your API key to the request.
- After the response comes back, the code checks for connection errors, then API errors, then extracts the assistant's reply from choices[0].message.content.
- Save chat.php.
✔️ Awesome, I've got everything!
Great. Double check you have saved chat.php in your ai-chat folder.
ⓧ I'd like to double check the full code
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Only POST requests are allowed']);
exit;
}
require_once 'config.php';
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || !isset($input['messages']) || empty($input['messages'])) {
http_response_code(400);
echo json_encode(['error' => 'No messages provided']);
exit;
}
$messages = $input['messages'];
$maxMessageLength = 2000;
foreach ($messages as $key => $message) {
if (!isset($message['role']) || !isset($message['content'])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid message format']);
exit;
}
if (!in_array($message['role'], ['user', 'assistant'])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid message role']);
exit;
}
if (strlen($message['content']) > $maxMessageLength) {
http_response_code(400);
echo json_encode(['error' => 'Message too long (max 2000 characters)']);
exit;
}
$messages[$key]['content'] = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $message['content']);
}
$systemPrompt = 'You are a helpful assistant. Keep responses concise and friendly.';
array_unshift($messages, [
'role' => 'system',
'content' => $systemPrompt
]);
$apiKey = getenv('OPENAI_API_KEY');
if (!$apiKey || $apiKey === 'your-api-key-here') {
http_response_code(500);
echo json_encode(['error' => 'API key not configured. Add your key to the .env file.']);
exit;
}
$data = [
'model' => 'gpt-4.1-nano',
'messages' => $messages,
'max_tokens' => 1000,
'temperature' => 0.7
];
$ch = curl_init('https://api.openai.com/v1/chat/completions');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
$response = curl_exec($ch);
if (curl_errno($ch)) {
http_response_code(500);
echo json_encode(['error' => 'Failed to connect to OpenAI: ' . curl_error($ch)]);
curl_close($ch);
exit;
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$responseData = json_decode($response, true);
if ($httpCode !== 200) {
$errorMessage = $responseData['error']['message'] ?? 'Unknown API error (HTTP ' . $httpCode . ')';
http_response_code($httpCode);
echo json_encode(['error' => $errorMessage]);
exit;
}
$assistantMessage = $responseData['choices'][0]['message']['content'] ?? 'No response received';
echo json_encode([
'success' => true,
'message' => $assistantMessage
]);
Test the endpoint manually
Your endpoint is live on Apache. You can prove it by visiting the URL directly in your browser, which sends a GET request.
- Open your browser and navigate to http://localhost/ai-chat/chat.php.
You should see this JSON error message displayed in your browser:
{"error":"Only POST requests are allowed"}
What does this error mean?
This is not a bug. Your browser sends a GET request when you visit a URL. Your code rejects anything that is not a POST request, which is exactly the behavior you want.
Seeing this JSON response proves two things: Apache is serving your PHP file correctly, and your method-checking logic works. The JavaScript frontend (built in the next step) will send proper POST requests.
Seeing a blank page or a PHP error instead?
- If you see a blank page, check that your file starts with <?php on the very first line with no spaces or characters before it.
- If you see a 500 error or "file not found" message, make sure chat.php and config.php are both saved in the same ai-chat folder.
- If you see "404 Not Found", confirm XAMPP Apache is running (green highlight in the XAMPP Control Panel).
- help me debug why chat.php isn't returning the expected JSON error
Your backend is live and responding. Next up, you'll build the chat interface that sends messages to this endpoint and displays the AI's responses.
Build the Chat Interface
Your PHP backend is ready to receive messages and relay them to OpenAI. Now you need a frontend that your users actually interact with.
In this step, you will build the HTML structure and CSS styling for a modern chat interface. The layout has three zones: a dark header with a title and Clear button, a scrollable messages area, and an input form pinned to the bottom.
In this step, get ready to:
- Create the HTML page with a chat layout structure.
- Style the chat interface with user messages in blue bubbles and assistant messages in gray bubbles.
Create index.html with the chat layout
The HTML file defines the skeleton of your chat interface. It creates the three zones (header, messages, input) and links to external CSS and JavaScript files that you will build next.
- In VS Code, create a new file called index.html inside your ai-chat folder.
- Paste the following code into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Chat Assistant</title>
<!-- Link to external stylesheet -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Main chat container -->
<div class="chat-container">
<!-- Header with title and clear button -->
<header class="chat-header">
<h1>AI Chat Assistant</h1>
<button id="clear-btn" title="Clear conversation">Clear</button>
</header>
<!-- Scrollable message display area -->
<div class="chat-messages" id="chat-messages">
<div class="message assistant">
<div class="message-content">Hello! I am your AI assistant. How can I help you today?</div>
</div>
</div>
<!-- Input form pinned to the bottom -->
<form class="chat-input" id="chat-form">
<input type="text" id="user-input" placeholder="Type your message..." autocomplete="off" required>
<button type="submit" id="send-btn">Send</button>
</form>
</div>
<!-- JavaScript loaded at the end so DOM is ready -->
<script src="script.js"></script>
</body>
</html>
What does this code do?
- The .chat-container div wraps everything and will become the card-like chat window.
- The header holds the title and a Clear button (wired up by JavaScript later).
- The #chat-messages div is the scrollable area where messages appear. It starts with one welcome message from the assistant.
- The #chat-form at the bottom has a text input and a Send button. The required attribute prevents empty submissions.
- Save index.html.
- Open your browser and navigate to http://localhost/ai-chat/.
- You should see unstyled text showing the title, welcome message, and an input field. It looks plain because the CSS file does not exist yet.
Seeing a blank page or a file listing?
- Make sure the file is named exactly index.html (not Index.html or index.htm). Apache looks for this specific filename.
- Confirm the file is inside htdocs/ai-chat/ and not nested in an extra subfolder.
- Check that XAMPP Apache is still running in the Control Panel.
Help me figure out why my index.html is not loading in the browser.
Create style.css for a modern chat design
The stylesheet transforms the raw HTML into a polished chat window. You will build it in three sections: the page layout, the message bubbles, and the input form.
- Create a new file called style.css inside your ai-chat folder.
- Start by adding the page layout and header styles:
/* Reset browser defaults */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Center the chat window on the page */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f0f2f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
/* The main card that holds the entire chat */
.chat-container {
width: 100%;
max-width: 700px;
height: 80vh;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Dark header bar with title and button */
.chat-header {
background: #2d3748;
color: #ffffff;
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header h1 {
font-size: 1.2rem;
font-weight: 600;
}
/* Semi-transparent clear button */
.chat-header button {
background: rgba(255, 255, 255, 0.2);
color: #ffffff;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
}
.chat-header button:hover {
background: rgba(255, 255, 255, 0.3);
}
What does this code do?
- The * reset removes default browser spacing so every element starts from zero.
- Flexbox on body centers the chat card both horizontally and vertically on the page.
- The .chat-container uses flex-direction: column so the header, messages, and input stack vertically.
- The header uses justify-content: space-between to push the title left and the Clear button right.
- Save style.css and refresh your browser.
- You should see a white card centered on a light gray background, with a dark header bar showing "AI Chat Assistant" on the left and a "Clear" button on the right.
Page still looks unstyled?
- Confirm the filename is style.css (matching the href in your HTML link tag exactly).
- Make sure style.css is in the same folder as index.html, not inside a subfolder.
- Try a hard refresh with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows) to clear the browser cache.
Why isn't my CSS loading in the browser?
- Next, add the message display styles below your existing CSS in style.css:
/* Scrollable area where messages appear */
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
/* Base message bubble */
.message {
display: flex;
max-width: 80%;
}
/* User messages align to the right */
.message.user {
align-self: flex-end;
}
/* Assistant messages align to the left */
.message.assistant {
align-self: flex-start;
}
/* Shared content styling for all message types */
.message-content {
padding: 12px 16px;
border-radius: 12px;
line-height: 1.5;
font-size: 0.95rem;
white-space: pre-wrap;
word-wrap: break-word;
}
/* Blue bubble for user messages */
.message.user .message-content {
background: #4a90d9;
color: #ffffff;
border-bottom-right-radius: 4px;
}
/* Gray bubble for assistant messages */
.message.assistant .message-content {
background: #f1f3f5;
color: #2d3748;
border-bottom-left-radius: 4px;
}
/* Italic loading indicator */
.message.loading .message-content {
background: #f1f3f5;
color: #718096;
font-style: italic;
}
/* Red error messages */
.message.error .message-content {
background: #fee2e2;
color: #dc2626;
}
What does this code do?
- The .chat-messages container uses flex: 1 to fill all available vertical space between the header and input form.
- User messages get align-self: flex-end (right side, blue background) while assistant messages get align-self: flex-start (left side, gray background).
- The flattened corner (border-bottom-right-radius: 4px for user, border-bottom-left-radius: 4px for assistant) creates the classic chat bubble "tail" effect.
- The .loading and .error classes are used by JavaScript later to show status messages.
- Save style.css and refresh your browser.
- The welcome message should now appear as a gray bubble aligned to the left side of the chat area.
Welcome message not styled as a bubble?
- Check that your HTML has class="message assistant" on the outer div and class="message-content" on the inner div. Class names are case-sensitive.
- Make sure you pasted the new CSS below your existing header styles, not replacing them.
Why is my chat message not showing the bubble styling?
- Finally, add the input form styles at the bottom of style.css:
/* Input area pinned to the bottom */
.chat-input {
display: flex;
padding: 16px 24px;
gap: 12px;
border-top: 1px solid #e2e8f0;
background: #ffffff;
}
/* Text input stretches to fill available width */
.chat-input input {
flex: 1;
padding: 12px 16px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 0.95rem;
outline: none;
transition: border-color 0.2s;
}
/* Blue border when the input is focused */
.chat-input input:focus {
border-color: #4a90d9;
}
/* Blue send button */
.chat-input button {
background: #4a90d9;
color: #ffffff;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
font-size: 0.95rem;
font-weight: 500;
transition: background 0.2s;
}
.chat-input button:hover {
background: #357abd;
}
/* Disabled state while waiting for a response */
.chat-input button:disabled {
background: #a0aec0;
cursor: not-allowed;
}
What does this code do?
- The .chat-input form uses flexbox with a gap to space the text field and button apart evenly.
- The text input uses flex: 1 to take all remaining horizontal space, pushing the button to the right.
- The :disabled style grays out the Send button while the app waits for an API response. JavaScript will toggle this state later.
- Save style.css and refresh your browser.
- You should now see the complete chat interface: a dark header with the title and Clear button, a gray welcome bubble on the left, and a text input with a blue Send button pinned to the bottom.
Input form not visible at the bottom?
- Make sure your .chat-container has display: flex and flex-direction: column. Without these, the three sections will not stack properly.
- Check that your browser window is tall enough. The container uses height: 80vh, so a very short window may crop the bottom.
Help me fix my chat layout so the input form appears at the bottom.
✔️ Awesome, I've got everything!
Great. Double check you have saved both files before continuing.
ⓧ I'd like to double check the full code
Here is the complete index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Chat Assistant</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="chat-container">
<header class="chat-header">
<h1>AI Chat Assistant</h1>
<button id="clear-btn" title="Clear conversation">Clear</button>
</header>
<div class="chat-messages" id="chat-messages">
<div class="message assistant">
<div class="message-content">Hello! I am your AI assistant. How can I help you today?</div>
</div>
</div>
<form class="chat-input" id="chat-form">
<input type="text" id="user-input" placeholder="Type your message..." autocomplete="off" required>
<button type="submit" id="send-btn">Send</button>
</form>
</div>
<script src="script.js"></script>
</body>
</html>
And here is the complete style.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f0f2f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.chat-container {
width: 100%;
max-width: 700px;
height: 80vh;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
overflow: hidden;
}
.chat-header {
background: #2d3748;
color: #ffffff;
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header h1 {
font-size: 1.2rem;
font-weight: 600;
}
.chat-header button {
background: rgba(255, 255, 255, 0.2);
color: #ffffff;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
}
.chat-header button:hover {
background: rgba(255, 255, 255, 0.3);
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.message {
display: flex;
max-width: 80%;
}
.message.user {
align-self: flex-end;
}
.message.assistant {
align-self: flex-start;
}
.message-content {
padding: 12px 16px;
border-radius: 12px;
line-height: 1.5;
font-size: 0.95rem;
white-space: pre-wrap;
word-wrap: break-word;
}
.message.user .message-content {
background: #4a90d9;
color: #ffffff;
border-bottom-right-radius: 4px;
}
.message.assistant .message-content {
background: #f1f3f5;
color: #2d3748;
border-bottom-left-radius: 4px;
}
.message.loading .message-content {
background: #f1f3f5;
color: #718096;
font-style: italic;
}
.message.error .message-content {
background: #fee2e2;
color: #dc2626;
}
.chat-input {
display: flex;
padding: 16px 24px;
gap: 12px;
border-top: 1px solid #e2e8f0;
background: #ffffff;
}
.chat-input input {
flex: 1;
padding: 12px 16px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 0.95rem;
outline: none;
transition: border-color 0.2s;
}
.chat-input input:focus {
border-color: #4a90d9;
}
.chat-input button {
background: #4a90d9;
color: #ffffff;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
font-size: 0.95rem;
font-weight: 500;
transition: background 0.2s;
}
.chat-input button:hover {
background: #357abd;
}
.chat-input button:disabled {
background: #a0aec0;
cursor: not-allowed;
}
Your chat interface looks great, but clicking Send does nothing yet. In the next step, you will write the JavaScript that connects this frontend to your PHP backend and brings the conversation to life.
Connect Frontend to Backend with JavaScript
Your chat interface looks great, but right now it does nothing when you type a message and hit Send. The HTML form submits, the page refreshes, and your message vanishes.
In this step, you will write the JavaScript that connects your frontend to the PHP backend. Your code will capture the user's message, send the entire conversation history to chat.php using the Fetch API, and display the AI's response in the chat window.
In this step, get ready to:
- Create script.js with DOM references and an in-memory conversation history array.
- Implement the message sending flow using AJAX to communicate with your PHP backend.
- Add the Clear button logic and error handling for network and API failures.
Create script.js with your conversation state
Your index.html already links to script.js at the bottom of the page. You need to create this file and set up the variables your chat logic depends on.
- In VS Code, create a new file called script.js inside your ai-chat folder.
- Add the DOM references and conversation history array by pasting this code:
const chatMessages = document.getElementById('chat-messages');
const chatForm = document.getElementById('chat-form');
const userInput = document.getElementById('user-input');
const sendBtn = document.getElementById('send-btn');
const clearBtn = document.getElementById('clear-btn');
// In-memory conversation history (cleared on page refresh)
let conversationHistory = [];
What does this code do?
The five const lines grab references to the HTML elements you built in the previous step. Each getElementById call finds a specific element by its id attribute so JavaScript can interact with it.
The conversationHistory array stores every message (user and assistant) as objects. This array gets sent to your PHP backend with each request so the AI has context from earlier in the conversation. Because it lives in memory, refreshing the page wipes it clean automatically.
Why store conversation history on the frontend?
OpenAI's API is stateless. It does not remember previous messages between requests. To have a multi-turn conversation, you must send the full message history with every request. Storing this array in JavaScript means no database is needed, and the conversation disappears on refresh.
You won't see a change yet. The next chunk adds the event listener that uses these references.
Implement the message sending flow
When the user submits the chat form, your code needs to do several things in sequence: display the message, send the history to your backend, wait for the AI's reply, and display it. This is your app's core logic.
- In script.js, add the form submit event listener below your conversationHistory declaration by pasting this code:
chatForm.addEventListener('submit', async function(e) {
e.preventDefault();
const message = userInput.value.trim();
if (!message) return;
// Add user message to display
appendMessage('user', message);
// Add to conversation history
conversationHistory.push({ role: 'user', content: message });
// Clear input and disable form while waiting
userInput.value = '';
sendBtn.disabled = true;
userInput.disabled = true;
// Show loading indicator
const loadingEl = appendMessage('loading', 'Thinking...');
try {
const response = await fetch('chat.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: conversationHistory })
});
const data = await response.json();
// Remove loading indicator
loadingEl.remove();
if (data.success) {
appendMessage('assistant', data.message);
conversationHistory.push({ role: 'assistant', content: data.message });
} else {
appendMessage('error', 'Error: ' + data.error);
// Remove failed user message from history
conversationHistory.pop();
}
} catch (error) {
loadingEl.remove();
appendMessage('error', 'Error: Could not connect to the server. Make sure XAMPP Apache is running.');
// Remove failed user message from history
conversationHistory.pop();
}
// Re-enable form
sendBtn.disabled = false;
userInput.disabled = false;
userInput.focus();
});
What does this code do?
- e.preventDefault() stops the form from refreshing the page (the default browser behavior for form submissions).
- appendMessage('user', message) displays the user's message in the chat window immediately, before waiting for the AI.
- The fetch('chat.php', ...) call sends the full conversation history as JSON to your PHP backend. This is AJAX in action: communicating with the server without reloading the page.
- The catch block handles network failures (like Apache not running). When an error occurs, conversationHistory.pop() removes the user's failed message so the history stays clean.
You won't see a change yet. The appendMessage function doesn't exist yet. You will define it in the next chunk.
Add the Clear button and error handling
The last piece is the Clear button listener and the appendMessage helper function. Both the submit listener and the Clear button rely on this helper to add messages to the chat display.
- In script.js, add the Clear button listener and appendMessage function below the submit listener by pasting this code:
clearBtn.addEventListener('click', function() {
conversationHistory = [];
chatMessages.innerHTML = '';
appendMessage('assistant', 'Conversation cleared. How can I help you?');
});
function appendMessage(role, content) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message ' + role;
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.textContent = content;
messageDiv.appendChild(contentDiv);
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
return messageDiv;
}
What does this code do?
- The Clear button empties the conversationHistory array and wipes the chat display. This means the AI loses all context on the next message, just like starting a fresh conversation.
- appendMessage creates a new div with the CSS class matching the role (user, assistant, loading, or error). Your CSS already styles each of these differently (blue bubbles for user, gray for assistant, red for errors).
- chatMessages.scrollTop = chatMessages.scrollHeight auto-scrolls the chat to the bottom after each new message, so the latest message is always visible.
- The function returns the created element. This is how the submit listener captures the loading indicator as loadingEl and removes it later with loadingEl.remove().
- Save script.js.
- Open your browser and navigate to http://localhost/ai-chat/. Type a short message like Hello and press **Send**.
- You should see your message appear as a blue bubble on the right, a brief "Thinking..." indicator, and then the AI's reply as a gray bubble on the left.
Not seeing a response from the AI?
- Open your browser's Developer Tools (press F12) and check the **Console** tab for JavaScript errors. A common issue is a typo in one of the element IDs.
- Check the **Network** tab to see if the request to chat.php is being sent. If you see a 500 error, your API key in .env may still be the placeholder value.
- Make sure XAMPP Apache is running. If the fetch request fails entirely, you will see the red error message "Could not connect to the server."
My chat is not showing a response from the AI. I see the following in my browser console: [paste error]. My .env file has my API key set. Help me debug this.
✔️ Awesome, I've got everything!
Great. Double check you have saved script.js and that your chat is responding to messages.
ⓧ I'd like to double check the full code
Here is the complete script.js file. Compare it with yours to spot any differences.
const chatMessages = document.getElementById('chat-messages');
const chatForm = document.getElementById('chat-form');
const userInput = document.getElementById('user-input');
const sendBtn = document.getElementById('send-btn');
const clearBtn = document.getElementById('clear-btn');
// In-memory conversation history (cleared on page refresh)
let conversationHistory = [];
chatForm.addEventListener('submit', async function(e) {
e.preventDefault();
const message = userInput.value.trim();
if (!message) return;
// Add user message to display
appendMessage('user', message);
// Add to conversation history
conversationHistory.push({ role: 'user', content: message });
// Clear input and disable form while waiting
userInput.value = '';
sendBtn.disabled = true;
userInput.disabled = true;
// Show loading indicator
const loadingEl = appendMessage('loading', 'Thinking...');
try {
const response = await fetch('chat.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: conversationHistory })
});
const data = await response.json();
// Remove loading indicator
loadingEl.remove();
if (data.success) {
appendMessage('assistant', data.message);
conversationHistory.push({ role: 'assistant', content: data.message });
} else {
appendMessage('error', 'Error: ' + data.error);
// Remove failed user message from history
conversationHistory.pop();
}
} catch (error) {
loadingEl.remove();
appendMessage('error', 'Error: Could not connect to the server. Make sure XAMPP Apache is running.');
// Remove failed user message from history
conversationHistory.pop();
}
// Re-enable form
sendBtn.disabled = false;
userInput.disabled = false;
userInput.focus();
});
clearBtn.addEventListener('click', function() {
conversationHistory = [];
chatMessages.innerHTML = '';
appendMessage('assistant', 'Conversation cleared. How can I help you?');
});
function appendMessage(role, content) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message ' + role;
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.textContent = content;
messageDiv.appendChild(contentDiv);
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
return messageDiv;
}
Your chat assistant is alive. It sends messages, receives AI responses, and handles errors gracefully. Next up, you will put it through a proper test run with multi-turn conversations and edge cases.
Test Your Chat Assistant
All six files are written and saved. Your PHP backend, HTML interface, CSS styling, and JavaScript logic are ready to work together.
In this step, you will launch the full application and confirm that every piece works end-to-end. You will test multi-turn memory, the Clear button, and edge cases like empty or long messages.
In this step, get ready to:
- Open your chat assistant in the browser.
- Verify multi-turn conversation memory works.
- Test edge cases and error handling.
Start XAMPP and open your project
Your XAMPP Apache server needs to be running for PHP to process requests. If you stopped it since the previous step, start it again now.
- Open the XAMPP Control Panel.
- Confirm that Apache shows a green status (running). If not, click Start next to Apache.
- Open your browser (Chrome or Firefox).
- Navigate to http://localhost/ai-chat/ in the address bar.
You should see your chat interface with a dark header reading "AI Chat Assistant", a welcome message from the assistant, and a text input at the bottom.
Seeing a blank page or file listing?
Make sure your project folder is named ai-chat and sits directly inside the XAMPP htdocs directory.
Check that your file is named exactly index.html (not index.html.txt or similar). Apache serves index.html automatically when you visit a folder URL.
help me troubleshoot why my AI chat page is not loading on localhost
Have a multi-turn conversation
The real test of your chat assistant is whether it remembers earlier messages. Your conversationHistory array sends the full message history with every request, giving the AI context about what was already discussed.
- Type My name is Alex into the input field and click Send.
- Wait for the AI to respond. You should see a "Thinking..." indicator, then a gray bubble with the assistant's reply.
- Type What is my name? and send it.
The AI should respond with "Alex" (or reference your name). This proves the conversation history is working correctly.
- Now click the Clear button in the top right corner of the header.
- Type What is my name? again and send it.
This time, the AI should not know your name. It will say something like "I don't know your name yet." This confirms the Clear button resets the conversationHistory array.
Why does clearing work?
The OpenAI API is stateless. It does not remember previous conversations on its own. Your JavaScript sends the entire conversationHistory array with every request.
When you click Clear, the array resets to empty. The next request has no prior messages, so the AI has no context to draw from.
Getting an error message instead of a response?
If you see "API key not configured", open your .env file and confirm it contains your real OpenAI API key (starting with sk-proj-), not the placeholder text.
If you see "Could not connect to the server", make sure Apache is running in the XAMPP Control Panel.
If you see an error mentioning "insufficient_quota", your free credits may have expired. Check your usage at platform.openai.com under Settings > Billing.
help me debug my OpenAI chat API error
Test edge cases
A solid application handles unexpected input gracefully. Your PHP backend includes validation that protects against malformed requests.
- Try clicking Send without typing anything in the input field.
Nothing should happen. The HTML required attribute on the input field blocks empty submissions before they reach JavaScript.
- Type a long message (a full paragraph of text) and send it.
The AI should respond normally. Your PHP validation allows messages up to 2000 characters.
- Type a message with special characters like What does <script>alert('hi')</script> do? and send it.
The message should appear in your chat without breaking the layout. Your JavaScript uses textContent (not innerHTML) to display messages, which prevents script injection.
Why does textContent matter for security?
Using textContent treats everything as plain text. Even if someone types HTML or JavaScript into the chat, it renders as visible characters instead of being executed by the browser.
This is a basic defense against Cross-Site Scripting (XSS) attacks.
Your AI chat assistant is fully functional. You have proven that the frontend, backend, and OpenAI API all communicate correctly. Next up, the Secret Mission challenges you to add a personality selector that changes how the AI responds.
Secret mission
Add a Personality Selector
Your chat assistant always responds in the same helpful style. What if you could switch between completely different AI personas mid-conversation? In this extension, you will add a dropdown that lets you choose between a helpful assistant, a code reviewer, a creative writer, and a pirate captain.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local XAMPP server, so there are no ongoing hosting costs. The only potential cost is continued use of your OpenAI API key, which draws from your free $5 credit balance.
Cost warning
Your OpenAI API key remains active until you revoke it. While gpt-4.1-nano costs are extremely low ($0.10 per million input tokens), anyone with access to your key could generate charges against your account.
If you are not planning to continue using the API, revoke your key from the OpenAI Platform dashboard.
Resources you used:
- OpenAI Platform account with an active API key.
- Project folder ai-chat inside your XAMPP htdocs directory.
- Files: .env, .htaccess, config.php, chat.php, index.html, style.css, script.js.
- XAMPP Apache server (local, no ongoing cost).
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with your chat assistant.
- Your project files stay in htdocs/ai-chat and are available anytime you start Apache.
- Your OpenAI API key remains active and draws from your free $5 credit balance.
- Conversation history resets automatically every time you refresh the page, so no data accumulates.
Pause - I'll come back to this later
Shut down XAMPP and optionally revoke your API key. Your project files remain on disk for when you return.
- Open the XAMPP Control Panel and click Stop next to Apache.
- If you want to prevent any accidental API usage, go to platform.openai.com, navigate to API Keys, and revoke your key. You can create a new one when you return.
- Leave your ai-chat folder in place. Everything will work again once you start Apache and (if revoked) generate a fresh API key.
Delete - I don't want to use this again
Remove all project files and revoke your API key to start completely fresh.
- Go to platform.openai.com, navigate to API Keys, and revoke (delete) your project key.
- Delete the entire ai-chat folder from your XAMPP htdocs directory.
- If you no longer need your OpenAI Platform account, you can delete it from your account settings. This is permanent.
Nice Work!
Nice Work!
You just built a fully functional AI chat assistant from scratch. Your application sends messages from the browser to a PHP backend, then securely relays them to OpenAI's API and displays the responses in real time.
Here's what you learned:
- Built a PHP backend proxy that securely communicates with OpenAI's Chat Completions API using cURL, keeping your API key hidden from the browser.
- Created a modern chat interface with HTML, CSS, and JavaScript that manages in-memory conversation history for multi-turn conversations.
- Learned the fundamental REST API integration pattern (frontend to backend to external API) that you can apply to any API in the future.
- Added a personality selector that demonstrates how system prompts shape AI behavior across different personas.
Ready to quiz yourself?