// A lesson round (סבב שיעור), opened from the home's map: a short round of the lesson's skills, one question at a time, with instant feedback // like the exercise screen. A wrong answer names the mistake and never marks the right choice. The last answer shows the lesson's stars. // The same sheet plays a mistakes round (חזרה על הטעויות): the questions a handed-in quiz, exercise test or simulation got wrong, with no stars. import { api, ApiError } from "./api.js"; import { figures } from "./figures.js"; import { esc, fmt } from "./math.js"; import { loadBoard } from "./exercise.js"; import { homeHref, pathHref, shuffled } from "./ui.js"; import { sendWorkQuietly } from "./boardwork.js"; const COUNTDOWN = 2; // seconds before the next question after a right answer let layer = null, round = null, timer = null, streak = 0; const q = selector => layer?.querySelector(selector); const stop = () => { clearTimeout(timer); timer = null; }; const starsText = n => "★".repeat(n) + "☆".repeat(3 - n); const CHEERS = ["נכון!", "יופי!", "מצוין!", "בדיוק!", "אלופים!"]; export const isOpen = () => !!layer; /** Start the lesson's round (or resume the open one). Returns the round, so the router knows its subject. */ export const start = lesson => api(`/api/lessons/${encodeURIComponent(lesson)}/round/start`, { method: "POST" }); /** Start (or resume) the mistakes round of a handed-in attempt. */ export const startRetry = attemptId => api(`/api/attempts/${encodeURIComponent(attemptId)}/retry/start`, { method: "POST" }); const isRetry = () => !!round?.source; /** Where closing leads: the home's map, or for a mistakes round its test's card (a quiz: the path). */ const leaveTo = () => round?.source ? (round.source.test_id ? `#/test/${round.source.test_id}` : pathHref()) : homeHref(); /** Show a round from `start` as a sheet over the home. */ export async function show(data) { stop(); await loadBoard(); round = data; streak = 0; if (!layer) { layer = document.createElement("div"); layer.className = "mx-layer round-layer"; document.body.append(layer); layer.addEventListener("click", onClick); } layer.innerHTML = `
`; render(); q(".mx-x")?.focus({ preventScroll: true }); } export function close() { if (!layer) return; stop(); window.MathlyBoard?.closeFull?.(); layer.remove(); layer = round = null; } const heading = () => `${ctx.picked ? `במבחן בחרתם: ${fmt(ctx.picked)}` : "במבחן לא עניתם על השאלה הזו."} נסו שוב.
${fmt(question.skill_name)}
${fmt(question.question)}
${figures(question)}${r.right} מתוך ${r.total} נכונות הפעם
${r.mastered.length ? `🎉 נשלטו: ${r.mastered.map(s => fmt(s.name)).join(", ")}
` : ""} ${r.missed.length ? `${m.n ? `שאלה ${m.n} · ` : ""}${fmt(m.skill_name)}
${fmt(m.question)}
${m.mistake ? `${esc(m.mistake.name)}${m.mistake.tip ? `
${fmt(m.mistake.tip)}` : ""}
הציון במבחן לא משתנה. התרגול נספר לשליטה במיומנויות.
${all ? "" : ` `}
${[0, 1, 2].map(i => `${i < r.stars ? "★" : "☆"}`).join("")}
${r.right} מתוך ${r.total}
${r.mastered.length ? `🎉 נשלטו: ${names(r.mastered)}
` : ""} ${r.almost.length ? `🔜 עוד סבב טוב ביום אחר ונשלטות: ${names(r.almost)}
` : ""} ${r.stars < 3 ? `${r.stars < 2 ? "8 מתוך 10 נותנים כוכב שני." : "הכוכב השלישי: כל המיומנויות נשלטו. חוזרים מחר לעוד סבב."}
` : ""} ${r.missed.length ? `${fmt(m.skill_name)}
${fmt(m.question)}
${m.mistake ? `${esc(m.mistake.name)}${m.mistake.tip ? `
${fmt(m.mistake.tip)}` : ""}
${esc(notice)}
` : ""}${progress()} ${round.state === "done" ? resultHtml() : questionHtml()}`; mountBoard(); } function updateProgress() { q("#rd-count").textContent = countText(); q("#rd-dots").innerHTML = dots(); } function countdown(next) { let left = COUNTDOWN; const label = q("#ex-countdown"); const tick = () => { if (!layer || !label?.isConnected) return; if (left === 0) return next(); label.textContent = `הבאה בעוד ${left}…`; left -= 1; timer = setTimeout(tick, 1000); }; tick(); } async function answer(button) { const buttons = [...layer.querySelectorAll(".ex-choice")]; buttons.forEach(b => { b.disabled = true; b.classList.toggle("dim", b !== button); }); button.classList.add("pending"); button.setAttribute("aria-busy", "true"); q("#ex-fb").innerHTML = `בודקים…
`; let result; try { result = await api(`/api/rounds/${encodeURIComponent(round.id)}/answer`, { method: "POST", body: { key: round.question.key, choice: Number(button.dataset.i) } }); } catch (error) { buttons.forEach(b => b.classList.remove("pending", "dim")); button.removeAttribute("aria-busy"); if (error instanceof ApiError && error.status === 409) { // answered in another tab: show where the round is now round = await api(`/api/rounds/${encodeURIComponent(round.id)}`); return render("הסבב התקדם בחלון אחר. זו השאלה שעל המסך עכשיו."); } buttons.forEach(b => { b.disabled = false; }); q("#ex-fb").innerHTML = `${esc(error.message || "משהו השתבש")}
`; return; } buttons.forEach(b => b.classList.remove("pending", "dim")); button.removeAttribute("aria-busy"); sendWorkQuietly(`round:${round.id}:${round.question.key}`, round.id, round.question.key); // the board of this answer, for the teacher const following = result.round; round = { ...round, results: following.results, state: following.state }; updateProgress(); const next = () => { stop(); round = following; render(); }; const last = following.state === "done"; const feedback = q("#ex-fb"); const mastered = result.mastered_now ? `🎉 ${fmt(result.mastered_now.name)}: נשלטה!
` : ""; if (result.correct) { streak += 1; button.classList.add("right"); button.insertAdjacentHTML("beforeend", ''); navigator.vibrate?.(25); feedback.innerHTML = `${fmt(m.tip)}
` : ""}${esc(error.message || "משהו השתבש")}
`); } } function onClick(event) { const target = event.target; if (target.closest("[data-close]")) { location.hash = leaveTo(); return; } if (target.closest("[data-board-toggle]")) { window.MathlyBoard?.toggle(); return; } if (target.closest("#rd-again")) { again(); return; } const choice = target.closest(".ex-choice"); if (choice && !choice.disabled) answer(choice); } window.addEventListener("mathly:leave", close); document.addEventListener("keydown", event => { if (event.key === "Escape" && layer && !document.querySelector(".mx-board.full")) location.hash = leaveTo(); });