const app = document.querySelector("#admin-app"); let quizzes = []; let sessions = []; let activeSid = null; let ws = null; let leaderboard = []; let roster = []; let liveHistogram = null; let currentState = null; const samplePool = { title: "Week 9 Recap: Computer Organization", score_fn: "linear_decay", time_limit_default: 60, questions: [ {id: "q1", text: "Which unit sequences control signals in a multi-cycle datapath?", options: {A: "ALU", B: "Control unit", C: "Register file", D: "Instruction memory"}, correct: "B"} ] }; function escapeText(value) { return String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", })[char]); } async function api(path, options = {}) { const response = await fetch(path, { credentials: "same-origin", headers: {"Content-Type": "application/json", ...(options.headers || {})}, ...options, }); if (!response.ok) throw new Error(await response.text()); const contentType = response.headers.get("content-type") || ""; return contentType.includes("json") ? response.json() : response.text(); } async function boot() { try { await refresh(); render(); } catch { renderLogin(); } } function renderLogin(error = "") { app.innerHTML = `

Admin Login

${error ? `

${escapeText(error)}

` : ""}
`; document.querySelector("#login-form").addEventListener("submit", async (event) => { event.preventDefault(); const form = new FormData(event.currentTarget); try { await api("/admin/login", {method: "POST", body: JSON.stringify({password: form.get("password")})}); await refresh(); render(); } catch { renderLogin("Login failed."); } }); } async function refresh() { quizzes = (await api("/admin/api/quizzes")).quizzes; sessions = (await api("/admin/api/sessions")).sessions; } function render() { app.innerHTML = `
${renderSession()}
`; document.querySelector("#new-quiz").addEventListener("click", renderQuizModal); document.querySelector("#new-session").addEventListener("click", renderSessionModal); document.querySelectorAll("[data-session]").forEach((button) => { button.addEventListener("click", () => connectSession(button.dataset.session)); }); bindControls(); } function renderSession() { if (!activeSid) return `

No active session

Create or select a session.

`; const session = sessions.find((item) => item.sid === activeSid); return `

${escapeText(session?.title || activeSid)}

${escapeText(currentState?.state || session?.state || "")}

Session ID: ${activeSid}

Download CSV

Roster (${roster.length})

${roster.map((p) => `${escapeText(p.student_id)} ${escapeText(p.name)}`).join("") || "No students yet."}

Live Histogram

${renderHistogram(liveHistogram?.histogram)}

Leaderboard

${renderLeaderboard(leaderboard)}
`; } function renderHistogram(histogram) { const data = histogram || {A: 0, B: 0, C: 0, D: 0, missed: 0, pending: 0}; return `
${Object.entries(data).map(([key, value]) => ( `
${key}${value}
` )).join("")}
`; } function renderLeaderboard(rows) { return `
    ${(rows || []).map((row) => ( `
  1. ${row.rank}. ${escapeText(row.name)} ${row.student_id ? `(${escapeText(row.student_id)})` : ""}${row.score}
  2. ` )).join("") || "
  3. No scores yet.
  4. "}
`; } function bindControls() { document.querySelectorAll("[data-command]").forEach((button) => { button.addEventListener("click", () => { if (!ws || ws.readyState !== WebSocket.OPEN) return; const command = button.dataset.command; if (command === "open_question") { ws.send(JSON.stringify({ type: command, question_idx: Number(document.querySelector("#question-idx").value || 0), time_limit: Number(document.querySelector("#time-limit").value || 60), })); } else { ws.send(JSON.stringify({type: command})); } }); }); } function renderQuizModal() { app.innerHTML = `

Add Pool

`; document.querySelector("#cancel").addEventListener("click", render); document.querySelector("#quiz-form").addEventListener("submit", async (event) => { event.preventDefault(); const pool = JSON.parse(new FormData(event.currentTarget).get("pool")); await api("/admin/api/quizzes", {method: "POST", body: JSON.stringify({pool_json: pool})}); await refresh(); render(); }); } async function renderSessionModal() { const options = quizzes.map((quiz) => ``).join(""); app.innerHTML = `

Create Session

`; document.querySelector("#cancel").addEventListener("click", render); document.querySelector("#session-form").addEventListener("submit", async (event) => { event.preventDefault(); const quizId = Number(new FormData(event.currentTarget).get("quiz_id")); const result = await api("/admin/api/sessions", {method: "POST", body: JSON.stringify({quiz_id: quizId})}); document.querySelector("#session-result").innerHTML = `

${result.sid}

${result.join_url}

QR code`; await refresh(); connectSession(result.sid); }); } function connectSession(sid) { activeSid = sid; if (ws) ws.close(); const protocol = window.location.protocol === "https:" ? "wss" : "ws"; ws = new WebSocket(`${protocol}://${window.location.host}/ws/instructor/${sid}`); ws.addEventListener("message", (event) => { const message = JSON.parse(event.data); if (message.type === "state") currentState = message; if (message.type === "lobby_update") roster = message.participants; if (message.type === "live_histogram") liveHistogram = message; if (message.type === "full_leaderboard") leaderboard = message.leaderboard; if (message.type === "question_closed") liveHistogram = {histogram: message.histogram}; render(); }); render(); } boot();