|
|
| (10 intermediate revisions by the same user not shown) |
| Line 1: |
Line 1: |
| <!-- Embedded HTML Study Application -->
| | {{#widget:520Unit2StudyBuddy}} |
| <html>
| |
| <!doctype html>
| |
| <html lang="en">
| |
| <head>
| |
| <meta charset="utf-8" />
| |
| <meta name="viewport" content="width=device-width,initial-scale=1" />
| |
| <title>Unit 2 Study Buddy (Patho/Immunity)</title>
| |
| <style>
| |
| :root{
| |
| --bg:#0b1220; --panel:#0f1a2e; --card:#111f3a; --muted:#9fb0d0; --text:#e9f0ff;
| |
| --accent:#7aa2ff; --good:#3ddc97; --warn:#ffd166; --bad:#ff5c7a; --line:#223458;
| |
| }
| |
| *{box-sizing:border-box}
| |
| body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;color:var(--text);
| |
| background:radial-gradient(1000px 600px at 15% 0%, #14224a 0%, rgba(20,34,74,0) 60%),
| |
| radial-gradient(900px 500px at 100% 20%, #1a2a59 0%, rgba(26,42,89,0) 55%),
| |
| linear-gradient(180deg,#070b14, #0b1220 35%, #070b14)}
| |
| .app{display:grid;grid-template-columns:350px 1fr;min-height:100vh}
| |
| aside{padding:18px;border-right:1px solid var(--line);background:rgba(15,26,46,.65);backdrop-filter: blur(8px)}
| |
| main{padding:18px 18px 32px}
| |
| h1{margin:4px 0 8px;font-size:18px}
| |
| h2{margin:0;font-size:16px}
| |
| .sub{color:var(--muted);font-size:12px;line-height:1.35}
| |
| .row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
| |
| .btn{border:1px solid var(--line);background:rgba(17,31,58,.9);color:var(--text);padding:10px 12px;border-radius:12px;cursor:pointer}
| |
| .btn:hover{border-color:#365089}
| |
| .btn.primary{background:rgba(122,162,255,.18);border-color:rgba(122,162,255,.45)}
| |
| .btn.danger{background:rgba(255,92,122,.12);border-color:rgba(255,92,122,.35)}
| |
| .btn:disabled{opacity:.5;cursor:not-allowed}
| |
| .statgrid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px}
| |
| .stat{border:1px solid var(--line);background:rgba(17,31,58,.7);border-radius:14px;padding:10px}
| |
| .stat .k{font-size:11px;color:var(--muted)}
| |
| .stat .v{font-size:18px;margin-top:4px}
| |
| .progressWrap{margin-top:14px;border:1px solid var(--line);border-radius:14px;padding:12px;background:rgba(17,31,58,.65)}
| |
| .bar{height:10px;border-radius:999px;background:#1b2a49;overflow:hidden;border:1px solid #223458}
| |
| .bar > div{height:100%;width:0;background:linear-gradient(90deg,var(--accent),#9bb8ff);transition:.2s}
| |
| .small{font-size:11px;color:var(--muted);margin-top:6px;display:flex;justify-content:space-between;gap:10px}
| |
| .filters{margin-top:14px;display:grid;grid-template-columns:1fr 1fr;gap:10px}
| |
| select,input[type="search"]{width:100%;padding:10px 10px;border-radius:12px;border:1px solid var(--line);background:rgba(17,31,58,.7);color:var(--text);outline:none}
| |
| .list{margin-top:12px;max-height:34vh;overflow:auto;padding-right:6px}
| |
| .qitem{border:1px solid var(--line);border-radius:12px;padding:10px;margin-bottom:10px;background:rgba(17,31,58,.6);cursor:pointer}
| |
| .qitem:hover{border-color:#365089}
| |
| .qitem .top{display:flex;justify-content:space-between;gap:10px}
| |
| .qitem .t{font-size:12px;color:var(--muted);margin-top:6px;line-height:1.25}
| |
| .tag{font-size:11px;color:var(--muted);border:1px solid var(--line);padding:4px 8px;border-radius:999px;white-space:nowrap}
| |
| .tag.good{border-color:rgba(61,220,151,.5);color:rgba(61,220,151,.95)}
| |
| .tag.bad{border-color:rgba(255,92,122,.5);color:rgba(255,92,122,.95)}
| |
| .tag.warn{border-color:rgba(255,209,102,.5);color:rgba(255,209,102,.95)}
| |
| .card{border:1px solid var(--line);border-radius:16px;padding:16px;background:rgba(17,31,58,.65)}
| |
| .header{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap}
| |
| .meta{display:flex;gap:8px;flex-wrap:wrap}
| |
| .badge{font-size:12px;padding:6px 10px;border-radius:999px;border:1px solid var(--line);background:rgba(15,26,46,.55)}
| |
| .prompt{margin:10px 0 0;color:var(--text);line-height:1.45}
| |
| .hint{margin:10px 0 0;color:var(--muted);font-size:12px}
| |
| .opts{margin-top:12px;display:grid;gap:10px}
| |
| | |
| .optCard{
| |
| border:1px solid var(--line);
| |
| border-radius:12px;
| |
| padding:12px;
| |
| background:rgba(15,26,46,.5);
| |
| cursor:pointer;
| |
| display:flex; gap:10px; align-items:flex-start;
| |
| user-select:none;
| |
| }
| |
| .optCard:hover{border-color:#365089}
| |
| .optMark{
| |
| width:18px;height:18px;border-radius:6px;
| |
| border:1px solid #365089;background:rgba(122,162,255,.06);
| |
| flex:0 0 auto;margin-top:2px;
| |
| }
| |
| .optCard.selected .optMark{
| |
| background:rgba(122,162,255,.35);
| |
| border-color:rgba(122,162,255,.8);
| |
| box-shadow:0 0 0 2px rgba(122,162,255,.12) inset;
| |
| }
| |
| .optText{flex:1}
| |
| | |
| .actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:12px}
| |
| .revealBox{margin-top:12px;border:1px dashed #365089;border-radius:14px;padding:12px;background:rgba(122,162,255,.06)}
| |
| .revealBox .rtitle{font-size:12px;color:var(--muted);margin-bottom:8px}
| |
| .split{display:grid;grid-template-columns:1.2fr .8fr;gap:12px}
| |
| .miniTable{width:100%;border-collapse:collapse;margin-top:10px}
| |
| .miniTable th,.miniTable td{padding:10px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}
| |
| .kpiRow{display:flex;justify-content:space-between;gap:10px;align-items:center;margin-top:10px}
| |
| .miniList{margin:10px 0 0;padding:0;list-style:none}
| |
| .miniList li{padding:8px 10px;border:1px solid var(--line);border-radius:12px;background:rgba(15,26,46,.45);margin-bottom:8px}
| |
| .miniList small{color:var(--muted)}
| |
| .chip{display:inline-block;margin-left:8px;font-size:11px;padding:3px 8px;border-radius:999px;border:1px solid var(--line);color:var(--muted)}
| |
| .chip.bad{border-color:rgba(255,92,122,.45);color:rgba(255,92,122,.95)}
| |
| .chip.warn{border-color:rgba(255,209,102,.45);color:rgba(255,209,102,.95)}
| |
| .caseBox{border:1px solid var(--line);border-radius:14px;padding:12px;background:rgba(15,26,46,.4);margin-top:12px}
| |
| .caseStep{border-top:1px solid var(--line);margin-top:12px;padding-top:12px}
| |
| .caseStep:first-child{border-top:none;margin-top:0;padding-top:0}
| |
| textarea{width:100%;min-height:72px;resize:vertical;padding:10px;border-radius:12px;border:1px solid var(--line);background:rgba(15,26,46,.55);color:var(--text);outline:none}
| |
| .table{width:100%;border-collapse:collapse;margin-top:10px}
| |
| .table th,.table td{padding:10px;border-bottom:1px solid var(--line);text-align:left}
| |
| .err{margin-top:12px;border:1px solid rgba(255,92,122,.35);background:rgba(255,92,122,.08);padding:10px;border-radius:12px;color:#ffd0d9;font-size:12px;display:none}
| |
| | |
| .logicGrid{display:grid;gap:10px;margin-top:10px}
| |
| .logicBox{border:1px solid var(--line);background:rgba(15,26,46,.45);border-radius:12px;padding:10px}
| |
| .logicBox .ttl{font-size:12px;color:var(--muted);margin-bottom:6px}
| |
| .logicBox .txt{line-height:1.35}
| |
| @media (max-width: 980px){.app{grid-template-columns:1fr} aside{border-right:none;border-bottom:1px solid var(--line)} .split{grid-template-columns:1fr}}
| |
| </style>
| |
| </head>
| |
| <body>
| |
| <div class="app">
| |
| <aside>
| |
| <h1>Unit 2 Study Buddy</h1>
| |
| <div class="sub">
| |
| Unit 2 Chapters Only.
| |
| <br/>(Immunity, Infection, Cancer, Stress)
| |
| </div>
| |
| | |
| <div class="progressWrap">
| |
| <div class="bar"><div id="barFill"></div></div>
| |
| <div class="small">
| |
| <span id="progText">Progress: 0/30</span>
| |
| <span id="pctText">0%</span>
| |
| </div>
| |
| | |
| <div class="kpiRow">
| |
| <div class="small" style="margin:0"><span>Topics to revisit (partial/missed)</span><span id="reviewCount">0</span></div>
| |
| </div>
| |
| <ul class="miniList" id="reviewTopics"></ul>
| |
| </div>
| |
| | |
| <div class="statgrid">
| |
| <div class="stat"><div class="k">Weighted Score</div><div class="v" id="scoreV">0</div></div>
| |
| <div class="stat"><div class="k">Readiness</div><div class="v" id="bandV">—</div></div>
| |
| </div>
| |
| | |
| <div class="filters">
| |
| <select id="filterMode">
| |
| <option value="all">All</option>
| |
| <option value="missed">Missed (below 70%)</option>
| |
| <option value="partial">Partial (70–99%)</option>
| |
| <option value="flagged">Flagged</option>
| |
| <option value="unanswered">Unanswered</option>
| |
| </select>
| |
| <select id="topicFilter"></select>
| |
| </div>
| |
| | |
| <div style="margin-top:10px">
| |
| <input id="searchBox" type="search" placeholder="Search prompt / topic…" />
| |
| </div>
| |
| | |
| <div class="row" style="margin-top:12px">
| |
| <button class="btn" id="prevBtn">Prev</button>
| |
| <button class="btn primary" id="nextBtn">Next</button>
| |
| <button class="btn" id="jumpReportBtn">Report</button>
| |
| <button class="btn danger" id="resetBtn">Reset</button>
| |
| </div>
| |
| | |
| <div class="list" id="qList"></div>
| |
| </aside>
| |
| | |
| <main>
| |
| <div class="card" id="viewer"></div>
| |
| <div class="err" id="errBox"></div>
| |
| | |
| <div class="card" id="reportCard" style="margin-top:16px">
| |
| <div class="header">
| |
| <div>
| |
| <h2>End Report</h2>
| |
| <div class="sub">Weighted performance + a clean list of topics to revisit (partial or missed).</div>
| |
| </div>
| |
| <div class="meta">
| |
| <span class="badge" id="repWeighted">Weighted: 0</span>
| |
| <span class="badge" id="repPct">Percent: 0%</span>
| |
| <span class="badge" id="repBand">Band: —</span>
| |
| </div>
| |
| </div>
| |
| | |
| <div class="split" style="margin-top:12px">
| |
| <div class="caseBox">
| |
| <div style="display:flex;justify-content:space-between;gap:10px;align-items:center;flex-wrap:wrap">
| |
| <div>
| |
| <div style="font-size:13px;color:var(--muted)">Concept mastery (by topic)</div>
| |
| <div class="sub">Percent is weighted by item format + difficulty.</div>
| |
| </div>
| |
| <button class="btn" id="downloadBtn">Download session JSON</button>
| |
| </div>
| |
| <table class="miniTable" id="topicTable">
| |
| <thead><tr><th>Topic</th><th>Mastery</th><th>Status</th></tr></thead>
| |
| <tbody></tbody>
| |
| </table>
| |
| </div>
| |
| | |
| <div class="caseBox">
| |
| <div style="font-size:13px;color:var(--muted)">Topics to revisit (list)</div>
| |
| <div class="sub">Anything partial or missed appears here, sorted by frequency/priority.</div>
| |
| <ul class="miniList" id="revisitList"></ul>
| |
| </div>
| |
| </div>
| |
| </div>
| |
| </main>
| |
| </div>
| |
| | |
| <script>
| |
| /* =========================
| |
| 1. GLOBAL ERROR CAPTURE
| |
| ========================= */
| |
| window.addEventListener("error", (e)=>{
| |
| const box = document.getElementById("errBox");
| |
| box.style.display = "block";
| |
| box.textContent = "Script error: " + (e.message || "Unknown") + (e.filename ? (" @ " + e.filename) : "");
| |
| });
| |
| | |
| /* =========================
| |
| 2. HELPER FUNCTIONS
| |
| ========================= */
| |
| const el = (id)=>document.getElementById(id);
| |
| const clamp=(n,min,max)=>Math.max(min,Math.min(max,n));
| |
| const uniq=(arr)=>Array.from(new Set(arr));
| |
| | |
| function escapeHtml(s){
| |
| return String(s)
| |
| .replaceAll("&","&")
| |
| .replaceAll("<","<")
| |
| .replaceAll(">",">")
| |
| .replaceAll('"',""")
| |
| .replaceAll("'","'");
| |
| }
| |
| | |
| /* --- ADDED MISSING FUNCTIONS (FIXED) --- */
| |
| function statusTag(id){
| |
| const sc = state.scores[id];
| |
| if (!sc) return { text:"Unanswered", cls:"" };
| |
| if (sc.status === "full") return { text:"Correct", cls:"good" };
| |
| if (sc.status === "partial") return { text:"Partial", cls:"warn" };
| |
| return { text:"Missed", cls:"bad" };
| |
| }
| |
| | |
| function weightedScoreOutOf1200(){
| |
| let earned=0, possible=0;
| |
| for(let id in state.scores){
| |
| earned += state.scores[id].earnedW;
| |
| possible += state.scores[id].possibleW;
| |
| }
| |
| if(!possible) return 0;
| |
| return Math.round((earned/possible)*1200);
| |
| }
| |
| | |
| function overallMastery(){
| |
| let earned=0, possible=0;
| |
| for(let id in state.scores){
| |
| earned += state.scores[id].earnedW;
| |
| possible += state.scores[id].possibleW;
| |
| }
| |
| if(!possible) return 0;
| |
| return (earned/possible)*100;
| |
| }
| |
| | |
| // Render the interactive question body based on type
| |
| function renderQuestionBody(q){
| |
| const container = document.createElement("div");
| |
| const prev = state.answers[q.id];
| |
| | |
| // MCQ / SATA
| |
| if(q.type === "mcq" || q.type === "sata"){
| |
| const d = document.createElement("div");
| |
| d.className = "opts";
| |
| q.options.forEach((opt, idx)=>{
| |
| const card = document.createElement("div");
| |
| card.className = "optCard";
| |
| const isSel = prev && prev.includes(idx);
| |
| if(isSel) card.classList.add("selected");
| |
|
| |
| card.innerHTML = `<div class="optMark"></div><div class="optText">${escapeHtml(opt)}</div>`;
| |
| card.onclick = () => {
| |
| if(q.type === "mcq"){
| |
| // Clear others
| |
| Array.from(d.children).forEach(c=>c.classList.remove("selected"));
| |
| card.classList.add("selected");
| |
| } else {
| |
| card.classList.toggle("selected");
| |
| }
| |
| };
| |
| d.appendChild(card);
| |
| });
| |
| container.appendChild(d);
| |
| }
| |
| | |
| // Dropdown
| |
| else if(q.type === "dropdown"){
| |
| const d = document.createElement("div");
| |
| d.className = "opts";
| |
| q.blanks.forEach((b, i)=>{
| |
| const row = document.createElement("div");
| |
| row.style.marginBottom = "10px";
| |
| const selVal = prev ? prev[i] : "";
| |
|
| |
| let html = `<div>${escapeHtml(b.text)}</div>`;
| |
| html += `<select data-idx="${i}" style="margin-top:6px">`;
| |
| html += `<option value="">Select...</option>`;
| |
| b.options.forEach(o=>{
| |
| html += `<option value="${escapeHtml(o)}" ${o===selVal?'selected':''}>${escapeHtml(o)}</option>`;
| |
| });
| |
| html += `</select>`;
| |
| row.innerHTML = html;
| |
| d.appendChild(row);
| |
| });
| |
| container.appendChild(d);
| |
| }
| |
| | |
| // Matrix
| |
| else if(q.type === "matrix"){
| |
| const tbl = document.createElement("table");
| |
| tbl.className = "table";
| |
| // Header
| |
| let thead = `<thead><tr><th>Row</th>`;
| |
| q.rows[0].options.forEach(o => thead += `<th>${escapeHtml(o)}</th>`);
| |
| thead += `</tr></thead>`;
| |
| tbl.innerHTML = thead;
| |
|
| |
| const tbody = document.createElement("tbody");
| |
| q.rows.forEach((r, rIdx)=>{
| |
| const tr = document.createElement("tr");
| |
| let td = `<td>${escapeHtml(r.row)}</td>`;
| |
| const selVal = prev ? prev[rIdx] : null;
| |
| r.options.forEach(opt=>{
| |
| const checked = (selVal === opt) ? "checked" : "";
| |
| td += `<td><input type="radio" name="mx_${rIdx}" value="${escapeHtml(opt)}" ${checked}></td>`;
| |
| });
| |
| tr.innerHTML = td;
| |
| tbody.appendChild(tr);
| |
| });
| |
| tbl.appendChild(tbody);
| |
| container.appendChild(tbl);
| |
| }
| |
| | |
| // Bowtie
| |
| else if(q.type === "bowtie"){
| |
| const wrap = document.createElement("div");
| |
| wrap.style.display="grid";
| |
| wrap.style.gridTemplateColumns="1fr 1fr 1fr";
| |
| wrap.style.gap="10px";
| |
|
| |
| // Left
| |
| const lDiv = document.createElement("div");
| |
| lDiv.className = "caseBox";
| |
| lDiv.innerHTML = `<div class="sub">${escapeHtml(q.left.label)} (Pick ${q.left.pick})</div>`;
| |
| q.left.options.forEach(o=>{
| |
| const isSel = prev && prev.left && prev.left.includes(o);
| |
| const chk = document.createElement("div");
| |
| chk.className = `optCard ${isSel?'selected':''}`;
| |
| chk.style.marginTop="6px";
| |
| chk.innerHTML = `<div class="optMark"></div><div class="optText" style="font-size:12px">${escapeHtml(o)}</div>`;
| |
| chk.onclick = ()=> chk.classList.toggle("selected");
| |
| chk.dataset.val = o;
| |
| chk.dataset.side = "left";
| |
| lDiv.appendChild(chk);
| |
| });
| |
| | |
| // Center
| |
| const cDiv = document.createElement("div");
| |
| cDiv.className = "caseBox";
| |
| cDiv.innerHTML = `<div class="sub">${escapeHtml(q.center.label)}</div>`;
| |
| q.center.options.forEach(o=>{
| |
| const isSel = prev && prev.center === o;
| |
| const chk = document.createElement("div");
| |
| chk.className = `optCard ${isSel?'selected':''}`;
| |
| chk.style.marginTop="6px";
| |
| chk.innerHTML = `<div class="optMark" style="border-radius:50%"></div><div class="optText" style="font-size:12px">${escapeHtml(o)}</div>`;
| |
| chk.onclick = ()=> {
| |
| Array.from(cDiv.querySelectorAll(".optCard")).forEach(x=>x.classList.remove("selected"));
| |
| chk.classList.add("selected");
| |
| };
| |
| chk.dataset.val = o;
| |
| chk.dataset.side = "center";
| |
| cDiv.appendChild(chk);
| |
| });
| |
| | |
| // Right
| |
| const rDiv = document.createElement("div");
| |
| rDiv.className = "caseBox";
| |
| rDiv.innerHTML = `<div class="sub">${escapeHtml(q.right.label)} (Pick ${q.right.pick})</div>`;
| |
| q.right.options.forEach(o=>{
| |
| const isSel = prev && prev.right && prev.right.includes(o);
| |
| const chk = document.createElement("div");
| |
| chk.className = `optCard ${isSel?'selected':''}`;
| |
| chk.style.marginTop="6px";
| |
| chk.innerHTML = `<div class="optMark"></div><div class="optText" style="font-size:12px">${escapeHtml(o)}</div>`;
| |
| chk.onclick = ()=> chk.classList.toggle("selected");
| |
| chk.dataset.val = o;
| |
| chk.dataset.side = "right";
| |
| rDiv.appendChild(chk);
| |
| });
| |
| | |
| wrap.appendChild(lDiv);
| |
| wrap.appendChild(cDiv);
| |
| wrap.appendChild(rDiv);
| |
| container.appendChild(wrap);
| |
| }
| |
| | |
| // Trends
| |
| else if(q.type === "trends"){
| |
| const tbl = document.createElement("table");
| |
| tbl.className="table";
| |
| let th = `<thead><tr>`;
| |
| q.table.headers.forEach(h=> th+=`<th>${escapeHtml(h)}</th>`);
| |
| th+=`</tr></thead><tbody>`;
| |
| q.table.rows.forEach(r=>{
| |
| th+=`<tr>`;
| |
| r.forEach(c=> th+=`<td>${escapeHtml(c)}</td>`);
| |
| th+=`</tr>`;
| |
| });
| |
| th+=`</tbody>`;
| |
| tbl.innerHTML = th;
| |
| container.appendChild(tbl);
| |
| | |
| q.parts.forEach((p, idx)=>{
| |
| const div = document.createElement("div");
| |
| div.className = "caseStep";
| |
| div.innerHTML = `<div style="margin-bottom:8px"><strong>Part ${idx+1}:</strong> ${escapeHtml(p.prompt)}</div>`;
| |
|
| |
| const subOpts = document.createElement("div");
| |
| subOpts.className = "opts";
| |
|
| |
| if(p.type==="mcq" || p.type==="sata"){
| |
| p.options.forEach((opt, oIdx)=>{
| |
| const card = document.createElement("div");
| |
| card.className = "optCard";
| |
| // prev[idx] might be int (mcq) or array (sata)
| |
| let isSel = false;
| |
| if(prev && prev[idx] !== undefined){
| |
| if(p.type==="mcq") isSel = (prev[idx] === oIdx);
| |
| else isSel = prev[idx].includes(oIdx);
| |
| }
| |
| if(isSel) card.classList.add("selected");
| |
|
| |
| card.innerHTML = `<div class="optMark"></div><div class="optText">${escapeHtml(opt)}</div>`;
| |
| card.onclick = () => {
| |
| if(p.type === "mcq"){
| |
| Array.from(subOpts.children).forEach(c=>c.classList.remove("selected"));
| |
| card.classList.add("selected");
| |
| } else {
| |
| card.classList.toggle("selected");
| |
| }
| |
| };
| |
| card.dataset.oidx = oIdx;
| |
| subOpts.appendChild(card);
| |
| });
| |
| } else if (p.type==="dropdown"){
| |
| p.blanks.forEach((b, bIdx)=>{
| |
| let html = `<span>${escapeHtml(b.text)}</span> <select data-bidx="${bIdx}">`;
| |
| html += `<option value="">...</option>`;
| |
| const pVal = (prev && prev[idx]) ? prev[idx][bIdx] : "";
| |
| b.options.forEach(o=> html+=`<option value="${escapeHtml(o)}" ${o===pVal?'selected':''}>${escapeHtml(o)}</option>`);
| |
| html += `</select>`;
| |
| const span = document.createElement("div");
| |
| span.innerHTML = html;
| |
| subOpts.appendChild(span);
| |
| });
| |
| }
| |
| div.appendChild(subOpts);
| |
| container.appendChild(div);
| |
| });
| |
| }
| |
| | |
| // Case
| |
| else if(q.type === "case"){
| |
| q.steps.forEach((s, idx)=>{
| |
| const div = document.createElement("div");
| |
| div.className = "caseStep";
| |
| div.innerHTML = `<div style="font-weight:bold;margin-bottom:6px">${escapeHtml(s.title)}</div><div class="prompt" style="margin-bottom:10px">${escapeHtml(s.prompt)}</div>`;
| |
|
| |
| const subOpts = document.createElement("div");
| |
| subOpts.className = "opts";
| |
| | |
| if(s.type==="mcq" || s.type==="sata"){
| |
| s.options.forEach((opt, oIdx)=>{
| |
| const card = document.createElement("div");
| |
| card.className = "optCard";
| |
| let isSel = false;
| |
| if(prev && prev[idx] !== undefined){
| |
| if(s.type==="mcq") isSel = (prev[idx] === oIdx);
| |
| else isSel = prev[idx].includes(oIdx);
| |
| }
| |
| if(isSel) card.classList.add("selected");
| |
|
| |
| card.innerHTML = `<div class="optMark"></div><div class="optText">${escapeHtml(opt)}</div>`;
| |
| card.onclick = () => {
| |
| if(s.type === "mcq"){
| |
| Array.from(subOpts.children).forEach(c=>c.classList.remove("selected"));
| |
| card.classList.add("selected");
| |
| } else {
| |
| card.classList.toggle("selected");
| |
| }
| |
| };
| |
| card.dataset.oidx = oIdx;
| |
| subOpts.appendChild(card);
| |
| });
| |
| } else if (s.type==="dropdown"){
| |
| s.blanks.forEach((b, bIdx)=>{
| |
| let html = `<span>${escapeHtml(b.text)}</span> <select data-bidx="${bIdx}">`;
| |
| html += `<option value="">...</option>`;
| |
| const pVal = (prev && prev[idx]) ? prev[idx][bIdx] : "";
| |
| b.options.forEach(o=> html+=`<option value="${escapeHtml(o)}" ${o===pVal?'selected':''}>${escapeHtml(o)}</option>`);
| |
| html += `</select>`;
| |
| const span = document.createElement("div");
| |
| span.innerHTML = html;
| |
| subOpts.appendChild(span);
| |
| });
| |
| }
| |
| div.appendChild(subOpts);
| |
| container.appendChild(div);
| |
| });
| |
| }
| |
| | |
| return container;
| |
| }
| |
| | |
| // Scrape answers from DOM based on type
| |
| function getSelection(q){
| |
| const viewer = document.getElementById("viewer");
| |
|
| |
| if(q.type === "mcq"){
| |
| const sel = viewer.querySelector(".optCard.selected");
| |
| if(!sel) return [];
| |
| // Find index
| |
| const all = Array.from(viewer.querySelectorAll(".optCard"));
| |
| return [all.indexOf(sel)];
| |
| }
| |
| if(q.type === "sata"){
| |
| const all = Array.from(viewer.querySelectorAll(".optCard"));
| |
| const idxs = [];
| |
| all.forEach((c,i)=>{ if(c.classList.contains("selected")) idxs.push(i); });
| |
| return idxs;
| |
| }
| |
| if(q.type === "dropdown"){
| |
| const selects = viewer.querySelectorAll("select");
| |
| const res = [];
| |
| selects.forEach(s => res.push(s.value));
| |
| return res;
| |
| }
| |
| if(q.type === "matrix"){
| |
| const res = {};
| |
| q.rows.forEach((r, i)=>{
| |
| const checked = viewer.querySelector(`input[name="mx_${i}"]:checked`);
| |
| res[i] = checked ? checked.value : "";
| |
| });
| |
| return res;
| |
| }
| |
| if(q.type === "bowtie"){
| |
| const left = [], right = [];
| |
| let center = "";
| |
| viewer.querySelectorAll(".optCard.selected").forEach(el=>{
| |
| const s = el.dataset.side;
| |
| const v = el.dataset.val;
| |
| if(s==="left") left.push(v);
| |
| else if(s==="right") right.push(v);
| |
| else if(s==="center") center = v;
| |
| });
| |
| return { left, center, right };
| |
| }
| |
| if(q.type === "trends" || q.type === "case"){
| |
| const steps = viewer.querySelectorAll(".caseStep");
| |
| const res = [];
| |
| steps.forEach((step, idx)=>{
| |
| const def = (q.type==="trends" ? q.parts[idx] : q.steps[idx]);
| |
| if(def.type==="mcq"){
| |
| const sel = step.querySelector(".optCard.selected");
| |
| res.push( sel ? parseInt(sel.dataset.oidx) : -1 );
| |
| } else if(def.type==="sata"){
| |
| const sells = step.querySelectorAll(".optCard.selected");
| |
| const arr = [];
| |
| sells.forEach(s=> arr.push(parseInt(s.dataset.oidx)));
| |
| res.push(arr);
| |
| } else if(def.type==="dropdown"){
| |
| const selects = step.querySelectorAll("select");
| |
| const arr = [];
| |
| selects.forEach(s=> arr.push(s.value));
| |
| res.push(arr);
| |
| }
| |
| });
| |
| return res;
| |
| }
| |
| return null;
| |
| }
| |
| /* --- END ADDED FUNCTIONS --- */
| |
| | |
| /* =========================
| |
| 3. CONFIG
| |
| ========================= */
| |
| const CONFIG = {
| |
| unitName: "Unit 2",
| |
| passingHesiScore: 875,
| |
| readinessBands: [
| |
| { name: "Needs Work", minPct: 0 },
| |
| { name: "Borderline", minPct: 70 },
| |
| { name: "Ready", minPct: 85 }
| |
| ],
| |
| difficultyWeights: { 1: 1, 2: 2, 3: 3 },
| |
| formatMultipliers: {
| |
| mcq: 1, sata: 1, dropdown: 1,
| |
| case: 2, matrix: 2, bowtie: 3, trends: 3
| |
| },
| |
| maxItemWeight: 6
| |
| };
| |
| | |
| /* =========================
| |
| 4. QUESTIONS (Unit 2 ONLY)
| |
| ========================= */
| |
| const QUESTIONS = [
| |
| { id:1, type:"mcq", topic:"Adaptive Immunity (B vs T)", difficulty:2,
| |
| stem:"A client recovered from chickenpox years ago and now has rapid antibody production after re-exposure. Which immune process best explains this faster response?",
| |
| options:["Innate immunity only", "Memory B-cell response", "Complement activation only", "Neutrophil chemotaxis"],
| |
| correct:[1],
| |
| rationale:"Memory B cells drive a faster, stronger secondary (anamnestic) antibody response after prior exposure.",
| |
| plain:"Your body took a 'mugshot' of the virus last time. B-cells remember faces. When the virus shows up again, the B-cells attack immediately.",
| |
| mnemonic:"B-cells = B-one marrow = Bullets (Antibodies)."
| |
| },
| |
| { id:2, type:"mcq", topic:"Adaptive Immunity (Cell-mediated)", difficulty:2,
| |
| stem:"A client has a history of recurrent viral infections despite normal antibody levels. Which immune arm is most likely impaired?",
| |
| options:["B-cell function", "T-cell (cell-mediated) immunity", "Platelet activation", "Erythropoiesis"],
| |
| correct:[1],
| |
| rationale:"T cells are critical for controlling viral infections and coordinating cell-mediated responses.",
| |
| plain:"Antibodies float in blood, but viruses hide *inside* cells. You need T-cells to go door-to-door and kill the infected cells. No T-cells = viruses run wild.",
| |
| mnemonic:"T-cells = Tough Troops (Hand-to-hand combat)."
| |
| },
| |
| { id:3, type:"sata", topic:"Alterations in Immunity (Hypersensitivity)", difficulty:3,
| |
| stem:"Which findings are most consistent with an immediate (Type I) hypersensitivity reaction? (Select all that apply)",
| |
| options:[
| |
| "Wheezing and urticaria minutes after exposure",
| |
| "Hypotension after a new medication dose",
| |
| "Joint pain weeks after strep infection",
| |
| "Positive Coombs test with hemolysis after transfusion mismatch",
| |
| "Facial/lip swelling shortly after a vaccine"
| |
| ],
| |
| correct:[0, 1, 4],
| |
| rationale:"Type I reactions are IgE-mediated and occur rapidly: hives, bronchospasm, angioedema, hypotension/anaphylaxis.",
| |
| plain:"This is an 'Allergy Alarm' gone wrong. The body sees a peanut or bee sting and instantly dumps histamine. Everything swells up and shuts down fast.",
| |
| mnemonic:"Type 1 = 1mmediate (Anaphylaxis)."
| |
| },
| |
| { id:4, type:"mcq", topic:"Infection (Stages)", difficulty:2,
| |
| stem:"A client reports malaise and low-grade fever two days after exposure to influenza but before classic symptoms peak. Which stage of infection is most likely?",
| |
| options:["Incubation", "Prodromal", "Illness", "Convalescence"],
| |
| correct:[1],
| |
| rationale:"Prodromal stage often includes nonspecific symptoms before maximal illness.",
| |
| plain:"You aren't full-blown sick yet, but you just feel 'off'—tired, a bit achy. The storm is coming, but hasn't hit hard yet.",
| |
| mnemonic:"Prodromal = Pre-sick (Just feeling 'blah')."
| |
| },
| |
| { id:5, type:"mcq", topic:"Infection (Transmission / PPE)", difficulty:2,
| |
| stem:"A client is suspected to have pulmonary tuberculosis. Which infection control approach is priority?",
| |
| options:["Contact precautions only", "Droplet precautions only", "Airborne precautions with fit-tested respirator", "Standard precautions only"],
| |
| correct:[2],
| |
| rationale:"TB requires airborne precautions due to aerosolized droplet nuclei.",
| |
| plain:"TB bacteria float in the air like dust motes for a long time. A regular mask isn't tight enough. You need a special N95 mask that seals to your face.",
| |
| mnemonic:"Airborne MTV (Measles, TB, Varicella)."
| |
| },
| |
| { id:6, type:"sata", topic:"Infection (Antimicrobial Resistance)", difficulty:2,
| |
| stem:"Which actions help reduce antimicrobial resistance? (Select all that apply)",
| |
| options:[
| |
| "Use cultures when possible before starting therapy",
| |
| "Stop antibiotics as soon as the fever breaks (no provider input)",
| |
| "Teach clients to complete prescribed courses when indicated",
| |
| "Use the narrowest effective antibiotic when feasible",
| |
| "Share leftover antibiotics with family members"
| |
| ],
| |
| correct:[0, 2, 3],
| |
| rationale:"Stewardship: culture-guided therapy, appropriate duration, and narrow-spectrum choices reduce resistance.",
| |
| plain:"If you don't kill all the bacteria (by stopping meds early) or use a 'nuke' (broad spectrum) for a small fight, the surviving bacteria learn how to fight back and become superbugs.",
| |
| mnemonic:"Culture before Cure (Antibiotics)."
| |
| },
| |
| { id:7, type:"mcq", topic:"Immunizing Agents (Vaccine Types)", difficulty:2,
| |
| stem:"Which vaccine type contains a weakened pathogen that replicates enough to produce a strong immune response?",
| |
| options:["Inactivated", "Live attenuated", "Toxoid", "Subunit/recombinant"],
| |
| correct:[1],
| |
| rationale:"Live attenuated vaccines use weakened organisms that can replicate and stimulate robust immunity.",
| |
| plain:"It's a 'defanged' version of the real virus. It's alive enough to teach the immune system a lesson, but too weak to make a healthy person sick.",
| |
| mnemonic:"Live = Lasting immunity, but Limited for immunocompromised."
| |
| },
| |
| { id:8, type:"mcq", topic:"Immunizing Agents (Passive vs Active)", difficulty:2,
| |
| stem:"A client receives rabies immune globulin after an animal bite. What type of immunity is this?",
| |
| options:["Naturally acquired active", "Artificially acquired active", "Artificially acquired passive", "Naturally acquired passive"],
| |
| correct:[2],
| |
| rationale:"Immune globulin provides ready-made antibodies (passive) given by injection (artificial).",
| |
| plain:"Active immunity is making your own dinner. Passive immunity is ordering takeout (getting antibodies someone else made). It's fast, but doesn't last.",
| |
| mnemonic:"Passive = Passed to you (borrowed protection)."
| |
| },
| |
| { id:9, type:"dropdown", topic:"Immunizing Agents (Teaching)", difficulty:1,
| |
| stem:"Complete the teaching point.",
| |
| blanks:[
| |
| { text:"After vaccination, the nurse should observe the client for at least ", options:["5 minutes", "15 minutes", "30 minutes", "60 minutes"], correct:"15 minutes" }
| |
| ],
| |
| rationale:"Post-vaccine observation helps detect early severe reactions (e.g., anaphylaxis).",
| |
| plain:"Most dangerous reactions (like throat closing up) happen within minutes. Stick around so we can save you if your body panics.",
| |
| mnemonic:"15 minutes saves lives."
| |
| },
| |
| { id:10, type:"mcq", topic:"Immunizations (Contraindications/Cautions)", difficulty:3,
| |
| stem:"Which client statement requires the nurse to hold a live vaccine and notify the provider?",
| |
| options:[
| |
| "“I have seasonal allergies.”",
| |
| "“I’m taking high-dose prednisone for an autoimmune flare.”",
| |
| "“I had a sore arm after my last shot.”",
| |
| "“I have mild diarrhea today.”"
| |
| ],
| |
| correct:[1],
| |
| rationale:"Significant immunosuppression is a contraindication/major caution for live vaccines due to infection risk.",
| |
| plain:"If your immune system is turned off (steroids/chemo), even a 'weak' live virus can beat you up and cause the actual disease.",
| |
| mnemonic:"No Live for Low immunity."
| |
| },
| |
| { id:11, type:"mcq", topic:"Immunosuppressants (Biologics Safety)", difficulty:3,
| |
| stem:"A client is starting a biologic immunosuppressant for an autoimmune disorder. Which pre-treatment screening is most important?",
| |
| options:["Hearing test", "TB screening", "Vision acuity test", "Peak flow baseline"],
| |
| correct:[1],
| |
| rationale:"Biologic immunosuppressants can reactivate latent infections; TB screening is a key safety step.",
| |
| plain:"Biologics turn down the immune system's surveillance. If TB is sleeping in your lungs (latent), the drug wakes it up, and it attacks.",
| |
| mnemonic:"Biologics wake up Bugs (TB)."
| |
| },
| |
| { id:12, type:"sata", topic:"Immunosuppressants (Teaching)", difficulty:2,
| |
| stem:"Teaching for a client on long-term immunosuppressants should include which points? (Select all that apply)",
| |
| options:[
| |
| "Report fever or new cough promptly",
| |
| "Avoid live vaccines unless specifically approved",
| |
| "Expect faster wound healing",
| |
| "Practice infection prevention (hand hygiene, avoiding sick contacts)",
| |
| "Stop the medication abruptly if you feel better"
| |
| ],
| |
| correct:[0, 1, 3],
| |
| rationale:"Immunosuppression increases infection risk; teach early reporting, prevention, and vaccine safety.",
| |
| plain:"You lost your shield. Wash your hands, stay away from sick people, and if you get a fever, call immediately—don't wait it out.",
| |
| mnemonic:"Protect the Patient (P-P-P: Prevent, Protect, Phone provider)."
| |
| },
| |
| { id:13, type:"mcq", topic:"Stress & Disease (Physiologic Response)", difficulty:2,
| |
| stem:"A client under prolonged stress has persistent insomnia and elevated blood pressure. Which mechanism best explains how chronic stress contributes to disease risk?",
| |
| options:["Decreased sympathetic tone", "Sustained cortisol/sympathetic activation", "Increased insulin sensitivity", "Suppressed inflammatory signaling only"],
| |
| correct:[1],
| |
| rationale:"Chronic stress can maintain cortisol and sympathetic activation, contributing to hypertension, metabolic changes, and immune dysregulation.",
| |
| plain:"Stress keeps the gas pedal floored (cortisol/adrenaline). Eventually, the engine overheats (high BP) and the maintenance crew (immune system) stops working.",
| |
| mnemonic:"Cortisol = Chronic wear and tear."
| |
| },
| |
| { id:14, type:"sata", topic:"Stress & Disease (Nursing Priorities)", difficulty:2,
| |
| stem:"Which nursing interventions best support a client experiencing stress-related symptoms? (Select all that apply)",
| |
| options:[
| |
| "Teach sleep hygiene and consistent bedtime routines",
| |
| "Encourage caffeine to improve daytime alertness",
| |
| "Introduce paced breathing or guided relaxation",
| |
| "Assess for maladaptive coping (substance use, isolation)",
| |
| "Tell the client stress is “all in their head”"
| |
| ],
| |
| correct:[0, 2, 3],
| |
| rationale:"Practical coping supports (sleep hygiene, relaxation) and screening for maladaptive coping reduce harm and improve function.",
| |
| plain:"Help them find the brake pedal. Deep breathing, sleep, and talking help reset the alarm system. Caffeine and 'tough love' just push the gas harder.",
| |
| mnemonic:"Rest, Relax, Relate."
| |
| },
| |
| { id:15, type:"mcq", topic:"Cancer Biology (Oncogenes/Tumor Suppressors)", difficulty:3,
| |
| stem:"A mutation removes a cell’s ability to stop the cell cycle when DNA damage is detected. This is most consistent with dysfunction of:",
| |
| options:["Tumor suppressor genes", "Hemoglobin genes", "Mitochondrial rRNA", "Histamine receptors"],
| |
| correct:[0],
| |
| rationale:"Tumor suppressor genes normally limit proliferation and promote repair/apoptosis when damage occurs.",
| |
| plain:"These genes are the brakes on a car. If you cut the brake lines (mutation), the cell speeds out of control and crashes (cancer).",
| |
| mnemonic:"Suppressor = Stop sign."
| |
| },
| |
| { id:16, type:"mcq", topic:"Cancer Epidemiology (Risk Reduction)", difficulty:2,
| |
| stem:"Which prevention strategy has the strongest evidence for reducing overall cancer risk at a population level?",
| |
| options:["High-protein diet", "Tobacco cessation", "Daily antibiotics", "Avoiding all sunlight completely"],
| |
| correct:[1],
| |
| rationale:"Tobacco use is a major modifiable risk factor for multiple cancers; cessation reduces risk over time.",
| |
| plain:"Smoking puts poison directly onto your cells over and over. Stopping is the single best way to stop damaging your DNA.",
| |
| mnemonic:"Smoke = Stroke & Tumor."
| |
| },
| |
| { id:17, type:"mcq", topic:"Obesity (Pathophysiology)", difficulty:2,
| |
| stem:"Which statement best describes why visceral (central) obesity is more strongly linked with metabolic complications than peripheral obesity?",
| |
| options:[
| |
| "Visceral fat is biologically inactive",
| |
| "Visceral fat is associated with chronic inflammation and insulin resistance",
| |
| "Peripheral fat produces no hormones",
| |
| "Peripheral fat increases catecholamines continuously"
| |
| ],
| |
| correct:[1],
| |
| rationale:"Visceral adiposity correlates with inflammatory cytokines, lipolysis, insulin resistance, and cardiometabolic risk.",
| |
| plain:"Belly fat isn't just stored energy; it's an angry organ. It pumps out chemicals that cause inflammation and mess up how you handle sugar.",
| |
| mnemonic:"Apple shape = Angry fat (Central)."
| |
| },
| |
| { id:18, type:"mcq", topic:"Starvation / Refeeding Syndrome", difficulty:3,
| |
| stem:"A severely malnourished client starts nutrition support and develops weakness, arrhythmias, and confusion. Which complication is most likely?",
| |
| options:["Ketoacidosis", "Refeeding syndrome", "Hypercalcemia", "Hemolysis"],
| |
| correct:[1],
| |
| rationale:"Refeeding syndrome can cause dangerous electrolyte shifts (especially phosphate) leading to cardiac/neurologic complications.",
| |
| plain:"When a starving body gets food too fast, it grabs all the electrolytes from the blood to digest it. The heart and brain run dry and crash.",
| |
| mnemonic:"Start Low, Go Slow (to save the flow)."
| |
| },
| |
| | |
| { id:19, type:"matrix", topic:"Immunizing Agents (Vaccine Types)", difficulty:2,
| |
| stem:"Match the vaccine type to the best description.",
| |
| rows:[
| |
| { row:"Live attenuated", options:["Weakened organism; strong immune response", "Killed organism; cannot replicate", "Inactivated toxin", "Only a piece of pathogen"], correct:"Weakened organism; strong immune response" },
| |
| { row:"Inactivated", options:["Weakened organism; strong immune response", "Killed organism; cannot replicate", "Inactivated toxin", "Only a piece of pathogen"], correct:"Killed organism; cannot replicate" },
| |
| { row:"Toxoid", options:["Weakened organism; strong immune response", "Killed organism; cannot replicate", "Inactivated toxin", "Only a piece of pathogen"], correct:"Inactivated toxin" }
| |
| ],
| |
| rationale:"Vaccine types differ by what’s delivered (weakened/killed/toxin fragment/subunit) and how immunity is generated.",
| |
| plain:"Live = Weakened zombie. Inactivated = Dead body. Toxoid = Disarmed weapon.",
| |
| mnemonic:"Live = Alive (Attenuated). Toxoid = Toxin."
| |
| },
| |
| { id:20, type:"matrix", topic:"Alterations in Immunity (Defects)", difficulty:3,
| |
| stem:"Match the immune problem to the most likely clinical pattern.",
| |
| rows:[
| |
| { row:"T-cell dysfunction", options:["Recurrent viral/fungal infections", "Bleeding/bruising", "Kidney stones", "Hypertension only"], correct:"Recurrent viral/fungal infections" },
| |
| { row:"Antibody (B-cell) deficiency", options:["Recurrent bacterial sinopulmonary infections", "Seizures only", "Bone fractures only", "Hyperthyroidism"], correct:"Recurrent bacterial sinopulmonary infections" },
| |
| { row:"Neutropenia", options:["High risk for severe bacterial/fungal infection", "Low risk for infection", "Only allergic reactions", "Only autoimmune thyroid disease"], correct:"High risk for severe bacterial/fungal infection" }
| |
| ],
| |
| rationale:"Different immune components protect against different pathogens; deficits create predictable infection patterns.",
| |
| plain:"T-cells fight viruses/fungi. B-cells (antibodies) fight bacteria in lungs/sinuses. Neutrophils fight bacteria/fungi everywhere.",
| |
| mnemonic:"T-cell = Tough viruses. B-cell = Bacteria."
| |
| },
| |
| | |
| { id:21, type:"bowtie", topic:"NGN Bowtie – Post-Vaccine Anaphylaxis", difficulty:3,
| |
| stem:"Bowtie: Identify the risk, contributing factors, and nursing actions after an immunization.",
| |
| center:{
| |
| label:"Primary risk",
| |
| options:["Anaphylaxis", "Dehydration", "Hyperglycemia", "Constipation"],
| |
| correct:"Anaphylaxis"
| |
| },
| |
| left:{
| |
| label:"Contributing cues (pick 2)", pick:2,
| |
| options:["Wheezing/stridor", "Generalized hives", "Mild soreness at injection site", "Stable blood pressure"],
| |
| correct:["Wheezing/stridor", "Generalized hives"]
| |
| },
| |
| right:{
| |
| label:"Priority actions (pick 2)", pick:2,
| |
| options:["Call for help and give IM epinephrine per protocol", "Place client supine and support airway/oxygen", "Send client home to rest", "Document later after the shift"],
| |
| correct:["Call for help and give IM epinephrine per protocol", "Place client supine and support airway/oxygen"]
| |
| },
| |
| rationale:"Anaphylaxis is rapid and life-threatening; treat immediately with airway support and epinephrine per protocol.",
| |
| plain:"Throat closing + Hives = Shock. Don't wait. Lay them down (blood to brain) and jab with Epi (open airway).",
| |
| mnemonic:"Epipen for Epic reaction."
| |
| },
| |
| { id:22, type:"bowtie", topic:"NGN Bowtie – Neutropenic Sepsis Risk", difficulty:3,
| |
| stem:"Bowtie: Identify the risk, contributing factors, and nursing actions for an immunocompromised client.",
| |
| center:{
| |
| label:"Primary risk",
| |
| options:["Sepsis", "Hyponatremia", "Pressure injury", "Renal colic"],
| |
| correct:"Sepsis"
| |
| },
| |
| left:{
| |
| label:"Contributing cues (pick 2)", pick:2,
| |
| options:["Fever with low ANC", "Chills and tachycardia", "Normal WBC/ANC", "No symptoms for 2 weeks"],
| |
| correct:["Fever with low ANC", "Chills and tachycardia"]
| |
| },
| |
| right:{
| |
| label:"Priority actions (pick 2)", pick:2,
| |
| options:["Obtain cultures and start broad-spectrum antibiotics per protocol", "Initiate neutropenic precautions", "Delay antibiotics until imaging is complete", "Encourage raw fruits/vegetables immediately"],
| |
| correct:["Obtain cultures and start broad-spectrum antibiotics per protocol", "Initiate neutropenic precautions"]
| |
| },
| |
| rationale:"Neutropenic fever is a medical emergency; timely cultures + antibiotics and infection prevention are key.",
| |
| plain:"Fever with no white blood cells is a 5-alarm fire with no fire department. Start antibiotics NOW before the house burns down.",
| |
| mnemonic:"Fever + Chemo = Emergency."
| |
| },
| |
| | |
| { id:23, type:"trends", topic:"NGN Trends – Chemo ANC/WBC", difficulty:3,
| |
| stem:"Trends: A client receiving chemotherapy has the following labs. Interpret risk and priority action.",
| |
| table:{
| |
| headers:["Day", "WBC (K/µL)", "ANC (/µL)", "Temp (°F)"],
| |
| rows:[
| |
| ["Mon", "4.2", "1800", "98.6"],
| |
| ["Tue", "3.1", "900", "99.1"],
| |
| ["Wed", "2.4", "450", "100.6"],
| |
| ["Thu", "2.0", "320", "101.2"]
| |
| ]
| |
| },
| |
| parts:[
| |
| { type:"mcq",
| |
| prompt:"This trend most strongly indicates:",
| |
| options:["Improving immune defense", "Rising neutropenic infection risk", "Normal post-chemo pattern with no action", "Allergic reaction only"],
| |
| correct:[1]
| |
| },
| |
| { type:"sata",
| |
| prompt:"Which actions are priority now? (Select all that apply)",
| |
| options:["Notify provider for neutropenic fever protocol", "Initiate infection prevention precautions", "Encourage large group visitors for support", "Obtain ordered cultures/labs promptly", "Delay assessment until morning rounds"],
| |
| correct:[0, 1, 3]
| |
| }
| |
| ],
| |
| rationale:"ANC is dropping into severe neutropenia with fever: treat as neutropenic fever emergency.",
| |
| plain:"White count is tanking and temp is rising. This is the danger zone. Stop everything and start the 'Neutropenic Fever' protocol.",
| |
| mnemonic:"NADIR = Neutrophils Are Down, Infection Risk."
| |
| },
| |
| { id:24, type:"trends", topic:"NGN Trends – Obesity Metabolic Risk", difficulty:2,
| |
| stem:"Trends: A client with central obesity is being monitored. Identify the most meaningful interpretation and next step.",
| |
| table:{
| |
| headers:["Visit", "Waist (in)", "Fasting Glucose", "BP"],
| |
| rows:[
| |
| ["1", "44", "102", "138/86"],
| |
| ["2", "45", "110", "142/88"],
| |
| ["3", "46", "118", "148/92"],
| |
| ["4", "46", "126", "152/94"]
| |
| ]
| |
| },
| |
| parts:[
| |
| { type:"mcq",
| |
| prompt:"The trend most strongly suggests:",
| |
| options:["Improving cardiometabolic risk", "Worsening cardiometabolic risk pattern", "No clinical relevance", "Only dehydration"],
| |
| correct:[1]
| |
| },
| |
| { type:"dropdown",
| |
| prompt:"Best next nursing focus:",
| |
| blanks:[
| |
| { text:"Focus: ", options:["Lifestyle + risk-factor management counseling and follow-up", "Stop all activity to prevent fatigue", "Encourage skipping meals", "No follow-up needed"], correct:"Lifestyle + risk-factor management counseling and follow-up" }
| |
| ]
| |
| }
| |
| ],
| |
| rationale:"Rising waist, glucose, and BP reflect increasing metabolic risk; nursing focus is risk reduction and coordinated follow-up.",
| |
| plain:"The belt is tighter, sugar is up, BP is up. This is 'Metabolic Syndrome' brewing. We need to fix the lifestyle before a heart attack happens.",
| |
| mnemonic:"Metabolic Syndrome = Waist, BP, Glucose, Lipids (We B Globin')."
| |
| },
| |
| | |
| { id:25, type:"case", topic:"Unfolding Case – Immunizing Agents Clinic Visit", difficulty:2,
| |
| stem:"A 28-year-old is at a vaccine clinic. They report a severe allergy to a prior vaccine component and are currently on high-dose steroids for an autoimmune flare.",
| |
| steps:[
| |
| { title:"Step 1 — Cue Recognition", type:"sata",
| |
| prompt:"Which cues increase concern for vaccine-related harm? (Select all that apply)",
| |
| options:["History of severe allergic reaction", "High-dose steroid therapy", "Mild anxiety about needles", "Normal vital signs", "Reports prior anaphylaxis symptoms"],
| |
| correct:[0, 1, 4],
| |
| rationale:"Severe allergy history and immunosuppression are major safety flags; prior anaphylaxis symptoms heighten risk.",
| |
| plain:"If they had a bad reaction before, don't do it again. If they are on steroids, a live vaccine is risky. Stop and ask the boss.",
| |
| mnemonic:"History predicts Future."
| |
| },
| |
| { title:"Step 2 — Best Action", type:"mcq",
| |
| prompt:"What is the nurse’s best next action?",
| |
| options:["Proceed and observe for 1 minute", "Hold vaccination and notify provider/clinic lead for guidance", "Tell the client to self-administer at home", "Give two vaccines to “get it over with”"],
| |
| correct:[1],
| |
| rationale:"Contraindications/cautions require escalation before administration.",
| |
| plain:"Don't guess. If you see a red flag (allergy + steroids), STOP. Get orders.",
| |
| mnemonic:"When in doubt, Hold it out."
| |
| },
| |
| { title:"Step 3 — Patient Teaching", type:"dropdown",
| |
| prompt:"If vaccination proceeds later, the nurse should observe the client for at least:",
| |
| blanks:[
| |
| { text:"", options:["5 minutes", "15 minutes", "30 minutes", "2 hours"], correct:"15 minutes" }
| |
| ],
| |
| rationale:"Observation helps identify early severe reactions.",
| |
| plain:"15 minutes is the standard safety window to catch instant bad reactions.",
| |
| mnemonic:"15 minutes."
| |
| }
| |
| ]
| |
| },
| |
| { id:26, type:"case", topic:"Unfolding Case – Neutropenic Fever (Infection)", difficulty:3,
| |
| stem:"A client on immunosuppressants presents with fever, chills, and fatigue. Labs show low ANC. The client looks ‘okay’ but reports feeling suddenly worse today.",
| |
| steps:[
| |
| { title:"Step 1 — Priority Interpretation", type:"mcq",
| |
| prompt:"What is the priority interpretation?",
| |
| options:["Likely mild viral illness; observe only", "High risk for rapid deterioration from infection", "Normal medication side effect only", "Anxiety-related symptoms"],
| |
| correct:[1],
| |
| rationale:"Immunosuppression + fever suggests high risk for serious infection/sepsis.",
| |
| plain:"Even if they look okay, a fever in a chemo patient is a ticking bomb.",
| |
| mnemonic:"Fever + Low ANC = Sepsis Risk."
| |
| },
| |
| { title:"Step 2 — Immediate Actions", type:"sata",
| |
| prompt:"Which actions should the nurse anticipate/prepare to implement? (Select all that apply)",
| |
| options:["Cultures/labs per protocol", "Early broad-spectrum antibiotics per protocol", "Delay care until confirmatory imaging", "Frequent vital signs and assessment for sepsis", "Encourage raw salad to ‘boost immunity’"],
| |
| correct:[0, 1, 3],
| |
| rationale:"Neutropenic fever: time-sensitive cultures + antibiotics; monitor closely for sepsis.",
| |
| plain:"Get cultures (find the bug) and give antibiotics (kill the bug) ASAP. Monitor vital signs like a hawk.",
| |
| mnemonic:"Culture then Kill (Antibiotics)."
| |
| },
| |
| { title:"Step 3 — Infection Prevention", type:"mcq",
| |
| prompt:"Which visitor instruction is most appropriate?",
| |
| options:["Anyone can visit if they wear perfume-free products", "Visitors with cold symptoms should not enter", "Bring fresh flowers to improve mood", "Share food from outside restaurants freely"],
| |
| correct:[1],
| |
| rationale:"Limit exposure to infectious sources; symptomatic visitors should not visit.",
| |
| plain:"If a visitor sneezes, the patient could die. Keep sick people out.",
| |
| mnemonic:"Sick visitors stay home."
| |
| }
| |
| ]
| |
| },
| |
| { id:27, type:"case", topic:"Unfolding Case – Biologic Immunosuppressant Teaching", difficulty:2,
| |
| stem:"A client is starting a biologic for rheumatoid arthritis. They ask why they need screening and what side effects matter most.",
| |
| steps:[
| |
| { title:"Step 1 — Pre-treatment Safety", type:"mcq",
| |
| prompt:"Which screening is most important before starting therapy?",
| |
| options:["TB screening", "Color vision test", "Hearing test", "Bone density scan same day"],
| |
| correct:[0],
| |
| rationale:"Biologics can reactivate latent TB; screening is a key safety step.",
| |
| plain:"Before we lower your shields, we need to make sure there isn't a sleeping enemy (TB) inside the castle.",
| |
| mnemonic:"Screen before you Suppress."
| |
| },
| |
| { title:"Step 2 — High-Priority Teaching", type:"sata",
| |
| prompt:"Which teaching points are priority? (Select all that apply)",
| |
| options:["Report fever, cough, or sores that won’t heal", "Avoid live vaccines unless cleared", "Stop med abruptly if pain improves", "Hand hygiene and avoiding sick contacts", "Expect no infection risk change"],
| |
| correct:[0, 1, 3],
| |
| rationale:"Immunosuppressants increase infection risk; teach early reporting, prevention, and vaccine safety.",
| |
| plain:"You are more likely to get sick. Wash hands, avoid sick people, and tell us if you get a fever.",
| |
| mnemonic:"Prevention is key."
| |
| },
| |
| { title:"Step 3 — When to Seek Care", type:"mcq",
| |
| prompt:"Which symptom should prompt urgent follow-up?",
| |
| options:["Mild soreness after exercise", "New fever and shortness of breath", "Occasional dry skin", "Mild fatigue after a long day"],
| |
| correct:[1],
| |
| rationale:"Fever + respiratory symptoms in an immunosuppressed client can signal serious infection.",
| |
| plain:"Fever + trouble breathing = serious infection. Go to the ER.",
| |
| mnemonic:"Fever = Danger."
| |
| }
| |
| ]
| |
| },
| |
| { id:28, type:"case", topic:"Unfolding Case – Anorexia of Aging", difficulty:2,
| |
| stem:"An 82-year-old reports ‘no appetite,’ early fullness, and unintentional weight loss. Family notes social isolation and multiple new medications.",
| |
| steps:[
| |
| { title:"Step 1 — Key Risk", type:"mcq",
| |
| prompt:"What is the most important risk associated with anorexia of aging?",
| |
| options:["Improved longevity", "Undernutrition leading to functional decline", "Increased muscle mass", "Decreased fall risk"],
| |
| correct:[1],
| |
| rationale:"Anorexia of aging can lead to undernutrition, frailty, and higher morbidity/mortality.",
| |
| plain:"If Grandpa doesn't eat, he gets weak. Weakness leads to falls and broken hips.",
| |
| mnemonic:"No Food = No Function."
| |
| },
| |
| { title:"Step 2 — Contributors", type:"sata",
| |
| prompt:"Which factors commonly contribute? (Select all that apply)",
| |
| options:["Diminished taste/smell", "Poor dentition", "Social isolation", "Increased hunger signals with age", "Medication effects"],
| |
| correct:[0, 1, 2, 4],
| |
| rationale:"Sensory decline, dentition issues, meds, and social factors commonly reduce intake in older adults.",
| |
| plain:"He can't taste it, his teeth hurt, he's lonely, and his meds make him nauseous. No wonder he isn't eating.",
| |
| mnemonic:"MEALS on Wheels (Meds, Emotional, Anorexia, Late life, Swallowing)."
| |
| },
| |
| { title:"Step 3 — Nursing Action", type:"mcq",
| |
| prompt:"Best next nursing action?",
| |
| options:["Advise fasting to reset appetite", "Assess nutrition, meds, and social supports; coordinate follow-up", "Tell family it’s normal and no action is needed", "Encourage only one large meal per day"],
| |
| correct:[1],
| |
| rationale:"Assess reversible causes and connect to nutrition/social supports to reduce harm.",
| |
| plain:"Find the cause (teeth? meds? loneliness?) and fix it. Don't just ignore it.",
| |
| mnemonic:"Assess and Address."
| |
| }
| |
| ]
| |
| },
| |
| | |
| { id:29, type:"mcq", topic:"Cancer Biology (Metastasis)", difficulty:2,
| |
| stem:"Which finding best suggests a malignant tumor rather than a benign tumor?",
| |
| options:["Encapsulation and slow growth", "Local invasion into surrounding tissue", "Cells closely resemble normal tissue", "No recurrence after excision"],
| |
| correct:[1],
| |
| rationale:"Malignancy is associated with invasion and the potential for metastasis.",
| |
| plain:"Benign tumors stay in their lane (encapsulated). Malignant tumors break fences and invade neighbors (invasion/metastasis).",
| |
| mnemonic:"Malignant = Means to Move."
| |
| },
| |
| { id:30, type:"mcq", topic:"Cancer Epidemiology (Screening Concept)", difficulty:2,
| |
| stem:"A community program aims to reduce cancer deaths. Which approach most directly supports early detection?",
| |
| options:["Secondary prevention (screening)", "Primary prevention only", "Tertiary rehabilitation only", "Palliative care only"],
| |
| correct:[0],
| |
| rationale:"Screening is secondary prevention and supports earlier detection and treatment.",
| |
| plain:"Primary = Prevention (Sunscreen). Secondary = Screening (Mammogram). Tertiary = Treatment (Rehab).",
| |
| mnemonic:"Secondary = Screening."
| |
| }
| |
| ];
| |
| | |
| /* =========================
| |
| State + Storage
| |
| ========================= */
| |
| const LS_KEY = "unit2_ngn_hesi_session_v5";
| |
| let state = loadState() || {
| |
| currentId: 1,
| |
| startedAt: Date.now(),
| |
| flagged: {},
| |
| answers: {},
| |
| scores: {},
| |
| topicAgg: {}
| |
| };
| |
| | |
| function saveState(){ localStorage.setItem(LS_KEY, JSON.stringify(state)); }
| |
| function loadState(){ try{ return JSON.parse(localStorage.getItem(LS_KEY)); }catch(e){ return null; } }
| |
| function resetState(){
| |
| localStorage.removeItem(LS_KEY);
| |
| state = {
| |
| currentId: 1,
| |
| startedAt: Date.now(),
| |
| flagged: {},
| |
| answers: {},
| |
| scores: {},
| |
| topicAgg: {}
| |
| };
| |
| renderAll();
| |
| }
| |
| | |
| /* =========================
| |
| Scoring
| |
| ========================= */
| |
| function itemWeight(q){
| |
| const d = CONFIG.difficultyWeights[q.difficulty] ?? 1;
| |
| const m = CONFIG.formatMultipliers[q.type] ?? 1;
| |
| return Math.min(CONFIG.maxItemWeight, d * m);
| |
| }
| |
| function computeStatus(pct){
| |
| if (pct >= 0.999) return "full";
| |
| if (pct >= 0.70) return "partial";
| |
| return "missed";
| |
| }
| |
| function mcqScore(selectedIdx, correctIdx){
| |
| const earned = (selectedIdx.length===1 && correctIdx.includes(selectedIdx[0])) ? 1 : 0;
| |
| return { earned, possible: 1 };
| |
| }
| |
| function sataScore(selectedIdx, correctIdx){
| |
| const correctSet = new Set(correctIdx);
| |
| let raw = 0;
| |
| for (const idx of selectedIdx){
| |
| raw += correctSet.has(idx) ? 1 : -1;
| |
| }
| |
| raw = Math.max(0, raw);
| |
| const possible = correctIdx.length;
| |
| const earned = Math.min(possible, raw);
| |
| return { earned, possible };
| |
| }
| |
| function dropdownScore(values, blanks){
| |
| let earned = 0;
| |
| for (let i=0;i<blanks.length;i++){
| |
| if ((values[i] ?? "") === blanks[i].correct) earned += 1;
| |
| }
| |
| return { earned, possible: blanks.length };
| |
| }
| |
| function matrixScore(map, rows){
| |
| let earned = 0;
| |
| for (let i=0;i<rows.length;i++){
| |
| if ((map[i] ?? "") === rows[i].correct) earned += 1;
| |
| }
| |
| return { earned, possible: rows.length };
| |
| }
| |
| function bowtieScore(payload, q){
| |
| let earned = 0;
| |
| const possible = 1 + q.left.pick + q.right.pick;
| |
| | |
| if ((payload.center ?? "") === q.center.correct) earned += 1;
| |
| | |
| const leftCorrect = new Set(q.left.correct);
| |
| let leftEarned = 0;
| |
| for (const v of (payload.left ?? [])){
| |
| leftEarned += leftCorrect.has(v) ? 1 : -1;
| |
| }
| |
| earned += clamp(leftEarned, 0, q.left.pick);
| |
| | |
| const rightCorrect = new Set(q.right.correct);
| |
| let rightEarned = 0;
| |
| for (const v of (payload.right ?? [])){
| |
| rightEarned += rightCorrect.has(v) ? 1 : -1;
| |
| }
| |
| earned += clamp(rightEarned, 0, q.right.pick);
| |
| | |
| return { earned, possible };
| |
| }
| |
| function trendsScore(payload, q){
| |
| let earned = 0, possible = 0;
| |
| q.parts.forEach((p, idx) => {
| |
| if (p.type === "mcq"){
| |
| const s = mcqScore([payload[idx]], p.correct);
| |
| earned += s.earned; possible += s.possible;
| |
| } else if (p.type === "sata"){
| |
| const s = sataScore(payload[idx] ?? [], p.correct);
| |
| earned += s.earned; possible += s.possible;
| |
| } else if (p.type === "dropdown"){
| |
| const s = dropdownScore(payload[idx] ?? [], p.blanks);
| |
| earned += s.earned; possible += s.possible;
| |
| }
| |
| });
| |
| return { earned, possible };
| |
| }
| |
| function caseScore(payload, q){
| |
| let earned = 0, possible = 0;
| |
| q.steps.forEach((s, idx) => {
| |
| if (s.type === "mcq"){
| |
| const r = mcqScore([payload[idx]], s.correct);
| |
| earned += r.earned; possible += r.possible;
| |
| } else if (s.type === "sata"){
| |
| const r = sataScore(payload[idx] ?? [], s.correct);
| |
| earned += r.earned; possible += r.possible;
| |
| } else if (s.type === "dropdown"){
| |
| const r = dropdownScore(payload[idx] ?? [], s.blanks);
| |
| earned += r.earned; possible += r.possible;
| |
| }
| |
| });
| |
| return { earned, possible };
| |
| }
| |
| function scoreQuestion(q, payload){
| |
| let earned=0, possible=0;
| |
| | |
| if (q.type==="mcq"){
| |
| ({earned, possible} = mcqScore(payload || [], q.correct));
| |
| } else if (q.type==="sata"){
| |
| ({earned, possible} = sataScore(payload || [], q.correct));
| |
| } else if (q.type==="dropdown"){
| |
| ({earned, possible} = dropdownScore(payload || [], q.blanks));
| |
| } else if (q.type==="matrix"){
| |
| ({earned, possible} = matrixScore(payload || {}, q.rows));
| |
| } else if (q.type==="bowtie"){
| |
| ({earned, possible} = bowtieScore(payload || {}, q));
| |
| } else if (q.type==="trends"){
| |
| ({earned, possible} = trendsScore(payload || [], q));
| |
| } else if (q.type==="case"){
| |
| ({earned, possible} = caseScore(payload || [], q));
| |
| }
| |
| | |
| const pct = possible ? (earned/possible) : 0;
| |
| const status = computeStatus(pct);
| |
| | |
| const w = itemWeight(q);
| |
| const earnedW = pct * w;
| |
| const possibleW = w;
| |
| | |
| return { earned, possible, pct, status, earnedW, possibleW, at: Date.now() };
| |
| }
| |
| | |
| // Ensure payload reader exists
| |
| function readAnswerPayload(q){
| |
| const payload = getSelection(q);
| |
| if (q.type==="mcq") return payload || [];
| |
| if (q.type==="sata") return payload || [];
| |
| if (q.type==="dropdown") return payload || [];
| |
| if (q.type==="matrix") return payload || {};
| |
| if (q.type==="bowtie") return payload || { center:"", left:[], right:[] };
| |
| if (q.type==="trends") return payload || [];
| |
| if (q.type==="case") return payload || [];
| |
| return payload;
| |
| }
| |
| | |
| /* =========================
| |
| Reveal Content (Uses custom fields)
| |
| ========================= */
| |
| function renderReveal(q){
| |
| const sc = state.scores[q.id];
| |
| const scoreLine = sc
| |
| ? `<div class="sub">Score: ${Math.round(sc.pct*100)}% • Status: <strong>${escapeHtml(sc.status)}</strong></div>`
| |
| : `<div class="sub">Submit first to compute score.</div>`;
| |
| | |
| // Custom fields
| |
| const nursingLogic = q.rationale || "Rationale not provided.";
| |
| const plainLogic = q.plain || "Plain speak explanation not provided.";
| |
| const mnemonic = q.mnemonic || "No specific mnemonic for this item.";
| |
| | |
| const keyBlock = (()=>{
| |
| if (!sc) return "";
| |
| if (q.type==="mcq" || q.type==="sata"){
| |
| const correctText = q.correct.map(i=>q.options[i]).map(escapeHtml);
| |
| return `<div style="margin-top:10px"><strong>Correct answer:</strong> ${correctText.join(q.type==="sata" ? " • " : "")}</div>`;
| |
| }
| |
| if (q.type==="dropdown"){
| |
| const key = q.blanks.map(b=>`${escapeHtml(b.text)} <strong>${escapeHtml(b.correct)}</strong>`).join("<br/>");
| |
| return `<div style="margin-top:10px"><strong>Correct answer:</strong><br/>${key}</div>`;
| |
| }
| |
| if (q.type==="matrix"){
| |
| const key = q.rows.map(r=>`${escapeHtml(r.row)} → <strong>${escapeHtml(r.correct)}</strong>`).join("<br/>");
| |
| return `<div style="margin-top:10px"><strong>Correct answer:</strong><br/>${key}</div>`;
| |
| }
| |
| if (q.type==="bowtie"){
| |
| return `<div style="margin-top:10px"><strong>Correct answer:</strong></div>
| |
| <div><small class="sub">${escapeHtml(q.center.label)}:</small> <strong>${escapeHtml(q.center.correct)}</strong></div>
| |
| <div style="margin-top:6px"><small class="sub">${escapeHtml(q.left.label)}:</small> <strong>${escapeHtml(q.left.correct.join(" • "))}</strong></div>
| |
| <div style="margin-top:6px"><small class="sub">${escapeHtml(q.right.label)}:</small> <strong>${escapeHtml(q.right.correct.join(" • "))}</strong></div>`;
| |
| }
| |
| if (q.type==="trends"){
| |
| const keyParts = q.parts.map((p, idx)=>{
| |
| if (p.type==="mcq") return `Part ${idx+1}: <strong>${escapeHtml(p.options[p.correct[0]])}</strong>`;
| |
| if (p.type==="sata") return `Part ${idx+1}: <strong>${escapeHtml(p.correct.map(i=>p.options[i]).join(" • "))}</strong>`;
| |
| if (p.type==="dropdown") return `Part ${idx+1}: <strong>${escapeHtml(p.blanks.map(b=>b.correct).join(" • "))}</strong>`;
| |
| return `Part ${idx+1}: <strong>—</strong>`;
| |
| }).join("<br/>");
| |
| return `<div style="margin-top:10px"><strong>Correct answer:</strong><br/>${keyParts}</div>`;
| |
| }
| |
| if (q.type==="case"){
| |
| const keySteps = q.steps.map((s, idx)=>{
| |
| if (s.type==="mcq") return `${escapeHtml(s.title)} → <strong>${escapeHtml(s.options[s.correct[0]])}</strong>`;
| |
| if (s.type==="sata") return `${escapeHtml(s.title)} → <strong>${escapeHtml(s.correct.map(i=>s.options[i]).join(" • "))}</strong>`;
| |
| if (s.type==="dropdown") return `${escapeHtml(s.title)} → <strong>${escapeHtml(s.blanks.map(b=>b.correct).join(" • "))}</strong>`;
| |
| return `${escapeHtml(s.title)} → <strong>—</strong>`;
| |
| }).join("<br/>");
| |
| return `<div style="margin-top:10px"><strong>Correct answer:</strong><br/>${keySteps}</div>`;
| |
| }
| |
| return "";
| |
| })();
| |
| | |
| return `
| |
| ${scoreLine}
| |
| ${keyBlock}
| |
| <div class="logicGrid">
| |
| <div class="logicBox">
| |
| <div class="ttl">Logic (nursing terminology)</div>
| |
| <div class="txt">${escapeHtml(nursingLogic)}</div>
| |
| </div>
| |
| <div class="logicBox">
| |
| <div class="ttl">Logic (plain talk)</div>
| |
| <div class="txt">${escapeHtml(plainLogic)}</div>
| |
| </div>
| |
| <div class="logicBox">
| |
| <div class="ttl">Mnemonic / memory hook</div>
| |
| <div class="txt">${escapeHtml(mnemonic)}</div>
| |
| </div>
| |
| </div>
| |
| `;
| |
| }
| |
| | |
| /* =========================
| |
| Topic Aggregation
| |
| ========================= */
| |
| function bumpTopic(topic, earnedW, possibleW, status){
| |
| if (!state.topicAgg[topic]) state.topicAgg[topic] = { earned:0, possible:0, missedCount:0, partialCount:0 };
| |
| state.topicAgg[topic].earned += earnedW;
| |
| state.topicAgg[topic].possible += possibleW;
| |
| if (status === "missed") state.topicAgg[topic].missedCount += 1;
| |
| if (status === "partial") state.topicAgg[topic].partialCount += 1;
| |
| }
| |
| function rebuildTopicAgg(){
| |
| state.topicAgg = {};
| |
| for (const q of QUESTIONS){
| |
| const sc = state.scores[q.id];
| |
| if (!sc) continue;
| |
| bumpTopic(q.topic, sc.earnedW, sc.possibleW, sc.status);
| |
| }
| |
| }
| |
| function buildRevisitList(){
| |
| rebuildTopicAgg();
| |
| const out = [];
| |
| for (const [topic, agg] of Object.entries(state.topicAgg)){
| |
| const mastery = agg.possible ? (agg.earned/agg.possible)*100 : 0;
| |
| const count = agg.missedCount + agg.partialCount;
| |
| if (count <= 0) continue;
| |
| const worst = agg.missedCount > 0 ? "missed" : "partial";
| |
| out.push({ topic, mastery, count, worst });
| |
| }
| |
| out.sort((a,b)=> (a.worst===b.worst ? 0 : (a.worst==="missed" ? -1 : 1)) || (b.count-a.count) || (a.mastery-b.mastery));
| |
| return out;
| |
| }
| |
| | |
| /* =========================
| |
| Render sidebar + progress + report + viewer
| |
| ========================= */
| |
| function renderSidebar(){
| |
| const topics = uniq(QUESTIONS.map(q=>q.topic)).sort((a,b)=>a.localeCompare(b));
| |
| const tf = el("topicFilter");
| |
| tf.innerHTML = `<option value="all">All topics</option>` + topics.map(t=>`<option value="${escapeHtml(t)}">${escapeHtml(t)}</option>`).join("");
| |
| | |
| const list = el("qList");
| |
| const mode = el("filterMode").value;
| |
| const topicSel = el("topicFilter").value;
| |
| const search = (el("searchBox").value || "").trim().toLowerCase();
| |
| | |
| list.innerHTML = "";
| |
| for (const q of QUESTIONS){
| |
| const sc = state.scores[q.id];
| |
| const flagged = !!state.flagged[q.id];
| |
| const tag = statusTag(q.id);
| |
| | |
| if (topicSel !== "all" && q.topic !== topicSel) continue;
| |
| if (search && !(q.stem.toLowerCase().includes(search) || q.topic.toLowerCase().includes(search))) continue;
| |
| | |
| if (mode === "flagged" && !flagged) continue;
| |
| if (mode === "unanswered" && sc) continue;
| |
| if (mode === "missed" && (!sc || sc.status !== "missed")) continue;
| |
| if (mode === "partial" && (!sc || sc.status !== "partial")) continue;
| |
| | |
| const div = document.createElement("div");
| |
| div.className = "qitem";
| |
| div.onclick = ()=>{ state.currentId = q.id; saveState(); renderAll(true); };
| |
| | |
| const flagChip = flagged ? `<span class="tag warn">Flag</span>` : "";
| |
| div.innerHTML = `
| |
| <div class="top">
| |
| <div><strong>Q${q.id}</strong> <span class="tag">${escapeHtml(q.type.toUpperCase())}</span></div>
| |
| <div style="display:flex;gap:8px;align-items:center;justify-content:flex-end">
| |
| ${flagChip}
| |
| <span class="tag ${tag.cls}">${tag.text}</span>
| |
| </div>
| |
| </div>
| |
| <div class="t">${escapeHtml(q.topic)} • Diff ${q.difficulty}</div>
| |
| `;
| |
| list.appendChild(div);
| |
| }
| |
| }
| |
| | |
| function renderProgress(){
| |
| const answered = Object.keys(state.scores).length;
| |
| const total = QUESTIONS.length;
| |
| const pct = Math.round((answered/total)*100);
| |
| el("barFill").style.width = `${pct}%`;
| |
| el("progText").textContent = `Progress: ${answered}/${total}`;
| |
| el("pctText").textContent = `${pct}%`;
| |
| | |
| const score1200 = weightedScoreOutOf1200();
| |
| el("scoreV").textContent = score1200;
| |
| | |
| el("bandV").textContent = (score1200 >= CONFIG.passingHesiScore) ? `PASS (≥${CONFIG.passingHesiScore})` : `NOT YET (<${CONFIG.passingHesiScore})`;
| |
| | |
| const revisit = buildRevisitList();
| |
| el("reviewCount").textContent = revisit.length;
| |
| const ul = el("reviewTopics");
| |
| ul.innerHTML = "";
| |
| revisit.slice(0,8).forEach(r=>{
| |
| const li = document.createElement("li");
| |
| li.innerHTML = `<strong>${escapeHtml(r.topic)}</strong>
| |
| <span class="chip ${r.worst==='missed'?'bad':'warn'}">${r.worst}</span>
| |
| <div><small>${r.count} item(s) impacted • mastery ${Math.round(r.mastery)}%</small></div>`;
| |
| ul.appendChild(li);
| |
| });
| |
| if (revisit.length > 8){
| |
| const li = document.createElement("li");
| |
| li.innerHTML = `<small>+ ${revisit.length-8} more… (see End Report)</small>`;
| |
| ul.appendChild(li);
| |
| }
| |
| | |
| const overallPct = overallMastery();
| |
| el("repWeighted").textContent = `Weighted: ${score1200}`;
| |
| el("repPct").textContent = `Percent: ${Math.round(overallPct)}%`;
| |
| el("repBand").textContent = `Benchmark: ${score1200 >= CONFIG.passingHesiScore ? "PASS" : "NOT YET"} (875)`;
| |
| }
| |
| | |
| function renderReport(){
| |
| rebuildTopicAgg();
| |
| const topics = uniq(QUESTIONS.map(q=>q.topic)).sort((a,b)=>a.localeCompare(b));
| |
| const tbody = el("topicTable").querySelector("tbody");
| |
| tbody.innerHTML = "";
| |
| topics.forEach(t=>{
| |
| const agg = state.topicAgg[t];
| |
| const mastery = agg && agg.possible ? (agg.earned/agg.possible)*100 : 0;
| |
| const status = mastery >= 85 ? "Strong" : (mastery >= 70 ? "Developing" : (agg ? "Needs Work" : "Not started"));
| |
| const tr = document.createElement("tr");
| |
| tr.innerHTML = `
| |
| <td>${escapeHtml(t)}</td>
| |
| <td>${Math.round(mastery)}%</td>
| |
| <td>${escapeHtml(status)}</td>
| |
| `;
| |
| tbody.appendChild(tr);
| |
| });
| |
| | |
| const rev = buildRevisitList();
| |
| const ul = el("revisitList");
| |
| ul.innerHTML = "";
| |
| if (rev.length === 0){
| |
| const li = document.createElement("li");
| |
| li.innerHTML = `<strong>No revisit topics yet.</strong><div><small>Answer items and this list will populate automatically.</small></div>`;
| |
| ul.appendChild(li);
| |
| } else {
| |
| rev.forEach(r=>{
| |
| const li = document.createElement("li");
| |
| li.innerHTML = `<strong>${escapeHtml(r.topic)}</strong>
| |
| <span class="chip ${r.worst==='missed'?'bad':'warn'}">${r.worst}</span>
| |
| <div><small>${r.count} item(s) impacted • mastery ${Math.round(r.mastery)}%</small></div>`;
| |
| ul.appendChild(li);
| |
| });
| |
| }
| |
| }
| |
| | |
| function renderViewer(){
| |
| const q = QUESTIONS.find(x=>x.id===state.currentId) || QUESTIONS[0];
| |
| if (!q) return;
| |
| | |
| const flagged = !!state.flagged[q.id];
| |
| const tag = statusTag(q.id);
| |
| const w = itemWeight(q);
| |
| | |
| const viewer = el("viewer");
| |
| viewer.innerHTML = `
| |
| <div class="header">
| |
| <div>
| |
| <h2>Q${q.id} of ${QUESTIONS.length}</h2>
| |
| <div class="prompt">${escapeHtml(q.stem)}</div>
| |
| <div class="hint">Pick your answer, submit, and the logic will auto-reveal.</div>
| |
| </div>
| |
| <div class="meta">
| |
| <span class="badge">${escapeHtml(q.type.toUpperCase())}</span>
| |
| <span class="badge">Diff ${q.difficulty}</span>
| |
| <span class="badge">Weight ${w}</span>
| |
| <span class="badge">${escapeHtml(q.topic)}</span>
| |
| <span class="badge">${escapeHtml(tag.text)}</span>
| |
| </div>
| |
| </div>
| |
| | |
| <div id="qBody"></div>
| |
| | |
| <div class="actions">
| |
| <button class="btn" id="flagBtn">${flagged ? "Unflag" : "Flag"}</button>
| |
| <button class="btn primary" id="submitBtn">Submit</button>
| |
| <button class="btn" id="clearBtn">Clear response</button>
| |
| </div>
| |
| | |
| <div class="revealBox" id="revealBox" style="display:none">
| |
| <div class="rtitle">Answer + Logic (3 ways)</div>
| |
| <div id="revealContent"></div>
| |
| </div>
| |
| `;
| |
| const body = viewer.querySelector("#qBody");
| |
| body.appendChild(renderQuestionBody(q));
| |
| | |
| viewer.querySelector("#flagBtn").onclick = ()=>{
| |
| state.flagged[q.id] = !state.flagged[q.id];
| |
| saveState(); renderAll(true);
| |
| };
| |
| | |
| viewer.querySelector("#clearBtn").onclick = ()=>{
| |
| delete state.answers[q.id];
| |
| delete state.scores[q.id];
| |
| rebuildTopicAgg();
| |
| saveState(); renderAll(true);
| |
| };
| |
| | |
| viewer.querySelector("#submitBtn").onclick = ()=>{
| |
| const payload = readAnswerPayload(q);
| |
| state.answers[q.id] = payload;
| |
| | |
| const scored = scoreQuestion(q, payload);
| |
| state.scores[q.id] = scored;
| |
| | |
| rebuildTopicAgg();
| |
| saveState();
| |
| renderAll(true);
| |
| | |
| const rb = el("viewer").querySelector("#revealBox");
| |
| rb.style.display = "block";
| |
| el("viewer").querySelector("#revealContent").innerHTML = renderReveal(q);
| |
| };
| |
| | |
| if (state.scores[q.id]){
| |
| const rb = viewer.querySelector("#revealBox");
| |
| rb.style.display = "block";
| |
| viewer.querySelector("#revealContent").innerHTML = renderReveal(q);
| |
| }
| |
| }
| |
| | |
| function next(){
| |
| const idx = QUESTIONS.findIndex(q=>q.id===state.currentId);
| |
| if (idx < QUESTIONS.length-1){
| |
| state.currentId = QUESTIONS[idx+1].id;
| |
| saveState(); renderAll(true);
| |
| }
| |
| }
| |
| function prev(){
| |
| const idx = QUESTIONS.findIndex(q=>q.id===state.currentId);
| |
| if (idx > 0){
| |
| state.currentId = QUESTIONS[idx-1].id;
| |
| saveState(); renderAll(true);
| |
| }
| |
| }
| |
| | |
| function renderAll(keepScroll=false){
| |
| renderSidebar();
| |
| renderProgress();
| |
| renderViewer();
| |
| renderReport();
| |
| if (!keepScroll){
| |
| window.scrollTo({top:0, behavior:"auto"});
| |
| }
| |
| }
| |
| | |
| /* =========================
| |
| Events
| |
| ========================= */
| |
| el("filterMode").addEventListener("change", ()=>renderSidebar());
| |
| el("topicFilter").addEventListener("change", ()=>renderSidebar());
| |
| el("searchBox").addEventListener("input", ()=>renderSidebar());
| |
| | |
| el("nextBtn").addEventListener("click", next);
| |
| el("prevBtn").addEventListener("click", prev);
| |
| el("resetBtn").addEventListener("click", ()=>{
| |
| if (confirm("Reset this Unit 2 session? This clears answers and progress.")) resetState();
| |
| });
| |
| el("jumpReportBtn").addEventListener("click", ()=>{
| |
| el("reportCard").scrollIntoView({behavior:"smooth", block:"start"});
| |
| });
| |
| el("downloadBtn").addEventListener("click", ()=>{
| |
| const payload = { unit: CONFIG.unitName, exportedAt: new Date().toISOString(), state };
| |
| const blob = new Blob([JSON.stringify(payload,null,2)], {type:"application/json"});
| |
| const url = URL.createObjectURL(blob);
| |
| const a = document.createElement("a");
| |
| a.href = url;
| |
| a.download = "unit1_session.json";
| |
| a.click();
| |
| URL.revokeObjectURL(url);
| |
| });
| |
| | |
| /* =========================
| |
| Init
| |
| ========================= */
| |
| (function init(){
| |
| rebuildTopicAgg();
| |
| renderAll();
| |
| })(); | |
| </script>
| |
| </body>
| |
| </html>
| |
| </html>
| |
|
| |
|
| | [[Category:HU NSG 520 Pathophysiology and Pharmacology]] |
| [[Category:Herzing University/Games]] | | [[Category:Herzing University/Games]] |