Download mathly2/static/manager.js from wzender/mathly: direct link, hf CLI and curl.
- Browser
- Download file 11.1 kB
-
https://huggingface.co/spaces/wzender/mathly/resolve/main/mathly2/static/manager.js
- Command line
-
hf download hf://spaces/wzender/mathly/mathly2/static/manager.js
-
curl -L -o manager.js https://huggingface.co/spaces/wzender/mathly/resolve/main/mathly2/static/manager.js
11.1 kB
| // The manager's screens: every class (make, change, delete), every student, the curriculum and the subjects. A manager sees no learning data. | |
| import { api } from "./api.js"; | |
| import { esc } from "./math.js"; | |
| import * as subjectsTab from "./subjects_tab.js"; | |
| let draw, tab = "classes", editing = null, draft = null, flash = { message: "", error: "" }, lessonId = null; | |
| // draft: what was typed in the editor when a save was refused, so the form keeps it and the person can fix one thing | |
| const note = () => `${flash.error && tab !== "edit" ? `<p class="error" role="alert">${esc(flash.error)}</p>` : ""}${flash.message ? `<p role="status" id="msg">${esc(flash.message)}</p>` : ""}`; | |
| const tabs = () => `<div class="subtabs" id="mgr-tabs" role="group" aria-label="诪谞讛诇 讻讬转讜转"> | |
| ${[["classes", "讻讬转讜转"], ["edit", editing ? "注专讬讻转 讻讬转讛" : "讻讬转讛 讞讚砖讛"], ["students", "转诇诪讬讚讬诐"], ["curriculum", "转讻谞讬转 诇讬诪讜讚讬诐"], ["subjects", "诪拽爪讜注讜转"]].map(([id, label]) => | |
| `<button type="button" data-tab="${id}" aria-pressed="${tab === id || (tab === "lesson" && id === "subjects")}">${label}</button>`).join("")}</div>`; | |
| async function render() { | |
| let body, failed = false; | |
| try { // a tab whose data cannot be read says so, instead of the click doing nothing | |
| if (tab === "edit") subjects = (await api("/api/subjects")).subjects; | |
| body = tab === "classes" ? await classesView() : tab === "students" ? await studentsView() : tab === "curriculum" ? await curriculumView() | |
| : tab === "subjects" ? await subjectsTab.view() : tab === "lesson" ? `<div id="lesson-slot"></div>` : editorView(); | |
| } catch (e) { | |
| failed = true; | |
| body = `<p class="error" role="alert" id="tab-error">诇讗 讛爪诇讞谞讜 诇驻转讜讞 讗转 讛诇砖讜谞讬转: ${esc(e.message)}</p>`; | |
| } | |
| draw(`<section id="mgr"><h2>诪谞讛诇 讻讬转讜转</h2>${tabs()}${note()}${body}</section>`); | |
| document.getElementById("mgr-tabs").addEventListener("click", event => { | |
| const next = event.target.closest("[data-tab]")?.dataset.tab; | |
| if (!next) return; | |
| if (next === "edit" && tab !== "edit") editing = null; | |
| draft = null; | |
| go(next); | |
| }); | |
| if (failed) { flash = { message: "", error: "" }; return; } | |
| if (tab === "classes") wireClasses(); | |
| if (tab === "curriculum") wireCurriculum(); | |
| const openLesson = id => { lessonId = id; go("lesson"); }; | |
| if (tab === "subjects") subjectsTab.wire(go, (message, error) => { flash = { message, error }; }, openLesson); | |
| if (tab === "lesson") (await import("./lesson_editor.js")).open(document.getElementById("lesson-slot"), lessonId, () => go("subjects")); | |
| if (tab === "edit") wireEditor(); | |
| if (tab === "edit" && flash.error) { // the form is long: bring the reason into view, next to the buttons that were pressed | |
| const reason = document.getElementById("form-error"); | |
| reason.scrollIntoView({ block: "center" }); | |
| reason.focus(); | |
| } | |
| flash = { message: "", error: "" }; | |
| } | |
| /** The curriculum: the date by which each lesson should be completed, one for the whole school. */ | |
| async function curriculumView() { | |
| const lessons = await api("/api/curriculum"); | |
| return `<form class="card" id="curriculum-form"><h3>转讻谞讬转 诇讬诪讜讚讬诐</h3> | |
| <p class="muted">讛转讗专讬讱 砖注讚 讗诇讬讜 讻诇 砖讬注讜专 讗诪讜专 诇讛住转讬讬诐. 转诇诪讬讚 砖诇讗 住讬讬诐 砖讬注讜专 讗讞专讬 讛转讗专讬讱 专讜讗讛 讗讜转讜 诪住讜诪谉 讘驻讬讙讜专. 讗驻砖专 诇讛砖讗讬专 转讗专讬讱 专讬拽.</p> | |
| ${[...new Set(lessons.map(l => l.subject?.id || "math"))].map(sid => { | |
| const mine = lessons.filter(l => (l.subject?.id || "math") === sid); | |
| return `<section class="curriculum-subject" data-subject="${esc(sid)}"><h4>${esc(mine[0].subject?.name || "诪转诪讟讬拽讛")}</h4> | |
| <ul class="plain curriculum">${mine.map(l => `<li><label for="due-${esc(l.id)}">${esc(l.name)} <span class="muted">${esc(l.month)}</span></label> | |
| <input id="due-${esc(l.id)}" type="date" data-lesson="${esc(l.id)}" value="${esc(l.due_date || "")}"></li>`).join("")}</ul></section>`; | |
| }).join("")} | |
| <p><button class="primary" type="submit" id="save-curriculum">砖诪讬专讛</button></p></form>`; | |
| } | |
| function wireCurriculum() { | |
| document.getElementById("curriculum-form").addEventListener("submit", async event => { | |
| event.preventDefault(); | |
| const dates = Object.fromEntries([...document.querySelectorAll("#curriculum-form [data-lesson]")].map(i => [i.dataset.lesson, i.value || null])); | |
| try { await api("/api/curriculum", { method: "PUT", body: { dates } }); flash = { message: "讛转讻谞讬转 谞砖诪专讛.", error: "" }; } | |
| catch (e) { flash = { message: "", error: e.message }; } | |
| go("curriculum"); | |
| }); | |
| } | |
| const go = next => { tab = next; return render(); }; | |
| async function classesView() { | |
| const classes = await api("/api/classes"); | |
| return `<div class="hrow" style="margin-bottom:10px"><button class="primary" type="button" id="new-class">+ 讻讬转讛 讞讚砖讛</button> | |
| <span class="muted">${classes.length} 讻讬转讜转</span></div> | |
| ${classes.length ? `<div class="cards" id="classes">${classes.map(c => `<article class="card" data-id="${c.id}"> | |
| <div class="hrow"><h3 class="grow">${esc(c.name)}</h3><button type="button" data-edit="${c.id}">注专讬讻讛</button><button type="button" class="danger" data-delete="${c.id}">诪讞讬拽讛</button></div> | |
| <p style="margin:0;font-size:14px">诪讜专讛: <span dir="ltr">${esc(c.teacher_email || "注讜讚 诇讗 谞讘讞专")}</span><br> | |
| ${c.students} 转诇诪讬讚讬诐 路 AI: <b>${c.ai_enabled ? "驻讜注诇" : "讻讘讜讬"}</b> 路 ${c.ai_daily_checks} 讘讚讬拽讜转 讘讬讜诐</p></article>`).join("")}</div>` | |
| : `<section class="card"><p class="muted">注讜讚 讗讬谉 讻讬转讜转. 驻转讞讜 讻讬转讛 专讗砖讜谞讛 讜讛讜住讬驻讜 讗转 讛转诇诪讬讚讬诐.</p></section>`}`; | |
| } | |
| function wireClasses() { | |
| document.getElementById("new-class").addEventListener("click", () => { editing = null; draft = null; go("edit"); }); | |
| document.getElementById("classes")?.addEventListener("click", async event => { | |
| const edit = event.target.closest("[data-edit]")?.dataset.edit, del = event.target.closest("[data-delete]")?.dataset.delete; | |
| if (edit) { editing = await api(`/api/classes/${edit}`); draft = null; return go("edit"); } | |
| if (del) { | |
| const card = event.target.closest("article"); | |
| if (!confirm(`诇诪讞讜拽 讗转 讛讻讬转讛 "${card.querySelector("h3").textContent}"? 讛讛转拽讚诪讜转 砖诇 讛转诇诪讬讚讬诐 谞砖诪专转.`)) return; | |
| try { await api(`/api/classes/${del}`, { method: "DELETE" }); flash.message = "讛讻讬转讛 谞诪讞拽讛."; } | |
| catch (e) { flash.error = e.message; } | |
| go("classes"); | |
| } | |
| }); | |
| } | |
| let subjects = []; // every subject, for the class editor's choice (loaded when the editor opens) | |
| function editorView() { | |
| const c = editing, d = draft; // a refused save keeps what was typed | |
| const value = (typed, saved, fallback = "") => d ? (typed ?? fallback) : (saved ?? fallback); | |
| const studies = d ? d.subjects : (c?.subjects || ["math"]); | |
| return `<form class="card" id="class-form"><h3>${c ? `注专讬讻转 ${esc(c.name)}` : "讻讬转讛 讞讚砖讛"}</h3> | |
| <label for="c-name">砖诐 讛讻讬转讛</label><input id="c-name" required maxlength="80" autocomplete="off" value="${esc(value(d?.name, c?.name))}"> | |
| <label for="c-teacher">诪讜专讛 讛讻讬转讛 (讻转讜讘转 讗讞转; 诇讗 讗讞讚 讛转诇诪讬讚讬诐 砖诇 讛讻讬转讛)</label> | |
| <input id="c-teacher" type="text" dir="ltr" autocomplete="off" value="${esc(value(d?.teacher_email, c?.teacher_email))}"> | |
| <label for="c-students">转诇诪讬讚讬诐 (讻转讜讘转 讘讻诇 砖讜专讛; 转诇诪讬讚 讬讻讜诇 诇讛讬讜转 讘讻诪讛 讻讬转讜转)</label> | |
| <textarea id="c-students" dir="ltr" spellcheck="false">${esc(d ? d.students : (c?.student_emails || []).join("\n"))}</textarea> | |
| <div class="hrow" style="margin-top:12px"><label class="switch"><input id="c-ai" type="checkbox" ${value(d?.ai_enabled, c?.ai_enabled, false) ? "checked" : ""}> AI 诇讻讬转讛</label> | |
| <label for="c-budget" style="margin:0">讘讚讬拽讜转 AI 讘讬讜诐</label><input id="c-budget" type="number" min="0" max="10000" style="width:110px" value="${value(d?.ai_daily_checks, c?.ai_daily_checks, 150)}"></div> | |
| <p class="muted">讛志AI 讻讘讜讬 注讚 砖讘讬转 讛住驻专 诪讗砖专 砖讛诪砖驻讞讜转 诪住讻讬诪讜转. 专拽 讗讞专讬 讝讛 诪驻注讬诇讬诐 讗讜转讜.</p> | |
| <fieldset class="c-subjects" id="c-subjects"><legend>诪拽爪讜注讜转 砖讛讻讬转讛 诇讜诪讚转</legend> | |
| ${subjects.map(s => `<label class="switch"><input type="checkbox" value="${esc(s.id)}" ${studies.includes(s.id) ? "checked" : ""}> <span aria-hidden="true">${esc(s.icon)}</span> ${esc(s.name)}</label>`).join("")} | |
| <p class="muted">讛讻讬转讛 诇讜诪讚转 讗转 讻诇 讛砖讬注讜专讬诐 砖驻讜专住诪讜 讘诪拽爪讜注讜转 砖谞讘讞专讜. 讛住专转 诪拽爪讜注 诇讗 诪讜讞拽转 讗转 讛讛转拽讚诪讜转 砖诇 讛转诇诪讬讚讬诐.</p></fieldset> | |
| ${flash.error ? `<p class="error" role="alert" id="form-error" tabindex="-1"><b>诇讗 谞砖诪专:</b> ${esc(flash.error)}</p>` : ""} | |
| <div class="hrow"><button class="primary" type="submit" id="c-save">${c ? "砖诪讬专转 讛砖讬谞讜讬讬诐" : "驻转讬讞转 讛讻讬转讛"}</button><button type="button" id="c-cancel">讘讬讟讜诇</button></div></form>`; | |
| } | |
| function wireEditor() { | |
| document.getElementById("c-cancel").addEventListener("click", () => { editing = null; draft = null; go("classes"); }); | |
| document.getElementById("class-form").addEventListener("submit", async event => { | |
| event.preventDefault(); | |
| const body = { | |
| name: document.getElementById("c-name").value, | |
| teacher_email: document.getElementById("c-teacher").value.trim() || null, | |
| students: document.getElementById("c-students").value, | |
| ai_enabled: document.getElementById("c-ai").checked, | |
| ai_daily_checks: Number(document.getElementById("c-budget").value), | |
| subjects: [...document.querySelectorAll("#c-subjects input:checked")].map(i => i.value), | |
| }; | |
| try { | |
| const saved = editing ? await api(`/api/classes/${editing.id}`, { method: "PUT", body }) : await api("/api/classes", { method: "POST", body }); | |
| flash.message = `${editing ? "讛砖讬谞讜讬讬诐 谞砖诪专讜" : "讛讻讬转讛 谞驻转讞讛"}: ${saved.name}.${saved.invalid?.length ? ` 诇讗 谞专讗讛 讻诪讜 诪讬讬诇: ${saved.invalid.join(", ")}` : ""}`; | |
| editing = null; draft = null; | |
| go("classes"); | |
| } catch (e) { draft = body; flash.error = e.message; go("edit"); } | |
| }); | |
| } | |
| async function studentsView() { | |
| const students = await api("/api/students"); | |
| return `<div class="tablewrap" id="students-list"><table class="data"><thead><tr><th>转诇诪讬讚</th><th>讻讬转讜转</th></tr></thead> | |
| <tbody>${students.length ? students.map(s => `<tr><td dir="ltr">${esc(s.email)}</td><td>${s.classes.map(esc).join(" 路 ")}</td></tr>`).join("") | |
| : `<tr><td colspan="2" class="muted">注讚讬讬谉 诇讗 谞讜住驻讜 转诇诪讬讚讬诐 诇讻讬转讜转.</td></tr>`}</tbody></table></div>`; | |
| } | |
| export async function show(me, view) { | |
| draw = view; | |
| tab = "classes"; editing = null; | |
| await render(); | |
| } | |