decks/learning-outcomes-assessment/script.js
2026-07-07 08:34:19 +05:30

616 lines
22 KiB
JavaScript

/*
* ANIMATION STORYBOARD
*
* 0ms old course run starts, all weeks reset
* 260ms next week activates, student log receives one consequence
* 1200ms active week reaches exam signal, learner state updates
* 2400ms run ends with diagnosis visible in course file
*/
const TIMING = {
oldCourseStep: 260,
feedbackDelay: 900
};
const $ = (selector, root = document) => root.querySelector(selector);
const $$ = (selector, root = document) => Array.from(root.querySelectorAll(selector));
const course = {
outcome: null,
practice: null,
assessment: null,
feedback: null,
opening: null,
classMoves: {},
memory: new Set()
};
const labels = {
outcome: {
vague: "understand trees, graphs, and hashing",
usable: "choose and justify a data structure for a stated constraint",
tooBig: "design industrial-scale storage systems from scratch"
},
practice: {
lecture: "lecture examples only",
compare: "compare structures for a constraint",
debug: "debug a bad choice in pairs"
},
assessment: {
definitions: "definition-heavy exam",
justify: "scenario justification task",
project: "mini-project with design memo"
},
feedback: {
late: "after the midterm",
quick: "same-class comparison feedback",
revision: "revision attempt after feedback"
},
opening: {
rules: "grading rules first",
mystery: "real design failure first",
support: "outcome, practice, and help route first"
},
move: {
hook: "opening problem",
predict: "prediction",
pair: "pair talk",
quiz: "quick quiz",
paper: "minute paper"
}
};
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function setText(selector, text) {
const node = typeof selector === "string" ? $(selector) : selector;
if (node) node.textContent = text;
}
function initProgress() {
const bar = $(".progress-line span");
const update = () => {
const max = document.documentElement.scrollHeight - window.innerHeight;
const percent = max <= 0 ? 0 : (window.scrollY / max) * 100;
bar.style.width = `${clamp(percent, 0, 100)}%`;
};
update();
window.addEventListener("scroll", update, { passive: true });
}
function initNav() {
const links = $$(".topbar nav a");
const byId = new Map(links.map((link) => [link.getAttribute("href").slice(1), link]));
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
links.forEach((link) => link.classList.remove("active"));
const link = byId.get(entry.target.id);
if (link) link.classList.add("active");
});
},
{ rootMargin: "-36% 0px -54% 0px", threshold: 0.01 }
);
links.forEach((link) => {
const section = $(link.getAttribute("href"));
if (section) observer.observe(section);
});
}
function initWeeks() {
const track = $("#weekTrack");
if (!track) return;
track.replaceChildren();
for (let i = 1; i <= 12; i += 1) {
const week = document.createElement("div");
week.className = "week";
week.textContent = `W${i}`;
track.append(week);
}
}
function runOldCourse() {
const weeks = $$(".week");
const log = $("#studentLog");
const entries = [
"Week 2: Maya copies the list implementation. It works, but she cannot explain the design choice.",
"Week 5: Trees arrive. The course has moved on before her earlier misconception is visible.",
"Week 8: The midterm asks definitions. Her effort shifts toward remembering names.",
"Week 12: She passes many recall items. Transfer is still fragile."
];
weeks.forEach((week) => week.classList.remove("active"));
log.replaceChildren();
setText(log.appendChild(document.createElement("p")), "The run starts from the topic list.");
let i = 0;
const timer = window.setInterval(() => {
if (i > 0) weeks[i - 1]?.classList.remove("active");
if (i >= weeks.length) {
window.clearInterval(timer);
updateLearnerNotes("diagnosed");
updateCourseFile();
return;
}
weeks[i].classList.add("active");
if ([1, 4, 7, 11].includes(i)) {
const p = document.createElement("p");
p.textContent = entries.shift();
log.append(p);
}
i += 1;
}, TIMING.oldCourseStep);
}
function outcomeFeedback(kind) {
const node = $("#outcomeFeedback");
const messages = {
vague: [
"This is a topic in disguise.",
"It does not tell you what students must produce, so assessment will drift."
],
usable: [
"This can design the course.",
"It names a student action, a judgment, and a realistic course-level capability."
],
tooBig: [
"This overpromises.",
"Ambition helps only when students can practice the component skills inside the course."
]
};
const [title, detail] = messages[kind];
node.innerHTML = `<strong>${title}</strong><span>${detail}</span>`;
}
function initOutcomes() {
$$("#outcomeChoices .choice-card").forEach((button) => {
button.addEventListener("click", () => {
$$("#outcomeChoices .choice-card").forEach((item) => item.classList.toggle("selected", item === button));
course.outcome = button.dataset.outcome;
outcomeFeedback(course.outcome);
updateLearnerNotes("outcome");
updateCourseFile();
updateAlignmentMap();
updatePlan();
});
});
}
function initAlignmentControls() {
$$("#practiceChoices button").forEach((button) => {
button.addEventListener("click", () => {
$$("#practiceChoices button").forEach((item) => item.classList.toggle("selected", item === button));
course.practice = button.dataset.practice;
updateAlignmentMap();
updateLearnerNotes("alignment");
updateCourseFile();
updatePlan();
});
});
$$("#assessmentChoices button").forEach((button) => {
button.addEventListener("click", () => {
$$("#assessmentChoices button").forEach((item) => item.classList.toggle("selected", item === button));
course.assessment = button.dataset.assessment;
updateAlignmentMap();
updateLearnerNotes("alignment");
updateCourseFile();
updatePlan();
});
});
}
function updateAlignmentMap() {
const outcome = course.outcome ? labels.outcome[course.outcome] : "not written";
const practice = course.practice ? labels.practice[course.practice] : "listening";
const assessment = course.assessment ? labels.assessment[course.assessment] : "recall exam";
const feedback = course.feedback ? labels.feedback[course.feedback] : "after exam";
setText("#mapOutcome", shorten(outcome, 24));
setText("#mapPractice", shorten(practice, 24));
setText("#mapAssessment", shorten(assessment, 24));
setText("#mapFeedback", shorten(feedback, 24));
const goodOutcome = course.outcome === "usable";
const goodPractice = course.practice === "compare" || course.practice === "debug";
const goodAssessment = course.assessment === "justify" || course.assessment === "project";
const goodFeedback = course.feedback === "quick" || course.feedback === "revision";
$(".wire-a")?.classList.toggle("strong", goodOutcome);
$(".wire-b")?.classList.toggle("strong", goodPractice);
$(".wire-c")?.classList.toggle("strong", goodAssessment);
$(".wire-d")?.classList.toggle("strong", goodFeedback);
$(".wire-c")?.classList.toggle("warn", course.assessment === "definitions");
const hidden = $("#hiddenCourse");
if (!course.assessment || course.assessment === "definitions") {
hidden.classList.remove("good");
hidden.innerHTML = "<strong>The hidden course is still recall.</strong><span>Students will protect marks by memorizing definitions.</span>";
} else if (goodOutcome && goodPractice && goodAssessment) {
hidden.classList.add("good");
hidden.innerHTML = "<strong>The hidden course now matches the stated course.</strong><span>Students can see that practice and marks both point toward justified choice.</span>";
} else {
hidden.classList.remove("good");
hidden.innerHTML = "<strong>The signals are mixed.</strong><span>One corner has improved, but students still receive competing messages about effort.</span>";
}
}
function shorten(text, limit) {
return text.length > limit ? `${text.slice(0, limit - 3)}...` : text;
}
function initClassBuilder() {
const palette = $$("#movePalette button");
const slotRow = $("#slotRow");
let selectedMove = null;
palette.forEach((button) => {
button.addEventListener("click", () => {
selectedMove = button.dataset.move;
palette.forEach((item) => item.classList.toggle("selected", item === button));
});
});
slotRow.replaceChildren();
[0, 10, 20, 30, 40, 50].forEach((minute) => {
const slot = document.createElement("button");
slot.type = "button";
slot.className = "slot";
slot.dataset.minute = String(minute);
slot.textContent = `${minute} min`;
slot.addEventListener("click", () => {
if (!selectedMove) {
setText("#classFeedback", "Choose a move first, then place it in the hour.");
return;
}
course.classMoves[minute] = selectedMove;
slot.classList.add("filled");
slot.textContent = `${minute}: ${labels.move[selectedMove]}`;
updateAttention();
updateLearnerNotes("class");
updateCourseFile();
updatePlan();
});
slotRow.append(slot);
});
updateAttention();
}
function attentionAt(minute) {
const placed = Object.entries(course.classMoves)
.map(([key, value]) => ({ minute: Number(key), move: value }))
.sort((a, b) => a.minute - b.minute);
const previous = placed.filter((item) => item.minute <= minute).pop();
const since = previous ? minute - previous.minute : minute;
const reset = previous && since < 5 ? 11 : 0;
const endLift = minute > 54 ? (minute - 54) * 2 : 0;
return clamp(86 - since * 2.15 + reset + endLift, 26, 94);
}
function updateAttention() {
const path = $("#attentionCurve");
const markers = $("#attentionMarkers");
if (!path || !markers) return;
const points = [];
for (let minute = 0; minute <= 60; minute += 2) {
const x = 42 + (minute / 60) * 700;
const y = 232 - (attentionAt(minute) / 100) * 190;
points.push(`${x},${y}`);
}
path.setAttribute("d", `M ${points.join(" L ")}`);
markers.replaceChildren();
Object.entries(course.classMoves).forEach(([minute, move]) => {
const x = 42 + (Number(minute) / 60) * 700;
const y = 232 - (attentionAt(Number(minute)) / 100) * 190;
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
circle.setAttribute("cx", x);
circle.setAttribute("cy", y);
circle.setAttribute("r", 8);
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute("x", x);
text.setAttribute("y", y - 15);
text.setAttribute("text-anchor", "middle");
text.textContent = labels.move[move];
markers.append(circle, text);
});
const moves = Object.values(course.classMoves);
const hasHook = moves.includes("hook");
const thinkingMoves = moves.filter((move) => ["predict", "pair", "quiz", "paper"].includes(move)).length;
if (hasHook && thinkingMoves >= 2) {
setText("#classFeedback", "This hour now asks students to predict, explain, or retrieve before the idea goes cold.");
} else if (thinkingMoves >= 1) {
setText("#classFeedback", "Good start. Add one more thinking move so the middle of the hour does not sag.");
} else {
setText("#classFeedback", "Place an opening problem and two short thinking moves.");
}
}
function initFeedbackLab() {
const bars = $("#persistenceBars");
bars.replaceChildren();
["same day", "one week", "midterm"].forEach((label) => {
const row = document.createElement("div");
row.className = "persistence-row";
row.innerHTML = `<span>${label}</span><div class="bar-shell"><span></span></div>`;
bars.append(row);
});
updatePersistence("late");
$$(".feedback-choices button").forEach((button) => {
button.addEventListener("click", () => {
$$(".feedback-choices button").forEach((item) => item.classList.toggle("selected", item === button));
course.feedback = button.dataset.feedback;
updatePersistence(course.feedback);
updateAlignmentMap();
updateLearnerNotes("feedback");
updateCourseFile();
updatePlan();
});
});
}
function updatePersistence(kind) {
const levels = {
late: [80, 72, 58],
quick: [58, 30, 18],
revision: [62, 24, 10]
};
$$(".bar-shell span").forEach((bar, index) => {
bar.style.setProperty("--amount", `${levels[kind][index]}%`);
bar.style.background = kind === "late" ? "var(--coral)" : "var(--teal)";
});
}
function initOpening() {
$$("#openingChoices button").forEach((button) => {
button.addEventListener("click", () => {
$$("#openingChoices button").forEach((item) => item.classList.toggle("selected", item === button));
course.opening = button.dataset.opening;
const response = $("#motivationResponse");
if (course.opening === "rules") {
response.classList.remove("good");
response.innerHTML = "<strong>Maya hears that marks matter.</strong><span>Value, expectancy, and support are still mostly implicit.</span>";
} else if (course.opening === "mystery") {
response.classList.add("good");
response.innerHTML = "<strong>Maya sees value first.</strong><span>The opening gives the outcome a reason to exist before naming the machinery.</span>";
} else {
response.classList.add("good");
response.innerHTML = "<strong>Maya sees the route.</strong><span>The opening joins value with expectancy: here is the goal, the practice path, and where help lives.</span>";
}
updateLearnerNotes("motivation");
updateCourseFile();
updatePlan();
});
});
}
function initMemoryChecks() {
$$(".memory-check").forEach((check) => {
$$("button", check).forEach((button) => {
button.addEventListener("click", () => {
$$("button", check).forEach((item) => item.classList.remove("correct", "incorrect"));
const isRight = button.dataset.answer === "right";
button.classList.add(isRight ? "correct" : "incorrect");
const feedback = $(".check-feedback", check);
if (isRight) {
course.memory.add(check.dataset.check);
feedback.textContent = "Settled. This will show up in the course file.";
} else {
feedback.textContent = "Not yet. Try the option that changes what the teacher designs next.";
}
updateCourseFile();
});
});
});
}
function updateLearnerNotes(stage) {
const notes = {
diagnosed: [
"She can repeat definitions after lecture.",
"She waits until the exam to find out what counts.",
"She cannot yet transfer the ideas to a new problem."
],
outcome: [
course.outcome === "usable" ? "She can see the course is about justified choices, not names alone." : "The destination is still hard to recognize from a student's seat.",
"She needs practice that matches the promise.",
"The exam will decide what she actually studies."
],
alignment: [
course.practice === "compare" || course.practice === "debug" ? "She has a place to practice choosing, not just hearing." : "She is still mostly watching someone else reason.",
course.assessment === "justify" || course.assessment === "project" ? "Marks now reward the capability the outcome names." : "The visible reward still points toward recall.",
"Alignment is becoming a message she can read."
],
class: [
Object.values(course.classMoves).length >= 3 ? "She has to predict, explain, and retrieve during class." : "The class has started to ask for thinking, but the rhythm is still thin.",
"Short tasks make confusion visible before the exam.",
"Attention is being used as a design material."
],
feedback: [
course.feedback === "late" ? "Her misconception survives until the midterm." : "Her misconception gets interrupted while it is still fixable.",
"Feedback now changes the next attempt, not just the grade record.",
"Formative work has become part of teaching."
],
motivation: [
course.opening === "rules" ? "She hears the grading system first." : "She sees why the outcome matters before the syllabus details.",
course.opening === "support" ? "She also sees how effort can succeed." : "Support still needs to be named clearly.",
"Motivation is being designed, not wished for."
]
};
const list = $("#learnerNotes");
list.replaceChildren();
notes[stage].forEach((note) => {
const item = document.createElement("li");
item.textContent = note;
list.append(item);
});
}
function designLevels() {
let use = 22;
let confidence = 18;
let transfer = 14;
if (course.outcome === "usable") {
use += 18;
transfer += 16;
} else if (course.outcome) {
use += 6;
}
if (course.practice === "compare" || course.practice === "debug") {
use += 16;
transfer += 14;
}
if (course.assessment === "justify" || course.assessment === "project") {
use += 16;
confidence += 14;
transfer += 12;
}
if (Object.values(course.classMoves).length >= 3) {
confidence += 14;
use += 10;
}
if (course.feedback === "quick" || course.feedback === "revision") {
confidence += 18;
transfer += 10;
}
if (course.opening === "mystery" || course.opening === "support") {
confidence += 10;
use += 8;
}
return {
use: clamp(use, 8, 96),
confidence: clamp(confidence, 8, 96),
transfer: clamp(transfer, 8, 96)
};
}
function updateCourseFile() {
setText("#spineOutcome", course.outcome ? labels.outcome[course.outcome] : "not yet written");
setText("#spinePractice", course.practice ? labels.practice[course.practice] : "mostly listening");
setText("#spineAssessment", course.assessment ? labels.assessment[course.assessment] : "midterm plus final");
setText("#spineFeedback", course.feedback ? labels.feedback[course.feedback] : "late and scarce");
setText("#spineMotivation", course.opening ? labels.opening[course.opening] : "grades carry the message");
const levels = designLevels();
$("#meterUse").style.setProperty("--fill", `${levels.use}%`);
$("#meterConfidence").style.setProperty("--fill", `${levels.confidence}%`);
$("#meterTransfer").style.setProperty("--fill", `${levels.transfer}%`);
const memory = $("#memoryTrace");
memory.replaceChildren();
if (course.memory.size === 0) {
const span = document.createElement("span");
span.textContent = "0 checks settled";
memory.append(span);
} else {
course.memory.forEach((check) => {
const span = document.createElement("span");
span.className = "settled";
span.textContent = check;
memory.append(span);
});
}
const aligned =
course.outcome === "usable" &&
(course.practice === "compare" || course.practice === "debug") &&
(course.assessment === "justify" || course.assessment === "project") &&
(course.feedback === "quick" || course.feedback === "revision");
if (aligned) {
setText("#fileStatus", "The course now gives students one coherent message.");
} else if (course.outcome || course.practice || course.assessment || course.feedback || course.opening) {
setText("#fileStatus", "The course is improving, but at least one signal still needs work.");
} else {
setText("#fileStatus", "Topic coverage is still driving the course.");
}
}
function updatePlan() {
const moves = Object.entries(course.classMoves)
.sort((a, b) => Number(a[0]) - Number(b[0]))
.map(([minute, move]) => `${minute} min: ${labels.move[move]}`);
const plan = `Data Structures I - first class redesign
Learning outcome
${course.outcome ? labels.outcome[course.outcome] : "Write one observable course outcome before class."}
Opening
${course.opening ? labels.opening[course.opening] : "Open by showing why the outcome matters to a real design problem."}
In-class practice
${moves.length ? moves.join("\n") : "0 min: opening problem\n15 min: prediction\n30 min: think-pair-share\n45 min: one-minute quiz"}
Assessment signal
${course.assessment ? labels.assessment[course.assessment] : "Use a scenario task that asks students to justify a structure."}
Feedback route
${course.feedback ? labels.feedback[course.feedback] : "Give quick formative feedback before the first high-stakes exam."}
Teacher check
Students should leave knowing what they are trying to learn, how they will practice it, how it will be assessed, and where to get help.`;
setText("#finalPlan", plan);
}
function initCopy() {
$("#copyPlan")?.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText($("#finalPlan").textContent);
setText("#copyStatus", "Copied.");
} catch {
const range = document.createRange();
range.selectNodeContents($("#finalPlan"));
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
setText("#copyStatus", "Selected the plan text.");
}
});
}
function initReset() {
$$("[data-action='reset']").forEach((button) => {
button.addEventListener("click", () => {
course.outcome = null;
course.practice = null;
course.assessment = null;
course.feedback = null;
course.opening = null;
course.classMoves = {};
course.memory.clear();
$$(".selected, .correct, .incorrect, .filled").forEach((node) => node.classList.remove("selected", "correct", "incorrect", "filled"));
$$(".slot").forEach((slot) => {
slot.textContent = `${slot.dataset.minute} min`;
});
updateLearnerNotes("diagnosed");
updateAlignmentMap();
updateAttention();
updatePersistence("late");
updateCourseFile();
updatePlan();
setText("#classFeedback", "Place an opening problem and two short thinking moves.");
$("#outcomeFeedback").innerHTML = "<strong>No outcome chosen yet.</strong><span>The course still has topics, not a destination.</span>";
$("#motivationResponse").innerHTML = "<strong>Maya is waiting to see what this course values.</strong><span>The opening has not yet told her why effort will be worth it.</span>";
$("#hiddenCourse").innerHTML = "<strong>The hidden course is still recall.</strong><span>Students will protect marks by memorizing definitions.</span>";
});
});
}
document.addEventListener("DOMContentLoaded", () => {
initProgress();
initNav();
initWeeks();
initOutcomes();
initAlignmentControls();
initClassBuilder();
initFeedbackLab();
initOpening();
initMemoryChecks();
initCopy();
initReset();
$("#runOldCourse")?.addEventListener("click", runOldCourse);
updateLearnerNotes("diagnosed");
updateAlignmentMap();
updateCourseFile();
updatePlan();
});