CommunityCoding & Developmentgithub.com

KyeYongChoo/mcq-quiz-page

Use when the user asks for a multiple-choice quiz or practice questions from slides or notes, as a clickable page with green/red answers, review mode, retry of mistakes and PDF export; can include structured and program-analysis questions.

What is mcq-quiz-page?

mcq-quiz-page is a Claude Code agent skill that use when the user asks for a multiple-choice quiz or practice questions from slides or notes, as a clickable page with green/red answers, review mode, retry of mistakes and PDF export; can include structured and program-analysis questions.

Works with✓Claude Code~Codex CLI✓Cursor
Part ofMobile
npx skills add https://github.com/KyeYongChoo/mcq-quiz-page-skill/tree/HEAD/mcq-quiz-page

Installed? Explore more Coding & Development skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

MCQ quiz page

The page design is finished. Do not redesign it. Your job is to write good questions, drop them into the template below as JSON, and publish it.

What the page does:

  • Multiple choice: tap an option and it turns green (right) or red (wrong, with the correct option turned green and an explanation shown). A small "Redo this question" button under every answered question resets just that one.
  • Structured / program-analysis questions (type "short"): the user types an answer (optional), taps Show model answer, then marks themselves "I got it" or "I missed it". Missed ones count as mistakes. Code snippets show in a monospace block.
  • Review tab lists only the mistakes with the correct answer / model answer right below each. Review mode has Export PDF, Copy as text, and Redo mistakes.
  • Redo mistakes clears those questions and opens a Retry tab that shows ONLY the earlier mistakes (no scrolling through the whole quiz). It shows progress (done/total), and when all are done a summary with "Retry the N still wrong" and "Back to all questions". The Retry tab stays available until Reset all.
  • Progress, typed answers and the retry list are kept in the viewer's browser.

Steps

  1. Read the source material (slides, notes, PDFs) first. Questions must come from it, not from memory. For PDFs use pdftotext -layout. If the user attaches tutorial or past-paper solutions, base both MCQs and structured/program-analysis questions on them (new variations, not verbatim copies, unless asked).
  2. Write the questions as a JSON array (schema below). Default to 4 options for MCQs. If the user gives a count, use exactly that count. Add structured / program-analysis questions when the source has short-answer or code questions, or when the user asks for them (put them after the MCQs of the same section).
  3. Check the questions with the balance rules below, ideally with a small script.
  4. Build the page: copy the template to index.html, replace the text __DATA__ with the JSON (escape every </ as <\/), and change only: the <title>, the <h1>, the .sub sentence, and the storage key bps-quiz-v1 (give each quiz a new key, e.g. quiz-<topic>-v1, so saved progress never mixes). Run node --check on the script if possible (extract the script block to a .js file). A quick Playwright smoke test (answer, mark, Review, Redo mistakes) is worth doing.
  5. Publish as a hand-built HTML artifact: call the artifact list_types first (there is no quiz type; use publish_files with index.html, a real title, icon quiz). Then call declare_capabilities with {"downloads": true} using the newest version number. The PDF button only works with that capability. When updating an existing quiz, republish with publish_files (mode patch, slug, base_version); the capability declaration carries over. If you cannot publish artifacts, deliver index.html as a file instead (the PDF button then stays hidden; Copy as text still works).
  6. Reply in a few sentences: what the page is, how many questions (MCQ and structured) and from which sections, and that the PDF export asks the viewer to confirm the download. Do not paste the questions into chat.

JSON schema (one object per question, in order)

Multiple choice:

{"n": 1, "section": "Lecture 4 \u00b7 Linkability", "q": "Question text?", "opts": ["A text", "B text", "C text", "D text"], "ans": 2, "ex": "One-sentence explanation."}

Structured or program analysis (optional code, optional kind, optional ex):

{"n": 5, "type": "short", "section": "Lecture 4 \u00b7 Linkability", "kind": "Structured question", "q": "Describe ...?", "model": ["Point 1 of the model answer", "Point 2"], "ex": "Optional extra note"}
{"n": 6, "type": "short", "section": "...", "kind": "Program analysis", "q": "What is wrong here and how do you fix it?", "code": "int f(char *s) {\n  char b[8];\n  strcpy(b, s);\n}", "model": ["(a) ...", "(b) ..."]}

kind defaults to "Short answer"; use "Structured question" or "Program analysis". Each model entry is one bullet; label sub-parts "(a)", "(b)" inside the bullets. Write code with \n newlines and 2-space indents.

