// One assignment: its material (pages or the bank's written quiz), the student's answer as pictures (from the board, the camera, a file or // the phone) and handing it in; afterwards its state, and the grade and note or the request to correct. import { api } from "./api.js"; import { stateWord } from "./ai_sheet.js"; import { loadBoard } from "./exercise.js"; import { figures } from "./figures.js"; import { assignmentState } from "./home.js"; import { withBusy } from "./busy.js"; import { esc, fmt } from "./math.js"; import { isTouch, pictureUrl, prepare, sendPicture } from "./media.js"; import * as phone from "./phone.js"; import { shortDate, tabs } from "./ui.js"; let draw = null, current = null, draft = [], message = ""; const draftKey = id => `assignment-draft:${id}`; const saveDraft = () => { try { sessionStorage.setItem(draftKey(current.id), JSON.stringify(draft)); } catch { /* private window */ } }; const loadDraft = id => { try { return JSON.parse(sessionStorage.getItem(draftKey(id)) || "null"); } catch { return null; } }; const q = selector => document.querySelector(selector); const editable = a => ["not_handed", "handed_in", "returned"].includes(a.state); /** Fill an with a private picture (fetched with the sign-in). */ async function fill(img, id) { try { img.src = await pictureUrl(id); img.hidden = false; } catch (error) { img.replaceWith(Object.assign(document.createElement("span"), { className: "muted", textContent: error.message })); } } function material(a) { if (a.kind === "exercise_test") return `

למבחן התרגול

`; if (a.kind === "quiz" && a.quiz) { return `

הבוחן הכתוב (${a.quiz.minutes} דקות · ${a.quiz.points} נקודות)

    ${a.quiz.questions.map(x => `
  1. ${fmt(x.question)}

    ${figures(x)}${x.points} נק׳
  2. `).join("")}
`; } return a.pages.length ? `

הדף

${a.pages.map((id, i) => ``).join("")}
` : ""; } function stateBox(a) { const s = assignmentState(a); const note = a.teacher_note ? `

הערת המורה: ${fmt(a.teacher_note)}

` : ""; const head = `

${s.word}

`; if (a.state === "returned") return `${head}
המורה החזיר/ה את המשימה לתיקון.${note}

תקנו והגישו שוב.

`; if (a.state === "graded") return `${head}

הציון: ${a.grade}

${note}`; return head; } /** What the student can do about a handed-in answer: ask for an AI check (it waits for the teacher), see its result as a suggestion, or ask * the teacher to look again. */ function aiBox(a) { const ai = a.ai; if (!ai || (!ai.can_check && !ai.can_look_again && !ai.asked_again && !ai.checks.length)) return ""; const checks = ai.checks.map(c => { const r = c.result; const body = c.state === "done" ? (r.score === null || r.score === undefined ? `

ה־AI לא הצליח לקרוא את התשובה. המורה יבדוק/תבדוק.

` : `

הציון המוצע: ${r.score} (המורה קובע/ת את הציון)

`) + `

${fmt(r.feedback || "")}

${r.next_step ? `

${fmt(r.next_step)}

` : ""}` : c.state === "declined" ? `

${c.teacher_note ? fmt(c.teacher_note) : "המורה לא שלח/ה את הבקשה."}

` : c.state === "failed" ? `

${esc(c.failure?.message || "לא הצלחנו כרגע. נסו שוב מאוחר יותר.")}

` : ""; return `

${esc(stateWord(c.state))}

${body}
`; }).join(""); return `

בדיקה נוספת

