Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 49 additions & 3 deletions src/cognit/mcp/assets/quiz_mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ let suppressResults = false; // local "Discard" → show answering even though r
let grading = false; // grading in flight → pause polling re-renders
let renderedSig = null; // signature of the rendered question structure
let view = null; // waiting | answering | results | published
// Practice mode (default): deterministic questions reveal the answer + explanation the
// moment you commit. Exam mode: the classic batch flow (answer all, then submit). Sticky
// per browser via localStorage. (Open questions always grade at submit — there's no
// client-side answer to reveal.)
let examMode = localStorage.getItem("cognit.examMode") === "1";

// ── small DOM helper ────────────────────────────────────────────
function el(tag, attrs = {}, children = []) {
Expand Down Expand Up @@ -121,6 +126,7 @@ async function flushAnswers() {
// ── question renderers ──────────────────────────────────────────
function selectMCQOption(q, opts, idx) {
postAnswer(q.id, q.options[idx]);
if (!examMode) { renderQuestions(); return; } // practice: re-render to reveal + lock
opts.forEach((o, j) => {
o.classList.toggle("selected", j === idx);
o.setAttribute("aria-checked", j === idx ? "true" : "false");
Expand Down Expand Up @@ -169,6 +175,7 @@ function renderMermaid(q) {
tabindex: i === 0 ? "0" : "-1",
onclick: () => {
postAnswer(q.id, label);
if (!examMode) { renderQuestions(); return; } // practice: re-render to reveal + lock
cards.forEach((c, j) => {
c.classList.toggle("selected", j === i);
c.setAttribute("aria-checked", j === i ? "true" : "false");
Expand Down Expand Up @@ -217,6 +224,7 @@ function renderTF(q) {
text: v.charAt(0).toUpperCase() + v.slice(1),
onclick: () => {
postAnswer(q.id, v);
if (!examMode) { renderQuestions(); return; } // practice: re-render to reveal + lock
cells.forEach((c, j) => {
c.classList.toggle("sel", j === i);
c.setAttribute("aria-checked", j === i ? "true" : "false");
Expand Down Expand Up @@ -362,7 +370,26 @@ function renderAnchor(q) {
return panel;
}

const DETERMINISTIC = new Set(["mcq", "tf", "mermaid"]);
function isDeterministic(q) { return DETERMINISTIC.has(q.type); }

// Correctness we can compute on the client (the answer ships in /state). tf compares
// against the stringified boolean; mcq is full option text; mermaid is the option key.
function isCorrectLocal(q) {
const v = answers[q.id];
return q.type === "tf" ? v === String(q.answer) : v === q.answer;
}

// Practice mode: a committed deterministic question reveals immediately and locks.
function shouldReveal(q) { return !examMode && isDeterministic(q) && isAnswered(q); }

function renderQuestion(q, i) {
if (shouldReveal(q)) {
// Reuse the results card (correct/your-pick rows + explanation), with a locally
// computed verdict — no server round-trip, the answer is already in /state.
const correct = isCorrectLocal(q);
return renderResultCard(q, { question_id: q.id, correct, score: correct ? 100 : 0, feedback: "" }, i);
}
const inputsByType = { mcq: renderMCQ, mermaid: renderMermaid, open: renderOpen, tf: renderTF };
const inputs = inputsByType[q.type](q);
return el("article", { class: "file" }, [
Expand All @@ -379,8 +406,25 @@ function renderQuestion(q, i) {
}

// ── sidebar (questions state) ───────────────────────────────────
function setExamMode(on) {
examMode = on;
localStorage.setItem("cognit.examMode", on ? "1" : "0");
renderQuestions();
}

function renderSidebar() {
sidebarRoot.innerHTML = "";
sidebarRoot.appendChild(el("div", { class: "side-block" }, [
el("div", { class: "side-title", text: "Mode" }),
el("label", { class: "mode-toggle" }, [
el("input", {
type: "checkbox",
checked: !examMode, // checked = practice (reveal as you go)
onchange: (e) => setExamMode(!e.target.checked),
}),
el("span", { text: "Reveal answers as I go" }),
]),
]));
sidebarRoot.appendChild(el("div", { class: "side-block" }, [
el("div", { class: "side-title", text: "Progress" }),
el("div", { class: "progress" },
Expand Down Expand Up @@ -470,9 +514,11 @@ function renderReviewbarSubmit() {
reviewbar.className = "reviewbar is-submit";
reviewbar.innerHTML = "";
const hasOpen = quiz.questions.some(q => q.type === "open");
reviewbar.appendChild(el("div", { class: "reviewbar__msg" }, [
hasOpen ? "Open question grades after submit." : "Answers stay private until you submit.",
]));
const msg = !examMode
? (hasOpen ? "Answers reveal as you go · the open question grades at submit."
: "Answers reveal as you go · private until you submit.")
: (hasOpen ? "Open question grades after submit." : "Answers stay private until you submit.");
reviewbar.appendChild(el("div", { class: "reviewbar__msg" }, [msg]));
reviewbar.appendChild(el("div", { class: "reviewbar__spacer" }));
const btn = el("button", {
class: "btn btn--primary", type: "button", text: "Submit quiz", onclick: submitQuiz,
Expand Down
7 changes: 7 additions & 0 deletions src/cognit/mcp/assets/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,13 @@ textarea.open:focus { border-color: var(--blue); outline: none; box-shadow: 0 0
.diff-line.anchor-hit { box-shadow: inset 3px 0 0 var(--blue); }
.codepanel__note { padding: 10px 12px; font-size: 12px; color: var(--fg-mute); background: var(--bg-canvas); border-radius: 0 0 6px 6px; }

/* practice/exam mode toggle (sidebar) ────────────────────────────── */
.mode-toggle {
display: flex; align-items: center; gap: 8px;
font-size: 13px; color: var(--fg); cursor: pointer;
}
.mode-toggle input { accent-color: var(--blue); width: 15px; height: 15px; cursor: pointer; flex: 0 0 auto; }

/* diff coverage map (sidebar) ────────────────────────────────────── */
.sidelist .ic.empty { background: transparent; color: var(--fg-faint); box-shadow: inset 0 0 0 1.5px var(--border); }
.sidelist.coverage .coverage__file {
Expand Down
Loading