Build a Hyperlocal Malaysia AI Buddy
Build a PHP web app that gives personalized Malaysian recommendations via Gemini AI.
Introduction
30 Second Summary
You're in Petaling Jaya on a Friday night. You're hungry, you've got RM30, and you have no idea what to eat or where to go. You ask a friend who knows every hidden gem in Malaysia.
In this project, you will build MY Buddy, a web app that acts as that friend. It takes your location, mood, budget, and interests, then uses AI to give you 5 personalized Malaysian lifestyle recommendations.
What You'll Build
A localhost web app powered by PHP and the Gemini API that delivers hyperlocal recommendations for food, activities, and places in Malaysia.
By the end of this project, you'll have:
- A PHP backend proxy that securely calls the Gemini 2.5 Flash API without exposing your API key to the browser.
- A modern Malaysian-themed frontend built with HTML, CSS, and vanilla JavaScript that collects user input and displays AI-generated recommendation cards.
- A demo-ready hackathon prototype that runs locally with zero frameworks, zero databases, and zero external dependencies.
- **Secret Mission:** A "Surprise Me!" button that randomly fills the form with fun Malaysian scenarios and auto-submits for instant recommendations.
Are there any prerequisites?
None. This project includes full setup instructions for every tool you need. You will install PHP, get a free Gemini API key, and create the project from scratch.
Before We Start
Before We Start...
Set Up Your Development Environment
Before you can build MY Buddy, you need three things ready on your machine: PHP to run the backend server, a Gemini API key to power the AI recommendations, and a project folder to hold your code.
This step gets all three set up so you can jump straight into coding in the next step.
In this step, get ready to:
- Install PHP on your machine.
- Get a free Gemini API key from Google AI Studio.
- Create the project folder and set the API key as an environment variable.
Check and install PHP
PHP is the programming language your backend server will use to securely call the Gemini API. Let's check if it's already on your machine.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Check if PHP is already installed by running this command:
php -v
This prints the PHP version number if PHP is installed.
✔️ I see a version number
You should see something like PHP 8.x.x followed by additional details. Any version 7.4 or higher works for this project. You're ready to move on.
ⓧ Command not found
PHP isn't installed yet. Follow the instructions for your operating system below.
macOS
- Install PHP using Homebrew by running this command:
brew install php
Don't have Homebrew?
Homebrew is a package manager for macOS that makes installing tools easy. Install it first by visiting https://brew.sh and running the install command shown on the page. Then run brew install php again.
- After installation completes, verify it worked by running:
php -v
You should now see a version number like PHP 8.x.x.
Windows
- Download PHP from https://windows.php.net/download/.
- Choose the latest VS16 x64 Thread Safe zip file.
- Extract the zip to C:\php.
- Add C:\php to your system PATH. Search for Environment Variables in the Windows search bar, open it, edit the Path variable, and add a new entry with C:\php.
- Close and reopen PowerShell, then verify by running:
php -v
You should now see a version number like PHP 8.x.x.
Still not seeing a version number?
Make sure you closed and reopened your terminal after installing PHP. Some installations require a fresh terminal session to pick up the new PATH.
Help me troubleshoot why PHP isn't recognized in my terminal after installation.
Get your free Gemini API key
The Gemini API is what powers MY Buddy's AI recommendations. You need an API key so your PHP backend can authenticate with Google's servers. The free tier gives you free input and output tokens with no credit card required.
- Open https://aistudio.google.com/apikey in your browser.
- Sign in with your Google account.
- Click Create API key.
- If prompted, select or create a Google Cloud project.
- Copy the API key that appears. It starts with AI... and you will need it in the next substep.
What is an API key?
An API key is a unique string that identifies your app when it makes requests to an external service. Google uses it to track your usage and apply free tier limits.
Keep this key private. Never paste it directly into frontend code that runs in the browser. In this project, only the PHP backend will use the key.
Can't create an API key?
Make sure you are signed into a personal Google account. Some workspace or school accounts restrict API key creation. Try a different Google account if you see permission errors.
Help me get a Gemini API key from Google AI Studio.
Create the project folder and set your API key
Now you will create a folder for your project files and store your API key as an environment variable. This keeps the key out of your code so it stays secure.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir my-buddy
cd my-buddy
- Confirm the folder was created by running:
ls
The folder should be empty for now. That's expected.
Next, set your Gemini API key as an environment variable. This lets your PHP backend read the key without hardcoding it in your files.
macOS
- Set the environment variable by running this command, replacing your_api_key_here with the key you copied from Google AI Studio:
export GEMINI_API_KEY="your_api_key_here"
Windows
- Set the environment variable by running this command, replacing your_api_key_here with the key you copied from Google AI Studio:
$env:GEMINI_API_KEY="your_api_key_here"
Why use an environment variable?
Environment variables let your app read sensitive values at runtime without putting them directly in your source code. If you accidentally share your code, the API key stays safe because it only exists in your terminal session.
This variable only lasts for your current terminal session. If you close the terminal, you will need to set it again before starting the server.
- Verify the key is set by running:
macOS
echo $GEMINI_API_KEY
Windows
echo $env:GEMINI_API_KEY
You should see your API key printed in the terminal. If you see a blank line, re-run the export command from above.
Seeing a blank line instead of your key?
Make sure you ran the export (or $env:) command in the same terminal window where you are now running echo. Opening a new terminal tab or window starts a fresh session without the variable.
Help me set the GEMINI_API_KEY environment variable in my terminal.
Your environment is locked and loaded. You have PHP, an API key, and an empty project folder ready to fill. Next up, you will build the PHP backend that calls the Gemini API.
Build the PHP Backend Proxy
Your environment is ready with PHP installed and a Gemini API key stored in your terminal session. Now it's time to build the brains of MY Buddy.
The frontend can't call the Gemini API directly because that would expose your API key in the browser's source code. You need a backend proxy that sits between the browser and Gemini. This file receives the user's preferences, builds a carefully crafted prompt, sends it to Gemini, and returns clean results.
In this step, get ready to:
- Create a PHP file that validates incoming form data and checks for the API key.
- Build a prompt that instructs Gemini to respond like a casual Malaysian buddy.
- Send the prompt to the Gemini API using cURL and parse the response.
Create chat.php with input validation
The backend proxy needs to validate every request before spending API credits on it. If a field is missing or the API key isn't set, the proxy should return a helpful error message immediately.
- Create a new file called chat.php inside your my-buddy folder.
- Paste the following code into chat.php:
<?php
header('Content-Type: application/json');
// Read the raw JSON body sent from the frontend
$input = json_decode(file_get_contents('php://input'), true);
// Reject requests that aren't valid JSON
if (!$input) {
echo json_encode(['success' => false, 'error' => 'Invalid request. Please send JSON data.']);
exit;
}
// Check that all required fields are present
$requiredFields = ['location', 'mood', 'budget', 'timeOfDay', 'interest', 'preference'];
foreach ($requiredFields as $field) {
if (empty($input[$field])) {
echo json_encode(['success' => false, 'error' => "Missing required field: $field"]);
exit;
}
}
// Retrieve the API key from the environment variable
$apiKey = getenv('GEMINI_API_KEY');
if (!$apiKey) {
echo json_encode([
'success' => false,
'error' => 'GEMINI_API_KEY not set. Run: export GEMINI_API_KEY="your_key_here" before starting the server.'
]);
exit;
}
What does this code do?
This section acts as a security gate for every incoming request.
- header('Content-Type: application/json') tells the browser to expect JSON responses from this file.
- file_get_contents('php://input') reads the raw request body that the frontend sends.
- The foreach loop checks every required field and returns a specific error if one is missing.
- getenv('GEMINI_API_KEY') reads the API key from your terminal session. The key never appears in any file the browser can see.
Why use a backend proxy?
If you called the Gemini API directly from JavaScript in the browser, anyone could open DevTools and steal your API key. The PHP proxy keeps the key server-side. The browser only talks to chat.php, which talks to Gemini on your behalf.
- Save chat.php.
You won't see any output yet. The next section adds the AI prompt that tells Gemini how to respond.
Build the Gemini API prompt
The quality of Gemini's recommendations depends entirely on the prompt you send it. A good prompt sets the tone, provides the user's context, and specifies the exact output format you need.
- In chat.php, add the following code directly below the API key check (after the closing } of the if (!$apiKey) block):
// Extract user inputs into variables for the prompt
$location = $input['location'];
$mood = $input['mood'];
$budget = $input['budget'];
$timeOfDay = $input['timeOfDay'];
$interest = $input['interest'];
$preference = $input['preference'];
// Build the prompt that tells Gemini how to behave and what to return
$prompt = <<<EOT
You are MY Buddy, a friendly Malaysian AI buddy. You speak in casual Malaysian tone - mix of English and Malay slang. You understand words like makan, lepak, pokai, healing, mamak, tapau, syok, boring gila.
A user needs recommendations. Here are their details:
- Location: $location
- Mood: $mood
- Budget: $budget
- Time of day: $timeOfDay
- Interest: $interest
- Preference style: $preference (Local = Malaysian style, Non-local = international options available in Malaysia, Mixed = both)
Give exactly 5 recommendations sorted from most suitable to least suitable.
Rules:
- Be budget-aware. Do not recommend things above their budget.
- Be time-aware. Do not recommend breakfast spots at night.
- Be location-aware. Recommend things near or in their location.
- If preference is "Local", focus on Malaysian-style recommendations.
- If preference is "Non-local", suggest international-style options available in Malaysia.
- If preference is "Mixed", include both local and non-local.
- Do NOT claim exact live events. For events, say things like "check nearby mall event boards, campus events, pasar malam schedule, or community pages first".
- Keep reasons short and casual in Malaysian buddy tone.
Return ONLY a JSON array with exactly 5 objects. Each object must have these fields:
- "title": recommendation name
- "category": one of "Food", "Activity", "Place", "Product", "Service"
- "estimatedBudget": budget range in RM (e.g., "RM10 - RM20")
- "reason": short reason why it fits (casual Malaysian tone, 1-2 sentences)
- "localVibe": one of "Local", "Non-local", "Mixed"
Return ONLY the JSON array. No markdown, no code blocks, no extra text.
EOT;
What does this prompt do?
This is prompt engineering in action. The prompt has four sections that work together.
- The persona section tells Gemini to act like a casual Malaysian friend, not a corporate chatbot.
- The user details section injects the actual form values using PHP variables.
- The rules section prevents hallucinations like recommending things outside the budget or claiming fake events.
- The output format section forces Gemini to return a strict JSON structure that your frontend can parse directly.
What is the <<<EOT syntax?
This is PHP's heredoc syntax. It lets you write a long multi-line string without escaping quotes. The string starts at <<<EOT and ends at EOT; on its own line. PHP variables like $location inside the string get replaced with their actual values automatically.
- Save chat.php.
The prompt is ready. The next section sends it to Gemini and handles the response.
Send the request and parse the response
With the prompt built, you need to send it to the Gemini API endpoint and handle whatever comes back. This includes network errors, API errors, and unexpected response formats.
- In chat.php, add the following code directly below the EOT; line:
// Gemini API endpoint for content generation
$url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent';
// Build the request body with the prompt and JSON output mode
$requestBody = json_encode([
'contents' => [
[
'parts' => [
['text' => $prompt]
]
]
],
'generationConfig' => [
'responseMimeType' => 'application/json'
]
]);
// Set up the cURL request with headers and timeout
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $requestBody);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'x-goog-api-key: ' . $apiKey
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
// Execute the request and capture the response
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
// Handle connection errors
if ($curlError) {
echo json_encode(['success' => false, 'error' => 'Connection error: ' . $curlError]);
exit;
}
// Handle non-200 HTTP responses
if ($httpCode !== 200) {
echo json_encode(['success' => false, 'error' => 'Gemini API error (HTTP ' . $httpCode . '). Check your API key and try again.']);
exit;
}
// Parse the Gemini response structure
$responseData = json_decode($response, true);
if (!$responseData || !isset($responseData['candidates'][0]['content']['parts'][0]['text'])) {
echo json_encode(['success' => false, 'error' => 'Unexpected response from Gemini API. Please try again.']);
exit;
}
// Extract the AI-generated text from the response
$aiText = $responseData['candidates'][0]['content']['parts'][0]['text'];
// Parse the JSON array of recommendations
$recommendations = json_decode($aiText, true);
if (!$recommendations || !is_array($recommendations)) {
echo json_encode([
'success' => false,
'error' => 'AI returned unexpected format. Raw response: ' . substr($aiText, 0, 200)
]);
exit;
}
// Return the recommendations to the frontend
echo json_encode(['success' => true, 'reply' => $recommendations]);
?>
What does this code do?
- responseMimeType: 'application/json' tells Gemini to return pure JSON instead of freeform text. This drastically reduces parsing failures.
- The x-goog-api-key header is how Gemini authenticates your request. The key comes from your environment variable.
- The response lives at candidates[0].content.parts[0].text in Gemini's response structure. That's where the generated text always appears.
- Three layers of error handling catch network failures, API errors, and malformed AI responses. Each returns a specific error message the frontend can display.
- Save chat.php.
- Check your file has no syntax errors by running this command in your terminal:
php -l chat.php
You should see No syntax errors detected in chat.php in the output.
Seeing a syntax error?
- Make sure the EOT; line has no spaces before it. PHP heredoc closing tags must start at the very beginning of the line.
- Check that the file starts with <?php and ends with ?>.
- Verify you didn't accidentally delete a closing bracket } or semicolon when pasting code between sections.
Help me fix a PHP syntax error in my chat.php file for MY Buddy.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here's the complete chat.php file. Make sure your code matches this exactly.
<?php
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
echo json_encode(['success' => false, 'error' => 'Invalid request. Please send JSON data.']);
exit;
}
$requiredFields = ['location', 'mood', 'budget', 'timeOfDay', 'interest', 'preference'];
foreach ($requiredFields as $field) {
if (empty($input[$field])) {
echo json_encode(['success' => false, 'error' => "Missing required field: $field"]);
exit;
}
}
$apiKey = getenv('GEMINI_API_KEY');
if (!$apiKey) {
echo json_encode([
'success' => false,
'error' => 'GEMINI_API_KEY not set. Run: export GEMINI_API_KEY="your_key_here" before starting the server.'
]);
exit;
}
$location = $input['location'];
$mood = $input['mood'];
$budget = $input['budget'];
$timeOfDay = $input['timeOfDay'];
$interest = $input['interest'];
$preference = $input['preference'];
$prompt = <<<EOT
You are MY Buddy, a friendly Malaysian AI buddy. You speak in casual Malaysian tone - mix of English and Malay slang. You understand words like makan, lepak, pokai, healing, mamak, tapau, syok, boring gila.
A user needs recommendations. Here are their details:
- Location: $location
- Mood: $mood
- Budget: $budget
- Time of day: $timeOfDay
- Interest: $interest
- Preference style: $preference (Local = Malaysian style, Non-local = international options available in Malaysia, Mixed = both)
Give exactly 5 recommendations sorted from most suitable to least suitable.
Rules:
- Be budget-aware. Do not recommend things above their budget.
- Be time-aware. Do not recommend breakfast spots at night.
- Be location-aware. Recommend things near or in their location.
- If preference is "Local", focus on Malaysian-style recommendations.
- If preference is "Non-local", suggest international-style options available in Malaysia.
- If preference is "Mixed", include both local and non-local.
- Do NOT claim exact live events. For events, say things like "check nearby mall event boards, campus events, pasar malam schedule, or community pages first".
- Keep reasons short and casual in Malaysian buddy tone.
Return ONLY a JSON array with exactly 5 objects. Each object must have these fields:
- "title": recommendation name
- "category": one of "Food", "Activity", "Place", "Product", "Service"
- "estimatedBudget": budget range in RM (e.g., "RM10 - RM20")
- "reason": short reason why it fits (casual Malaysian tone, 1-2 sentences)
- "localVibe": one of "Local", "Non-local", "Mixed"
Return ONLY the JSON array. No markdown, no code blocks, no extra text.
EOT;
$url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent';
$requestBody = json_encode([
'contents' => [
[
'parts' => [
['text' => $prompt]
]
]
],
'generationConfig' => [
'responseMimeType' => 'application/json'
]
]);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $requestBody);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'x-goog-api-key: ' . $apiKey
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($curlError) {
echo json_encode(['success' => false, 'error' => 'Connection error: ' . $curlError]);
exit;
}
if ($httpCode !== 200) {
echo json_encode(['success' => false, 'error' => 'Gemini API error (HTTP ' . $httpCode . '). Check your API key and try again.']);
exit;
}
$responseData = json_decode($response, true);
if (!$responseData || !isset($responseData['candidates'][0]['content']['parts'][0]['text'])) {
echo json_encode(['success' => false, 'error' => 'Unexpected response from Gemini API. Please try again.']);
exit;
}
$aiText = $responseData['candidates'][0]['content']['parts'][0]['text'];
$recommendations = json_decode($aiText, true);
if (!$recommendations || !is_array($recommendations)) {
echo json_encode([
'success' => false,
'error' => 'AI returned unexpected format. Raw response: ' . substr($aiText, 0, 200)
]);
exit;
}
echo json_encode(['success' => true, 'reply' => $recommendations]);
?>
Your backend proxy is complete. It validates input, builds a Malaysian-flavored prompt, calls Gemini, and returns structured recommendations. Next up, you'll build the frontend that users actually see and interact with.
Build the Frontend Page
Your backend proxy is ready to receive requests and talk to Gemini. Now you need something for users to actually interact with.
In this step, you will build the frontend that collects user input and displays results. The page has a hero section introducing MY Buddy, a form where users describe what they want, and a results area where recommendations appear as styled cards.
In this step, get ready to:
- Create the HTML page with a form for user input and a section for AI results.
- Style the page with a modern Malaysian-themed CSS design.
Create the HTML page structure
The page needs three main areas. A hero section introduces the app and sets the tone. A form collects 6 pieces of information from the user (location, mood, budget, time, interest, and preference). A results container sits empty until JavaScript fills it with recommendation cards.
- Create a new file called index.php inside your my-buddy folder and paste the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MY Buddy - Hyperlocal Malaysia AI Buddy</title>
<!-- Link to our stylesheet -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<!-- Hero section introduces the app -->
<header class="hero">
<h1>🇲🇾 MY Buddy</h1>
<p class="tagline">Your hyperlocal Malaysia AI buddy. Nak makan apa? Nak pergi mana? Let ME help you decide!</p>
<p class="description">Tell me where you are, how you feel, and what you want. I'll give you 5 solid recommendations like a friend who knows Malaysia inside out.</p>
</header>
<!-- Form section collects user input -->
<section class="form-section">
<form id="buddy-form">
<div class="form-grid">
<div class="form-group">
<label for="location">📍 Location in Malaysia</label>
<input type="text" id="location" name="location" placeholder="e.g., Petaling Jaya, Penang, JB...">
</div>
<div class="form-group">
<label for="mood">😊 Mood</label>
<input type="text" id="mood" name="mood" placeholder="e.g., hungry, bored, stressed, adventurous...">
</div>
<div class="form-group">
<label for="budget">💰 Budget</label>
<input type="text" id="budget" name="budget" placeholder="e.g., RM20, RM50, RM100...">
</div>
<div class="form-group">
<label for="timeOfDay">🕐 Time of Day</label>
<select id="timeOfDay" name="timeOfDay">
<option value="">Select time...</option>
<option value="morning">Morning (6am - 12pm)</option>
<option value="afternoon">Afternoon (12pm - 5pm)</option>
<option value="evening">Evening (5pm - 9pm)</option>
<option value="night">Night (9pm - 12am)</option>
<option value="late night">Late Night (12am - 6am)</option>
</select>
</div>
<div class="form-group">
<label for="interest">🎯 Interest</label>
<select id="interest" name="interest">
<option value="">Select interest...</option>
<option value="Food">Food</option>
<option value="Activities">Activities</option>
<option value="Places">Places</option>
<option value="Products">Products</option>
<option value="Services">Services</option>
</select>
</div>
<div class="form-group">
<label for="preference">🌏 Recommendation Style</label>
<select id="preference" name="preference">
<option value="">Select style...</option>
<option value="Local">Local (Malaysian style)</option>
<option value="Non-local">Non-local (International)</option>
<option value="Mixed">Mixed (Both!)</option>
</select>
</div>
</div>
<!-- Submit button -->
<div class="button-group">
<button type="submit" id="submit-btn">🚀 Get Recommendations</button>
</div>
</form>
</section>
<!-- Results section where AI recommendations appear -->
<section class="results-section" id="results-section">
<div id="results-container"></div>
</section>
</div>
<!-- JavaScript handles form submission and displays results -->
<script src="script.js"></script>
</body>
</html>
What does this code do?
The page is split into three logical sections:
- The hero header introduces MY Buddy with a tagline and description so users immediately know what the app does.
- The buddy-form uses a 2-column grid with 3 text inputs (location, mood, budget) and 3 dropdowns (time, interest, preference) to collect everything the AI needs.
- The results-container div starts empty. JavaScript fills it with recommendation cards after the API responds.
Why is it index.php instead of index.html?
Even though this file contains only HTML, using the .php extension means the PHP built-in server will serve it as the default page when you visit localhost:8000. It also gives you the option to add PHP code later if needed.
- Save index.php.
You won't see a styled page yet. The next substep adds the CSS that brings the design to life.
Add the Malaysian-themed stylesheet
The visual design uses a purple gradient background to give the page energy. The form lives inside a white rounded card that floats above the gradient. On mobile, the 2-column form collapses to a single column.
- Create a new file called style.css inside your my-buddy folder and paste the page layout and form styles:
/* Reset default browser spacing */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Purple gradient background for the whole page */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
/* Center content with a max width */
.container {
max-width: 800px;
margin: 0 auto;
}
/* Hero section in white text */
.hero {
text-align: center;
padding: 40px 20px;
color: white;
}
.hero h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
}
.hero .tagline {
font-size: 1.2rem;
opacity: 0.95;
margin-bottom: 10px;
}
.hero .description {
font-size: 0.95rem;
opacity: 0.85;
max-width: 600px;
margin: 0 auto;
}
/* White card that holds the form */
.form-section {
background: white;
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
margin-bottom: 30px;
}
/* 2-column grid layout for form fields */
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 25px;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group label {
font-size: 0.9rem;
font-weight: 600;
color: #333;
margin-bottom: 6px;
}
/* Rounded inputs and selects with focus highlight */
.form-group input,
.form-group select {
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 0.95rem;
transition: border-color 0.3s ease;
outline: none;
}
.form-group input:focus,
.form-group select:focus {
border-color: #667eea;
}
.form-group input::placeholder {
color: #aaa;
}
What does this code do?
- The body gets a purple-to-violet diagonal gradient that fills the entire viewport.
- The .form-section creates a white rounded card with a large box shadow so the form appears to float above the background.
- CSS Grid with grid-template-columns: 1fr 1fr arranges the 6 form fields into 2 equal columns.
- Inputs highlight with a purple border on focus, giving clear visual feedback.
- Below the placeholder styles you just pasted, add the button, results, and responsive styles:
/* Center the submit button */
.button-group {
display: flex;
gap: 10px;
justify-content: center;
}
/* Gradient button with hover lift effect */
#submit-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 15px 40px;
font-size: 1.1rem;
font-weight: 600;
border-radius: 12px;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
#submit-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
}
#submit-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.results-section {
margin-top: 10px;
}
/* Pulsing loading text while waiting for API */
.loading-text {
text-align: center;
color: white;
font-size: 1.2rem;
padding: 30px;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Red error box for failed requests */
.error-message {
background: #ffe0e0;
color: #d32f2f;
padding: 20px;
border-radius: 12px;
text-align: center;
font-weight: 500;
}
/* Individual recommendation cards */
.recommendation-card {
background: white;
border-radius: 16px;
padding: 25px;
margin-bottom: 15px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
}
.recommendation-card:hover {
transform: translateY(-3px);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.card-title {
font-size: 1.15rem;
font-weight: 700;
color: #333;
}
/* Purple pill badge for category */
.card-category {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
}
.card-budget {
color: #4caf50;
font-weight: 600;
font-size: 0.95rem;
margin-bottom: 8px;
}
.card-reason {
color: #555;
font-size: 0.9rem;
line-height: 1.5;
margin-bottom: 10px;
}
.card-vibe {
display: inline-block;
background: #f0f0f0;
color: #666;
padding: 4px 10px;
border-radius: 8px;
font-size: 0.8rem;
font-weight: 500;
}
/* Collapse to single column on mobile */
@media (max-width: 600px) {
.form-grid {
grid-template-columns: 1fr;
}
.hero h1 {
font-size: 2rem;
}
.form-section {
padding: 20px;
}
.card-header {
flex-direction: column;
gap: 8px;
}
}
What does this code do?
- The submit button uses the same purple gradient as the page background, and lifts upward on hover with translateY(-2px).
- The .loading-text class has a pulsing opacity animation that plays while waiting for the API response.
- Each .recommendation-card is a white rounded card that lifts on hover. The category shows as a purple pill badge in the top right.
- The media query at the bottom collapses the 2-column form into 1 column on screens under 600px.
- Save style.css.
- Open index.php directly in your browser by double-clicking the file in Finder (macOS) or File Explorer (Windows).
- You should see a purple gradient background with a white form card containing 6 labeled fields and a purple "Get Recommendations" button.
Seeing unstyled plain text?
- Make sure style.css is in the same folder as index.php. The <link> tag references style.css by filename, so both files must be in the same directory.
- Check that the file is named exactly style.css (not styles.css or style.CSS). Filenames are case-sensitive.
- Try a hard refresh with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows) to clear cached styles.
Help me troubleshoot why my CSS styles are not loading.
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the complete index.php file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MY Buddy - Hyperlocal Malaysia AI Buddy</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<header class="hero">
<h1>🇲🇾 MY Buddy</h1>
<p class="tagline">Your hyperlocal Malaysia AI buddy. Nak makan apa? Nak pergi mana? Let ME help you decide!</p>
<p class="description">Tell me where you are, how you feel, and what you want. I'll give you 5 solid recommendations like a friend who knows Malaysia inside out.</p>
</header>
<section class="form-section">
<form id="buddy-form">
<div class="form-grid">
<div class="form-group">
<label for="location">📍 Location in Malaysia</label>
<input type="text" id="location" name="location" placeholder="e.g., Petaling Jaya, Penang, JB...">
</div>
<div class="form-group">
<label for="mood">😊 Mood</label>
<input type="text" id="mood" name="mood" placeholder="e.g., hungry, bored, stressed, adventurous...">
</div>
<div class="form-group">
<label for="budget">💰 Budget</label>
<input type="text" id="budget" name="budget" placeholder="e.g., RM20, RM50, RM100...">
</div>
<div class="form-group">
<label for="timeOfDay">🕐 Time of Day</label>
<select id="timeOfDay" name="timeOfDay">
<option value="">Select time...</option>
<option value="morning">Morning (6am - 12pm)</option>
<option value="afternoon">Afternoon (12pm - 5pm)</option>
<option value="evening">Evening (5pm - 9pm)</option>
<option value="night">Night (9pm - 12am)</option>
<option value="late night">Late Night (12am - 6am)</option>
</select>
</div>
<div class="form-group">
<label for="interest">🎯 Interest</label>
<select id="interest" name="interest">
<option value="">Select interest...</option>
<option value="Food">Food</option>
<option value="Activities">Activities</option>
<option value="Places">Places</option>
<option value="Products">Products</option>
<option value="Services">Services</option>
</select>
</div>
<div class="form-group">
<label for="preference">🌏 Recommendation Style</label>
<select id="preference" name="preference">
<option value="">Select style...</option>
<option value="Local">Local (Malaysian style)</option>
<option value="Non-local">Non-local (International)</option>
<option value="Mixed">Mixed (Both!)</option>
</select>
</div>
</div>
<div class="button-group">
<button type="submit" id="submit-btn">🚀 Get Recommendations</button>
</div>
</form>
</section>
<section class="results-section" id="results-section">
<div id="results-container"></div>
</section>
</div>
<script src="script.js"></script>
</body>
</html>
And here is the complete style.css file:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
}
.hero {
text-align: center;
padding: 40px 20px;
color: white;
}
.hero h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
}
.hero .tagline {
font-size: 1.2rem;
opacity: 0.95;
margin-bottom: 10px;
}
.hero .description {
font-size: 0.95rem;
opacity: 0.85;
max-width: 600px;
margin: 0 auto;
}
.form-section {
background: white;
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
margin-bottom: 30px;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 25px;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group label {
font-size: 0.9rem;
font-weight: 600;
color: #333;
margin-bottom: 6px;
}
.form-group input,
.form-group select {
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 0.95rem;
transition: border-color 0.3s ease;
outline: none;
}
.form-group input:focus,
.form-group select:focus {
border-color: #667eea;
}
.form-group input::placeholder {
color: #aaa;
}
.button-group {
display: flex;
gap: 10px;
justify-content: center;
}
#submit-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 15px 40px;
font-size: 1.1rem;
font-weight: 600;
border-radius: 12px;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
#submit-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
}
#submit-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.results-section {
margin-top: 10px;
}
.loading-text {
text-align: center;
color: white;
font-size: 1.2rem;
padding: 30px;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.error-message {
background: #ffe0e0;
color: #d32f2f;
padding: 20px;
border-radius: 12px;
text-align: center;
font-weight: 500;
}
.recommendation-card {
background: white;
border-radius: 16px;
padding: 25px;
margin-bottom: 15px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
}
.recommendation-card:hover {
transform: translateY(-3px);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.card-title {
font-size: 1.15rem;
font-weight: 700;
color: #333;
}
.card-category {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
}
.card-budget {
color: #4caf50;
font-weight: 600;
font-size: 0.95rem;
margin-bottom: 8px;
}
.card-reason {
color: #555;
font-size: 0.9rem;
line-height: 1.5;
margin-bottom: 10px;
}
.card-vibe {
display: inline-block;
background: #f0f0f0;
color: #666;
padding: 4px 10px;
border-radius: 8px;
font-size: 0.8rem;
font-weight: 500;
}
@media (max-width: 600px) {
.form-grid {
grid-template-columns: 1fr;
}
.hero h1 {
font-size: 2rem;
}
.form-section {
padding: 20px;
}
.card-header {
flex-direction: column;
gap: 8px;
}
}
Your page looks great, but the form does nothing when you click the button. Next up, you will add JavaScript that collects the form data, sends it to your PHP backend, and displays the AI recommendations as styled cards.
Add JavaScript Interactivity
Your frontend looks great, but right now the form does nothing when you click the button. The HTML and CSS are purely visual.
In this step, you'll write the JavaScript that connects your form to the PHP backend. When a user clicks Get Recommendations, your script will collect their inputs, send them to chat.php, and display the AI-generated recommendations as styled cards.
In this step, get ready to:
- Build a form handler that collects user input and validates all fields.
- Send form data to your PHP backend using fetch() and render AI results as cards.
- Add error handling and HTML sanitization for a polished user experience.
Create the form handler and API caller
The form needs a JavaScript function that intercepts the submit event, reads every field, and sends the data to your backend. This function also manages the button state and loading animation so the user knows something is happening.
- Create a new file called script.js inside your my-buddy folder.
- Add the form event listener and the main getRecommendations() function by pasting this code:
document.getElementById('buddy-form').addEventListener('submit', function(e) {
e.preventDefault();
getRecommendations();
});
function getRecommendations() {
// Read all form field values
const location = document.getElementById('location').value.trim();
const mood = document.getElementById('mood').value.trim();
const budget = document.getElementById('budget').value.trim();
const timeOfDay = document.getElementById('timeOfDay').value;
const interest = document.getElementById('interest').value;
const preference = document.getElementById('preference').value;
// Validate that no fields are empty
if (!location || !mood || !budget || !timeOfDay || !interest || !preference) {
showError('Eh, isi semua field dulu ya! All fields are required.');
return;
}
// Disable button and show loading state
const submitBtn = document.getElementById('submit-btn');
const resultsContainer = document.getElementById('results-container');
submitBtn.disabled = true;
submitBtn.textContent = '⏳ Thinking...';
resultsContainer.innerHTML = '<div class="loading-text">MY Buddy tengah fikir jap... 🤔</div>';
// Package form data as JSON
const data = {
location: location,
mood: mood,
budget: budget,
timeOfDay: timeOfDay,
interest: interest,
preference: preference
};
// Send POST request to PHP backend
fetch('chat.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(function(response) {
return response.json();
})
.then(function(result) {
if (result.success && result.reply) {
displayRecommendations(result.reply);
} else {
showError(result.error || 'Something went wrong. Try again!');
}
})
.catch(function(error) {
showError('Network error. Make sure the server is running! Error: ' + error.message);
})
.finally(function() {
submitBtn.disabled = false;
submitBtn.textContent = '🚀 Get Recommendations';
});
}
What does this code do?
This is the core engine of your app. Here is what each section handles:
- The event listener intercepts the form submit and calls getRecommendations() instead of reloading the page.
- The validation check ensures all six fields are filled before making the API call.
- Disabling the button and showing a loading message prevents duplicate submissions while waiting for the AI.
- fetch() sends the form data as JSON to chat.php, which acts as a proxy to the Gemini API.
- .finally() always re-enables the button, whether the request succeeded or failed.
You won't see a visible change yet. The next chunk adds the function that renders the AI results as cards.
Render recommendation cards
When the Gemini API returns recommendations, the app needs to turn that raw JSON array into visual cards the user can read. Each card shows the recommendation title, category, budget, reason, and local vibe.
- In script.js, add the displayRecommendations() function below your existing code:
function displayRecommendations(recommendations) {
const container = document.getElementById('results-container');
let html = '';
// Build a card for each recommendation
recommendations.forEach(function(rec, index) {
html += '<div class="recommendation-card">';
html += '<div class="card-header">';
html += '<span class="card-title">' + (index + 1) + '. ' + escapeHtml(rec.title) + '</span>';
html += '<span class="card-category">' + escapeHtml(rec.category) + '</span>';
html += '</div>';
html += '<div class="card-budget">💰 ' + escapeHtml(rec.estimatedBudget) + '</div>';
html += '<div class="card-reason">' + escapeHtml(rec.reason) + '</div>';
html += '<span class="card-vibe">🌏 ' + escapeHtml(rec.localVibe) + '</span>';
html += '</div>';
});
// Insert all cards into the results container
container.innerHTML = html;
}
What does this code do?
This function loops through the 5 recommendations returned by Gemini and builds an HTML card for each one. Each card uses the CSS classes you already defined in style.css (like .recommendation-card and .card-title).
Every piece of text goes through escapeHtml() before being inserted into the page. This prevents any malicious HTML from the AI response being executed in the browser.
You won't see a change yet. The next chunk adds the error display and the escapeHtml() function that both chunks rely on.
Add error display and HTML sanitization
If the API key is missing, the server is down, or Gemini returns an unexpected response, the user needs to see a clear error message instead of a blank screen. You also need a utility function that sanitizes text before inserting it into the page.
- In script.js, add the showError() and escapeHtml() functions below your existing code:
function showError(message) {
// Display error message in the results area
const container = document.getElementById('results-container');
container.innerHTML = '<div class="error-message">⚠️ ' + escapeHtml(message) + '</div>';
}
function escapeHtml(text) {
// Sanitize text to prevent XSS attacks
if (!text) return '';
var div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
}
What does this code do?
showError() displays a styled error message in the results container using the .error-message CSS class you defined earlier.
escapeHtml() uses a clever DOM trick to sanitize text. It creates a temporary div, inserts the text as a text node (which automatically escapes special characters), then reads back the safe HTML. This prevents cross-site scripting if the AI response contains unexpected HTML tags.
- Save script.js.
- Confirm the file exists by running:
ls
- You should see chat.php, index.php, script.js, and style.css all listed in the output.
Don't see script.js in the output?
- Make sure you created the file inside the my-buddy folder, not somewhere else on your computer.
- Check that the file is named exactly script.js (lowercase, no spaces).
- Make sure your terminal is in the my-buddy directory. Run pwd to confirm.
- Why isn't my script.js file showing up in the project folder?
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on to the next step.
ⓧ I'd like to double check the full code
Here is the complete script.js file. Make sure yours matches exactly:
document.getElementById('buddy-form').addEventListener('submit', function(e) {
e.preventDefault();
getRecommendations();
});
function getRecommendations() {
const location = document.getElementById('location').value.trim();
const mood = document.getElementById('mood').value.trim();
const budget = document.getElementById('budget').value.trim();
const timeOfDay = document.getElementById('timeOfDay').value;
const interest = document.getElementById('interest').value;
const preference = document.getElementById('preference').value;
if (!location || !mood || !budget || !timeOfDay || !interest || !preference) {
showError('Eh, isi semua field dulu ya! All fields are required.');
return;
}
const submitBtn = document.getElementById('submit-btn');
const resultsContainer = document.getElementById('results-container');
submitBtn.disabled = true;
submitBtn.textContent = '⏳ Thinking...';
resultsContainer.innerHTML = '<div class="loading-text">MY Buddy tengah fikir jap... 🤔</div>';
const data = {
location: location,
mood: mood,
budget: budget,
timeOfDay: timeOfDay,
interest: interest,
preference: preference
};
fetch('chat.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(function(response) {
return response.json();
})
.then(function(result) {
if (result.success && result.reply) {
displayRecommendations(result.reply);
} else {
showError(result.error || 'Something went wrong. Try again!');
}
})
.catch(function(error) {
showError('Network error. Make sure the server is running! Error: ' + error.message);
})
.finally(function() {
submitBtn.disabled = false;
submitBtn.textContent = '🚀 Get Recommendations';
});
}
function displayRecommendations(recommendations) {
const container = document.getElementById('results-container');
let html = '';
recommendations.forEach(function(rec, index) {
html += '<div class="recommendation-card">';
html += '<div class="card-header">';
html += '<span class="card-title">' + (index + 1) + '. ' + escapeHtml(rec.title) + '</span>';
html += '<span class="card-category">' + escapeHtml(rec.category) + '</span>';
html += '</div>';
html += '<div class="card-budget">💰 ' + escapeHtml(rec.estimatedBudget) + '</div>';
html += '<div class="card-reason">' + escapeHtml(rec.reason) + '</div>';
html += '<span class="card-vibe">🌏 ' + escapeHtml(rec.localVibe) + '</span>';
html += '</div>';
});
container.innerHTML = html;
}
function showError(message) {
const container = document.getElementById('results-container');
container.innerHTML = '<div class="error-message">⚠️ ' + escapeHtml(message) + '</div>';
}
function escapeHtml(text) {
if (!text) return '';
var div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
}
Why use a PHP proxy instead of calling the API directly from JavaScript?
Your JavaScript sends data to chat.php, which then calls the Gemini API. This extra step exists for security. If you called the Gemini API directly from the browser, your API key would be visible to anyone who opens DevTools.
The PHP backend keeps your key hidden on the server. The browser never sees it.
All four project files are now in place. Next up, you'll start the PHP server and see MY Buddy in action for the first time.
Test Your App Locally
You have all four files ready in your my-buddy folder. The frontend, styling, JavaScript logic, and PHP backend are all in place.
Now it is time to see everything work together. You will start a local PHP server, submit the form with real data, and watch Gemini respond like a Malaysian buddy.
In this step, get ready to:
- Start the PHP built-in development server.
- Submit the form with test data and verify 5 recommendation cards appear.
- Check error handling works when the API key is missing.
Start the PHP development server
PHP has a built-in web server designed for local development. It serves your files without needing to install Apache or XAMPP.
- Make sure you are in the same terminal session where you set GEMINI_API_KEY earlier. The API key only exists in that session.
- Navigate into your project folder by running this command:
cd ~/Desktop/my-buddy
- Start the PHP built-in server by running this command:
php -S localhost:8000
What does this command do?
The -S flag tells PHP to start its built-in server. localhost:8000 means the server listens on port 8000 of your local machine.
PHP serves all the files in the current directory. Since you are inside my-buddy/, it will serve index.php as the homepage.
You should see output like PHP Development Server (http://localhost:8000) started in your terminal.
Why is my terminal stuck?
This is normal. The server is running and waiting for requests. Your terminal will show each request as it comes in. Do not close this terminal window or press Ctrl+C while testing.
Seeing 'Address already in use'?
Another process is using port 8000. Try a different port by running php -S localhost:8080 instead. If you previously started the server and forgot to stop it, close that terminal window first.
help me fix 'Address already in use' when starting my PHP server
Fill in the form and get recommendations
With the server running, you can open the app in your browser and try a real request to the Gemini API.
- Open your browser (Chrome or Safari).
- Type http://localhost:8000 in the address bar and press Enter.
You should see the MY Buddy page with a purple gradient background, a hero section, and the input form.
- In the Location in Malaysia field, type Petaling Jaya.
- In the Mood field, type hungry.
- In the Budget field, type RM20.
- From the Time of Day dropdown, select Evening (5pm - 9pm).
- From the Interest dropdown, select Food.
- From the Recommendation Style dropdown, select Local (Malaysian style).
- Click the Get Recommendations button.
The button changes to "Thinking..." and a loading message appears below. After a few seconds, 5 recommendation cards should appear.
What should I see?
Each recommendation card shows a title, category badge (like "Food"), an estimated budget in RM, a short reason in casual Malaysian tone, and a local vibe tag.
The recommendations are personalized to your inputs. Since you chose "hungry" with a RM20 budget in Petaling Jaya during the evening, you should see affordable food spots nearby.
Not seeing any cards?
Check the terminal window running your PHP server. If you see a red error, it usually means the API key is not set or has expired.
Open your browser Developer Tools by pressing Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows). Click the Console tab to see any JavaScript errors.
help me debug why MY Buddy is not showing recommendation cards
Verify error handling works
A good app tells you what went wrong when something fails. Your PHP backend has built-in error messages for missing fields and missing API keys.
- Go back to your browser and refresh the page at http://localhost:8000.
- Leave all fields empty.
- Click the Get Recommendations button.
You should see a red error box with the message "Eh, isi semua field dulu ya! All fields are required." This validation runs in JavaScript before any request hits the server.
- Now check your terminal output. Switch to the terminal window running the PHP server.
- Look for lines showing each request your browser made. You should see entries with status codes like 200 for successful requests.
What do the terminal logs mean?
Each line in the terminal shows one HTTP request. A 200 status means success. A 404 means a file was not found (usually a favicon request, which is harmless).
If you see the POST request to chat.php returning 200, the Gemini API call succeeded.
Test the API key error message
To verify the API key error handling, stop the server with Ctrl+C, then start it again without the environment variable in a new terminal tab. The app should show: "GEMINI_API_KEY not set. Run: export GEMINI_API_KEY=your_key_here before starting the server."
After testing, go back to your original terminal (where the key is set) and restart with php -S localhost:8000.
Secret mission
Add a Surprise Me Button
Your form works great, but what if users don't know what to ask for? Add a "Surprise Me!" button that randomly fills the form with fun Malaysian scenarios and auto-submits for instant recommendations.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
This project runs entirely locally on your machine, so there are no ongoing cloud costs. The Gemini API free tier does not charge you for idle time.
Resources you used:
- PHP built-in server (running process on localhost:8000).
- Project folder (my-buddy) with 5 files: index.php, style.css, script.js, chat.php, README.md.
- Gemini API key (created in Google AI Studio).
- Environment variable (GEMINI_API_KEY, session-only, disappears when you close the terminal).
Keep everything running
No action needed. Choose this if you are still actively building or want to demo MY Buddy again soon.
- Leave your terminal open with the PHP server running.
- Visit http://localhost:8000 any time to use the app.
- Remember that your GEMINI_API_KEY environment variable only lasts for the current terminal session. If you close and reopen your terminal, you will need to set it again.
Pause - I'll come back to this later
Shut down the running server to free up the terminal. Your project files stay on disk and are ready to restart any time.
- In the terminal where the PHP server is running, press Ctrl+C to stop the server.
- Your my-buddy folder and all files remain on your Desktop.
- To restart later, open a new terminal, navigate to the project folder, set your API key again with export GEMINI_API_KEY="your_key", and run php -S localhost:8000.
Delete - I don't want to use this again
Remove all project files and optionally delete the API key. This frees up disk space and removes any traces of the project.
- In the terminal where the PHP server is running, press Ctrl+C to stop the server.
- Delete the project folder by running rm -rf ~/Desktop/my-buddy (macOS/Linux) or removing the my-buddy folder from your Desktop in File Explorer (Windows).
- To delete your Gemini API key, go to https://aistudio.google.com/apikey, find the key you created, and click the delete icon next to it.
- The GEMINI_API_KEY environment variable disappears automatically when you close the terminal. No extra action needed.
That's a Wrap!
That's a Wrap!
Tahniah! You just built MY Buddy, a working AI-powered hyperlocal recommendation app for Malaysia.
You've learned how to:
- Built a PHP backend proxy that securely calls the Gemini 2.5 Flash API without exposing the API key to the browser.
- Connected frontend JavaScript to backend PHP using fetch() and JSON to display AI-generated recommendations as styled cards.
- Engineered a prompt that makes Gemini respond like a friendly Malaysian buddy with budget-aware, location-aware, and time-aware recommendations.
- Secret Mission: Added a "Surprise Me!" feature that randomly fills the form with fun Malaysian scenarios and auto-submits for instant recommendations.
Ready to quiz yourself?