File size: 11,220 Bytes
0815e01 fd475e7 0815e01 fd475e7 0815e01 fd475e7 0815e01 fd475e7 0815e01 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | // 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 <img> 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 `<p><a class="button primary" href="#/exam/${a.test.id}/${a.test.number}">למבחן התרגול</a></p>`;
if (a.kind === "quiz" && a.quiz) {
return `<h3>הבוחן הכתוב <span class="muted">(${a.quiz.minutes} דקות · ${a.quiz.points} נקודות)</span></h3><ol class="qz-written" id="written-quiz">${a.quiz.questions.map(x =>
`<li><p class="ex-text" dir="auto">${fmt(x.question)}</p>${figures(x)}<span class="muted">${x.points} נק׳</span></li>`).join("")}</ol>`;
}
return a.pages.length ? `<h3>הדף</h3><div class="pages" id="pages">${a.pages.map((id, i) => `<img data-picture="${esc(id)}" alt="עמוד ${i + 1}" hidden>`).join("")}</div>` : "";
}
function stateBox(a) {
const s = assignmentState(a);
const note = a.teacher_note ? `<p class="teacher-note" id="teacher-note"><b>הערת המורה:</b> ${fmt(a.teacher_note)}</p>` : "";
const head = `<p id="state" role="status"><span class="tag st-${esc(a.state)}"><span aria-hidden="true">${s.symbol}</span> ${s.word}</span></p>`;
if (a.state === "returned") return `${head}<div class="returned" id="returned"><b>המורה החזיר/ה את המשימה לתיקון.</b>${note}<p>תקנו והגישו שוב.</p></div>`;
if (a.state === "graded") return `${head}<p class="grade-big" id="grade">הציון: <b>${a.grade}</b></p>${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 ? `<p>ה־AI לא הצליח לקרוא את התשובה. המורה יבדוק/תבדוק.</p>` : `<p>הציון המוצע: <b>${r.score}</b> (המורה קובע/ת את הציון)</p>`) + `<p>${fmt(r.feedback || "")}</p>${r.next_step ? `<p class="muted">${fmt(r.next_step)}</p>` : ""}`
: c.state === "declined" ? `<p>${c.teacher_note ? fmt(c.teacher_note) : "המורה לא שלח/ה את הבקשה."}</p>`
: c.state === "failed" ? `<p class="error">${esc(c.failure?.message || "לא הצלחנו כרגע. נסו שוב מאוחר יותר.")}</p>` : "";
return `<div class="ai-check" data-check="${esc(c.id)}" data-state="${esc(c.state)}"><p><span class="tag st-${esc(c.state)}">${esc(stateWord(c.state))}</span></p>${body}</div>`;
}).join("");
return `<section class="card" id="ai-box"><h3>בדיקה נוספת</h3>
${ai.can_check ? `<p><button type="button" id="ask-check">בקשת בדיקה של AI</button> <span class="muted">המורה יראה/תראה את הבקשה קודם.</span></p>` : ""}
${ai.can_look_again ? `<p><button type="button" id="ask-again">בקשה מהמורה לבדוק שוב</button></p>` : ""}
${ai.asked_again ? `<p class="muted" id="again-asked">ביקשתם מהמורה לבדוק שוב.</p>` : ""}${checks}</section>`;
}
function draftHtml() {
return draft.length ? draft.map(id => `<li data-picture-id="${esc(id)}"><img data-picture="${esc(id)}" alt="תמונה של התשובה" hidden>
<button type="button" class="remove" data-remove="${esc(id)}" aria-label="הסרת התמונה">✕</button></li>`).join("") : `<li class="muted" id="draft-empty">עוד לא הוספתם תמונה.</li>`;
}
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 ? `<section class="card" id="answer"><h3>התשובה שהגשתי</h3><div class="pages" id="submitted">${a.pictures.map(p => p.deleted
? `<p class="muted" data-deleted>התמונות נמחקו</p>` : `<img data-picture="${esc(p.id)}" alt="התשובה שהגשתי" hidden>`).join("")}</div></section>` : "";
}
return `<section class="card" id="answer"><h3>${a.state === "handed_in" ? "התשובה שהגשתי (אפשר להחליף עד שהמורה בודק/ת)" : "התשובה שלי"}</h3>
<ul class="plain drafts" id="draft">${draftHtml()}</ul>
<p class="actions" id="add-actions"><button type="button" id="board-btn">✎ כתיבה על הלוח</button>
${isTouch() ? `<label class="button file">📷 צילום<input type="file" accept="image/*" capture="environment" hidden data-file="camera"></label>` : ""}
<label class="button file">🖼 העלאת תמונה<input type="file" accept="image/jpeg,image/png,image/webp" multiple hidden data-file="upload"></label>
${!isTouch() ? `<button type="button" id="ask-phone">📱 בקשת צילום מהטלפון</button>` : ""}</p>
<div id="phone-panel"></div>
<p class="error" id="add-error" role="alert" hidden></p>
<p><button type="button" class="primary" id="hand-in" disabled>${a.state === "handed_in" ? "החלפת ההגשה" : "הגשה"}</button></p>
<p id="hand-msg" role="status" class="muted">${esc(message)}</p></section>`;
}
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")}<p><a class="back" href="#/submissions">‹ ההגשות</a></p>
<section class="card" id="assignment" data-id="${current.id}" data-state="${esc(current.state)}"><h2>${fmt(current.title)}</h2>
<p class="muted">${esc(current.class.name)} · עד ${shortDate(current.due_date)}${current.overdue ? " · באיחור" : ""}</p>
${current.note ? `<p id="assignment-note">${fmt(current.note)}</p>` : ""}${stateBox(current)}</section>
<section class="card" id="material">${material(current)}</section>${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")));
}
|