const chatContainer = document.getElementById("chat-container");
const chatBox = document.getElementById("chat-box");
const input = document.getElementById("user-input");
const intro = document.getElementById("intro");
const scrollBtn = document.getElementById("scroll-to-bottom-btn");
const currentUser = localStorage.getItem('mentorGenUser') || "guest_user";
let chats = [];
let currentChat = [];
let currentChatId = null;
let generatingSessions = new Set();
let abortControllers = new Map();
let currentImageData = null;
/* --- AUDIO ENGINE: MECHANICAL TYPING SOUND --- */
let audioCtx;
function initAudio() {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') audioCtx.resume();
}
function playTypingSound() {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(600 + Math.random() * 200, audioCtx.currentTime);
gain.gain.setValueAtTime(0.015, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.04);
osc.connect(gain); gain.connect(audioCtx.destination);
osc.start(); osc.stop(audioCtx.currentTime + 0.04);
}
/* --- MOBILE LAYOUT LOGIC --- */
function toggleMobileSidebar() {
document.getElementById('main-sidebar').classList.toggle('mobile-open');
document.getElementById('sidebar-overlay').classList.toggle('active');
}
function closeMobileSidebar() {
const sidebar = document.getElementById('main-sidebar');
const overlay = document.getElementById('sidebar-overlay');
if (sidebar) sidebar.classList.remove('mobile-open');
if (overlay) overlay.classList.remove('active');
}
/* --- ATTACHMENT LOGIC --- */
function toggleAttachMenu() {
const menu = document.getElementById('attach-menu');
const icon = document.getElementById('plus-icon');
if(menu) menu.classList.toggle('show');
if(icon) icon.style.transform = menu.classList.contains('show') ? 'rotate(45deg)' : 'rotate(0deg)';
}
document.addEventListener('click', (e) => {
const wrapper = document.querySelector('.attach-wrapper');
if (wrapper && !wrapper.contains(e.target)) {
const menu = document.getElementById('attach-menu');
const icon = document.getElementById('plus-icon');
if (menu) menu.classList.remove('show');
if (icon) icon.style.transform = 'rotate(0deg)';
}
});
function triggerFileInput(type) { toggleAttachMenu(); document.getElementById('file-input').click(); }
function mockDriveUpload() { toggleAttachMenu(); showToast("Drive integration requires active API keys."); }
function handleImageUpload(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
currentImageData = e.target.result;
document.getElementById('image-preview').src = currentImageData;
document.getElementById('image-preview-container').style.display = 'inline-block';
input.focus();
};
reader.readAsDataURL(file);
}
function removeImage() {
currentImageData = null;
document.getElementById('file-input').value = "";
document.getElementById('image-preview-container').style.display = 'none';
}
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); input.focus(); }
if (e.key === 'ArrowUp' && input.value.trim() === '') {
e.preventDefault();
for (let i = currentChat.length - 1; i >= 0; i--) {
if (currentChat[i].role === 'user') { input.value = currentChat[i].text; autoResize(input); break; }
}
}
});
chatContainer.addEventListener("scroll", () => {
if (!scrollBtn) return;
const scrollFromBottom = chatContainer.scrollHeight - chatContainer.scrollTop - chatContainer.clientHeight;
scrollBtn.style.display = scrollFromBottom > 150 ? "flex" : "none";
});
function scrollToBottom() { chatContainer.scrollTo({ top: chatContainer.scrollHeight, behavior: "smooth" }); }
marked.setOptions({
highlight: function(code, lang) {
if (lang && hljs.getLanguage(lang)) { return hljs.highlight(code, { language: lang }).value; }
return hljs.highlightAuto(code).value;
},
langPrefix: 'hljs language-'
});
function toggleTheme() {
const body = document.body; const icon = document.getElementById("theme-icon");
if (body.classList.contains("dark-theme")) { body.classList.replace("dark-theme", "light-theme"); icon.classList.replace("fa-moon", "fa-sun"); }
else { body.classList.replace("light-theme", "dark-theme"); icon.classList.replace("fa-sun", "fa-moon"); }
}
function fillPrompt(text) { input.value = text; autoResize(input); input.focus(); }
function autoResize(el) { el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px'; }
input.addEventListener('keydown', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } });
function showToast(message, isError = false) {
const toast = document.getElementById("toast");
const icon = isError ? '' : '';
toast.innerHTML = `${icon} ${message}`; toast.classList.add("show");
setTimeout(() => toast.classList.remove("show"), 3000);
}
function shareChat() {
if (!currentChatId || currentChat.length === 0) return showToast("Please start a session first.", true);
const shareUrl = window.location.origin + "/chat?session=" + currentChatId;
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(shareUrl).then(() => showToast("Shareable link copied to clipboard!")).catch(() => fallbackCopyTextToClipboard(shareUrl));
} else { fallbackCopyTextToClipboard(shareUrl); }
}
function fallbackCopyTextToClipboard(text) {
const textArea = document.createElement("textarea"); textArea.value = text; textArea.style.position = "fixed";
document.body.appendChild(textArea); textArea.focus(); textArea.select();
try { document.execCommand('copy'); showToast("Shareable link copied to clipboard!"); } catch (err) { showToast("Failed to copy link.", true); }
document.body.removeChild(textArea);
}
function copyMessageText(iconElement, text) {
navigator.clipboard.writeText(text).then(() => {
const originalClass = iconElement.className;
iconElement.className = "fas fa-check"; iconElement.style.color = "#10b981";
setTimeout(() => { iconElement.className = originalClass; iconElement.style.color = ""; }, 2000);
}).catch(err => showToast("Failed to copy.", true));
}
function startListening() {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) return showToast("Microphone not supported.", true);
const rec = new SpeechRecognition(); rec.start(); input.placeholder = "Listening...";
rec.onresult = e => { input.value += e.results[0][0].transcript; autoResize(input); };
rec.onend = () => input.placeholder = "Ask anything or attach a photo...";
}
let botVoice = null;
function setVoice() {
const voices = window.speechSynthesis.getVoices();
if (voices.length === 0) return;
botVoice = voices.find(v => (v.name.includes('Male') || v.name.includes('David') || v.name.includes('Mark')) && !v.name.includes('Female'));
if (!botVoice) botVoice = voices[0];
}
window.speechSynthesis.onvoiceschanged = setVoice; setVoice();
function speakText(text) {
speechSynthesis.cancel();
let cleanText = text.replace(/`{3}[\s\S]*?`{3}/g, ' [Code block omitted] ')
.replace(/`[^`]*`/g, ' ')
.replace(/[*#_=\-><~]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
const utterance = new SpeechSynthesisUtterance(cleanText);
if (botVoice) utterance.voice = botVoice;
speechSynthesis.speak(utterance);
}
function stopSpeaking() { speechSynthesis.cancel(); }
function stopGeneration() { if (abortControllers.has(currentChatId)) abortControllers.get(currentChatId).abort(); }
function getCurrentTime() { return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }
async function sendMessage(textOverride = null, skipUserAppend = false) {
if (currentChatId && generatingSessions.has(currentChatId)) return;
initAudio();
const text = textOverride || input.value.trim();
if (!text && !currentImageData) return;
if(intro) intro.style.display = "none";
if (currentChatId === null) currentChatId = "Session_" + Date.now();
const reqChatId = currentChatId; const timeSent = getCurrentTime(); const sentImage = currentImageData;
if (!skipUserAppend) {
appendUser(text, timeSent, sentImage);
currentChat.push({ role: "user", text: text, image: sentImage, time: timeSent });
if(!textOverride) { input.value = ""; input.style.height = 'auto'; }
removeImage(); saveToHistory(); window.history.replaceState(null, null, "?session=" + reqChatId);
}
generatingSessions.add(reqChatId);
const controller = new AbortController(); abortControllers.set(reqChatId, controller);
appendPremiumThinking(); setSendButtonState("stop");
const startTime = Date.now();
try {
const res = await fetch("/chat", {
method: "POST", headers: { "Content-Type": "application/json", "ngrok-skip-browser-warning": "true" },
body: JSON.stringify({ username: currentUser, message: text, image: sentImage }), signal: controller.signal
});
const data = await res.json();
const responseTime = ((Date.now() - startTime) / 1000).toFixed(1);
const timeReceived = getCurrentTime();
if (currentChatId === reqChatId) {
removeThinkingUI();
currentChat.push({ role: "bot", text: data.reply, time: timeReceived, respTime: responseTime });
simulateTyping(data.reply, reqChatId, timeReceived, responseTime);
saveToHistory();
} else {
const sessionIndex = chats.findIndex(c => c.id === reqChatId);
if (sessionIndex > -1) {
chats[sessionIndex].messages.push({ role: "bot", text: data.reply, time: timeReceived, respTime: responseTime });
saveToHistorySilent(chats[sessionIndex]);
}
}
} catch (e) {
let finalMsg = "System error. Check backend connection.";
if (e.name === 'AbortError') finalMsg = "Response stopped by user.";
if (currentChatId === reqChatId) {
removeThinkingUI();
currentChat.push({ role: "bot", text: finalMsg, time: getCurrentTime(), respTime: null });
appendBot(finalMsg, getCurrentTime(), null); saveToHistory();
}
} finally {
generatingSessions.delete(reqChatId); abortControllers.delete(reqChatId);
if (currentChatId === reqChatId) setSendButtonState("send"); renderHistory();
}
}
function setSendButtonState(state) {
const sendBtn = document.getElementById("send-btn");
if (state === "stop") {
sendBtn.innerHTML = ''; sendBtn.classList.add("stop-mode"); sendBtn.title = "Stop Generation"; sendBtn.onclick = stopGeneration;
} else {
sendBtn.innerHTML = ''; sendBtn.classList.remove("stop-mode"); sendBtn.title = "Send Message"; sendBtn.onclick = () => sendMessage();
}
}
function simulateTyping(text, reqChatId, timeStr, respTime) {
const wrapper = document.createElement("div"); wrapper.className = "message-wrapper bot";
const header = document.createElement("div"); header.className = "bot-header";
header.innerHTML = `
MentoraGen is thinking...`;
wrapper.appendChild(header);
const msg = document.createElement("div"); msg.className = "message";
wrapper.appendChild(msg); chatBox.appendChild(wrapper);
let i = 0; let buffer = ""; let tickCounter = 0;
const typeInterval = setInterval(() => {
if (currentChatId !== reqChatId) { clearInterval(typeInterval); return; }
buffer += text.substr(i, 4); i += 4;
if (tickCounter % 3 === 0) playTypingSound();
tickCounter++;
// Inject the WhatsApp style time at the end of the parsed markdown
msg.innerHTML = marked.parse(buffer) + `${timeStr}`;
chatContainer.scrollTop = chatContainer.scrollHeight;
if (i >= text.length) {
clearInterval(typeInterval);
msg.innerHTML = marked.parse(text) + `${timeStr}`;
msg.querySelectorAll('pre code').forEach((block) => { try { hljs.highlightElement(block); } catch(e) {} });
addCodeCopyButtons(msg);
header.innerHTML = `
Thinking for ${respTime} seconds`;
wrapper.appendChild(createBotActions(text, timeStr)); // Time is removed from the actions visually now
chatContainer.scrollTop = chatContainer.scrollHeight;
}
}, 10);
}
function addCodeCopyButtons(container) {
container.querySelectorAll('pre').forEach(pre => {
if (pre.querySelector('.code-header')) return;
const codeBlock = pre.querySelector('code'); let lang = "Code";
if (codeBlock && codeBlock.className) { const match = codeBlock.className.match(/language-(\w+)/); if (match) lang = match[1]; }
const header = document.createElement('div'); header.className = 'code-header'; header.innerHTML = `${lang}`;
const btn = document.createElement('button'); btn.className = 'copy-code-btn'; btn.innerHTML = ' Copy code';
btn.onclick = () => { navigator.clipboard.writeText(codeBlock.innerText); btn.innerHTML = ' Copied!';
setTimeout(() => { btn.innerHTML = ' Copy code'; }, 2000); };
header.appendChild(btn); pre.prepend(header);
});
}
function appendPremiumThinking() {
if (document.getElementById("thinking-wrapper")) return;
const wrapper = document.createElement("div"); wrapper.className = "message-wrapper bot"; wrapper.id = "thinking-wrapper";
wrapper.innerHTML = `
MentoraGen is thinking...
${latencyText}