Files
portfoliowai/assets/js/chat.js
T

222 lines
8.2 KiB
JavaScript

document.addEventListener("DOMContentLoaded", () => {
const toggleBtn = document.getElementById("chatbot-toggle-btn");
const closeBtn = document.getElementById("chatbot-close-btn");
const chatWindow = document.getElementById("chatbot-window");
const chatForm = document.getElementById("chatbot-input-form");
const inputField = document.getElementById("chatbot-input-field");
const messagesContainer = document.getElementById("chatbot-messages");
const pulseDot = document.querySelector(".pulse-notification-dot");
let chatHistory = [];
let isWaiting = false;
// Toggle Chat Window
toggleBtn.addEventListener("click", () => {
chatWindow.classList.toggle("hidden");
chatWindow.classList.toggle("active");
if (chatWindow.classList.contains("active")) {
inputField.focus();
if (pulseDot) pulseDot.style.display = "none";
}
});
closeBtn.addEventListener("click", () => {
chatWindow.classList.add("hidden");
chatWindow.classList.remove("active");
});
// Handle form submit
chatForm.addEventListener("submit", async (e) => {
e.preventDefault();
if (isWaiting) return;
const message = inputField.value.trim();
if (!message) return;
// Add user message to UI and history
addMessage(message, "user-msg");
chatHistory.push({ role: "user", text: message });
inputField.value = "";
// Cap history to last 10 messages
if (chatHistory.length > 10) {
chatHistory = chatHistory.slice(-10);
}
// Add typing indicator
const typingIndicator = showTypingIndicator();
isWaiting = true;
// Try backend proxy first, fallback to client-side if it fails
let replySuccess = false;
try {
const response = await fetch("api/chat.php", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
message: message,
history: chatHistory.slice(0, -1)
})
});
const data = await response.json();
if (response.ok && data.reply) {
removeTypingIndicator(typingIndicator);
const botReply = data.reply;
addMessage(botReply, "bot-msg");
chatHistory.push({ role: "model", text: botReply });
replySuccess = true;
}
} catch (error) {
console.warn("Backend API call failed, attempting client-side fallback. Details:", error);
}
// 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/v1beta/models/gemini-2.5-flash-preview-native-audio-dialog: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
function addMessage(text, className) {
const messageDiv = document.createElement("div");
messageDiv.className = `chat-message ${className}`;
const bubbleDiv = document.createElement("div");
bubbleDiv.className = "msg-bubble";
bubbleDiv.innerHTML = formatMessageText(text);
messageDiv.appendChild(bubbleDiv);
messagesContainer.appendChild(messageDiv);
scrollToBottom();
}
// Helper: Show Typing Indicator
function showTypingIndicator() {
const indicatorDiv = document.createElement("div");
indicatorDiv.className = "chat-message bot-msg typing-indicator-container";
indicatorDiv.innerHTML = `
<div class="msg-bubble typing-bubble">
<span class="dot-bounce"></span>
<span class="dot-bounce"></span>
<span class="dot-bounce"></span>
</div>
`;
messagesContainer.appendChild(indicatorDiv);
scrollToBottom();
return indicatorDiv;
}
// Helper: Remove Typing Indicator
function removeTypingIndicator(indicatorElement) {
if (indicatorElement && indicatorElement.parentNode) {
indicatorElement.parentNode.removeChild(indicatorElement);
}
}
// Helper: Scroll to bottom
function scrollToBottom() {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
// Helper: Simple Markdown Formatter
function formatMessageText(text) {
let escaped = text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
escaped = escaped.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
const lines = escaped.split("\n");
let inList = false;
let formatted = "";
lines.forEach(line => {
const trimmed = line.trim();
if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) {
if (!inList) {
formatted += "<ul>";
inList = true;
}
formatted += `<li>${trimmed.substring(2)}</li>`;
} else {
if (inList) {
formatted += "</ul>";
inList = false;
}
formatted += line + "<br>";
}
});
if (inList) {
formatted += "</ul>";
}
return formatted.replace(/(<br>\s*){3,}/g, "<br><br>");
}
});