Optional per-option notes (ONLY when the user asks for extra text under the wrong options): add "notes": ["", "Why B is wrong", "", "Why D is wrong"] to an MCQ, one string per option in the same order, with "" for options that need none (usually the correct one). Notes appear under each option after the user answers, and in Copy/PDF. Do not add notes unless asked.

n runs 1..N with no gaps across all questions (the page indexes by n-1). ans is the 0-based index of the correct option. section groups questions on the page; use the same string for questions in one group and keep groups contiguous. Avoid characters outside plain Latin text where possible (the PDF uses a built-in font; unusual symbols print as ?).

Question quality rules (the user cares about these)

  • Distractors must be convincing: use real concepts from the same material attached to the wrong question (the property of a neighbouring term, a nearby number, a plausible mechanism). No joke options, no "all of the above".
  • Do not let the right answer be the longest. Write the correct option and the distractors at similar length and level of detail, and sometimes make a distractor the longest. Target: the correct option is strictly the longest on no more than about 30% of questions with full-sentence options (check with a script; it is easy to overshoot).
  • Spread the correct letter evenly across A-D (about 25% each) and never the same letter three times in a row. Easiest method: write the correct option and three distractors separately, then shuffle with a balanced position list in code.
  • Test one idea per question. Keep question text self-contained.
  • Every MCQ gets an explanation in ex that says why the answer is right (and, when helpful, what the distractors actually are).
  • Structured/program-analysis model answers should mirror the source solution's key points as short bullets, so the user can mark themselves honestly.
  • If a slide is only a diagram you cannot read, say so rather than inventing details.

Template (index.html)

