1) 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.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,'');
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);
})();