fix: back to top button, side navigation

This commit is contained in:
ssbg04 committed 2026-06-05 23:10:11 +08:00
1 parent 762a2d0f66
commit 9eeb82ed89
6 files changed
+744 -186

No files matched your search

+25 -10
View File
@@ -122,30 +122,44 @@ $systemPrompt .= "4. Do not make up facts or external projects not listed in the
$systemPrompt .= "5. Format your output using simple markdown (like bolding or bullet points) to make it readable.\n"; $systemPrompt .= "5. Format your output using simple markdown (like bolding or bullet points) to make it readable.\n";
// Map chat history // Map chat history
// Map chat history and prepend system instructions to the first user message
$contents = []; $contents = [];
if (isset($inputData['history']) && is_array($inputData['history'])) { $firstMsgPrepended = false;
foreach ($inputData['history'] as $msg) {
if (isset($inputData['history']) && is_array($inputData['history']) && !empty($inputData['history'])) {
foreach ($inputData['history'] as $index => $msg) {
if (isset($msg['role']) && isset($msg['text'])) { if (isset($msg['role']) && isset($msg['text'])) {
$role = ($msg['role'] === 'user') ? 'user' : 'model'; $role = ($msg['role'] === 'user') ? 'user' : 'model';
$text = $msg['text'];
// Prepend system prompt to the first user message in history
if (!$firstMsgPrepended && $role === 'user') {
$text = "System Context & Instructions:\n" . $systemPrompt . "\n\nUser Message: " . $text;
$firstMsgPrepended = true;
}
$contents[] = [ $contents[] = [
'role' => $role, 'role' => $role,
'parts' => [['text' => $msg['text']]] 'parts' => [['text' => $text]]
]; ];
} }
} }
} }
// Add the current user message
// Add the current user message (prepend system instructions if history was empty)
$currentUserText = $userMessage;
if (!$firstMsgPrepended) {
$currentUserText = "System Context & Instructions:\n" . $systemPrompt . "\n\nUser Message: " . $userMessage;
}
$contents[] = [ $contents[] = [
'role' => 'user', 'role' => 'user',
'parts' => [['text' => $userMessage]] 'parts' => [['text' => $currentUserText]]
]; ];
// Setup request body // Setup request body without systemInstruction key
$requestPayload = [ $requestPayload = [
'contents' => $contents, 'contents' => $contents,
'systemInstruction' => [
'parts' => [['text' => $systemPrompt]]
],
'generationConfig' => [ 'generationConfig' => [
'temperature' => 0.7, 'temperature' => 0.7,
'maxOutputTokens' => 800 'maxOutputTokens' => 800
@@ -153,7 +167,7 @@ $requestPayload = [
]; ];
// Call Gemini API via cURL // Call Gemini API via cURL
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=" . urlencode($apiKey); $url = "https://generativelanguage.googleapis.com/v1/models/gemini-3.5-flash:generateContent?key=" . urlencode($apiKey);
$ch = curl_init($url); $ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
@@ -161,6 +175,7 @@ curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestPayload)); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestPayload));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_TIMEOUT, 15); curl_setopt($ch, CURLOPT_TIMEOUT, 15);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch); $response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+23
View File
@@ -0,0 +1,23 @@
<?php
require_once __DIR__ . '/../config/db.php';
$apiKey = getenv('GEMINI_API_KEY') ?: ($_ENV['GEMINI_API_KEY'] ?? '');
if (empty($apiKey)) {
echo "API Key is missing in .env";
exit;
}
$url = "https://generativelanguage.googleapis.com/v1/models?key=" . urlencode($apiKey);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP Code: $httpCode\n\n";
echo "Response:\n";
echo $response;
+500 -143
View File
File diff suppressed because it is too large. Load diff
+71 -16
View File
@@ -38,7 +38,7 @@ document.addEventListener("DOMContentLoaded", () => {
chatHistory.push({ role: "user", text: message }); chatHistory.push({ role: "user", text: message });
inputField.value = ""; inputField.value = "";
// Cap history to last 10 messages to save context limits // Cap history to last 10 messages
if (chatHistory.length > 10) { if (chatHistory.length > 10) {
chatHistory = chatHistory.slice(-10); chatHistory = chatHistory.slice(-10);
} }
@@ -47,6 +47,9 @@ document.addEventListener("DOMContentLoaded", () => {
const typingIndicator = showTypingIndicator(); const typingIndicator = showTypingIndicator();
isWaiting = true; isWaiting = true;
// Try backend proxy first, fallback to client-side if it fails
let replySuccess = false;
try { try {
const response = await fetch("api/chat.php", { const response = await fetch("api/chat.php", {
method: "POST", method: "POST",
@@ -55,28 +58,85 @@ document.addEventListener("DOMContentLoaded", () => {
}, },
body: JSON.stringify({ body: JSON.stringify({
message: message, message: message,
history: chatHistory.slice(0, -1) // pass history excluding the current user message (which is passed in "message" parameter) history: chatHistory.slice(0, -1)
}) })
}); });
const data = await response.json(); const data = await response.json();
removeTypingIndicator(typingIndicator);
if (response.ok && data.reply) { if (response.ok && data.reply) {
removeTypingIndicator(typingIndicator);
const botReply = data.reply; const botReply = data.reply;
addMessage(botReply, "bot-msg"); addMessage(botReply, "bot-msg");
chatHistory.push({ role: "model", text: botReply }); chatHistory.push({ role: "model", text: botReply });
} else { replySuccess = true;
const errMsg = data.error || "Sorry, I encountered an error. Please try again.";
addMessage(errMsg, "bot-msg error-msg");
} }
} catch (error) { } catch (error) {
removeTypingIndicator(typingIndicator); console.warn("Backend API call failed, attempting client-side fallback. Details:", error);
addMessage("Unable to connect to assistant. Please check your internet connection.", "bot-msg error-msg");
console.error("Chatbot Error:", error);
} finally {
isWaiting = false;
} }
// CLIENT-SIDE FALLBACK (e.g. for InfinityFree or local cert errors)
if (!replySuccess) {
const config = window.chatbotConfig;
if (config && config.apiKey && config.systemPrompt) {
try {
// Structure payload for Gemini client-side and inject systemInstructions in first user message
let firstMsgPrepended = false;
const contents = chatHistory.map(msg => {
let text = msg.text;
const role = msg.role === 'user' ? 'user' : 'model';
if (!firstMsgPrepended && role === 'user') {
text = `System Context & Instructions:\n${config.systemPrompt}\n\nUser Message: ${text}`;
firstMsgPrepended = true;
}
return {
role: role,
parts: [{ text: text }]
};
});
const requestPayload = {
contents: contents,
generationConfig: {
temperature: 0.7,
maxOutputTokens: 800
}
};
const url = `https://generativelanguage.googleapis.com/v1/models/gemini-3.5-flash:generateContent?key=${encodeURIComponent(config.apiKey)}`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(requestPayload)
});
const data = await response.json();
removeTypingIndicator(typingIndicator);
if (response.ok && data.candidates && data.candidates[0].content.parts[0].text) {
const botReply = data.candidates[0].content.parts[0].text;
addMessage(botReply, "bot-msg");
chatHistory.push({ role: "model", text: botReply });
replySuccess = true;
} else {
const errMsg = data.error?.message || "Sorry, I encountered an error. Please try again.";
addMessage(errMsg, "bot-msg error-msg");
}
} catch (fallbackError) {
removeTypingIndicator(typingIndicator);
addMessage("Unable to connect to assistant. Please check your internet connection.", "bot-msg error-msg");
console.error("Direct API Fallback Error:", fallbackError);
}
} else {
removeTypingIndicator(typingIndicator);
addMessage("API request blocked by host firewall and fallback config is missing.", "bot-msg error-msg");
}
}
isWaiting = false;
}); });
// Helper: Add message bubble to logs // Helper: Add message bubble to logs
@@ -87,7 +147,6 @@ document.addEventListener("DOMContentLoaded", () => {
const bubbleDiv = document.createElement("div"); const bubbleDiv = document.createElement("div");
bubbleDiv.className = "msg-bubble"; bubbleDiv.className = "msg-bubble";
// Format simple Markdown conversions (bold, bullet points, line breaks)
bubbleDiv.innerHTML = formatMessageText(text); bubbleDiv.innerHTML = formatMessageText(text);
messageDiv.appendChild(bubbleDiv); messageDiv.appendChild(bubbleDiv);
@@ -125,16 +184,13 @@ document.addEventListener("DOMContentLoaded", () => {
// Helper: Simple Markdown Formatter // Helper: Simple Markdown Formatter
function formatMessageText(text) { function formatMessageText(text) {
// Safe escaping (minimal to allow HTML format helper if needed, but safe)
let escaped = text let escaped = text
.replace(/&/g, "&amp;") .replace(/&/g, "&amp;")
.replace(/</g, "&lt;") .replace(/</g, "&lt;")
.replace(/>/g, "&gt;"); .replace(/>/g, "&gt;");
// Bold (**bold**)
escaped = escaped.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>"); escaped = escaped.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
// Bullet points starting with "- " or "* "
const lines = escaped.split("\n"); const lines = escaped.split("\n");
let inList = false; let inList = false;
let formatted = ""; let formatted = "";
@@ -160,7 +216,6 @@ document.addEventListener("DOMContentLoaded", () => {
formatted += "</ul>"; formatted += "</ul>";
} }
// Clean up double linebreaks
return formatted.replace(/(<br>\s*){3,}/g, "<br><br>"); return formatted.replace(/(<br>\s*){3,}/g, "<br><br>");
} }
}); });
+93
View File
@@ -1,3 +1,96 @@
<?php
// Since this is included in index.php, $mysqli is already defined and loaded.
// Let's retrieve context safely. If for some reason $mysqli is not available, we have fallback strings.
$settings = null;
$projects = [];
$certs = [];
$articles = [];
if (isset($mysqli) && !$mysqli->connect_error) {
// 1. Settings / About
$settingsQuery = $mysqli->query("SELECT * FROM settings WHERE id = 1");
if ($settingsQuery) {
$settings = $settingsQuery->fetch_assoc();
}
// 2. Projects
$projectsQuery = $mysqli->query("SELECT title, des, techstack, url FROM projects");
if ($projectsQuery) {
while ($row = $projectsQuery->fetch_assoc()) {
$projects[] = $row;
}
}
// 3. Certifications
$certsQuery = $mysqli->query("SELECT title, issuer, date_issued FROM certifications");
if ($certsQuery) {
while ($row = $certsQuery->fetch_assoc()) {
$certs[] = $row;
}
}
// 4. Articles
$articlesQuery = $mysqli->query("SELECT title, excerpt, date_published, tag, url FROM articles");
if ($articlesQuery) {
while ($row = $articlesQuery->fetch_assoc()) {
$articles[] = $row;
}
}
}
// Build system instructions
$systemPrompt = "You are a friendly, professional, and intelligent AI assistant built into the personal portfolio website of Cris Charles Garcia.\n";
$systemPrompt .= "Your role is to represent Cris, assist recruiters, potential clients, and visitors, and answer questions about his background, experience, skills, and projects.\n\n";
$systemPrompt .= "Cris's Profile & Biography:\n";
if ($settings) {
$systemPrompt .= "- Bio Paragraph 1: " . $settings['about_text_1'] . "\n";
$systemPrompt .= "- Bio Paragraph 2: " . $settings['about_text_2'] . "\n";
$systemPrompt .= "- Years coding: " . $settings['years_coding'] . "\n";
$systemPrompt .= "- Projects completed: " . $settings['projects_count'] . "\n";
$systemPrompt .= "- Certifications: " . $settings['certifications_count'] . "\n";
$systemPrompt .= "- Github: " . $settings['github_url'] . "\n";
$systemPrompt .= "- LinkedIn: " . $settings['linkedin_url'] . "\n";
$systemPrompt .= "- Instagram: " . $settings['instagram_url'] . "\n";
$systemPrompt .= "- Facebook: " . $settings['facebook_url'] . "\n";
$systemPrompt .= "- TikTok: " . $settings['tiktok_url'] . "\n";
} else {
$systemPrompt .= "- Cris is a senior IT student majoring in Software Development based in Laguna, Philippines.\n";
}
$systemPrompt .= "\nCris's Projects:\n";
foreach ($projects as $p) {
$systemPrompt .= "- **" . $p['title'] . "**: " . $p['des'] . " (Tech: " . $p['techstack'] . "). Link: " . $p['url'] . "\n";
}
$systemPrompt .= "\nCris's Certifications:\n";
foreach ($certs as $c) {
$systemPrompt .= "- " . $c['title'] . " by " . $c['issuer'] . " (Issued: " . $c['date_issued'] . ")\n";
}
$systemPrompt .= "\nCris's Latest Articles/Blog Posts:\n";
foreach ($articles as $a) {
$systemPrompt .= "- \"" . $a['title'] . "\": " . $a['excerpt'] . " [Tag: " . $a['tag'] . ", Date: " . $a['date_published'] . ", Link: " . $a['url'] . "]\n";
}
$systemPrompt .= "\nGuidelines for your responses:\n";
$systemPrompt .= "1. Respond in first-person (e.g., \"I am a senior IT student...\" or \"In my project UniMarket...\") or as a dedicated assistant of Cris. Represent Cris accurately, warmly, and professionally.\n";
$systemPrompt .= "2. Keep responses brief, engaging, and directly related to the question. Recruiters appreciate concise answers.\n";
$systemPrompt .= "3. If asked about contact options or hiring, point them to his contact form at the bottom of the page or his professional links (Github, LinkedIn).\n";
$systemPrompt .= "4. Do not make up facts or external projects not listed in the context. If you don't know the answer, politely invite the user to contact Cris directly.\n";
$systemPrompt .= "5. Format your output using simple markdown (like bolding or bullet points) to make it readable.\n";
$apiKey = getenv('GEMINI_API_KEY') ?: ($_ENV['GEMINI_API_KEY'] ?? '');
?>
<!-- Pass DB configuration context directly to javascript to allow direct API requests on restricted hosting environments (like InfinityFree) -->
<script>
window.chatbotConfig = {
apiKey: <?php echo json_encode($apiKey); ?>,
systemPrompt: <?php echo json_encode($systemPrompt); ?>
};
</script>
<!-- Chatbot Widget HTML --> <!-- Chatbot Widget HTML -->
<div id="portfolio-chatbot-widget"> <div id="portfolio-chatbot-widget">
<!-- Floating Chat Toggle Bubble --> <!-- Floating Chat Toggle Bubble -->
+32 -17
View File
@@ -11,6 +11,7 @@
</div> </div>
<div class="nav-links" id="nav-links" role="navigation"> <div class="nav-links" id="nav-links" role="navigation">
<button class="nav-close-btn" id="nav-close-btn" aria-label="Close menu">×</button>
<a href="index.php#about" class="nav-item">About</a> <a href="index.php#about" class="nav-item">About</a>
<a href="index.php#projects" class="nav-item">Work</a> <a href="index.php#projects" class="nav-item">Work</a>
<a href="index.php#certificates" class="nav-item">Certs</a> <a href="index.php#certificates" class="nav-item">Certs</a>
@@ -23,6 +24,9 @@
</div> </div>
</nav> </nav>
<!-- Nav overlay backdrop -->
<div class="nav-overlay" id="nav-overlay"></div>
<!-- Shared Navigation Script --> <!-- Shared Navigation Script -->
<script> <script>
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
@@ -36,39 +40,50 @@ document.addEventListener("DOMContentLoaded", () => {
} }
// 2. Mobile Hamburger Menu // 2. Mobile Hamburger Menu
const hamburger = document.getElementById("hamburger"); const hamburger = document.getElementById("hamburger");
const navLinks = document.getElementById("nav-links"); const navLinks = document.getElementById("nav-links");
const navItems = document.querySelectorAll(".nav-item"); const navCloseBtn = document.getElementById("nav-close-btn");
const navOverlay = document.getElementById("nav-overlay");
const navItems = document.querySelectorAll(".nav-item");
if (hamburger && navLinks) { if (hamburger && navLinks) {
function openMenu() {
hamburger.classList.add("active");
navLinks.classList.add("active");
if (navOverlay) navOverlay.classList.add("active");
hamburger.setAttribute("aria-expanded", "true");
document.body.style.overflow = "hidden";
}
function closeMenu() { function closeMenu() {
hamburger.classList.remove("active"); hamburger.classList.remove("active");
navLinks.classList.remove("active"); navLinks.classList.remove("active");
if (navOverlay) navOverlay.classList.remove("active");
hamburger.setAttribute("aria-expanded", "false"); hamburger.setAttribute("aria-expanded", "false");
document.body.style.overflow = "";
} }
hamburger.addEventListener("click", (e) => { hamburger.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
const isOpen = hamburger.classList.toggle("active"); if (hamburger.classList.contains("active")) {
navLinks.classList.toggle("active"); closeMenu();
hamburger.setAttribute("aria-expanded", isOpen); } else {
openMenu();
}
}); });
// Close via X button
if (navCloseBtn) navCloseBtn.addEventListener("click", closeMenu);
// Close via overlay click
if (navOverlay) navOverlay.addEventListener("click", closeMenu);
// Close on nav link click // Close on nav link click
navItems.forEach(item => item.addEventListener("click", closeMenu)); navItems.forEach(item => item.addEventListener("click", closeMenu));
// Close when clicking outside the menu
document.addEventListener("click", (e) => {
if (navLinks.classList.contains("active") && !navLinks.contains(e.target) && !hamburger.contains(e.target)) {
closeMenu();
}
});
// Close on scroll // Close on scroll
window.addEventListener('scroll', () => { window.addEventListener("scroll", () => {
if (navLinks.classList.contains('active')) { if (navLinks.classList.contains("active")) closeMenu();
closeMenu();
}
}); });
} }
}); });