document.addEventListener("DOMContentLoaded", function () { const lastChapter = localStorage.getItem("lastAccessedChapter"); console.log("Last Save Chapter: "+ lastChapter) if (lastChapter) { const chapter = JSON.parse(lastChapter); loadChapter({ chapterTitle: chapter.chapterTitle }); // Auto-load last accessed chapter } const loginLink = document.getElementById('login-link'); // Login button container const userInfoDiv = document.getElementById('user-info'); // User info container (after login) const logoutButton = document.getElementById('logout-button'); // Logout button const userNameDisplay = document.getElementById('user-name'); // Where to display the username const userButton = document.getElementById('user-button'); // User initials button const token = localStorage.getItem('authToken'); // Get token from localStorage const menuButton = document.getElementById("menuButton"); const sidebar = document.querySelector(".sidebar"); const overlay = document.querySelector(".overlay"); const content = document.querySelector("#content"); // Main content area const nextBtn = document.getElementById('nextChapterButton'); if (nextBtn) nextBtn.addEventListener('click', loadNextChapter); const loadingContainer = document.getElementById('loading-container'); // Loading container const loadingBar = document.getElementById('loading-bar'); // Loading bar const loadingText = document.getElementById('loading-text'); // Loading text // Show loading bar when the page starts loadingContainer.style.display = 'flex'; // Fetch and initialize data from MongoDB fetch('https://thevera-botveradb.hf.space/get_chapter_titles') // fetch('https://thevera-botveradb.hf.space/get_chapters') .then(response => response.json()) .then(responseData => { if (responseData.success) { initializePage(responseData.data); } else { console.error('Error loading data:', responseData.message); } // Hide the loading bar after data is loaded loadingContainer.style.display = 'none'; }) .catch(error => { console.error('Error loading data:', error); // Hide the loading bar in case of error loadingContainer.style.display = 'none'; }); if (token) { // Check token validity with backend fetch('https://thevera-botveradb.hf.space/validate_token', { method: 'GET', headers: { 'Authorization': `Bearer ${token}` } }) .then(response => response.json()) .then(data => { if (data.message === "Token is valid!") { // Token is valid, decode it to get user info const userObject = jwt_decode(token); // Decode the token to get user info const email = userObject.email; // Assuming the email is stored in the token const username = email.split('@')[0]; // Extract username (first part of email) // Update UI with user's info userNameDisplay.innerText = username; // Display first part of email as username userInfoDiv.style.display = 'block'; // Show user info section // Change user button to show user's initials const userInitials = username.charAt(0).toUpperCase(); // Get first letter of username userButton.innerHTML = `
${userInitials}
`; // Display initials // Show the logout button and hide the login link logoutButton.style.display = 'inline'; loginLink.style.display = 'none'; // **Update UI for Mobile** document.getElementById('user-info-mobile').style.display = 'block'; document.getElementById('user-initials').innerText = userInitials; document.getElementById('user-name-mobile').innerText = email.split('@')[0]; document.getElementById('logout-button-mobile').style.display = 'block'; document.getElementById('loginButton').style.display = 'none'; } else { // Token is invalid, clear invalid token localStorage.removeItem('authToken'); console.log('Invalid token, user not logged in.'); loginLink.style.display = 'inline'; // Show login link } }) .catch(error => { console.error('Error:', error); // Handle the error (e.g., network issues) localStorage.removeItem('authToken'); // Clear token in case of error loginLink.style.display = 'inline'; // Show login link }); } else { // No token found, show login link console.log('No token found, user not logged in.'); loginLink.style.display = 'inline'; } // Logout functionality if (logoutButton) { logoutButton.onclick = function() { localStorage.removeItem('authToken'); // Clear token window.location.reload(); // Refresh page to update UI }; } // Optional: If you want to show the dropdown when clicking on the user initials button if (userButton) { userButton.addEventListener('click', () => { const userDropdown = document.getElementById('user-dropdown'); userDropdown.style.display = userDropdown.style.display === 'block' ? 'none' : 'block'; }); } // Sidebar toggle for mobile and desktop menuButton.addEventListener("click", function () { if (sidebar && overlay && content) { sidebar.classList.toggle("shown"); overlay.classList.toggle("active"); // Adjust content margin when sidebar is visible if (sidebar.classList.contains("shown")) { content.style.marginLeft = "250px"; // Sidebar width } else { content.style.marginLeft = "0"; } } }); // Close sidebar on overlay click for mobile view overlay.addEventListener("click", function () { if (sidebar && overlay && content) { sidebar.classList.remove("shown"); overlay.classList.remove("active"); content.style.marginLeft = "0"; // Reset content margin } }); }); // Mobile Menu Buttion js code document.addEventListener("DOMContentLoaded", function () { const menuButton = document.getElementById("menuButton"); const mobileMenu = document.getElementById("mobileMenu"); if (menuButton && mobileMenu) { menuButton.addEventListener("click", function (event) { event.stopPropagation(); // Prevents immediate close mobileMenu.classList.toggle("show"); // Toggle menu visibility }); // Close menu when clicking outside document.addEventListener("click", function (event) { if (!mobileMenu.contains(event.target) && !menuButton.contains(event.target)) { mobileMenu.classList.remove("show"); } }); } else { console.error("Menu button or mobile menu not found in the DOM."); } }); // ===== Voice Manager (persisted TTS settings) ===== // const TTS_STORE_KEY = 'ttsSettings_v1'; // const defaultTTS = { voiceURI: null, rate: 0.9, pitch: 0.9 }; // function getTTSSettings(){ // try { return { ...defaultTTS, ...(JSON.parse(localStorage.getItem(TTS_STORE_KEY))||{}) }; } // catch { return { ...defaultTTS }; } // } // function saveTTSSettings(patch){ // const now = { ...getTTSSettings(), ...(patch||{}) }; // localStorage.setItem(TTS_STORE_KEY, JSON.stringify(now)); // return now; // } // function waitForVoices(timeoutMs = 1500) { // return new Promise(resolve => { // const have = speechSynthesis.getVoices(); // if (have && have.length) return resolve(have); // const onVoices = () => { // const v = speechSynthesis.getVoices(); // if (v && v.length) { // speechSynthesis.removeEventListener('voiceschanged', onVoices); // resolve(v); // } // }; // speechSynthesis.addEventListener('voiceschanged', onVoices); // setTimeout(() => { // speechSynthesis.removeEventListener('voiceschanged', onVoices); // resolve(speechSynthesis.getVoices() || []); // }, timeoutMs); // }); // } // function preferredSort(a,b){ // // Prefer sa-IN > hi-IN > en-IN, then by name // const rank = v => (/sa-IN/i.test(v.lang)?0:(/hi-IN/i.test(v.lang)?1:(/en-IN/i.test(v.lang)?2:3))); // const r = rank(a)-rank(b); // if (r!==0) return r; return a.name.localeCompare(b.name); // } // async function populateVoiceSelect(){ // const select = document.getElementById('voiceSelect'); // if (!select) return; // select.innerHTML = ''; // const voices = await waitForVoices(); // voices.sort(preferredSort).forEach(v=>{ // const opt = document.createElement('option'); // opt.value = v.voiceURI; // persistable id // opt.textContent = `${v.name} — ${v.lang}`; // select.appendChild(opt); // }); // const st = getTTSSettings(); // if (st.voiceURI){ // const found = [...select.options].some(o=>o.value===st.voiceURI); // if (found) select.value = st.voiceURI; // } else if (voices.length){ // // auto-pick first preferred // saveTTSSettings({ voiceURI: voices[0].voiceURI }); // select.value = voices[0].voiceURI; // } // } // function attachVoiceUI(){ // const select = document.getElementById('voiceSelect'); // const refresh = document.getElementById('voiceRefresh'); // const sample = document.getElementById('voiceSample'); // const rate = document.getElementById('voiceRate'); // const pitch = document.getElementById('voicePitch'); // const st = getTTSSettings(); // if (rate) rate.value = st.rate; if (pitch) pitch.value = st.pitch; // if (select) select.addEventListener('change', ()=> saveTTSSettings({ voiceURI: select.value })); // if (rate) rate.addEventListener('input', ()=> saveTTSSettings({ rate: parseFloat(rate.value) })); // if (pitch) pitch.addEventListener('input', ()=> saveTTSSettings({ pitch: parseFloat(pitch.value) })); // if (refresh) refresh.addEventListener('click', populateVoiceSelect); // if (sample) sample.addEventListener('click', async ()=>{ // try { await speakClientSide("ॐ श्रीं नमः | यह एक नमूना आवाज़ है ।"); } catch(e){ console.warn(e); } // }); // } // function resolveSelectedVoice(voices){ // const st = getTTSSettings(); // let v = voices.find(v=>v.voiceURI===st.voiceURI); // if (!v){ // // best-effort locale pick // v = voices.sort(preferredSort)[0] || null; // if (v) saveTTSSettings({ voiceURI: v.voiceURI }); // } // return v; // } // Call once on load (safe no-op if controls are not present) // (function initVoiceControls(){ // if (!('speechSynthesis' in window)) return; // populateVoiceSelect(); // attachVoiceUI(); // })(); // Handle toggle for "Shastra Collection" header document.getElementById('shastraHeader').addEventListener('click', function() { const shastraList = document.getElementById('shastraList'); shastraList.classList.toggle('hidden'); // Toggle the 'hidden' class on the sidebar }); let chaptersArray = []; let currentChapterIndex = -1; // Initialize with -1 as default to indicate no chapter is loaded yet let resumeTime = 0; let player; function initializePage(data) { const shastraList = document.getElementById('shastraList'); if (!shastraList) { console.error("Element with ID 'shastraList' not found."); return; } Object.keys(data).forEach((collection) => { // Create main group (e.g., Bhagwat Mahapuran) const mainGroupItem = document.createElement('li'); mainGroupItem.classList.add('main-group'); mainGroupItem.innerHTML = ` ${collection} `; const itemList = document.createElement('ul'); itemList.classList.add('nested-list'); itemList.style.display = 'none'; // Toggle display of main group chapters mainGroupItem.addEventListener('click', () => { itemList.style.display = itemList.style.display === 'none' ? 'block' : 'none'; }); data[collection].forEach((item) => { if (item.skandaTitle) { // Create Skanda group with + icon const skandaGroup = document.createElement('li'); skandaGroup.classList.add('skanda-group'); skandaGroup.innerHTML = ` ${item.skandaTitle} `; const skandaChapterList = document.createElement('ul'); skandaChapterList.classList.add('nested-list'); skandaChapterList.style.display = 'none'; item.chapters.forEach((chapter) => { const chapterItem = document.createElement('li'); chapterItem.classList.add('chapter-item'); chapterItem.innerHTML = ` ${chapter.chapterTitle} `; chapterItem.addEventListener('click', (event) => { event.stopPropagation(); fetchChapterData(chapter.chapterTitle).then(chapterDetails => { if (chapterDetails) { loadChapter(chapterDetails); closeSidebarOnMobile(); } }); }); skandaChapterList.appendChild(chapterItem); chaptersArray.push({ chapterTitle: chapter.chapterTitle }); }); skandaGroup.addEventListener('click', (event) => { event.stopPropagation(); skandaChapterList.style.display = skandaChapterList.style.display === 'none' ? 'block' : 'none'; const toggleIcon = skandaGroup.querySelector('.toggle-icon'); if (toggleIcon) { toggleIcon.classList.toggle('fa-plus'); toggleIcon.classList.toggle('fa-minus'); } }); skandaGroup.appendChild(skandaChapterList); itemList.appendChild(skandaGroup); } else { const chapterItem = document.createElement('li'); chapterItem.classList.add('chapter-item'); chapterItem.innerHTML = ` ${item.chapterTitle}`; chapterItem.addEventListener('click', (event) => { event.stopPropagation(); fetchChapterData(item.chapterTitle).then(chapterDetails => { if (chapterDetails) { loadChapter(chapterDetails); closeSidebarOnMobile(); } }); }); itemList.appendChild(chapterItem); chaptersArray.push({ chapterTitle: item.chapterTitle }); } }); mainGroupItem.appendChild(itemList); shastraList.appendChild(mainGroupItem); }); // Place this right after the forEach loop ends inside initializePage if (!localStorage.getItem("menuTipShown")) { const tip = document.createElement('div'); tip.innerText = "📚 Browse chapters here!"; tip.style.cssText = ` position: fixed; left: 20px; top: 70px; background: #fff8e1; padding: 8px 12px; border-radius: 5px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); z-index: 9999; font-weight: bold; `; document.body.appendChild(tip); setTimeout(() => { tip.remove(); localStorage.setItem("menuTipShown", "true"); }, 4000); } } function extractSkandaNumber(skandaTitle) { // e.g. "Skanda 1" or "Skanda One" or "Skanda I" const match = skandaTitle?.match(/(\d+)/); return match ? parseInt(match[1], 10) : 0; } function initializePageOld(data) { const shastraList = document.getElementById('shastraList'); if (!shastraList) { console.error("Element with ID 'shastraList' not found."); return; } Object.keys(data).forEach((collection) => { const mainGroupItem = document.createElement('li'); mainGroupItem.textContent = collection; mainGroupItem.classList.add('main-group'); const itemList = document.createElement('ul'); itemList.classList.add('nested-list'); itemList.style.display = 'none'; data[collection].forEach((item) => { if (item.skandaTitle) { const skandaGroup = document.createElement('li'); // skandaGroup.textContent = item.skandaTitle; skandaGroup.innerHTML = ` ${item.skandaTitle}`; skandaGroup.classList.add('skanda-group'); const skandaChapterList = document.createElement('ul'); skandaChapterList.classList.add('nested-list'); skandaChapterList.style.display = 'none'; item.chapters.forEach((chapter) => { const chapterItem = document.createElement('li'); // chapterItem.textContent = chapter.chapterTitle; chapterItem.innerHTML = ` ${chapter.chapterTitle}`; chapterItem.classList.add('chapter-item'); chapterItem.addEventListener('click', (event) => { event.stopPropagation(); fetchChapterData(chapter.chapterTitle).then(chapterDetails => { if (chapterDetails) { loadChapter(chapterDetails); closeSidebarOnMobile(); } }); }); skandaChapterList.appendChild(chapterItem); // Populate chaptersArray with chapter metadata (title only for now) chaptersArray.push({ chapterTitle: chapter.chapterTitle }); }); skandaGroup.addEventListener('click', (event) => { event.stopPropagation(); skandaChapterList.style.display = skandaChapterList.style.display === 'none' ? 'block' : 'none'; }); skandaGroup.appendChild(skandaChapterList); itemList.appendChild(skandaGroup); } else { const chapterItem = document.createElement('li'); chapterItem.textContent = item.chapterTitle; chapterItem.classList.add('chapter-item'); chapterItem.addEventListener('click', (event) => { event.stopPropagation(); fetchChapterData(item.chapterTitle).then(chapterDetails => { if (chapterDetails) { loadChapter(chapterDetails); closeSidebarOnMobile(); } }); }); itemList.appendChild(chapterItem); // Populate chaptersArray with chapter metadata (title only for now) chaptersArray.push({ chapterTitle: item.chapterTitle }); } }); mainGroupItem.addEventListener('click', () => { itemList.style.display = itemList.style.display === 'none' ? 'block' : 'none'; }); mainGroupItem.appendChild(itemList); shastraList.appendChild(mainGroupItem); }); } async function fetchChapterData(chapterTitle) { try { const response = await fetch(`https://thevera-botveradb.hf.space/get_chapter_details?chapterTitle=${encodeURIComponent(chapterTitle)}`); const result = await response.json(); if (response.ok && result.success) { return result.data; // Return chapter details } else { console.error("Error fetching chapter details:", result.message); return null; } } catch (error) { console.error("Error in fetchChapterData:", error); return null; } } // Function to fetch audio URL async function fetchAudioUrl(chapterTitle) { try { const response = await fetch(`https://thevera-botveradb.hf.space/get_audio_url?chapterTitle=${encodeURIComponent(chapterTitle)}`); const result = await response.json(); if (response.ok && result.success) { return result.audioUrl; } else { console.error('Error fetching audio:', result.message); return null; } } catch (error) { console.error('Error fetching audio:', error); return null; } } async function loadChapter(chapter) { console.log("Loading chapter..."); console.log(chapter.chapterTitle); // Save the last accessed chapter in localStorage localStorage.setItem("lastAccessedChapter", JSON.stringify({ chapterTitle: chapter.chapterTitle })); const chapterTitle = document.getElementById('chapterTitle'); const slokeContent = document.getElementById('slokeContent'); const videoPlayer = document.getElementById('videoPlayer'); const transcriptBlock = document.getElementById('transcript-content'); // Ensure the elements are loaded correctly if (!chapterTitle || !slokeContent || !videoPlayer || !transcriptBlock) { console.error("Missing elements in chapter display."); return; } // Reset content chapterTitle.textContent = 'Loading...'; // Temporary loading message slokeContent.innerHTML = ''; videoPlayer.src = ''; transcriptBlock.innerHTML = ''; // Clear all transcript content try { // Fetch chapter details from the backend const chapterDetails = await fetchChapterData(chapter.chapterTitle); if (!chapterDetails) { chapterTitle.textContent = 'Chapter details not found.'; return; } // Update UI with chapter details chapterTitle.textContent = chapterDetails.chapterTitle + " / " + chapterDetails.about || 'Chapter Details'; // Process and display the slokes const slokesArray = chapterDetails.slokes ? chapterDetails.slokes.split('\n') : []; slokesArray.forEach((slokeText) => { const slokeElement = document.createElement('p'); slokeElement.classList.add('sloke-text'); // slokeElement.innerHTML = slokeText.replace(/श्लोक \d+:/g, '$&'); slokeElement.innerHTML = slokeText.replace(/(श्लोक \d+:|अर्थ \d+:)/g, '$&'); const speakIcon = createSpeakIcon(slokeText); slokeElement.appendChild(speakIcon); slokeContent.appendChild(slokeElement); }); // Update video and transcript blocks // videoPlayer.src = chapterDetails.video || ''; loadYouTubeVideo(chapterDetails.video); const transcriptArray = chapterDetails.transcript ? chapterDetails.transcript.split('\n') : []; transcriptArray.forEach((transcriptText) => { const transcriptElement = document.createElement('p'); transcriptElement.classList.add('transcript-text'); transcriptElement.textContent = transcriptText; // Add transcript text transcriptBlock.appendChild(transcriptElement); }); // Show the content blocks document.querySelector('.main').classList.add('active'); document.querySelector('.sloke-block').classList.add('active'); document.querySelector('.video-block').classList.add('active'); document.querySelector('.transcript-block').classList.add('active'); // Setup play buttons setupPlayAllButton(slokesArray); setupPlayAllTranscriptButton(transcriptArray); // Auto-click to minimize the sidebar group const chapterGroup = document.querySelector(`.main-group`); if (chapterGroup) { chapterGroup.click(); // This simulates a click to collapse the group } // Update currentChapterIndex for "Next Chapter" functionality currentChapterIndex = chaptersArray.findIndex(ch => ch.chapterTitle === chapter.chapterTitle); console.log("Updated Current Index:", currentChapterIndex); document.getElementById('shastraHeader').click(); // Simulate a click on the header } catch (error) { console.error("Error loading chapter details:", error); chapterTitle.textContent = 'Failed to load chapter details. Please try again.'; } } function loadYouTubeVideo(videoUrl) { if (!videoUrl) { console.warn("[Video] No video URL provided"); return; } const videoIdMatch = videoUrl.match(/\/embed\/([a-zA-Z0-9_-]+)/); if (!videoIdMatch) { console.error("[Video] Invalid video URL format", videoUrl); return; } const videoId = videoIdMatch[1]; currentVideoId = videoId; const storageKey = `videoTime_${videoId}`; // resumeTime = parseFloat(localStorage.getItem(storageKey)) || 0; resumeTime = Math.max((parseFloat(localStorage.getItem(storageKey)) || 0) - 5, 0); console.log(`[Video] Loading ID: ${videoId}, Resuming at: ${resumeTime}s`); if (player) { player.loadVideoById({ videoId, startSeconds: resumeTime }); } else { player = new YT.Player('videoPlayer', { height: '315', width: '100%', videoId, playerVars: { autoplay: 1, controls: 1, start: resumeTime }, events: { 'onReady': (event) => { console.log("[Player] onReady"); event.target.seekTo(resumeTime, true); event.target.playVideo(); }, 'onStateChange': onPlayerStateChange } }); } } function onPlayerStateChange(event) { if (event.data === YT.PlayerState.PLAYING) { console.log("[Player] Playing"); // Save playback time every 5 seconds clearInterval(window.saveTimeInterval); window.saveTimeInterval = setInterval(() => { if (player && currentVideoId) { const time = player.getCurrentTime(); localStorage.setItem(`videoTime_${currentVideoId}`, time); console.log(`[Resume Save] ${currentVideoId}: ${time.toFixed(1)}s`); } }, 5000); } if (event.data === YT.PlayerState.ENDED) { console.log("[Player] Video Ended. Clearing resume time."); localStorage.removeItem(`videoTime_${currentVideoId}`); clearInterval(window.saveTimeInterval); } } function loadNextChapter() { console.log("Next Chapter clicked..."); if (currentChapterIndex + 1 < chaptersArray.length) { currentChapterIndex++; // Increment currentChapterIndex to move to the next chapter // Auto-click to minimize the sidebar group const chapterGroup = document.querySelector(`.main-group`); if (chapterGroup) { chapterGroup.click(); // This simulates a click to collapse the group } loadChapter(chaptersArray[currentChapterIndex]); } else { alert("This is the last chapter."); } } function closeSidebarOnMobile() { const sidebar = document.querySelector(".sidebar"); const overlay = document.querySelector(".overlay"); if (window.innerWidth < 768 && sidebar && overlay) { sidebar.classList.remove("shown"); overlay.classList.remove("active"); } } // Other functions such as createSpeakIcon, cleanSlokText, playTTS, and setupPlayAllButton remain as you provided function createSpeakIcon(slokeText) { const speakIcon = document.createElement('i'); speakIcon.classList.add('fas', 'fa-volume-up', 'speak-icon'); speakIcon.setAttribute('title', 'Speak Slok'); speakIcon.addEventListener('click', () => { console.log(`Speaking: ${slokeText}`); playTTS(slokeText); }); return speakIcon; } function cleanSlokText(text) { // Regex to match and remove introductory (e.g., "श्लोक 1:") and ending markers (e.g., "१") // return text.replace(/श्लोक \d+: /g, '').replace(/ \d+$/g, ''); return text.replace(/(श्लोक \d+:|अर्थ \d+:)/g,'').replace(/ \d+$/g, ''); } let currentSlokeIndex = 0; // Track the current sloke index const audioPlayer = document.getElementById('audioPlayer'); // Play all slokes function (replace your existing setupPlayAllButton function) function setupPlayAllButton(slokesArray) { const playAllButton = document.getElementById('playAllButton'); playAllButton.onclick = async () => { currentSlokeIndex = 0; // Reset to start playNextSloke(slokesArray); }; } // Function to play each sloke in sequence async function playNextSloke(slokesArray) { if (currentSlokeIndex >= slokesArray.length) { return; // End if all slokes have been played } // Clean and highlight the current sloke text // const slokeText = cleanSlokText(slokesArray[currentSlokeIndex]); const slokeText = slokesArray[currentSlokeIndex]; highlightCurrentSloke(currentSlokeIndex); // Generate TTS audio for the current sloke text playTTS(slokeText).then(() => { // Move to the next sloke after the current one finishes currentSlokeIndex++; playNextSloke(slokesArray); }); } // Function to highlight the current sloke and scroll it into view function highlightCurrentSloke(index) { const slokeElements = document.querySelectorAll('.sloke-text'); // Assume each sloke text has this class // Remove previous highlights slokeElements.forEach(el => el.classList.remove('highlight')); // Highlight and scroll the current sloke const currentSloke = slokeElements[index]; if (currentSloke) { currentSloke.classList.add('highlight'); currentSloke.scrollIntoView({ behavior: "smooth", block: "center" }); } } let currentTranscriptIndex = 0; // Track the current transcript index // Setup Play All for Transcript function setupPlayAllTranscriptButton(transcriptArray) { const playAllTranscriptButton = document.getElementById('playTranscriptButton'); playAllTranscriptButton.onclick = async () => { currentTranscriptIndex = 0; // Reset to start playNextTranscript(transcriptArray); }; } // Function to play each transcript in sequence async function playNextTranscript(transcriptArray) { if (currentTranscriptIndex >= transcriptArray.length) { return; // End if all transcripts have been played } // Highlight the current transcript text highlightCurrentTranscript(currentTranscriptIndex); // Generate TTS audio for the current transcript text const transcriptText = transcriptArray[currentTranscriptIndex]; playTTS(transcriptText).then(() => { // Move to the next transcript after the current one finishes currentTranscriptIndex++; playNextTranscript(transcriptArray); }); } // Function to highlight the current transcript text function highlightCurrentTranscript(index) { const allTranscripts = document.querySelectorAll('.transcript-text'); allTranscripts.forEach((transcript, i) => { if (i === index) { transcript.classList.add('highlight'); // Add highlight class } else { transcript.classList.remove('highlight'); // Remove highlight class } }); } let currentAudio; // Global variable to hold the audio object for server-generated audio let isPlaying = false; // Track play/pause state for playback let currentUtterance; // Track current SpeechSynthesisUtterance for sa-IN playback // --- Voice helpers --- function pickPreferredVoice(voices) { const tests = [ v => /sa-IN/i.test(v.lang) && /(male|deep|bass|india)/i.test(v.name), v => /sa-IN/i.test(v.lang), v => /hi-IN/i.test(v.lang) && /(male|deep|bass|india)/i.test(v.name), v => /hi-IN/i.test(v.lang), v => /en-IN/i.test(v.lang) && /(male|deep|bass|india)/i.test(v.name), v => /en-IN/i.test(v.lang), ]; for (const t of tests) { const found = voices.find(t); if (found) return found; } return voices[0] || null; } function waitForVoices(timeoutMs = 1500) { return new Promise(resolve => { const have = speechSynthesis.getVoices(); if (have && have.length) return resolve(have); const onVoices = () => { const v = speechSynthesis.getVoices(); if (v && v.length) { speechSynthesis.removeEventListener('voiceschanged', onVoices); resolve(v); } }; speechSynthesis.addEventListener('voiceschanged', onVoices); setTimeout(() => { speechSynthesis.removeEventListener('voiceschanged', onVoices); resolve(speechSynthesis.getVoices() || []); }, timeoutMs); }); } async function speakClientSide(text) { const voices = await waitForVoices(); if (!voices.length) throw new Error('No TTS voices available'); const voice = pickPreferredVoice(voices); const langFallback = voice?.lang || 'sa-IN'; // Stop any previous utterances try { speechSynthesis.cancel(); } catch {} // Split by pipe/comma and add small pauses after delimiters const parts = text.split(/(\||,)/).map(s => s.trim()).filter(Boolean); for (const chunk of parts) { const u = new SpeechSynthesisUtterance(chunk); if (voice) u.voice = voice; u.lang = voice?.lang || langFallback; u.rate = 0.9; // or your saved rate u.pitch = 0.9; // or your saved pitch u.volume = 1.0; await new Promise((res, rej) => { u.onend = res; u.onerror = rej; speechSynthesis.speak(u); }); if (chunk === '|' || chunk === ',') { await new Promise(r => setTimeout(r, 220)); } } } // Speak text in segments (pauses at "|" and ",") async function speakClientSideOld(text) { const st = ttsGet(); const all = await getAllVoices(); if (!all.length) throw new Error('No TTS voices available'); // pick language preference sa-IN -> hi-IN -> en-IN const prefLangs = ['sa-IN', 'hi-IN', 'en-IN']; let chosenLang = prefLangs.find(l => st.byLang[l]?.voiceURI) || prefLangs.find(l => all.some(v => v.lang && v.lang.startsWith(l))); if (!chosenLang) chosenLang = all[0].lang || 'hi-IN'; let voice = null; const savedURI = st.byLang[chosenLang]?.voiceURI; if (savedURI) voice = all.find(v => v.voiceURI === savedURI) || null; if (!voice) { const list = voicesForLang(all, chosenLang); voice = list[0] || all[0] || null; } const parts = text.split(/(\||,)/).map(s => s.trim()).filter(Boolean); for (const chunk of parts) { const u = new SpeechSynthesisUtterance(chunk); if (voice) u.voice = voice; u.lang = voice?.lang || chosenLang; u.rate = st.rate; u.pitch = st.pitch; await new Promise((res, rej)=>{ u.onend = res; u.onerror = rej; speechSynthesis.speak(u); }); if (chunk === '|' || chunk === ',') await new Promise(r=>setTimeout(r,220)); } } async function playTTS(text) { console.log('Speaking mantra'); if (!text || text.trim() === '') { console.error('No text provided'); return; } const cleanText = typeof cleanSlokText === 'function' ? cleanSlokText(text) : text; // Helper to play a URL in your existing