${ai.can_check ? `

המורה יראה/תראה את הבקשה קודם.

` : ""} ${ai.can_look_again ? `

` : ""} ${ai.asked_again ? `

ביקשתם מהמורה לבדוק שוב.

` : ""}${checks}
`; } function draftHtml() { return draft.length ? draft.map(id => `
  • `).join("") : `
  • עוד לא הוספתם תמונה.
  • `; } function renderDraft() { q("#draft").innerHTML = draftHtml(); q("#draft").querySelectorAll("img[data-picture]").forEach(img => fill(img, img.dataset.picture)); q("#hand-in").disabled = !draft.length; saveDraft(); } function answerBox(a) { if (a.kind === "exercise_test") return ""; if (!editable(a)) { return a.pictures.length ? `

    התשובה שהגשתי

    ${a.pictures.map(p => p.deleted ? `

    התמונות נמחקו

    ` : ``).join("")}
    ` : ""; } return `

    ${a.state === "handed_in" ? "התשובה שהגשתי (אפשר להחליף עד שהמורה בודק/ת)" : "התשובה שלי"}

    ${isTouch() ? `` : ""} ${!isTouch() ? `` : ""}

    ${esc(message)}

    `; } function add(id) { if (!q("#draft")) return; // the page was left while a photo was on its way if (!draft.includes(id)) draft.push(id); renderDraft(); } async function addFiles(files) { const error = q("#add-error"); error.hidden = true; for (const file of files) { if (draft.length >= 10) { error.textContent = "אפשר להגיש עד 10 תמונות"; error.hidden = false; break; } try { add((await sendPicture("/api/pictures", await prepare(file))).id); } catch (e) { error.textContent = e.message; error.hidden = false; } } } async function openBoard() { await loadBoard(); if (!window.MathlyBoard?.openFull) return; const host = Object.assign(document.createElement("div"), { className: "mx-layer" }); document.body.append(host); window.MathlyBoard.openFull({ host, key: `assignment:${current.id}`, title: "כתיבה על הלוח", sendLabel: "הוספה להגשה", submit: async (page, blob) => { if (draft.length >= 10) return { error: "אפשר להגיש עד 10 תמונות" }; try { add((await sendPicture("/api/pictures", blob)).id); return {}; } catch (e) { return { error: e.message }; } }, done: () => host.remove(), }); } async function handIn() { const button = q("#hand-in"); button.disabled = true; q("#hand-msg").textContent = "שולחים…"; try { await api(`/api/assignments/${current.id}/submission`, { method: "POST", body: { pictures: draft } }); try { sessionStorage.removeItem(draftKey(current.id)); } catch { /* nothing */ } message = "ההגשה נשלחה. המורה יבדוק/תבדוק."; await show(draw, current.id); } catch (error) { button.disabled = false; q("#hand-msg").textContent = ""; q("#add-error").textContent = error.message || "משהו השתבש"; q("#add-error").hidden = false; } } let pollTimer = null; function wireAi(id) { clearTimeout(pollTimer); const ask = async path => { try { await api(path, { method: "POST" }); } catch (e) { message = e.message; } await show(draw, id); }; q("#ask-check")?.addEventListener("click", e => withBusy(e.currentTarget, ask(`/api/assignments/${current.id}/ai-check`))); q("#ask-again")?.addEventListener("click", e => withBusy(e.currentTarget, ask(`/api/assignments/${current.id}/look-again`))); if ((current.ai?.checks || []).some(c => ["waiting", "processing"].includes(c.state)) && q("#assignment")) { // look again while a check is on its way pollTimer = setTimeout(() => { if (q("#assignment")?.dataset.id === String(current.id)) show(draw, id); }, 5000); } } export async function show(view, id) { draw = view; current = await api(`/api/assignments/${encodeURIComponent(id)}`); const saved = loadDraft(current.id); draft = editable(current) ? (saved ?? current.pictures.filter(p => !p.deleted).map(p => p.id)) : []; view(`${tabs("submissions")}

    ‹ ההגשות

    ${fmt(current.title)}

    ${esc(current.class.name)} · עד ${shortDate(current.due_date)}${current.overdue ? " · באיחור" : ""}

    ${current.note ? `

    ${fmt(current.note)}

    ` : ""}${stateBox(current)}
    ${material(current)}
    ${answerBox(current)}${aiBox(current)}`); wireAi(id); document.querySelectorAll("img[data-picture]:not(#draft img)").forEach(img => fill(img, img.dataset.picture)); if (!editable(current) || current.kind === "exercise_test") return; renderDraft(); q("#board-btn").addEventListener("click", openBoard); q("#hand-in").addEventListener("click", handIn); q("#draft").addEventListener("click", event => { const id = event.target.closest("[data-remove]")?.dataset.remove; if (id) { draft = draft.filter(x => x !== id); renderDraft(); } }); document.querySelectorAll("[data-file]").forEach(input => input.addEventListener("change", async () => { const files = [...input.files]; input.value = ""; await withBusy(input.closest("label"), addFiles(files)); })); q("#ask-phone")?.addEventListener("click", () => phone.askFromComputer(current, id => add(id), q("#phone-panel"))); }