222 lines
8.2 KiB
JavaScript
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, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
|
|
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>");
|
|
}
|
|
});
|