/* ===== Language Tutor ===== */ (function () { "use strict"; const LANG_VOICE = { hindi:"hi-IN", tamil:"ta-IN", telugu:"te-IN", bengali:"bn-IN", marathi:"mr-IN", gujarati:"gu-IN", kannada:"kn-IN", malayalam:"ml-IN", english:"en-US", spanish:"es-ES", french:"fr-FR", japanese:"ja-JP", }; // ===== State ===== const S = { step:1, targetLang:null, instructionLang:null, level:null, teacherId:null, topic:null, sessionId:null, isMuted:false, isRecording:false, mediaRecorder:null, audioChunks:[], xp:0, curriculum:null, teachers:null, }; // ===== Speech (Browser TTS) ===== const Speech = { speaking:false, queue:[], speak(text){ if(S.isMuted||!text) return; const clean = text .replace(/\[CULTURAL NOTE:.*?\]/gs,"") .replace(/\[DIALOGUE\].*?\[\/DIALOGUE\]/gs,"") .replace(/\[SENTENCE_BUILDER:.*?\]/gs,"") .replace(/\*\*/g,"").replace(/\*/g,"") .replace(/\[.*?\]/g,"").trim(); if(!clean) return; this.queue.push(clean); if(!this.speaking) this._next(); }, _next(){ if(!this.queue.length){ this.speaking=false; Avatar.setIdle(); return; } this.speaking=true; Avatar.setSpeaking(); const text=this.queue.shift(); const u=new SpeechSynthesisUtterance(text); const code=LANG_VOICE[S.targetLang]||"en-US"; const v=speechSynthesis.getVoices().find(v=>v.lang===code||v.lang.startsWith(code.split("-")[0])); if(v) u.voice=v; u.lang=code; u.rate=0.9; u.onend=()=>this._next(); u.onerror=()=>this._next(); speechSynthesis.speak(u); }, stop(){ this.queue=[]; speechSynthesis.cancel(); this.speaking=false; Avatar.setIdle(); } }; // ===== Avatar ===== const Avatar = { blinkTimer:null, load(id,color){ const c=document.getElementById("avatarContainer"); c.innerHTML=`${id}`; document.getElementById("avatarName").textContent=id[0].toUpperCase()+id.slice(1); document.getElementById("avatarName").style.color=color; document.documentElement.style.setProperty("--teacher-color",color); this._blink(); }, setSpeaking(){ document.getElementById("avatarContainer").classList.add("speaking"); }, setIdle(){ document.getElementById("avatarContainer").classList.remove("speaking"); }, _blink(){ if(this.blinkTimer) clearInterval(this.blinkTimer); this.blinkTimer=setInterval(()=>{ try{ const svg=document.getElementById("avatarContainer").querySelector("svg"); if(!svg) return; const eyes=svg.querySelector("#eyes"); if(!eyes) return; eyes.style.transform="scaleY(0.1)"; setTimeout(()=>{ eyes.style.transform="scaleY(1)"; },150); }catch(e){} },3500+Math.random()*2000); } }; // ===== API ===== async function api(url,opts={}){ try{ const r=await fetch(url,opts); if(!r.ok) throw new Error(`HTTP ${r.status}`); return await r.json(); } catch(e){ console.error("API:",e); return {error:e.message}; } } // ===== Onboarding ===== function goStep(n){ S.step=n; document.querySelectorAll(".ob-step").forEach(el=>el.classList.remove("active")); document.getElementById("step"+n).classList.add("active"); // progress bar document.getElementById("progressBar").style.width=(n*20)+"%"; // step labels document.querySelectorAll(".ob-step-label").forEach((lbl,i)=>{ lbl.classList.remove("active","done"); if(i+1===n) lbl.classList.add("active"); else if(i+11) bb.classList.add("visible"); else bb.classList.remove("visible"); } function renderLangs(containerId, onSelect, preselect){ const c=document.getElementById(containerId); c.innerHTML=""; if(!S.curriculum) return; for(const[key,lang] of Object.entries(S.curriculum.languages)){ const d=document.createElement("div"); d.className="lang-card"+(key===preselect?" selected":""); d.innerHTML=`${lang.flag}${lang.name}${lang.native_name}`; d.onclick=()=>{ c.querySelectorAll(".lang-card").forEach(x=>x.classList.remove("selected")); d.classList.add("selected"); onSelect(key); }; c.appendChild(d); } } function renderLevels(){ const c=document.getElementById("levelCards"); c.innerHTML=""; if(!S.curriculum) return; for(const[key,lv] of Object.entries(S.curriculum.levels)){ const d=document.createElement("div"); d.className="level-card"; d.innerHTML=`

${lv.name}

${lv.description}

`; d.onclick=()=>{ c.querySelectorAll(".level-card").forEach(x=>x.classList.remove("selected")); d.classList.add("selected"); S.level=key; setTimeout(()=>{ renderTeachers(); goStep(4); },250); }; c.appendChild(d); } } function renderTeachers(){ const c=document.getElementById("teacherCards"); c.innerHTML=""; if(!S.teachers||!S.teachers.length){ c.innerHTML='

Loading teachers...

'; return; } for(const t of S.teachers){ const d=document.createElement("div"); d.className="teacher-card"; d.innerHTML=`
${t.name}
${t.name}
${t.title}
${t.style}
${t.description}
`; d.onclick=(e)=>{ if(e.target.closest(".btn-voice")) return; c.querySelectorAll(".teacher-card").forEach(x=>x.classList.remove("selected")); d.classList.add("selected"); S.teacherId=t.id; document.documentElement.style.setProperty("--teacher-color",t.color); document.documentElement.style.setProperty("--teacher-light",t.color_light); setTimeout(()=>{ renderTopics(); goStep(5); },250); }; c.appendChild(d); } // voice previews c.querySelectorAll(".btn-voice").forEach(btn=>{ btn.onclick=async(e)=>{ e.stopPropagation(); btn.textContent="Playing..."; const d=await api("/api/teacher/voice-sample",{ method:"POST",headers:{"Content-Type":"application/json"}, body:JSON.stringify({teacher_id:btn.dataset.tid,target_lang:S.targetLang}) }); btn.innerHTML="▶ Preview voice"; if(d.text){ const u=new SpeechSynthesisUtterance(d.text); const code=LANG_VOICE[S.targetLang]||"en-US"; const v=speechSynthesis.getVoices().find(v=>v.lang===code||v.lang.startsWith(code.split("-")[0])); if(v) u.voice=v; u.lang=code; u.rate=0.9; speechSynthesis.speak(u); } }; }); } function renderTopics(){ const c=document.getElementById("topicGrid"); c.innerHTML=""; if(!S.curriculum||!S.level) return; const topics=S.curriculum.curriculum[S.level]||[]; for(const t of topics){ const d=document.createElement("div"); d.className="topic-card"; d.innerHTML=`${t.icon}${t.title}${t.description}`; d.onclick=()=>{ c.querySelectorAll(".topic-card").forEach(x=>x.classList.remove("selected")); d.classList.add("selected"); S.topic=t.id; document.getElementById("btnStart").disabled=false; }; c.appendChild(d); } } // ===== Lesson ===== async function startLesson(){ const btn=document.getElementById("btnStart"); btn.disabled=true; btn.querySelector("span").textContent="Starting..."; const data=await api("/api/session/start",{ method:"POST",headers:{"Content-Type":"application/json"}, body:JSON.stringify({ target_lang:S.targetLang, instruction_lang:S.instructionLang, level:S.level, topic:S.topic, teacher_id:S.teacherId }) }); if(data.error){ btn.querySelector("span").textContent="Start Lesson"; btn.disabled=false; alert("Failed: "+data.error); return; } S.sessionId=data.session_id; document.getElementById("onboarding").classList.add("hidden"); document.getElementById("lesson").classList.remove("hidden"); const langName=S.curriculum?.languages[S.targetLang]?.name||S.targetLang; const topicInfo=(S.curriculum?.curriculum[S.level]||[]).find(t=>t.id===S.topic); document.getElementById("lessonLang").textContent=langName; document.getElementById("lessonTopic").textContent=topicInfo?.title||S.topic; const teacher=S.teachers?.find(t=>t.id===S.teacherId)||{color:"#6C63FF"}; Avatar.load(S.teacherId,teacher.color); addMsg("tutor",data.greeting); Speech.speak(data.greeting); document.getElementById("inputMessage").focus(); } // ===== Chat ===== function addMsg(role,text){ const c=document.getElementById("chatMessages"); const d=document.createElement("div"); d.className="msg "+role; d.innerHTML=fmtMsg(text); c.appendChild(d); renderSpecial(d,text); scrollBottom(); } function fmtMsg(t){ let h=t.replace(/&/g,"&").replace(//g,">"); h=h.replace(/\*\*(.+?)\*\*/g,"$1"); h=h.replace(/\*(.+?)\*/g,"$1"); h=h.replace(/\n/g,"
"); h=h.replace(/\[CULTURAL NOTE:\s*(.*?)\]/gs,'
Cultural Insight
$1
'); h=h.replace(/\[DIALOGUE\](.*?)\[\/DIALOGUE\]/gs,'
$1
'); return h; } function renderSpecial(el,text){ const m=text.match(/\[SENTENCE_BUILDER:\s*(.*?)\]/); if(!m) return; const words=m[1].split("|").map(w=>w.trim()); const shuffled=[...words].sort(()=>Math.random()-.5); const correct=words.join(" "); const blk=document.createElement("div"); blk.className="sentence-builder"; blk.innerHTML=`
Arrange the words correctly:
${shuffled.map(w=>`${w}`).join("")}
`; el.appendChild(blk); const tiles=blk.querySelectorAll(".word-tile"); const zone=blk.querySelector(".sentence-drop-zone"); tiles.forEach(tile=>{ tile.onclick=()=>{ if(tile.classList.contains("placed")) return; tile.classList.add("placed"); const p=document.createElement("span"); p.className="placed-word"; p.textContent=tile.dataset.w; p.onclick=()=>{ p.remove(); tile.classList.remove("placed"); }; zone.appendChild(p); }; }); blk.querySelector(".btn-check-sentence").onclick=()=>{ const user=Array.from(zone.querySelectorAll(".placed-word")).map(e=>e.textContent).join(" "); if(user===correct){ zone.style.borderColor="var(--success)"; zone.style.background="#F0FDF4"; S.xp+=10; updateXP(); } else{ zone.style.borderColor="var(--danger)"; zone.style.background="#FEF2F2"; } }; blk.querySelector(".btn-reset-sentence").onclick=()=>{ zone.innerHTML=""; zone.style.borderColor=""; zone.style.background=""; tiles.forEach(t=>t.classList.remove("placed")); }; } function esc(s){ return s.replace(/"/g,""").replace(/'/g,"'"); } function showTyping(){ const c=document.getElementById("chatMessages"); const d=document.createElement("div"); d.className="typing-indicator"; d.id="typingIndicator"; d.innerHTML=''; c.appendChild(d); scrollBottom(); } function hideTyping(){ const e=document.getElementById("typingIndicator"); if(e) e.remove(); } function scrollBottom(){ const a=document.getElementById("chatArea"); requestAnimationFrame(()=>{ a.scrollTop=a.scrollHeight; }); } function updateXP(){ document.getElementById("xpBadge").querySelector("span").textContent=S.xp+" XP"; } async function sendMessage(){ const inp=document.getElementById("inputMessage"); const text=inp.value.trim(); if(!text||!S.sessionId) return; inp.value=""; addMsg("student",text); showTyping(); const data=await api("/api/chat",{ method:"POST",headers:{"Content-Type":"application/json"}, body:JSON.stringify({session_id:S.sessionId,message:text}) }); hideTyping(); if(data.error){ addMsg("tutor","Sorry, something went wrong. Please try again."); return; } addMsg("tutor",data.reply); if(data.xp){ S.xp=data.xp; updateXP(); } Speech.speak(data.reply); } // ===== Voice Input ===== async function toggleMic(){ if(S.isRecording){ if(S.mediaRecorder) S.mediaRecorder.stop(); S.isRecording=false; document.getElementById("btnMic").classList.remove("recording"); return; } try{ const stream=await navigator.mediaDevices.getUserMedia({audio:true}); S.audioChunks=[]; S.mediaRecorder=new MediaRecorder(stream); S.mediaRecorder.ondataavailable=e=>{ if(e.data.size>0) S.audioChunks.push(e.data); }; S.mediaRecorder.onstop=async()=>{ stream.getTracks().forEach(t=>t.stop()); const blob=new Blob(S.audioChunks,{type:"audio/webm"}); showTyping(); const form=new FormData(); form.append("session_id",S.sessionId); form.append("audio",blob,"audio.webm"); const data=await api("/api/voice",{method:"POST",body:form}); hideTyping(); if(data.transcribed) addMsg("student",data.transcribed); if(data.reply){ addMsg("tutor",data.reply); Speech.speak(data.reply); } if(data.xp){ S.xp=data.xp; updateXP(); } }; S.mediaRecorder.start(); S.isRecording=true; document.getElementById("btnMic").classList.add("recording"); }catch(e){ console.error("Mic:",e); } } // ===== Events ===== function bind(){ document.getElementById("btnSend").onclick=sendMessage; document.getElementById("inputMessage").onkeydown=e=>{ if(e.key==="Enter"&&!e.shiftKey){ e.preventDefault(); sendMessage(); } }; document.getElementById("btnMic").onclick=toggleMic; document.getElementById("btnMute").onclick=()=>{ S.isMuted=!S.isMuted; document.getElementById("iconSpeaker").classList.toggle("hidden",S.isMuted); document.getElementById("iconMuted").classList.toggle("hidden",!S.isMuted); if(S.isMuted) Speech.stop(); }; document.getElementById("btnBack").onclick=()=>{ Speech.stop(); S.sessionId=null; document.getElementById("chatMessages").innerHTML=""; document.getElementById("lesson").classList.add("hidden"); document.getElementById("onboarding").classList.remove("hidden"); document.getElementById("btnStart").querySelector("span").textContent="Start Lesson"; document.getElementById("btnStart").disabled=true; document.querySelectorAll("#topicGrid .topic-card").forEach(c=>c.classList.remove("selected")); }; document.getElementById("btnStart").onclick=startLesson; // Step back button document.getElementById("btnStepBack").onclick=()=>{ if(S.step>1) goStep(S.step-1); }; // Preload voices if(window.speechSynthesis){ speechSynthesis.getVoices(); speechSynthesis.onvoiceschanged=()=>speechSynthesis.getVoices(); } } // ===== Init ===== async function init(){ bind(); const[curr,teach]=await Promise.all([ api("/api/curriculum"), api("/api/teachers") ]); if(!curr.error) S.curriculum=curr; if(!teach.error) S.teachers=teach.teachers; // Step 1: target language renderLangs("targetLangGrid",(key)=>{ S.targetLang=key; setTimeout(()=>{ renderLangs("instructionLangGrid",(k)=>{ S.instructionLang=k; setTimeout(()=>{ renderLevels(); goStep(3); },250); },"english"); goStep(2); },250); }); } document.addEventListener("DOMContentLoaded",init); })();