(function(){ "use strict"; var CUR = window.CURRICULUM || []; var N = CUR.length; var SECTIONS = [ {id:"words", icon:"📖", label:"Слова"}, {id:"dialogue", icon:"💬", label:"Диалог"}, {id:"reading", icon:"📰", label:"Чтение"}, {id:"listening", icon:"🎧", label:"Аудио"}, {id:"speaking", icon:"🎤", label:"Говорю"}, {id:"writing", icon:"✍️", label:"Пишу"} ]; var ACTIVE = "words"; var curIdx = 0; var todayContentIdx = 0; var dayKey = 0; var state = null; /* ---------- storage ---------- */ function todayStr(){ var d=new Date(); return d.getFullYear()+"-"+String(d.getMonth()+1).padStart(2,"0")+"-"+String(d.getDate()).padStart(2,"0"); } function dateStr(d){ return d.getFullYear()+"-"+String(d.getMonth()+1).padStart(2,"0")+"-"+String(d.getDate()).padStart(2,"0"); } function load(){ try{ state = JSON.parse(localStorage.getItem("enTrainer")||"null"); }catch(e){ state=null; } var today = todayStr(); if(!state){ state={startDate:today, streak:1, lastVisit:today, completed:{}, wordsLearned:[], reminderOn:false, reminderTime:"20:00", lastNotified:null, tests:{}}; } if(state.reminderOn===undefined) state.reminderOn=false; if(!state.reminderTime) state.reminderTime="20:00"; if(state.lastNotified===undefined) state.lastNotified=null; if(!state.tests) state.tests={}; // streak if(state.lastVisit !== today){ var y=new Date(); y.setDate(y.getDate()-1); state.streak = (state.lastVisit===dateStr(y)) ? state.streak+1 : 1; state.lastVisit = today; } if(!state.completed) state.completed={}; if(!state.completed[today]) state.completed[today]=[]; if(!state.wordsLearned) state.wordsLearned=[]; save(); } function save(){ localStorage.setItem("enTrainer", JSON.stringify(state)); } function completedToday(){ return state.completed[todayStr()] || (state.completed[todayStr()]=[]); } function isDone(id){ return completedToday().indexOf(id)>=0; } function setDone(id, on){ var c=completedToday(); var i=c.indexOf(id); if(on && i<0){ c.push(id); } if(!on && i>=0){ c.splice(i,1); } save(); } /* ---------- day math ---------- */ function computeDay(){ var s=new Date(state.startDate+"T00:00:00"); var t=new Date(); t.setHours(0,0,0,0); dayKey = Math.max(0, Math.round((t-s)/86400000)); todayContentIdx = ((dayKey % N)+N)%N; curIdx = todayContentIdx; } function day(){ return CUR[curIdx]; } /* ---------- misc helpers ---------- */ function shuffle(a){ a=a.slice(); for(var i=a.length-1;i>0;i--){ var j=Math.floor(Math.random()*(i+1)); var t=a[i]; a[i]=a[j]; a[j]=t; } return a; } function flatten(a){ var r=[]; a.forEach(function(x){ x.forEach(function(y){ r.push(y); }); }); return r; } function currentMonth(){ return Math.min(5, Math.floor(dayKey/30)); } function monthLabel(m){ return "Месяц "+(m+1)+" из 6"; } /* ---------- speech ---------- */ var voices=[]; var synth = window.speechSynthesis; function loadVoices(){ if(synth) voices = synth.getVoices(); } if(synth){ loadVoices(); synth.onvoiceschanged=loadVoices; } function enVoice(pref){ var en = voices.filter(function(v){ return /en(-|_)?US/i.test(v.lang) || /en(-|_)?GB/i.test(v.lang); }); if(!en.length) en = voices.filter(function(v){ return /^en/i.test(v.lang); }); if(pref) for(var i=0;i=0.8?"good":sim>=0.5?"ok":"bad"; } function stopRec(){ if(currentRec){ try{currentRec.stop();}catch(e){} currentRec=null; } } function startRec(onResult, onEnd){ if(!REC){ toast("Микрофон не поддерживается в этом браузере 🙁 Попробуй Chrome"); return false; } stopRec(); stopSpeak(); var r=new REC(); r.lang="en-US"; r.interimResults=false; r.maxAlternatives=3; r.onresult=function(e){ var t=""; for(var i=0;i/g,">"); } var toastT; function toast(msg){ var t=$("toast"); t.textContent=msg; t.classList.add("show"); clearTimeout(toastT); toastT=setTimeout(function(){ t.classList.remove("show"); },2200); } /* ---------- render: header + dashboard ---------- */ function renderTop(){ $("streak").innerHTML="🔥 "+state.streak; $("level").textContent="Intermediate"; $("bellBtn").classList.toggle("on", !!state.reminderOn); } function renderDashboard(){ var elapsed = dayKey+1; var total=180; var pct = Math.min(100, Math.round(elapsed/total*100)); var left = Math.max(0, total-elapsed); $("goalTitle").textContent = "Цель: свободный английский за 6 месяцев"; $("goalSub").textContent = "Ты на пути! Каждый день — новый шаг к свободной речи с коллегами."; $("goalBar").style.width = pct+"%"; $("goalMetaL").innerHTML = "День "+elapsed+" из "+total; $("goalMetaR").innerHTML = "Осталось "+left+" дн."; // mission chips var h=""; SECTIONS.forEach(function(s){ var d=isDone(s.id); h+='
'+s.icon+''+s.label+'
'; }); $("mission").innerHTML=h; $("mission").querySelectorAll(".mchip").forEach(function(el){ el.addEventListener("click",function(){ switchTab(el.getAttribute("data-sec")); window.scrollTo({top:0,behavior:"smooth"}); }); }); } /* ---------- render: tabs ---------- */ function renderTabs(){ var h=""; SECTIONS.forEach(function(s){ h+=''; }); h+=''; $("tabrow").innerHTML=h; $("tabrow").querySelectorAll(".tab").forEach(function(el){ el.addEventListener("click",function(){ switchTab(el.getAttribute("data-sec")); }); }); } function switchTab(id){ stopSpeak(); stopRec(); ACTIVE=id; document.querySelectorAll(".tab").forEach(function(el){ el.classList.toggle("active", el.getAttribute("data-sec")===id); }); document.querySelectorAll(".panel").forEach(function(p){ p.classList.remove("active"); }); $(id).classList.add("active"); renderPanel(id); var isTest = id==="test"; $("dayBar").style.display = isTest ? "none" : ""; document.querySelector(".done-bar").style.display = isTest ? "none" : ""; document.body.style.paddingBottom = isTest ? "24px" : "84px"; if(!isTest) renderDoneBar(); } /* ---------- day bar ---------- */ function renderDayBar(){ $("dayTag").innerHTML = 'День '+(curIdx+1)+' / '+N+(curIdx===todayContentIdx?' · сегодня':' · повторение'); } function bindDayNav(){ $("prevDay").onclick=function(){ if(ACTIVE==="test") return; curIdx=(curIdx-1+N)%N; rerender(); }; $("nextDay").onclick=function(){ if(ACTIVE==="test") return; curIdx=(curIdx+1)%N; rerender(); }; } /* ---------- panel router ---------- */ function renderPanel(id){ var d=day(); if(id==="words") renderWords(d); if(id==="dialogue") renderDialogue(d); if(id==="reading") renderReading(d); if(id==="listening") renderListening(d); if(id==="speaking") renderSpeaking(d); if(id==="writing") renderWriting(d); if(id==="test") renderTest(); } function renderTopicCard(d){ return '
'+d.emoji+'
'+esc(d.topic)+'
Тема дня · Intermediate
'; } /* ---------- words ---------- */ function renderWords(d){ var h=renderTopicCard(d); h+='

📖 Новые слова ('+d.words.length+')

Нажми на карточку, чтобы увидеть значение. ▶ — послушать произношение.
'; d.words.forEach(function(w,i){ var learned = state.wordsLearned.indexOf(w.en)>=0; h+='
'+esc(w.en)+'
'+esc(w.ru)+'
'+esc(w.def)+'
"'+esc(w.ex)+'"
'+(learned?"⭐ Изучено":"")+'
'; }); h+='
'; h+='
'; $("words").innerHTML=h; $("words").querySelectorAll(".word").forEach(function(el){ el.addEventListener("click",function(e){ if(e.target.closest(".play")) return; el.classList.toggle("open"); }); }); $("words").querySelectorAll(".play").forEach(function(el){ el.addEventListener("click",function(e){ e.stopPropagation(); var i=+el.getAttribute("data-play"); var w=d.words[i]; stopSpeak(); speak(w.en); if(state.wordsLearned.indexOf(w.en)<0){ state.wordsLearned.push(w.en); save(); } var card=el.closest(".word"); card.querySelector(".stars").textContent="⭐ Изучено"; }); }); $("playAll").onclick=function(){ stopSpeak(); var p=Promise.resolve(); d.words.forEach(function(w){ p=p.then(function(){ return speak(w.en,{rate:0.95}).then(function(){ return new Promise(function(r){ setTimeout(r,250); }); }); }); }); }; } /* ---------- dialogue ---------- */ function renderDialogue(d){ var h=renderTopicCard(d); h+='

💬 Диалог дня

▶ — проиграть. Нажми строку, чтобы увидеть перевод.
'; h+='
'; h+='
'; d.dialogue.forEach(function(l,i){ h+='
'+esc(l.name)+'
'+esc(l.text)+'
'+esc(l.tr)+'
'; }); h+='
'; h+='
Совет: повторяй реплики вслух за диктором — это тренирует произношение и скорость речи.
'; h+='
'; $("dialogue").innerHTML=h; $("lines").querySelectorAll(".line").forEach(function(el){ el.addEventListener("click",function(){ el.classList.toggle("show-tr"); }); }); $("toggleTr").onclick=function(){ var all=$("lines").querySelectorAll(".line"); var anyOff=false; all.forEach(function(el){ if(!el.classList.contains("show-tr")) anyOff=true; }); all.forEach(function(el){ el.classList.toggle("show-tr", anyOff); }); }; $("playAllDlg").onclick=function(){ stopSpeak(); stopRec(); var p=Promise.resolve(); d.dialogue.forEach(function(l){ p=p.then(function(){ return speak(l.text,{rate:0.95, pitch: l.who==="A"?1:1.15, voice: l.who==="A"?"Male":"Female"}); }).then(function(){ return new Promise(function(r){ setTimeout(r,350); }); }); }); }; } /* ---------- reading ---------- */ function renderReading(d){ var r=d.reading; var h=renderTopicCard(d); h+='

📰 '+esc(r.title)+'

Прочитай текст и ответь на вопросы.
'; h+='

'+esc(r.text)+'

'; r.questions.forEach(function(qi,idx){ h+='
'+(idx+1)+'. '+esc(qi.q)+'
'; qi.options.forEach(function(op,oi){ h+=''; }); h+='
'+esc(qi.ex)+'
'; }); h+='
'; $("reading").innerHTML=h; bindQuiz("reading", r.questions); } /* ---------- listening ---------- */ function renderListening(d){ var l=d.listening; var h=renderTopicCard(d); h+='

🎧 Аудирование

Послушай аудио и ответь на вопросы. Можно менять скорость.
'; h+='
'; h+=''; h+='
'+esc(l.text)+'
'; l.questions.forEach(function(qi,idx){ h+='
'+(idx+1)+'. '+esc(qi.q)+'
'; qi.options.forEach(function(op,oi){ h+=''; }); h+='
'+esc(qi.ex)+'
'; }); h+='
'; $("listening").innerHTML=h; var rate=1; $("playL").onclick=function(){ stopSpeak(); speak(l.text,{rate:rate}); }; $("listening").querySelectorAll(".speed button").forEach(function(b){ b.onclick=function(){ rate=parseFloat(b.getAttribute("data-sp")); $("listening").querySelectorAll(".speed button").forEach(function(x){ x.classList.remove("on"); }); b.classList.add("on"); }; }); $("showText").onclick=function(){ $("lText").classList.toggle("show"); this.textContent=$("lText").classList.contains("show")?"Скрыть текст":"Показать текст"; }; bindQuiz("listening", l.questions); } /* ---------- quiz shared ---------- */ function bindQuiz(panelId, questions){ var root=$(panelId); root.querySelectorAll(".q").forEach(function(qEl, qIdx){ var answered=false; qEl.querySelectorAll(".opt").forEach(function(oEl){ oEl.addEventListener("click",function(){ if(answered) return; answered=true; var o=+oEl.getAttribute("data-o"); var correct=questions[qIdx].answer; qEl.querySelectorAll(".opt").forEach(function(x){ x.classList.add("disabled"); }); if(o===correct){ oEl.classList.add("correct"); } else{ oEl.classList.add("wrong"); var c=qEl.querySelector('.opt[data-o="'+correct+'"]'); if(c) c.classList.add("correct"); } qEl.querySelector(".explain").classList.add("show"); updateScore(root, questions.length, panelId); }); }); }); } function updateScore(root, total, panelId){ var got=0; root.querySelectorAll(".q").forEach(function(qEl){ if(qEl.querySelector(".opt.correct") && !qEl.querySelector(".opt.wrong")) got++; }); var sc=$(panelId==="reading"?"rScore":"lScore"); if(sc){ sc.textContent="Результат: "+got+" / "+total+(got===total?" 🎉 Отлично!":""); sc.classList.add("show"); } } /* ---------- speaking ---------- */ function renderSpeaking(d){ var s=d.speaking; var sup = !!REC; var h=renderTopicCard(d); h+='

🎤 Практика говорения

Прочитай задание и ответь вслух. Я распознаю речь и сравню с образцом.
'; h+='
Задание: '+esc(s.prompt)+'
'; h+='
'; if(sup){ h+=''; h+='
Нажми на микрофон и говори
'; } else { h+='
🎤
'; h+='
Микрофон недоступен в этом браузере
'; h+='
Открой в Chrome на телефоне или компьютере, чтобы практиковать речь.
'; } h+='
Твой ответ появится здесь…
'; h+='
'; h+='
Образцовый ответ:'+esc(s.target)+'
'; h+='
'; h+='
💡 Не бойся ошибок — главное пробовать говорить вслух каждый день. Это ключ к свободной речи.
'; h+='
'; $("speaking").innerHTML=h; $("hearModel").onclick=function(){ stopSpeak(); speak(s.target,{rate:0.9}); }; $("showTarget").onclick=function(){ $("target").classList.toggle("show"); }; if(sup){ var mic=$("mic"), state2=$("micState"), tr=$("tr"); mic.onclick=function(){ if(mic.classList.contains("rec")){ stopRec(); return; } var ok=startRec(function(t){ tr.innerHTML=esc(t); var sim=similarity(t, s.target); var lv=levelOf(sim); var label = lv==="good"?"Отлично! 🎉":lv==="ok"?"Неплохо, почти! 👍":"Попробуй ещё раз 💪"; var tr2=document.createElement("div"); tr2.className="match "+lv; tr2.textContent=label+" ("+Math.round(sim*100)+"% совпадение)"; tr.appendChild(tr2); },function(){ mic.classList.remove("rec"); mic.textContent="🎤"; state2.textContent="Нажми на микрофон и говори"; }); if(ok){ mic.classList.add("rec"); mic.textContent="⏹"; state2.innerHTML='Слушаю…'; } }; } } /* ---------- writing ---------- */ function renderWriting(d){ var w=d.writing; var key="w_"+todayStr()+"_"+curIdx; var saved=localStorage.getItem(key)||""; var h=renderTopicCard(d); h+='

✍️ Практика письма

Напиши ответ и сравни с образцом.
'; h+='
Задание: '+esc(w.prompt)+'
'; h+=''; h+='
'; h+='
Образцовый ответ:'+esc(w.model)+'
'; h+='
💡 '+esc(w.tips)+'
'; h+='
'; $("writing").innerHTML=h; $("saveW").onclick=function(){ localStorage.setItem(key, $("ta").value); toast("Сохранено ✅"); }; $("showModel").onclick=function(){ $("model").classList.toggle("show"); }; } /* ---------- done bar ---------- */ function renderDoneBar(){ var sec=SECTIONS.filter(function(s){return s.id===ACTIVE;})[0]; var done=isDone(ACTIVE); var c=completedToday(); $("doneInfo").innerHTML = 'Сегодня выполнено: '+c.length+"/"+SECTIONS.length+' · '+sec.label; var b=$("doneBtn"); b.textContent=done?"✓ Выполнено":"Отметить выполненным"; b.classList.toggle("done", done); } function bindDoneBar(){ $("doneBtn").onclick=function(){ var on=!isDone(ACTIVE); setDone(ACTIVE, on); renderDoneBar(); renderDashboard(); if(on){ var c=completedToday(); if(c.length===SECTIONS.length) toast("Все задания дня готовы! Так держать 🚀"); else toast("Готово! +1 к прогрессу 💪"); } }; } /* ---------- rerender ---------- */ function rerender(){ renderDayBar(); renderPanel(ACTIVE); renderDoneBar(); } /* ---------- reminders ---------- */ function openReminder(){ $("remTime").value=state.reminderTime; $("remOn").checked=!!state.reminderOn; var perm = window.Notification ? Notification.permission : "unsupported"; $("remPerm").textContent = perm==="granted"?"Уведомления разрешены ✅":perm==="denied"?"Уведомления заблокированы в браузере":"Разрешение ещё не запрошено"; $("remModal").classList.add("show"); } function closeReminder(){ $("remModal").classList.remove("show"); } function saveReminder(){ state.reminderTime=$("remTime").value||"20:00"; var on=$("remOn").checked; if(on && window.Notification && Notification.permission!=="granted" && Notification.permission!=="denied"){ Notification.requestPermission(); } state.reminderOn=on; save(); renderTop(); scheduleReminder(); closeReminder(); toast(on ? ("Напоминания включены на "+state.reminderTime) : "Напоминания выключены"); } function maybeNotify(){ if(!state.reminderOn) return; var c=completedToday(); if(c.length>=SECTIONS.length) return; var today=todayStr(); if(state.lastNotified===today) return; var now=new Date(); var hh=+state.reminderTime.slice(0,2), mm=+state.reminderTime.slice(3,5); if(now.getHours()*60+now.getMinutes() < hh*60+mm) return; state.lastNotified=today; save(); showBanner(); if(window.Notification && Notification.permission==="granted"){ try{ new Notification("SpeakDaily 🔔",{body:"Пора заниматься английским! Осталось "+(SECTIONS.length-c.length)+" блок(ов) сегодня."}); }catch(e){} } } function showBanner(){ var b=$("reminderBanner"); var c=completedToday(); b.innerHTML='🔔 Пора заниматься! Осталось '+(SECTIONS.length-c.length)+' из '+SECTIONS.length+' блоков сегодня.'; b.hidden=false; $("banGo").onclick=function(){ switchTab("words"); window.scrollTo({top:0,behavior:"smooth"}); }; } function scheduleReminder(){ clearTimeout(window._remT); if(!state.reminderOn) return; var now=new Date(); var hh=+state.reminderTime.slice(0,2), mm=+state.reminderTime.slice(3,5); var target=new Date(now); target.setHours(hh,mm,0,0); if(target<=now) target.setDate(target.getDate()+1); window._remT=setTimeout(function(){ maybeNotify(); scheduleReminder(); }, target-now+1000); } /* ---------- monthly test ---------- */ var testState=null; function genTest(){ var allWords=flatten(CUR.map(function(d){return d.words;})); var wq=shuffle(allWords).slice(0,6).map(function(w){ var distr=shuffle(allWords.filter(function(x){return x.en!==w.en;})).slice(0,2).map(function(x){return x.ru;}); var opts=shuffle([w.ru].concat(distr)); return {q:'Перевод слова: "'+w.en+'"', options:opts, answer:opts.indexOf(w.ru), ex:'Правильно: '+w.ru}; }); var dq=[]; shuffle(CUR.slice()).slice(0,3).forEach(function(d){ var i=1+Math.floor(Math.random()*(d.dialogue.length-1)); var prev=d.dialogue[i-1], cur=d.dialogue[i]; var pool=flatten(CUR.map(function(x){return x.dialogue;})).filter(function(l){return l.text!==cur.text;}); var distr=shuffle(pool).slice(0,2).map(function(l){return l.text;}); var opts=shuffle([cur.text].concat(distr)); dq.push({q:'После реплики: "'+prev.text+'" что отвечает '+cur.name+'?', options:opts, answer:opts.indexOf(cur.text), ex:cur.text}); }); var rq=[]; shuffle(CUR.slice()).slice(0,2).forEach(function(d){ var qi=d.reading.questions[Math.floor(Math.random()*d.reading.questions.length)]; rq.push({title:d.reading.title, q:qi.q, options:qi.options, answer:qi.answer, ex:qi.ex}); }); var lq=[]; shuffle(CUR.slice()).slice(0,2).forEach(function(d){ var qi=d.listening.questions[Math.floor(Math.random()*d.listening.questions.length)]; lq.push({audio:d.listening.text, q:qi.q, options:qi.options, answer:qi.answer, ex:qi.ex}); }); var sq=shuffle(CUR.slice()).slice(0,2).map(function(d){return d.speaking;}); var wri=shuffle(CUR.slice())[0].writing; return {words:wq, dialogue:dq, reading:rq, listening:lq, speaking:sq, writing:wri, answers:{speaking:{}, writing:0}}; } function qhtml(block,i,qi){ var h='
'+(i+1)+'. '+esc(qi.q)+'
'; qi.options.forEach(function(op,oi){ h+=''; }); h+='
'+esc(qi.ex)+'
'; return h; } function speakTestHtml(i,s){ var sup=!!REC; var h='
Задание: '+esc(s.prompt)+'
'; if(sup){ h+='
Нажми и говори
'; } else { h+='
🎤
Микрофон недоступен
'; } h+='
Твой ответ…
'; h+='
'; h+='
Образец:'+esc(s.target)+'
'; return h; } function renderTest(){ var m=currentMonth(); var past=state.tests[m]; var h='
🎯
Тест месяца
'+monthLabel(m)+' · проверка по всем блокам
'; h+='

📅 История тестов

'; var any=false; for(var i=0;i<=5;i++){ if(state.tests[i]){ any=true; h+='
'+monthLabel(i)+(i===m?' · текущий':'')+''+state.tests[i].pct+'%
'; } else { h+='
'+monthLabel(i)+(i===m?' · текущий':'')+'не пройден
'; } } if(!any) h+='
Пройди тест в конце месяца, чтобы проверить прогресс по каждому блоку.
'; h+='
'; if(testState){ h+='

📖 Блок: Слова

'; testState.words.forEach(function(qi,i){ h+=qhtml("words",i,qi); }); h+='
'; h+='

💬 Блок: Диалог

'; testState.dialogue.forEach(function(qi,i){ h+=qhtml("dialogue",i,qi); }); h+='
'; h+='

📰 Блок: Чтение

'; testState.reading.forEach(function(qi,i){ if(qi.title) h+='
Текст: '+esc(qi.title)+'
'; h+=qhtml("reading",i,qi); }); h+='
'; h+='

🎧 Блок: Аудирование

'; testState.listening.forEach(function(qi,i){ h+='
'; h+=qhtml("listening",i,qi); }); h+='
'; h+='

🎤 Блок: Говорение

'; testState.speaking.forEach(function(s,i){ h+=speakTestHtml(i,s); }); h+='
'; h+='

✍️ Блок: Письмо

'; h+='
Задание: '+esc(testState.writing.prompt)+'
'; h+=''; h+='
'; h+='
Образец:'+esc(testState.writing.model)+'
'; h+='
Сравни с образцом и оцени себя:
'; h+='
'; h+='
'; h+=''; } else if(past){ h+='

✅ Результат за '+monthLabel(m)+'

'; SECTIONS.forEach(function(s){ var r=past[s.id]; var cls=r?(r.score/r.total>=0.7?"good":r.score/r.total>=0.4?"ok":"bad"):""; h+='
'+s.icon+'
'+s.label.toUpperCase()+'
'+(r?r.score+'/'+r.total:'—')+'
'; }); h+='
Итог: '+past.pct+'%
'; h+='
'; } else { h+='

Готов к проверке?

Тест по 6 блокам: слова, диалог, чтение, аудио, говорение и письмо. Вопросы собираются из всех тем курса.
'; } $("test").innerHTML=h; if($("startTest")) $("startTest").onclick=function(){ testState=genTest(); renderTest(); window.scrollTo({top:0,behavior:"smooth"}); }; if($("retakeTest")) $("retakeTest").onclick=function(){ testState=genTest(); renderTest(); window.scrollTo({top:0,behavior:"smooth"}); }; if($("finishTest")) $("finishTest").onclick=finishTest; if(testState) bindTestInteractions(); } function bindTestInteractions(){ var root=$("test"); // MCQ root.querySelectorAll(".q").forEach(function(qEl){ var answered=false; qEl.querySelectorAll(".opt").forEach(function(oEl){ oEl.addEventListener("click",function(){ if(answered) return; answered=true; var block=qEl.getAttribute("data-block"); var o=+oEl.getAttribute("data-o"); var qs=testState[block][+qEl.getAttribute("data-i")]; var correct=qs.answer; qEl.querySelectorAll(".opt").forEach(function(x){ x.classList.add("disabled"); }); if(o===correct){ oEl.classList.add("correct"); } else{ oEl.classList.add("wrong"); var c=qEl.querySelector('.opt[data-o="'+correct+'"]'); if(c) c.classList.add("correct"); } qEl.querySelector(".explain").classList.add("show"); }); }); }); // listening play root.querySelectorAll(".tplay").forEach(function(b){ b.onclick=function(){ var i=+b.getAttribute("data-i"); stopSpeak(); speak(testState.listening[i].audio); }; }); // speaking root.querySelectorAll(".tmic").forEach(function(mic){ mic.onclick=function(){ var i=+mic.getAttribute("data-i"); var s=testState.speaking[i]; if(mic.classList.contains("rec")){ stopRec(); return; } var ok=startRec(function(t){ var tr=root.querySelector('.ttr[data-i="'+i+'"]'); tr.innerHTML=esc(t); var sim=similarity(t,s.target); testState.answers.speaking[i]=sim; var lv=levelOf(sim); var d=document.createElement("div"); d.className="match "+lv; d.textContent=(lv==="good"?"Отлично!":lv==="ok"?"Неплохо!":"Ещё раз")+" ("+Math.round(sim*100)+"%)"; tr.appendChild(d); },function(){ mic.classList.remove("rec"); mic.textContent="🎤"; root.querySelector('.tstate[data-i="'+i+'"]').textContent="Нажми и говори"; }); if(ok){ mic.classList.add("rec"); mic.textContent="⏹"; root.querySelector('.tstate[data-i="'+i+'"]').innerHTML='Слушаю…'; } }; }); root.querySelectorAll(".thear").forEach(function(b){ b.onclick=function(){ var i=+b.getAttribute("data-i"); stopSpeak(); speak(testState.speaking[i].target,{rate:0.9}); }; }); root.querySelectorAll(".tshow").forEach(function(b){ b.onclick=function(){ var i=+b.getAttribute("data-i"); root.querySelector('.ttarget[data-i="'+i+'"]').classList.toggle("show"); }; }); // writing if($("twModel")) $("twModel").onclick=function(){ $("twModelBox").classList.toggle("show"); }; root.querySelectorAll(".wrate").forEach(function(b){ b.onclick=function(){ testState.answers.writing=+b.getAttribute("data-r"); root.querySelectorAll(".wrate").forEach(function(x){ x.classList.remove("primary"); x.classList.add("outline"); }); b.classList.remove("outline"); b.classList.add("primary"); }; }); } function finishTest(){ var t=testState, ans=t.answers; function blockScore(block){ var got=0,total=0; $("test").querySelectorAll('.q[data-block="'+block+'"]').forEach(function(qEl){ total++; if(qEl.querySelector(".opt.correct")&&!qEl.querySelector(".opt.wrong")) got++; }); return {score:got,total:total}; } var res={}; res.words=blockScore("words"); res.dialogue=blockScore("dialogue"); res.reading=blockScore("reading"); res.listening=blockScore("listening"); var sg=0; t.speaking.forEach(function(s,i){ if((ans.speaking[i]||0)>=0.6) sg++; }); res.speaking={score:sg,total:t.speaking.length}; res.writing={score:ans.writing||0,total:2}; var ts=0,tt=0; SECTIONS.forEach(function(s){ ts+=res[s.id].score; tt+=res[s.id].total; }); res.pct=Math.round(ts/tt*100); res.date=todayStr(); var m=currentMonth(); state.tests[m]=res; save(); testState=null; renderTest(); toast("Тест завершён! Итог: "+res.pct+"%"); window.scrollTo({top:0,behavior:"smooth"}); } /* ---------- init ---------- */ function init(){ load(); computeDay(); renderTop(); renderDashboard(); renderTabs(); renderDayBar(); bindDayNav(); bindDoneBar(); $("bellBtn").onclick=openReminder; $("remSave").onclick=saveReminder; $("remCancel").onclick=closeReminder; $("remModal").addEventListener("click",function(e){ if(e.target===$("remModal")) closeReminder(); }); scheduleReminder(); maybeNotify(); switchTab("words"); } if(document.readyState!=="loading") init(); else document.addEventListener("DOMContentLoaded", init); })();