<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Topic Quiz</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@500&display=swap">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<style>
:root{--bg:#f5f6f8;--card:#ffffff;--ink:#16202e;--mute:#5b6678;--line:#d5dae3;--accent:#1d4ed8;--accent-soft:#e6ecff;
--ok:#0f7a4d;--ok-soft:#dcf3e6;--ok-line:#52b887;--bad:#b42318;--bad-soft:#fde4e1;--bad-line:#e57a70;
--body:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",sans-serif;--mono:"IBM Plex Mono",ui-monospace,Menlo,monospace}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#10151d;--card:#171e29;--ink:#e6eaf1;--mute:#9aa6b8;--line:#2e3849;--accent:#8aa9ff;--accent-soft:#1f2a45;--ok:#6fe0aa;--ok-soft:#12332a;--ok-line:#2f8a63;--bad:#ff9a8f;--bad-soft:#3b1b1a;--bad-line:#a94a42;color-scheme:dark}}
:root[data-theme="dark"]{--bg:#10151d;--card:#171e29;--ink:#e6eaf1;--mute:#9aa6b8;--line:#2e3849;--accent:#8aa9ff;--accent-soft:#1f2a45;--ok:#6fe0aa;--ok-soft:#12332a;--ok-line:#2f8a63;--bad:#ff9a8f;--bad-soft:#3b1b1a;--bad-line:#a94a42;color-scheme:dark}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font:400 16px/1.55 var(--body)}
main{max-width:760px;margin:0 auto;padding:24px 16px 64px}
h1{font-size:1.7rem;line-height:1.2;margin:0 0 6px;text-wrap:balance}
.sub{color:var(--mute);margin:0 0 14px}
.bar{position:sticky;top:0;z-index:3;background:var(--bg);border-bottom:1px solid var(--line);padding:10px 0;display:flex;flex-wrap:wrap;gap:8px 10px;align-items:center}
.tabs{display:inline-flex;border:1px solid var(--line);border-radius:9px;overflow:hidden;background:var(--card)}
.tabs button{border:0;background:transparent;color:var(--ink);font:500 .88rem var(--body);padding:8px 14px;cursor:pointer}
.tabs button[aria-pressed="true"]{background:var(--accent);color:var(--bg)}
.stats{font-size:.88rem;color:var(--mute);font-variant-numeric:tabular-nums;display:flex;gap:12px;flex-wrap:wrap}
.stats b{color:var(--ink)}
.acts{margin-left:auto;display:flex;gap:8px;flex-wrap:wrap}
.btn{font:500 .85rem var(--body);color:var(--ink);background:var(--card);border:1px solid var(--line);border-radius:8px;padding:7px 12px;cursor:pointer;text-decoration:none;display:inline-flex;gap:8px;align-items:center}
.btn.primary{background:var(--accent);border-color:var(--accent);color:var(--bg)}
.btn:disabled{opacity:.45;cursor:not-allowed}
.btn:not(:disabled):hover{filter:brightness(.95)}
:focus-visible{outline:3px solid var(--accent);outline-offset:2px}
.jump{display:flex;flex-wrap:wrap;gap:8px;margin:14px 0 0}
.jump a{font:500 .85rem var(--body);color:var(--ink);background:var(--card);border:1px solid var(--line);border-radius:8px;padding:7px 11px;text-decoration:none}
.jump a span{color:var(--mute);margin-left:8px;font-variant-numeric:tabular-nums}
h2{font-size:1.08rem;margin:30px 0 14px;padding-bottom:8px;border-bottom:2px solid var(--accent);text-wrap:balance;scroll-margin-top:70px}
.banner{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px 16px;margin:18px 0 6px}
.banner p{margin:0}.banner p+p{margin-top:8px;color:var(--mute);font-size:.92rem}
.q{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:16px 16px 14px;margin-bottom:14px;scroll-margin-top:70px}
h3{font-size:1rem;font-weight:600;margin:0 0 12px;display:flex;gap:10px;line-height:1.45}
.num{font:500 .8rem var(--mono);color:var(--accent);background:var(--accent-soft);border-radius:6px;padding:2px 7px;height:fit-content;font-variant-numeric:tabular-nums}
.sec{display:block;font-weight:400;font-size:.78rem;color:var(--mute);margin-bottom:2px}
.opts{display:grid;gap:8px}
.opt{display:flex;gap:10px;align-items:flex-start;text-align:left;width:100%;font:inherit;color:inherit;background:transparent;border:1px solid var(--line);border-radius:8px;padding:10px 12px;cursor:pointer}
.opt b{font:500 .85rem var(--mono);color:var(--mute);min-width:1.2em;padding-top:1px}
.opt:not(:disabled):hover{border-color:var(--accent);background:var(--accent-soft)}
.opt:disabled{cursor:default}
.opt.ok{background:var(--ok-soft);border-color:var(--ok-line)}
.opt.ok b,.opt.ok .tag{color:var(--ok)}
.opt.bad{background:var(--bad-soft);border-color:var(--bad-line)}
.opt.bad b,.opt.bad .tag{color:var(--bad)}
.opt .txt{flex:1}
.tag{font:500 .75rem var(--mono);white-space:nowrap;padding-top:2px}
.fb{margin-top:10px;border-radius:8px;padding:10px 12px;background:var(--accent-soft)}
.fb p{margin:0}.fb .head{font-weight:600}.fb.good .head{color:var(--ok)}.fb.miss .head{color:var(--bad)}
.fb .why{color:var(--mute);font-size:.92rem;margin-top:4px}
.empty{text-align:center;color:var(--mute);padding:36px 12px}
.code{margin:0 0 12px;padding:10px 12px;background:var(--bg);border:1px solid var(--line);border-radius:8px;font:500 .82rem/1.5 var(--mono);overflow-x:auto;white-space:pre;tab-size:2}
textarea.ans{width:100%;font:inherit;color:var(--ink);background:var(--card);border:1px solid var(--line);border-radius:8px;padding:10px 12px;resize:vertical;min-height:84px}
textarea.ans[readonly]{background:var(--bg)}
.row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:10px}
.ask{color:var(--mute);font-size:.9rem}
.model{margin:6px 0 0;padding-left:20px}
.model li{margin:3px 0}
.mark{margin:10px 0 0;font-weight:600;font-size:.92rem}
.mark.good{color:var(--ok)}.mark.miss{color:var(--bad)}
.onote{display:block;margin-top:4px;font-size:.86rem;font-weight:400;color:var(--mute)}
.btn.small{padding:5px 10px;font-size:.8rem;color:var(--mute)}
</style></head><body><main>
<h1>Topic Quiz</h1>
<p class="sub">N questions from the course material. Tap an option to answer. In Review mode you see only the ones you got wrong and can export them as a PDF.</p>
<div class="bar" role="toolbar" aria-label="Quiz controls">
  <div class="tabs" role="group" aria-label="Mode">
    <button type="button" id="tab-quiz" aria-pressed="true">Quiz</button>
    <button type="button" id="tab-review" aria-pressed="false">Review</button>
    <button type="button" id="tab-retry" aria-pressed="false" hidden>Retry</button>
  </div>
  <div class="stats" id="stats" aria-live="polite"></div>
  <div class="acts" id="acts"></div>
</div>
<div id="view"></div>
</main>
<script>
var DATA=__DATA__;
var L="ABCD", KEY="bps-quiz-v1", DKEY=KEY+"-d", RKEY=KEY+"-r";
var answers={}, drafts={}, retry=[], mode="quiz", dl=null;
try{var s=localStorage.getItem(KEY); if(s) answers=JSON.parse(s)||{};}catch(e){}
try{var s2=localStorage.getItem(DKEY); if(s2) drafts=JSON.parse(s2)||{};}catch(e){}
try{var s3=localStorage.getItem(RKEY); if(s3) retry=JSON.parse(s3)||[];}catch(e){}
function save(){try{localStorage.setItem(KEY,JSON.stringify(answers));}catch(e){}}
function saveD(){try{localStorage.setItem(DKEY,JSON.stringify(drafts));}catch(e){}}
function saveR(){try{localStorage.setItem(RKEY,JSON.stringify(retry));}catch(e){}}
function esc(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");}
function byN(n){return DATA[n-1];}
function isDone(q){var a=answers[q.n]; return q.type==="short" ? (a==="ok"||a==="miss") : a!==undefined;}
function wrongList(){return DATA.filter(function(q){return q.type==="short" ? answers[q.n]==="miss" : (answers[q.n]!==undefined&&answers[q.n]!==q.ans);});}
function doneCount(){return DATA.filter(isDone).length;}

function shortHTML(q,ctx){
  var review=(ctx!=="quiz"), a=answers[q.n], marked=(a==="ok"||a==="miss"), shown=marked||a==="rev";
  var lab=(review?esc(q.section)+" - ":"")+esc(q.kind||"Short answer");
  var h='<article class="q" id="q'+q.n+'"><h3><span class="num">'+q.n+'</span><span><span class="sec">'+lab+'</span>'+esc(q.q)+'</span></h3>';
  if(q.code) h+='<pre class="code">'+esc(q.code)+'</pre>';
  h+='<textarea class="ans" data-n="'+q.n+'" rows="4" aria-label="Your answer" placeholder="Write your answer here (optional), then reveal the model answer."'+(marked?" readonly":"")+'>'+esc(drafts[q.n]||"")+'</textarea>';
  if(!shown){
    h+='<div class="row"><button type="button" class="btn primary" data-act="reveal" data-n="'+q.n+'">Show model answer</button></div>';
  } else {
    h+='<div class="fb"><p class="head">Model answer</p><ul class="model">'+q.model.map(function(m){return '<li>'+esc(m)+'</li>';}).join("")+'</ul>'+(q.ex?'<p class="why">'+esc(q.ex)+'</p>':'')+'</div>';
    if(!marked){
      h+='<div class="row"><span class="ask">How did you do?</span><button type="button" class="btn" data-act="ok" data-n="'+q.n+'">I got it</button><button type="button" class="btn" data-act="miss" data-n="'+q.n+'">I missed it</button></div>';
    } else {
      h+='<p class="mark '+(a==="ok"?"good":"miss")+'">'+(a==="ok"?"Marked as got it":"Marked as missed")+'</p>';
    }
  }
  if(shown&&ctx!=="review") h+=resetBtn(q);
  return h+'</article>';
}
function resetBtn(q){return '<div class="row"><button type="button" class="btn small" data-act="reset" data-n="'+q.n+'">Redo this question</button></div>';}

function qHTML(q,ctx){
  if(q.type==="short") return shortHTML(q,ctx);
  var review=(ctx!=="quiz"), a=answers[q.n], done=a!==undefined, right=done&&a===q.ans, o="";
  q.opts.forEach(function(t,j){
    var cls="opt",tag="";
    if(done){
      if(j===q.ans){cls+=" ok";tag=(j===a)?"Your answer":"Correct";}
      else if(j===a){cls+=" bad";tag="Your answer";}
    }
    o+='<button type="button" class="'+cls+'" data-n="'+q.n+'" data-j="'+j+'"'+(done?" disabled":"")+'><b>'+L[j]+'</b><span class="txt">'+esc(t)+((done&&q.notes&&q.notes[j])?'<span class="onote">'+esc(q.notes[j])+'</span>':'')+'</span>'+(tag?'<span class="tag">'+tag+'</span>':'')+'</button>';
  });
  var fb="";
  if(done){
    fb='<div class="fb '+(right?"good":"miss")+'"><p class="head">'+(right?"Correct":"Not quite. Correct answer: "+L[q.ans])+'</p><p class="why">'+esc(q.ex)+'</p></div>';
  }
  return '<article class="q" id="q'+q.n+'"><h3><span class="num">'+q.n+'</span><span>'+(review?'<span class="sec">'+esc(q.section)+'</span>':'')+esc(q.q)+'</span></h3><div class="opts">'+o+'</div>'+fb+((done&&ctx!=="review")?resetBtn(q):"")+'</article>';
}

function renderStats(){
  var ans=doneCount(), w=wrongList().length, c=ans-w;
  document.getElementById("stats").innerHTML='<span>Answered <b>'+ans+'/'+DATA.length+'</b></span><span>Correct <b>'+c+'</b></span><span>Wrong <b>'+w+'</b></span>';
  document.getElementById("tab-review").textContent="Review ("+w+")";
  var tr=document.getElementById("tab-retry"), ritems=retry.map(byN).filter(Boolean);
  tr.hidden=!ritems.length; tr.textContent="Retry ("+ritems.filter(isDone).length+"/"+ritems.length+")";
  var acts=document.getElementById("acts"), h="";
  if(mode==="retry"){
    h+='<button type="button" class="btn" data-act="backquiz">Back to all questions</button>';
  } else if(mode==="review"){
    h+='<button type="button" class="btn" id="redo"'+(w?"":" disabled")+'>Redo mistakes</button>';
    h+='<button type="button" class="btn" id="copy"'+(w?"":" disabled")+'>Copy as text</button>';
    if(dl) h+='<button type="button" class="btn primary" id="pdf"'+(w?"":" disabled")+'>Export PDF</button>';
  } else {
    h+='<button type="button" class="btn" id="reset"'+(ans||Object.keys(answers).length?"":" disabled")+'>Reset all</button>';
  }
  acts.innerHTML=h;
}

function retryBanner(){
  var items=retry.map(byN).filter(Boolean), dn=items.filter(isDone).length, bad=wrongList(), still=items.filter(function(q){return bad.indexOf(q)>-1;});
  var h='<div class="banner" id="rbanner"><p>Retrying <b>'+items.length+'</b> question'+(items.length===1?"":"s")+' you got wrong earlier. <b>'+dn+'/'+items.length+'</b> done.</p>';
  if(dn===items.length&&items.length){
    h+='<p>'+(still.length?(items.length-still.length)+' now correct, '+still.length+' still wrong.':'All of them are correct now.')+'</p><div class="row">'+(still.length?'<button type="button" class="btn primary" data-act="retryrem">Retry the '+still.length+' still wrong</button>':'')+'<button type="button" class="btn" data-act="backquiz">Back to all questions</button></div>';
  } else { h+='<p>Only the earlier mistakes are shown here. Answer each one below.</p>'; }
  return h+'</div>';
}
function startRetry(list){
  retry=list.map(function(q){return q.n;});
  list.forEach(function(q){delete answers[q.n];delete drafts[q.n];});
  save(); saveD(); saveR(); mode="retry"; render(); window.scrollTo(0,0);
}

function render(){
  var v=document.getElementById("view"), h="";
  document.getElementById("tab-quiz").setAttribute("aria-pressed",mode==="quiz");
  document.getElementById("tab-review").setAttribute("aria-pressed",mode==="review");
  document.getElementById("tab-retry").setAttribute("aria-pressed",mode==="retry");
  if(mode==="quiz"){
    var secs=[],cur=null;
    DATA.forEach(function(q){if(!cur||cur.t!==q.section){cur={t:q.section,qs:[]};secs.push(cur);}cur.qs.push(q);});
    h+='<nav class="jump" aria-label="Sections">'+secs.map(function(s,i){return '<a href="#s'+(i+1)+'">'+esc(s.t.split(" · ")[0])+'<span>'+s.qs[0].n+'–'+s.qs[s.qs.length-1].n+'</span></a>';}).join("")+'</nav>';
    secs.forEach(function(s,i){h+='<h2 id="s'+(i+1)+'">'+esc(s.t)+'</h2>'+s.qs.map(function(q){return qHTML(q,"quiz");}).join("");});
  } else if(mode==="retry"){
    h+=retryBanner()+retry.map(byN).filter(Boolean).map(function(q){return qHTML(q,"retry");}).join("");
  } else {
    var w=wrongList(), ans=doneCount();
    if(!ans){h='<div class="empty">You have not answered anything yet. Switch to Quiz and try a few questions.</div>';}
    else if(!w.length){h='<div class="empty">No mistakes so far. Keep going in Quiz mode.</div>';}
    else{
      h+='<div class="banner"><p><b>'+w.length+'</b> mistake'+(w.length===1?"":"s")+' out of '+ans+' answered.</p><p>Multiple-choice picks are marked red with the correct option green. Short answers show the model answer you marked as missed.</p></div>';
      h+=w.map(function(q){return qHTML(q,"review");}).join("");
    }
  }
  v.innerHTML=h; renderStats();
}

document.getElementById("tab-quiz").addEventListener("click",function(){mode="quiz";render();window.scrollTo(0,0);});
document.getElementById("tab-review").addEventListener("click",function(){mode="review";render();window.scrollTo(0,0);});

document.getElementById("tab-retry").addEventListener("click",function(){mode="retry";render();window.scrollTo(0,0);});

document.addEventListener("input",function(e){
  var t=e.target;
  if(t&&t.classList&&t.classList.contains("ans")){drafts[+t.dataset.n]=t.value;saveD();}
});

document.addEventListener("click",function(e){
  var act=e.target.closest("[data-act]");
  if(act){
    var k=act.dataset.act;
    if(k==="backquiz"){mode="quiz";render();window.scrollTo(0,0);return;}
    if(k==="retryrem"){var bad0=wrongList();startRetry(retry.map(byN).filter(function(q){return q&&bad0.indexOf(q)>-1;}));return;}
    var n0=+act.dataset.n;
    if(k==="reset"){delete answers[n0];delete drafts[n0];save();saveD();}
    else{answers[n0]=(k==="reveal")?"rev":k; save();}
    var art0=document.getElementById("q"+n0);
    if(art0){art0.outerHTML=qHTML(byN(n0),mode);}
    var rb0=document.getElementById("rbanner"); if(rb0&&mode==="retry"){rb0.outerHTML=retryBanner();}
    renderStats(); return;
  }
  var b=e.target.closest(".opt");
  if(b&&!b.disabled){
    var n=+b.dataset.n, j=+b.dataset.j;
    answers[n]=j; save();
    var art=document.getElementById("q"+n);
    if(art){art.outerHTML=qHTML(byN(n),mode);}
    var rb=document.getElementById("rbanner"); if(rb&&mode==="retry"){rb.outerHTML=retryBanner();}
    renderStats(); return;
  }
  var id=e.target.closest("button.btn"); if(!id) return;
  if(id.id==="reset"){ if(confirmReset(id)){answers={};drafts={};retry=[];save();saveD();saveR();render();} }
  else if(id.id==="redo"){ startRetry(wrongList()); }
  else if(id.id==="copy"){ copyText(id); }
  else if(id.id==="pdf"){ exportPdf(id); }
});

var resetArmed=false;
function confirmReset(btn){
  if(resetArmed){resetArmed=false;return true;}
  resetArmed=true; btn.textContent="Tap again to clear all answers";
  setTimeout(function(){resetArmed=false;var r=document.getElementById("reset");if(r)r.textContent="Reset all";},3000);
  return false;
}

function plain(s){return String(s).replace(/[–—]/g,"-").replace(/[‘’]/g,"'").replace(/[“”]/g,'"').replace(/·/g,"-").replace(/[^\x00-\xFF]/g,"?");}

function mistakesText(){
  var w=wrongList(), t="Quiz - my mistakes ("+w.length+")\n\n";
  w.forEach(function(q){
    t+=q.n+". "+q.q+"\n";
    if(q.type==="short"){
      if(q.code) t+=q.code.split("\n").map(function(l){return "      "+l;}).join("\n")+"\n";
      if(drafts[q.n]) t+="   Your answer: "+drafts[q.n]+"\n";
      t+="   Model answer:\n";
      q.model.forEach(function(m){t+="     - "+m+"\n";});
      if(q.ex) t+="   Note: "+q.ex+"\n";
      t+="\n";
    } else {
      q.opts.forEach(function(o,j){t+="   "+L[j]+". "+o+(j===q.ans?"   [CORRECT]":"")+(j===answers[q.n]?"   [YOUR ANSWER]":"")+"\n"+(q.notes&&q.notes[j]?"        Note: "+q.notes[j]+"\n":"");});
      t+="   Why: "+q.ex+"\n\n";
    }
  });
  return t;
}
function copyText(btn){
  var old=btn.textContent;
  function done(m){btn.textContent=m;setTimeout(function(){btn.textContent=old;},1800);}
  try{navigator.clipboard.writeText(mistakesText()).then(function(){done("Copied");},function(){done("Copy blocked");});}
  catch(e){done("Copy blocked");}
}

function buildPdf(){
  var J=window.jspdf&&window.jspdf.jsPDF; if(!J) return null;
  var doc=new J({unit:"mm",format:"a4"}), W=210, M=16, TW=W-2*M, y=M, PH=297;
  function need(h){ if(y+h>PH-M){doc.addPage();y=M;} }
  function para(text,size,style,color,indent,gap,fam){
    doc.setFont(fam||"helvetica",style); doc.setFontSize(size); doc.setTextColor(color[0],color[1],color[2]);
    var lh=size*0.43, lines=doc.splitTextToSize(plain(text),TW-indent);
    lines.forEach(function(ln){need(lh);doc.text(ln,M+indent,y+lh*0.8);y+=lh;});
    y+=gap||0;
  }
  var w=wrongList(), ans=doneCount(), d=new Date();
  para(document.title+" - Mistakes",16,"bold",[22,32,46],0,2);
  para(w.length+" wrong out of "+ans+" answered  |  "+d.toISOString().slice(0,10),10,"normal",[91,102,120],0,6);
  w.forEach(function(q){
    need(30);
    para(q.n+". "+q.q,11,"bold",[22,32,46],0,1.5);
    if(q.type==="short"){
      if(q.code){
        q.code.split("\n").forEach(function(l){ if(l.trim()==="") {y+=2;} else {para(l,8.5,"normal",[60,70,85],4,0,"courier");} });
        y+=2;
      }
      if(drafts[q.n]) para("Your answer: "+drafts[q.n],10,"normal",[180,35,24],4,1);
      para("Model answer:",10,"bold",[15,122,77],4,0.5);
      q.model.forEach(function(m){para("- "+m,10,"normal",[15,122,77],8,0.6);});
      if(q.ex) para("Note: "+q.ex,9.5,"italic",[91,102,120],4,0);
    } else {
      q.opts.forEach(function(o,j){
        var isC=j===q.ans, isY=j===answers[q.n];
        var tag=isC?"  [CORRECT ANSWER]":(isY?"  [YOUR ANSWER]":"");
        var col=isC?[15,122,77]:(isY?[180,35,24]:[60,70,85]);
        para(L[j]+".  "+o+tag,10,(isC||isY)?"bold":"normal",col,4,0.8);
        if(q.notes&&q.notes[j]) para("Note: "+q.notes[j],9,"italic",[91,102,120],9,0.8);
      });
      para("Why: "+q.ex,9.5,"italic",[91,102,120],4,0);
    }
    y+=3; need(4);
    doc.setDrawColor(213,218,227); doc.line(M,y,W-M,y); y+=4;
  });
  return doc;
}
function exportPdf(btn){
  var old=btn.textContent;
  function msg(m){btn.textContent=m;setTimeout(function(){btn.textContent=old;},2200);}
  var doc=buildPdf(); if(!doc){msg("PDF library not loaded");return;}
  dl.save({filename:"quiz-mistakes.pdf",data:doc.output("blob")}).then(function(){msg("Saved");},function(err){
    msg(err&&err.code==="declined"?"Cancelled":"Could not save");
  });
}

render();
try{ if(window.claude&&window.claude.use){ window.claude.use("downloads").then(function(x){dl=x||null;renderStats();},function(){}); } }catch(e){}
</script></body></html>

Notes

  • Printing and direct downloads are blocked inside artifact pages; the downloads capability is the only route for the PDF. If it is unavailable the button hides and Copy as text remains.
  • The PDF uses jsPDF 2.5.1 from cdnjs with the built-in Helvetica (and Courier for code) font, so text is Latin-1 only.
  • The template in this file already includes the per-question "Redo this question" button, the Retry tab for mistakes, structured/program-analysis questions, and optional per-option notes. Do not strip them.
  • Changing the colour palette, layout or behaviour is only for when the user asks. Otherwise reuse the template as is.

Related Skills