|
|
| (4 intermediate revisions by the same user not shown) |
| Line 1: |
Line 1: |
| <html>
| | {{#widget:520Unit1StudyBuddy}} |
| <!doctype html>
| |
| <html lang="en">
| |
| <head>
| |
| <meta charset="utf-8" />
| |
| <meta name="viewport" content="width=device-width,initial-scale=1" />
| |
| <title>Unit 1 ONLY</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 1 Study Buddy</h1>
| |
| <div class="sub">
| |
| Unit 1 concept domains (Altered Cellular/Tissue Biology + injury/death + toxins/infection/inflammation + radiation/pressure + aging/frailty + genetics/epigenetics + core math safety).
| |
| <br/>No Unit 2 or dosage calc content.
| |
| </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>
| |
| /* =========================
| |
| 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) : "");
| |
| });
| |
| | |
| /* =========================
| |
| CONFIG (editable)
| |
| ========================= */
| |
| const CONFIG = {
| |
| unitName: "Unit 1",
| |
| passingHesiScore: 875, // <-- ONLY CHANGE: benchmark for passing
| |
| 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
| |
| };
| |
| | |
| /* =========================
| |
| QUESTIONS (Unit 1 ONLY) – PATHO/THEORY
| |
| ========================= */
| |
| const QUESTIONS = [
| |
| { id:1, type:"mcq", topic:"Cellular Adaptation", difficulty:2,
| |
| stem:"A client has chronic GERD. Endoscopy notes that the distal esophagus now has columnar-appearing epithelium. Which adaptive change best fits this finding?",
| |
| options:["Hyperplasia","Metaplasia","Atrophy","Apoptosis"],
| |
| correct:[1],
| |
| rationale:"Metaplasia is a reversible replacement of one differentiated cell type with another better suited to chronic stress (e.g., acid exposure).",
| |
| plain:"The esophagus cells changed their 'outfit' to handle the acid better. It's not cancer yet, just a swap for protection.",
| |
| mnemonic:"Metaplasia = Morphing (changing shape/type)."
| |
| },
| |
| { id:2, type:"mcq", topic:"Dysplasia", difficulty:2,
| |
| stem:"A cervical biopsy report mentions dysplasia. Which statement best describes dysplasia?",
| |
| options:[
| |
| "A normal, expected increase in cell size with exercise",
| |
| "A reversible change where one mature cell type is replaced by another",
| |
| "Disordered growth with abnormal size/shape/organization that can precede cancer",
| |
| "A programmed cell death process that prevents inflammation"
| |
| ],
| |
| correct:[2],
| |
| rationale:"Dysplasia involves disorganized, atypical cellular growth and can be a precursor to malignancy depending on severity and persistence.",
| |
| plain:"The cells are looking weird, messy, and disorganized. This is the 'warning zone' before actual cancer.",
| |
| mnemonic:"Dysplasia = Disordered (Bad growth)."
| |
| },
| |
| { id:3, type:"mcq", topic:"Ischemic/Hypoxic Injury", difficulty:2,
| |
| stem:"During a prolonged hypoxic event, which cellular change most directly contributes to early cell swelling?",
| |
| options:["Activation of DNA repair enzymes","Failure of ATP-dependent ion pumps","Increased collagen synthesis","Immediate lysosomal rupture"],
| |
| correct:[1],
| |
| rationale:"ATP depletion impairs ion pumps (especially Na+/K+ ATPase), causing Na+ and water influx and swelling.",
| |
| plain:"No oxygen = No energy (ATP). Without energy, the bilge pump stops working, water floods the ship (cell), and it swells.",
| |
| mnemonic:"No Air? Sodium's There (and water follows)."
| |
| },
| |
| { id:4, type:"mcq", topic:"Ischemia–Reperfusion Injury", difficulty:3,
| |
| stem:"After return of blood flow to ischemic tissue, which mechanism most contributes to additional injury?",
| |
| options:["Decreased inflammatory signaling","Reactive oxygen species generation and inflammation","Permanent restoration of membrane stability","Reduced intracellular calcium"],
| |
| correct:[1],
| |
| rationale:"Reperfusion can trigger ROS bursts and inflammatory cascades that extend tissue damage beyond the initial ischemia.",
| |
| plain:"Bringing oxygen back too fast is like fanning a fire. It creates 'sparks' (free radicals) that cause fresh damage.",
| |
| mnemonic:"Reperfusion = Rust (Oxidative stress)."
| |
| },
| |
| { id:5, type:"mcq", topic:"Oxidative Stress", difficulty:2,
| |
| stem:"Which clinical explanation best connects oxidative stress to cellular injury?",
| |
| options:[
| |
| "Reactive species improve enzyme efficiency",
| |
| "Reactive species can damage lipids, proteins, and DNA",
| |
| "Oxidative stress prevents cell death in severe injury",
| |
| "Oxidative stress only affects extracellular fluid"
| |
| ],
| |
| correct:[1],
| |
| rationale:"ROS can cause lipid peroxidation, protein modification, and DNA injury, disrupting cell function and viability.",
| |
| plain:"Free radicals are like bulls in a china shop—they smash cell walls (lipids) and tear up the instruction manual (DNA).",
| |
| mnemonic:"ROS = Rotting Our Structures."
| |
| },
| |
| { id:6, type:"mcq", topic:"Chemical/Toxic Injury", difficulty:2,
| |
| stem:"A client is brought in after exposure to a high concentration of household ammonia in a poorly ventilated space. Which immediate nursing concern most closely matches this toxin exposure pattern?",
| |
| options:["Delayed joint inflammation only","Airway irritation/bronchospasm and impaired oxygenation","Purely neurologic injury without respiratory effects","Urinary retention due to smooth muscle hypertrophy"],
| |
| correct:[1],
| |
| rationale:"Many inhaled irritants cause acute airway inflammation/bronchospasm, leading to oxygenation compromise.",
| |
| plain:"Inhaling strong chemicals burns the lungs. The airway swells shut to protect itself, blocking air.",
| |
| mnemonic:"Ammonia = Airway (A-A)."
| |
| },
| |
| { id:7, type:"mcq", topic:"Infectious Injury", difficulty:2,
| |
| stem:"A client with neutropenia develops a new fever and chills. Which pathophysiologic concept best explains why this client can deteriorate quickly?",
| |
| options:["Enhanced adaptive immunity with rapid antibody formation","Reduced first-line cellular defenses against infection","Decreased oxidative stress during infection","Increased ability to form granulomas rapidly"],
| |
| correct:[1],
| |
| rationale:"Neutropenia reduces critical innate cellular defenses, increasing risk for rapid progression of infection.",
| |
| plain:"No soldiers (neutrophils) means the enemy (bacteria) takes over fast, with no alarm bells (pus/swelling) ringing.",
| |
| mnemonic:"Neutropenia = No Protection."
| |
| },
| |
| { id:8, type:"mcq", topic:"Immunologic/Inflammatory Injury", difficulty:2,
| |
| stem:"A client develops widespread hives and wheezing minutes after receiving IV contrast. Which mechanism best explains the immediate clinical risk?",
| |
| options:["Gradual fibrosis from chronic injury","Acute immune-mediated mediator release affecting airway and circulation","Atrophy of respiratory epithelium","Coagulative necrosis of bronchioles"],
| |
| correct:[1],
| |
| rationale:"Acute hypersensitivity can cause mediator release leading to bronchospasm, airway edema, and circulatory collapse.",
| |
| plain:"The immune system overreacts instantly. It dumps chemicals that close the throat and drop blood pressure.",
| |
| mnemonic:"Anaphylaxis = Airway & Arteries (both close/drop)."
| |
| },
| |
| { id:9, type:"mcq", topic:"Ionizing Radiation", difficulty:2,
| |
| stem:"Which statement best explains why ionizing radiation can increase long-term cancer risk?",
| |
| options:["It always causes immediate cell swelling only","It can cause DNA damage and mutations","It permanently prevents cell division in all tissues","It only affects skin cells and never internal organs"],
| |
| correct:[1],
| |
| rationale:"Ionizing radiation can damage DNA, increasing mutation risk and potential malignant transformation.",
| |
| plain:"Radiation zaps the cell's instruction manual (DNA). If the manual is rewritten wrong, the cell might grow uncontrollably (cancer).",
| |
| mnemonic:"Radiation = Rewriting DNA."
| |
| },
| |
| { id:10, type:"mcq", topic:"Aging/Frailty", difficulty:2,
| |
| stem:"Which clinical pattern best supports frailty (as a syndrome) rather than normal aging alone?",
| |
| options:["Stable chronic disease with independent ADLs","Unintentional weight loss, weakness, slowed gait, and low activity","Mild presbyopia requiring new glasses","Occasional constipation"],
| |
| correct:[1],
| |
| rationale:"Frailty commonly includes weight loss, weakness, low activity, exhaustion, and slowed performance, increasing vulnerability to stressors.",
| |
| plain:"Frailty isn't just getting old; it's running out of gas. A small bump (illness) can knock them down like a sledgehammer.",
| |
| mnemonic:"Frailty = Fragile (Handle with care)."
| |
| },
| |
| | |
| { id:11, type:"sata", topic:"Irreversible Cell Injury", difficulty:3,
| |
| stem:"Which findings most strongly suggest irreversible cellular injury? (Select all that apply)",
| |
| options:[
| |
| "Loss of plasma membrane integrity",
| |
| "Marked mitochondrial dysfunction with inability to restore ATP",
| |
| "Mild cell swelling that resolves after oxygen is restored",
| |
| "Nuclear fragmentation (karyorrhexis)",
| |
| "Ribosomes detach temporarily and later reattach"
| |
| ],
| |
| correct:[0,1,3],
| |
| rationale:"Irreversible injury is suggested by membrane disruption, severe mitochondrial failure, and nuclear breakdown.",
| |
| plain:"If the shell breaks (membrane), the engine dies (mitochondria), or the brain dissolves (nucleus), the cell is gone for good.",
| |
| mnemonic:"The 3 M's of Death: Membrane, Mitochondria, Melting Nucleus."
| |
| },
| |
| { id:12, type:"sata", topic:"Cellular Manifestations: Accumulations", difficulty:2,
| |
| stem:"Which intracellular accumulations can occur with injury or metabolic derangements? (Select all that apply)",
| |
| options:["Lipids","Glycogen","Proteins","Calcium","Oxygen molecules stored in lysosomes"],
| |
| correct:[0,1,2,3],
| |
| rationale:"Common abnormal accumulations include lipids, glycogen, proteins, and calcium; oxygen is not stored as a lysosomal “accumulation.”",
| |
| plain:"Sick cells act like hoarders. They pile up fat, sugar, proteins, or calcium because they can't clean house effectively.",
| |
| mnemonic:"Cells Hoard Junk (Ca, Fat, Protein)."
| |
| },
| |
| { id:13, type:"sata", topic:"Necrosis vs Apoptosis", difficulty:2,
| |
| stem:"Which findings align more with necrosis than apoptosis? (Select all that apply)",
| |
| options:[
| |
| "Cell swelling and membrane rupture",
| |
| "Inflammation in surrounding tissue",
| |
| "Orderly cell shrinkage with minimal inflammation",
| |
| "Leakage of intracellular enzymes into blood",
| |
| "Formation of membrane-bound apoptotic bodies"
| |
| ],
| |
| correct:[0,1,3],
| |
| rationale:"Necrosis involves swelling, membrane rupture, enzyme leakage, and inflammatory response.",
| |
| plain:"Necrosis is a messy murder scene—cells explode and cause inflammation. Apoptosis is a tidy, planned exit.",
| |
| mnemonic:"Necrosis = Nasty/Messy. Apoptosis = Polite/Planned."
| |
| },
| |
| { id:14, type:"sata", topic:"Environmental Toxins: Nursing Risk Reduction", difficulty:2,
| |
| stem:"A nurse is teaching about reducing exposure to common environmental toxins at home. Which recommendations are appropriate? (Select all that apply)",
| |
| options:[
| |
| "Ensure adequate ventilation when using cleaning chemicals",
| |
| "Store chemicals in original containers and out of children’s reach",
| |
| "Mix bleach and ammonia for stronger cleaning",
| |
| "Use gloves/eye protection when indicated on labels",
| |
| "Ignore label warnings if symptoms don’t appear immediately"
| |
| ],
| |
| correct:[0,1,3],
| |
| rationale:"Ventilation, safe storage, and PPE reduce risk; mixing chemicals can create toxic gases; label warnings matter even without immediate symptoms.",
| |
| plain:"Don't mix chemicals (it makes poison gas), wear gear, and open a window. Safety 101.",
| |
| mnemonic:"Vent, Vest (PPE), and Verify (labels)."
| |
| },
| |
| { id:15, type:"sata", topic:"Genetics & Epigenetics", difficulty:3,
| |
| stem:"Which statements about epigenetics are accurate? (Select all that apply)",
| |
| options:[
| |
| "Epigenetics can alter gene expression without changing the DNA sequence",
| |
| "DNA methylation and histone modification are epigenetic mechanisms",
| |
| "Epigenetic changes are fixed and cannot shift across life",
| |
| "miRNAs can influence gene expression regulation",
| |
| "Epigenetics is unrelated to disease risk"
| |
| ],
| |
| correct:[0,1,3],
| |
| rationale:"Epigenetic regulation can change over time and influences development and disease risk via multiple mechanisms.",
| |
| plain:"The DNA book stays the same, but epigenetics uses sticky notes to say 'Skip this page' or 'Read this loudly.'",
| |
| mnemonic:"Epi = Above (Above the genes)."
| |
| },
| |
| { id:16, type:"sata", topic:"Math Safety Foundations", difficulty:2,
| |
| stem:"Which habits reduce calculation-related medication errors? (Select all that apply)",
| |
| options:[
| |
| "Convert units to a single system before solving",
| |
| "Estimate whether your final answer makes sense clinically",
| |
| "Skip unit labels to save time",
| |
| "Recheck decimal placement before finalizing",
| |
| "Use a second check for high-alert meds when available"
| |
| ],
| |
| correct:[0,1,3,4],
| |
| rationale:"Unit consistency, estimation, decimal safety, and double-checks reduce error risk.",
| |
| plain:"Math errors kill. Check your units, watch your decimals, and ask 'Does this dose make sense?'",
| |
| mnemonic:"Stop and Smell the Roses (Reasonableness, Order, Safety)."
| |
| },
| |
| | |
| { id:17, type:"dropdown", topic:"Cellular Adaptation", difficulty:2,
| |
| stem:"Choose the best term for each situation.",
| |
| blanks:[
| |
| { text:"Decreased muscle bulk after prolonged bedrest: ", options:["Atrophy","Hypertrophy","Hyperplasia","Metaplasia"], correct:"Atrophy" },
| |
| { text:"Increased glandular tissue in pregnancy to prepare for lactation: ", options:["Hyperplasia","Atrophy","Necrosis","Calcification"], correct:"Hyperplasia" }
| |
| ],
| |
| rationale:"Atrophy = decreased size; hyperplasia = increased number of cells (common in hormonally stimulated tissues).",
| |
| plain:"Use it or lose it (Atrophy). Pregnant body needs MORE machinery, so it builds more cells (Hyperplasia).",
| |
| mnemonic:"A-trophy (Absent trophy = small). Hyper-plasia (Plus cells)."
| |
| },
| |
| { id:18, type:"dropdown", topic:"Ischemia/Hypoxia", difficulty:2,
| |
| stem:"Complete the statement.",
| |
| blanks:[
| |
| { text:"Early hypoxic cell injury often starts with ATP depletion leading to failure of ", options:["DNA replication","ATP-dependent ion pumps","Collagen synthesis","Antibody formation"], correct:"ATP-dependent ion pumps" }
| |
| ],
| |
| rationale:"ATP depletion impairs ion gradients, producing swelling and dysfunction.",
| |
| plain:"No fuel (ATP) -> The pump stops -> Water floods the boat.",
| |
| mnemonic:"No ATP = Pump Fail = Swell."
| |
| },
| |
| { id:19, type:"dropdown", topic:"Epigenetic Mechanisms", difficulty:3,
| |
| stem:"Match mechanism to description.",
| |
| blanks:[
| |
| { text:"Adding methyl groups to DNA to affect expression: ", options:["DNA methylation","Coagulative necrosis","Ion channel upregulation","Fibrosis"], correct:"DNA methylation" },
| |
| { text:"Small RNAs that can suppress translation: ", options:["miRNAs","Neutrophils","Keratin","Urate"], correct:"miRNAs" }
| |
| ],
| |
| rationale:"DNA methylation and miRNAs are classic gene-expression regulators without changing DNA sequence.",
| |
| plain:"Methylation = Muting the gene. miRNA = Micro-managers blocking the message.",
| |
| mnemonic:"Methylation Mutes."
| |
| },
| |
| { id:20, type:"dropdown", topic:"Math Foundations", difficulty:2,
| |
| stem:"Select the correct option.",
| |
| blanks:[
| |
| { text:"A ratio compares ", options:["Two quantities","Only time","Only temperature","Only oxygen"], correct:"Two quantities" },
| |
| { text:"A proportion means two ratios are ", options:["Unrelated","Equal","Always smaller","Always larger"], correct:"Equal" }
| |
| ],
| |
| rationale:"Ratios compare quantities; proportions set two ratios equal (common in dosage setups).",
| |
| plain:"Ratio is This vs. That. Proportion is saying 'This match is equal to That match.'",
| |
| mnemonic:"Proportion = Perfectly Equal."
| |
| },
| |
| | |
| { id:21, type:"case", topic:"Unfolding Case: Hypoxia → Injury", difficulty:3,
| |
| stem:"A 67-year-old with COPD arrives with increasing dyspnea. VS: RR 30, HR 118, SpO2 86% RA, using accessory muscles. ABG pending.",
| |
| steps:[
| |
| { title:"Step 1 — Cues", type:"sata",
| |
| prompt:"Which cues suggest high risk for hypoxic cellular injury? (Select all that apply)",
| |
| options:["SpO2 86% on room air","RR 30 with accessory use","HR 118","New mild ankle edema only","Dyspnea with fatigue"],
| |
| correct:[0,1,2,4],
| |
| rationale:"Severe hypoxemia and signs of increased work of breathing with tachycardia and fatigue indicate oxygen delivery mismatch.",
| |
| plain:"Look for signs of struggle: Fast breathing, fast heart, low oxygen. The engine is running hot but getting no air.",
| |
| mnemonic:"Air Hunger = Cell Danger."
| |
| },
| |
| { title:"Step 2 — Analysis", type:"mcq",
| |
| prompt:"Which cellular change is most likely early if hypoxia persists?",
| |
| options:["ATP depletion leading to ion pump failure and swelling","Immediate nuclear fragmentation","Instant fibrosis of alveoli","Urate crystal deposition in lungs"],
| |
| correct:[0],
| |
| rationale:"Early reversible injury often begins with ATP depletion and ion pump dysfunction.",
| |
| plain:"If the air cuts off, the first thing to go is the battery power (ATP). Then the cell swells.",
| |
| mnemonic:"Hypoxia -> No ATP -> Swell."
| |
| },
| |
| { title:"Step 3 — Action", type:"dropdown",
| |
| prompt:"Choose the best immediate nursing priority.",
| |
| blanks:[
| |
| { text:"Priority: ", options:["Apply oxygen and reassess response","Delay interventions until ABG returns","Encourage oral fluids","Provide sedative first"], correct:"Apply oxygen and reassess response" }
| |
| ],
| |
| rationale:"Immediate oxygenation support and reassessment are key to preventing progression to irreversible injury.",
| |
| plain:"Don't wait for lab results when the patient is suffocating. Give O2 now.",
| |
| mnemonic:"Airway First."
| |
| }
| |
| ]
| |
| },
| |
| { id:22, type:"case", topic:"Unfolding Case: Reperfusion Red Flags", difficulty:3,
| |
| stem:"A client undergoes revascularization for acute limb ischemia. Post-op baseline pulses were present. Two hours later: escalating pain, tight swelling, numbness.",
| |
| steps:[
| |
| { title:"Step 1 — Cues", type:"sata",
| |
| prompt:"Which cues raise concern for emergent complication? (Select all that apply)",
| |
| options:["Escalating pain out of proportion","Tense swelling","New numbness/tingling","Pulses weaker than baseline","Comfortable pain control with stable sensation"],
| |
| correct:[0,1,2,3],
| |
| rationale:"Pain out of proportion, neuro changes, swelling, and decreasing pulses suggest compartment syndrome/neurovascular compromise risk.",
| |
| plain:"Pain that meds won't touch + tight skin + numbness = BAD. The limb is under pressure.",
| |
| mnemonic:"The 5 P's: Pain, Pallor, Pulselessness, Paresthesia, Paralysis."
| |
| },
| |
| { title:"Step 2 — Analysis", type:"mcq",
| |
| prompt:"Which mechanism can worsen injury after blood flow is restored?",
| |
| options:["Reactive oxygen species generation and inflammation","Complete shutdown of inflammatory mediators","Permanent normalization of calcium balance","Immediate collagen remodeling preventing edema"],
| |
| correct:[0],
| |
| rationale:"Reperfusion can trigger ROS/inflammation that extends damage.",
| |
| plain:"The return of blood flow caused a 'flash fire' of inflammation (ROS), making the swelling worse.",
| |
| mnemonic:"Reperfusion = Re-injury risk."
| |
| },
| |
| { title:"Step 3 — Action", type:"dropdown",
| |
| prompt:"Best nursing response now:",
| |
| blanks:[
| |
| { text:"Response: ", options:[
| |
| "Document and recheck in 1 hour",
| |
| "Notify provider urgently and perform frequent neurovascular reassessments",
| |
| "Encourage ambulation to improve circulation",
| |
| "Apply heat and elevate above heart aggressively"
| |
| ], correct:"Notify provider urgently and perform frequent neurovascular reassessments" }
| |
| ],
| |
| rationale:"This is time-sensitive. Escalate and reassess neurovascular status frequently.",
| |
| plain:"This is an emergency (Compartment Syndrome). Call the provider immediately.",
| |
| mnemonic:"Time is Tissue."
| |
| }
| |
| ]
| |
| },
| |
| { id:23, type:"case", topic:"Unfolding Case: Hypersensitivity Injury", difficulty:2,
| |
| stem:"A client starts a new antibiotic. On day 6 they report fever, diffuse rash, and joint aches. No shortness of breath currently.",
| |
| steps:[
| |
| { title:"Step 1 — Cues", type:"mcq",
| |
| prompt:"This pattern is most consistent with:",
| |
| options:["Immunologic (hypersensitivity) injury","Pure ischemic injury","Ionizing radiation injury","Mechanical pressure injury"],
| |
| correct:[0],
| |
| rationale:"Fever, rash, and arthralgias after a new med suggests immune-mediated drug reaction.",
| |
| plain:"New drug + rash + fever = The immune system hates the drug.",
| |
| mnemonic:"Rash + Fever = Drug Reaction?"
| |
| },
| |
| { title:"Step 2 — Analysis", type:"sata",
| |
| prompt:"Which assessments help identify escalation risk? (Select all that apply)",
| |
| options:["Airway symptoms (wheezing, throat tightness)","Mucosal involvement (oral lesions)","Vital signs trends","Cap refill of toes only","Extent/progression of rash"],
| |
| correct:[0,1,2,4],
| |
| rationale:"Airway/mucosal involvement and worsening vitals/rash progression suggest higher severity.",
| |
| plain:"Check the ABCs. Is the throat closing? Is the skin peeling (mucosa)? Is the rash spreading fast?",
| |
| mnemonic:"Watch the Airway & Mucosa."
| |
| },
| |
| { title:"Step 3 — Action", type:"dropdown",
| |
| prompt:"Choose the best next step.",
| |
| blanks:[
| |
| { text:"Next step: ", options:[
| |
| "Continue medication to confirm it’s the cause",
| |
| "Hold the suspected agent per protocol and notify provider",
| |
| "Tell the client to stop all fluids",
| |
| "Ignore unless blisters appear"
| |
| ], correct:"Hold the suspected agent per protocol and notify provider" }
| |
| ],
| |
| rationale:"Stop/hold suspected trigger and escalate to prevent progression.",
| |
| plain:"Stop the drug immediately. Don't add fuel to the fire.",
| |
| mnemonic:"Stop the Trigger."
| |
| }
| |
| ]
| |
| },
| |
| { id:24, type:"case", topic:"Unfolding Case: Aging/Frailty Risk", difficulty:2,
| |
| stem:"An 82-year-old lives alone and reports recent unintentional weight loss and fatigue. They walk slower and avoid stairs now. No acute illness today.",
| |
| steps:[
| |
| { title:"Step 1 — Cues", type:"sata",
| |
| prompt:"Which cues support frailty risk? (Select all that apply)",
| |
| options:["Unintentional weight loss","Slowed gait","Fatigue/exhaustion","Avoiding activity due to weakness","Occasional reading glasses use"],
| |
| correct:[0,1,2,3],
| |
| rationale:"Frailty patterns include weight loss, weakness, exhaustion, slow gait, and low activity.",
| |
| plain:"The engine is sputtering: losing weight, moving slow, no energy. This isn't just 'old age'.",
| |
| mnemonic:"Fried's Phenotype (Weight, Exhaustion, Grip, Speed, Activity)."
| |
| },
| |
| { title:"Step 2 — Analysis", type:"mcq",
| |
| prompt:"Why does frailty matter clinically?",
| |
| options:[
| |
| "It only affects mood, not outcomes",
| |
| "It increases vulnerability to stressors (infection, surgery, falls)",
| |
| "It guarantees cancer within 1 year",
| |
| "It is identical to normal aging"
| |
| ],
| |
| correct:[1],
| |
| rationale:"Frailty increases vulnerability and worsens outcomes after physiologic stress.",
| |
| plain:"A frail person is like a house of cards. One small gust of wind (illness/fall) can knock the whole thing down.",
| |
| mnemonic:"Frailty = Zero Reserve."
| |
| },
| |
| { title:"Step 3 — Action", type:"dropdown",
| |
| prompt:"Best nursing planning focus?",
| |
| blanks:[
| |
| { text:"Focus: ", options:[
| |
| "Ignore because vitals are stable",
| |
| "Screen nutrition, strength/mobility, and safety needs; coordinate supports",
| |
| "Only provide medication education",
| |
| "Encourage maximal exertion without rest"
| |
| ], correct:"Screen nutrition, strength/mobility, and safety needs; coordinate supports" }
| |
| ],
| |
| rationale:"Frailty care emphasizes risk reduction, functional support, and coordinated interventions.",
| |
| plain:"Build them up. Get nutrition on board, check safety, and get help at home.",
| |
| mnemonic:"Support the Foundation."
| |
| }
| |
| ]
| |
| },
| |
| | |
| { id:25, type:"matrix", topic:"Adaptation vs Death: Pattern ID", difficulty:2,
| |
| stem:"Match each scenario to the best concept.",
| |
| rows:[
| |
| { row:"Chronic airway irritation leads to more protective epithelial type", options:["Metaplasia","Hypertrophy","Atrophy","Necrosis"], correct:"Metaplasia" },
| |
| { row:"Increased number of endometrial glands due to hormonal stimulation", options:["Hyperplasia","Apoptosis","Calcification","Atrophy"], correct:"Hyperplasia" },
| |
| { row:"Programmed cell removal during normal tissue turnover", options:["Apoptosis","Necrosis","Urate accumulation","Fibrosis"], correct:"Apoptosis" },
| |
| { row:"Tissue death with enzyme leakage and inflammation", options:["Necrosis","Hyperplasia","Metaplasia","Autophagy"], correct:"Necrosis" }
| |
| ],
| |
| rationale:"Matrix checks recognition of hallmark patterns rather than memorized examples.",
| |
| plain:"Metaplasia = Swap. Hyperplasia = More cells. Apoptosis = Clean death. Necrosis = Messy death.",
| |
| mnemonic:"Meta (Change). Hyper (More). Apo (Pop/Gone). Necro (Dead/Rot)."
| |
| },
| |
| { id:26, type:"matrix", topic:"Cell Injury Mechanisms", difficulty:3,
| |
| stem:"Match mechanism to best description.",
| |
| rows:[
| |
| { row:"ATP depletion", options:["Ion pump failure and swelling","Immediate antibody production","Collagen cross-linking","Urate precipitation"], correct:"Ion pump failure and swelling" },
| |
| { row:"Reactive oxygen species", options:["Lipid/protein/DNA injury","Guaranteed tissue regeneration","Only helpful signaling","Prevents inflammation"], correct:"Lipid/protein/DNA injury" },
| |
| { row:"Ischemia–reperfusion", options:["ROS + inflammatory extension of injury","Instant full recovery of tissue","Only affects skin","Stops calcium entry completely"], correct:"ROS + inflammatory extension of injury" }
| |
| ],
| |
| rationale:"Mechanism matching strengthens cue→patho→outcome reasoning.",
| |
| plain:"No ATP? Swelling. Free radicals? Structural damage. Reperfusion? Second wave of injury.",
| |
| mnemonic:"ATP = Pump. ROS = Damage. Reperfusion = Inflammation."
| |
| },
| |
| | |
| { id:27, type:"bowtie", topic:"NGN Bowtie: Hypoxic Injury", difficulty:3,
| |
| stem:"Bowtie: Identify the likely problem, contributing factors, and priority actions.",
| |
| center:{ label:"Most likely problem", options:["Hypoxic/ischemic cellular injury","Autoimmune flare","Primary endocrine disorder","Urate arthropathy"], correct:"Hypoxic/ischemic cellular injury" },
| |
| left:{ label:"Contributing factors (pick 2)", pick:2, options:[
| |
| "Low oxygen saturation with increased work of breathing",
| |
| "Reduced perfusion/hypotension",
| |
| "Recent vaccination",
| |
| "Increased sunlight exposure"
| |
| ], correct:["Low oxygen saturation with increased work of breathing","Reduced perfusion/hypotension"] },
| |
| right:{ label:"Priority actions (pick 2)", pick:2, options:[
| |
| "Support oxygenation/perfusion and reassess response",
| |
| "Monitor for deterioration and escalate early",
| |
| "Delay assessment to avoid anxiety",
| |
| "Encourage fluid restriction as first-line for hypoxia"
| |
| ], correct:["Support oxygenation/perfusion and reassess response","Monitor for deterioration and escalate early"] },
| |
| rationale:"Correct center + cause/action pairing reflects NGN integrated reasoning under pressure.",
| |
| plain:"Problem: Cells starving for air. Causes: Low blood flow or low lung function. Fix: Give air and fix flow.",
| |
| mnemonic:"Hypoxia Kills Cells."
| |
| },
| |
| { id:28, type:"bowtie", topic:"NGN Bowtie: Hypersensitivity Reaction", difficulty:2,
| |
| stem:"Bowtie: Identify the likely problem, contributing factors, and priority actions.",
| |
| center:{ label:"Most likely problem", options:["Immunologic (hypersensitivity) injury","Reperfusion injury","Radiation injury","Pressure injury"], correct:"Immunologic (hypersensitivity) injury" },
| |
| left:{ label:"Contributing factors (pick 2)", pick:2, options:[
| |
| "New medication exposure",
| |
| "Rash with fever/joint symptoms",
| |
| "Weeks of immobilization",
| |
| "High altitude exposure only"
| |
| ], correct:["New medication exposure","Rash with fever/joint symptoms"] },
| |
| right:{ label:"Priority actions (pick 2)", pick:2, options:[
| |
| "Hold suspected trigger per protocol and notify provider",
| |
| "Assess airway/breathing and severity; monitor vitals",
| |
| "Encourage repeat dosing to confirm",
| |
| "Ignore unless pain is present"
| |
| ], correct:["Hold suspected trigger per protocol and notify provider","Assess airway/breathing and severity; monitor vitals"] },
| |
| rationale:"Drug hypersensitivity requires rapid severity assessment and escalation to prevent progression.",
| |
| plain:"Problem: Bad reaction to drug. Signs: Rash/Fever. Action: STOP the drug and watch breathing.",
| |
| mnemonic:"See Rash? Dash (to stop the med)."
| |
| },
| |
| | |
| { id:29, type:"trends", topic:"NGN Trends: Infection Deterioration", difficulty:3,
| |
| stem:"Trends: A client is being monitored for infection. Identify when escalation is most indicated.",
| |
| table:{
| |
| headers:["Time","Temp (°F)","HR","BP","RR","SpO2"],
| |
| rows:[
| |
| ["0h", "99.3", "94", "122/76", "18", "96%"],
| |
| ["2h", "100.6","108", "112/70", "20", "95%"],
| |
| ["4h", "101.8","118", "100/64", "24", "93%"],
| |
| ["6h", "102.4","128", "90/56", "28", "92%"]
| |
| ]
| |
| },
| |
| parts:[
| |
| { type:"mcq",
| |
| prompt:"At which time point does the trend most strongly indicate deterioration requiring escalation?",
| |
| options:["0h","2h","4h","6h"],
| |
| correct:[3]
| |
| },
| |
| { type:"sata",
| |
| prompt:"Which changes are most concerning? (Select all that apply)",
| |
| options:["Rising temperature","Increasing HR","Dropping BP","Increasing RR","Improving oxygenation"],
| |
| correct:[0,1,2,3]
| |
| }
| |
| ],
| |
| rationale:"Worsening fever with tachycardia, hypotension, tachypnea, and falling SpO2 suggests escalating systemic compromise.",
| |
| plain:"Everything is going the wrong way. Heart is racing, BP is crashing, fever is spiking. This is sepsis territory.",
| |
| mnemonic:"Sepsis = Hypotension + Tachycardia."
| |
| },
| |
| { id:30, type:"trends", topic:"NGN Trends: Neurovascular Compromise", difficulty:3,
| |
| stem:"Trends: A client after limb revascularization is reassessed over time.",
| |
| table:{
| |
| headers:["Time","Pain (0–10)","Pulses","Cap Refill","Sensation","Swelling"],
| |
| rows:[
| |
| ["PACU", "2", "Present", "<2s", "Intact", "Mild"],
| |
| ["1h", "4", "Present", "2–3s","Tingling","Moderate"],
| |
| ["2h", "6", "Weaker", "3–4s","Numbness","Increasing"],
| |
| ["3h", "8", "Faint", ">4s", "Reduced", "Tense"]
| |
| ]
| |
| },
| |
| parts:[
| |
| { type:"mcq",
| |
| prompt:"Which trend change is the strongest trigger to escalate immediately?",
| |
| options:["Pain 4 at 1h","Moderate swelling at 1h","Faint pulses with delayed cap refill at 3h","Mild swelling in PACU"],
| |
| correct:[2]
| |
| },
| |
| { type:"dropdown",
| |
| prompt:"Choose the best immediate nursing response.",
| |
| blanks:[
| |
| { text:"Best response: ", options:[
| |
| "Continue routine checks",
| |
| "Notify provider urgently and perform frequent neurovascular reassessments",
| |
| "Encourage walking to relieve pain",
| |
| "Apply heat and reassess later"
| |
| ], correct:"Notify provider urgently and perform frequent neurovascular reassessments" }
| |
| ]
| |
| }
| |
| ],
| |
| rationale:"Progressive neurovascular compromise after reperfusion is time-sensitive and requires urgent escalation.",
| |
| plain:"The pulse is disappearing and the pain is skyrocketing. The limb is dying. Scream for help.",
| |
| mnemonic:"Lost Pulse = Lost Limb."
| |
| }
| |
| ];
| |
| | |
| /* =========================
| |
| State + Storage (Moved to top for safety)
| |
| ========================= */
| |
| const LS_KEY = "unit1_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();
| |
| }
| |
| | |
| 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("'","'");
| |
| }
| |
| | |
| /* =========================
| |
| 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() };
| |
| }
| |
| | |
| /* =========================
| |
| Topic aggregation + revisit tracker
| |
| ========================= */
| |
| 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;
| |
| }
| |
| | |
| /* =========================
| |
| Summary metrics
| |
| ========================= */
| |
| function overallMastery(){
| |
| let e=0,p=0;
| |
| for (const q of QUESTIONS){
| |
| const sc = state.scores[q.id];
| |
| if (!sc) continue;
| |
| e += sc.earnedW; p += sc.possibleW;
| |
| }
| |
| return p ? (e/p)*100 : 0;
| |
| }
| |
| function weightedScoreOutOf1200(){
| |
| const pct = overallMastery();
| |
| const score = Math.round(300 + (pct/100) * 900);
| |
| return clamp(score, 300, 1200);
| |
| }
| |
| function statusTag(qid){
| |
| const sc = state.scores[qid];
| |
| if (!sc) return { text:"Unanswered", cls:"" };
| |
| if (sc.status === "full") return { text:"Full", cls:"good" };
| |
| if (sc.status === "partial") return { text:"Partial", cls:"warn" };
| |
| return { text:"Missed", cls:"bad" };
| |
| }
| |
| | |
| /* =========================
| |
| Clickable option engine
| |
| ========================= */
| |
| function getSelection(q){ return state.answers[q.id]; }
| |
| function setSelection(q, payload){ state.answers[q.id] = payload; saveState(); }
| |
| function toggleSingle(q, idx){ setSelection(q, [idx]); }
| |
| function toggleMulti(q, idx){
| |
| const cur = getSelection(q) || [];
| |
| const set = new Set(cur);
| |
| if (set.has(idx)) set.delete(idx); else set.add(idx);
| |
| setSelection(q, [...set].sort((a,b)=>a-b));
| |
| }
| |
| | |
| /* =========================
| |
| Render helpers
| |
| ========================= */
| |
| function renderOptionCards(q, isMulti){
| |
| const wrap = document.createElement("div");
| |
| wrap.className = "opts";
| |
| const selected = getSelection(q) || [];
| |
| q.options.forEach((opt, idx)=>{
| |
| const card = document.createElement("div");
| |
| const active = isMulti ? selected.includes(idx) : (selected[0]===idx);
| |
| card.className = "optCard" + (active ? " selected" : "");
| |
| card.tabIndex = 0;
| |
| card.setAttribute("role","button");
| |
| card.setAttribute("aria-pressed", active ? "true" : "false");
| |
| card.innerHTML = `<div class="optMark"></div><div class="optText">${escapeHtml(opt)}</div>`;
| |
| card.onclick = ()=>{
| |
| if (isMulti) toggleMulti(q, idx);
| |
| else toggleSingle(q, idx);
| |
| renderAll(true);
| |
| };
| |
| card.onkeydown = (e)=>{
| |
| if (e.key==="Enter" || e.key===" "){
| |
| e.preventDefault();
| |
| card.click();
| |
| }
| |
| };
| |
| wrap.appendChild(card);
| |
| });
| |
| return wrap;
| |
| }
| |
| | |
| function renderQuestionBody(q){
| |
| const wrap = document.createElement("div");
| |
| const existing = getSelection(q);
| |
| | |
| if (q.type === "mcq"){
| |
| wrap.appendChild(renderOptionCards(q, false));
| |
| }
| |
| | |
| if (q.type === "sata"){
| |
| wrap.appendChild(renderOptionCards(q, true));
| |
| }
| |
| | |
| if (q.type === "dropdown"){
| |
| const box = document.createElement("div");
| |
| box.className = "caseBox";
| |
| const vals = existing || [];
| |
| q.blanks.forEach((b, i)=>{
| |
| const sel = document.createElement("select");
| |
| sel.dataset.blankIndex = i;
| |
| sel.innerHTML = `<option value="">Select…</option>` + b.options.map(o=>`<option value="${escapeHtml(o)}">${escapeHtml(o)}</option>`).join("");
| |
| sel.value = vals[i] || "";
| |
| sel.onchange = ()=>{
| |
| const cur = (getSelection(q) || []).slice();
| |
| cur[i] = sel.value || "";
| |
| setSelection(q, cur);
| |
| };
| |
| const line = document.createElement("div");
| |
| line.style.marginTop = "8px";
| |
| line.innerHTML = `<div style="color:var(--muted);font-size:12px">${escapeHtml(b.text)}</div>`;
| |
| line.appendChild(sel);
| |
| box.appendChild(line);
| |
| });
| |
| wrap.appendChild(box);
| |
| }
| |
| | |
| if (q.type === "matrix"){
| |
| const box = document.createElement("div");
| |
| box.className = "caseBox";
| |
| const table = document.createElement("table");
| |
| table.className = "table";
| |
| table.innerHTML = `<thead><tr><th>Row</th><th>Pick one</th></tr></thead>`;
| |
| const tb = document.createElement("tbody");
| |
| | |
| const map = existing || {};
| |
| q.rows.forEach((r, i)=>{
| |
| const tr = document.createElement("tr");
| |
| const td1 = document.createElement("td");
| |
| td1.textContent = r.row;
| |
| | |
| const td2 = document.createElement("td");
| |
| const sel = document.createElement("select");
| |
| sel.innerHTML = `<option value="">Select…</option>` + r.options.map(o=>`<option value="${escapeHtml(o)}">${escapeHtml(o)}</option>`).join("");
| |
| sel.value = map[i] || "";
| |
| sel.onchange = ()=>{
| |
| const cur = Object.assign({}, (getSelection(q) || {}));
| |
| cur[i] = sel.value || "";
| |
| setSelection(q, cur);
| |
| };
| |
| td2.appendChild(sel);
| |
| | |
| tr.appendChild(td1); tr.appendChild(td2);
| |
| tb.appendChild(tr);
| |
| });
| |
| | |
| table.appendChild(tb);
| |
| box.appendChild(table);
| |
| wrap.appendChild(box);
| |
| }
| |
| | |
| if (q.type === "bowtie"){
| |
| const box = document.createElement("div");
| |
| box.className = "caseBox";
| |
| const payload = existing || { center:"", left:[], right:[] };
| |
| | |
| const center = document.createElement("div");
| |
| center.innerHTML = `<div style="color:var(--muted);font-size:12px;margin-bottom:6px">${escapeHtml(q.center.label)}</div>`;
| |
| const cSel = document.createElement("select");
| |
| cSel.innerHTML = `<option value="">Select…</option>` + q.center.options.map(o=>`<option value="${escapeHtml(o)}">${escapeHtml(o)}</option>`).join("");
| |
| cSel.value = payload.center || "";
| |
| cSel.onchange = ()=>{
| |
| setSelection(q, { ...payload, center: cSel.value || "" });
| |
| };
| |
| center.appendChild(cSel);
| |
| | |
| const mkPickList = (side, cfg)=>{
| |
| const sec = document.createElement("div");
| |
| sec.className = "caseStep";
| |
| sec.innerHTML = `<div style="color:var(--muted);font-size:12px;margin-bottom:6px">${escapeHtml(cfg.label)} (pick ${cfg.pick})</div>`;
| |
| cfg.options.forEach((o)=>{
| |
| const card = document.createElement("div");
| |
| const selected = (payload[side] || []).includes(o);
| |
| card.className = "optCard" + (selected ? " selected" : "");
| |
| card.innerHTML = `<div class="optMark"></div><div class="optText">${escapeHtml(o)}</div>`;
| |
| card.onclick = ()=>{
| |
| const cur = new Set(payload[side] || []);
| |
| if (cur.has(o)) cur.delete(o);
| |
| else cur.add(o);
| |
| const arr = [...cur];
| |
| if (arr.length > cfg.pick) return;
| |
| const next = { ...payload, [side]: arr };
| |
| setSelection(q, next);
| |
| renderAll(true);
| |
| };
| |
| sec.appendChild(card);
| |
| });
| |
| return sec;
| |
| };
| |
| | |
| box.appendChild(center);
| |
| box.appendChild(mkPickList("left", q.left));
| |
| box.appendChild(mkPickList("right", q.right));
| |
| wrap.appendChild(box);
| |
| }
| |
| | |
| if (q.type === "trends"){
| |
| const box = document.createElement("div");
| |
| box.className = "caseBox";
| |
| | |
| const t = document.createElement("table");
| |
| t.className = "table";
| |
| const head = `<tr>${q.table.headers.map(h=>`<th>${escapeHtml(h)}</th>`).join("")}</tr>`;
| |
| const rows = q.table.rows.map(r=>`<tr>${r.map(c=>`<td>${escapeHtml(String(c))}</td>`).join("")}</tr>`).join("");
| |
| t.innerHTML = `<thead>${head}</thead><tbody>${rows}</tbody>`;
| |
| box.appendChild(t);
| |
| | |
| const payload = existing || [];
| |
| | |
| q.parts.forEach((p, idx)=>{
| |
| const sec = document.createElement("div");
| |
| sec.className = "caseStep";
| |
| sec.innerHTML = `<div style="font-weight:600;margin-bottom:8px">${escapeHtml(p.prompt)}</div>`;
| |
| | |
| if (p.type==="mcq"){
| |
| const chosen = payload[idx] ?? null;
| |
| p.options.forEach((opt, oi)=>{
| |
| const card = document.createElement("div");
| |
| const selected = chosen === oi;
| |
| card.className = "optCard" + (selected ? " selected" : "");
| |
| card.innerHTML = `<div class="optMark"></div><div class="optText">${escapeHtml(opt)}</div>`;
| |
| card.onclick = ()=>{
| |
| const cur = (getSelection(q) || []).slice();
| |
| cur[idx] = oi;
| |
| setSelection(q, cur);
| |
| renderAll(true);
| |
| };
| |
| sec.appendChild(card);
| |
| });
| |
| } else if (p.type==="sata"){
| |
| const chosen = payload[idx] || [];
| |
| p.options.forEach((opt, oi)=>{
| |
| const selected = chosen.includes(oi);
| |
| const card = document.createElement("div");
| |
| card.className = "optCard" + (selected ? " selected" : "");
| |
| card.innerHTML = `<div class="optMark"></div><div class="optText">${escapeHtml(opt)}</div>`;
| |
| card.onclick = ()=>{
| |
| const cur = (getSelection(q) || []).slice();
| |
| const set = new Set(cur[idx] || []);
| |
| if (set.has(oi)) set.delete(oi); else set.add(oi);
| |
| cur[idx] = [...set].sort((a,b)=>a-b);
| |
| setSelection(q, cur);
| |
| renderAll(true);
| |
| };
| |
| sec.appendChild(card);
| |
| });
| |
| } else if (p.type==="dropdown"){
| |
| const vals = payload[idx] || [];
| |
| p.blanks.forEach((b, bi)=>{
| |
| const sel = document.createElement("select");
| |
| sel.innerHTML = `<option value="">Select…</option>` + b.options.map(o=>`<option value="${escapeHtml(o)}">${escapeHtml(o)}</option>`).join("");
| |
| sel.value = vals[bi] || "";
| |
| sel.onchange = ()=>{
| |
| const cur = (getSelection(q) || []).slice();
| |
| const part = (cur[idx] || []).slice();
| |
| part[bi] = sel.value || "";
| |
| cur[idx] = part;
| |
| setSelection(q, cur);
| |
| };
| |
| const line = document.createElement("div");
| |
| line.style.marginTop = "8px";
| |
| line.innerHTML = `<div style="color:var(--muted);font-size:12px">${escapeHtml(b.text)}</div>`;
| |
| line.appendChild(sel);
| |
| sec.appendChild(line);
| |
| });
| |
| }
| |
| | |
| box.appendChild(sec);
| |
| });
| |
| | |
| wrap.appendChild(box);
| |
| }
| |
| | |
| if (q.type === "case"){
| |
| const box = document.createElement("div");
| |
| box.className = "caseBox";
| |
| box.innerHTML = `<div style="color:var(--muted);font-size:12px;margin-bottom:8px">Unfolding case (3 steps)</div>`;
| |
| const payload = existing || [];
| |
| | |
| q.steps.forEach((s, idx)=>{
| |
| const sec = document.createElement("div");
| |
| sec.className = "caseStep";
| |
| sec.innerHTML = `<div style="font-weight:700;margin-bottom:6px">${escapeHtml(s.title)}</div>
| |
| <div style="margin-bottom:10px">${escapeHtml(s.prompt)}</div>`;
| |
| | |
| if (s.type==="mcq"){
| |
| const chosen = payload[idx] ?? null;
| |
| s.options.forEach((opt, oi)=>{
| |
| const card = document.createElement("div");
| |
| const selected = chosen === oi;
| |
| card.className = "optCard" + (selected ? " selected" : "");
| |
| card.innerHTML = `<div class="optMark"></div><div class="optText">${escapeHtml(opt)}</div>`;
| |
| card.onclick = ()=>{
| |
| const cur = (getSelection(q) || []).slice();
| |
| cur[idx] = oi;
| |
| setSelection(q, cur);
| |
| renderAll(true);
| |
| };
| |
| sec.appendChild(card);
| |
| });
| |
| } else if (s.type==="sata"){
| |
| const chosen = payload[idx] || [];
| |
| s.options.forEach((opt, oi)=>{
| |
| const selected = chosen.includes(oi);
| |
| const card = document.createElement("div");
| |
| card.className = "optCard" + (selected ? " selected" : "");
| |
| card.innerHTML = `<div class="optMark"></div><div class="optText">${escapeHtml(opt)}</div>`;
| |
| card.onclick = ()=>{
| |
| const cur = (getSelection(q) || []).slice();
| |
| const set = new Set(cur[idx] || []);
| |
| if (set.has(oi)) set.delete(oi); else set.add(oi);
| |
| cur[idx] = [...set].sort((a,b)=>a-b);
| |
| setSelection(q, cur);
| |
| renderAll(true);
| |
| };
| |
| sec.appendChild(card);
| |
| });
| |
| } else if (s.type==="dropdown"){
| |
| const vals = payload[idx] || [];
| |
| s.blanks.forEach((b, bi)=>{
| |
| const sel = document.createElement("select");
| |
| sel.innerHTML = `<option value="">Select…</option>` + b.options.map(o=>`<option value="${escapeHtml(o)}">${escapeHtml(o)}</option>`).join("");
| |
| sel.value = vals[bi] || "";
| |
| sel.onchange = ()=>{
| |
| const cur = (getSelection(q) || []).slice();
| |
| const step = (cur[idx] || []).slice();
| |
| step[bi] = sel.value || "";
| |
| cur[idx] = step;
| |
| setSelection(q, cur);
| |
| };
| |
| const line = document.createElement("div");
| |
| line.style.marginTop = "8px";
| |
| line.innerHTML = `<div style="color:var(--muted);font-size:12px">${escapeHtml(b.text)}</div>`;
| |
| line.appendChild(sel);
| |
| sec.appendChild(line);
| |
| });
| |
| }
| |
| | |
| box.appendChild(sec);
| |
| });
| |
| | |
| wrap.appendChild(box);
| |
| }
| |
| | |
| return wrap;
| |
| }
| |
| | |
| /* =========================
| |
| Read payloads for scoring
| |
| ========================= */
| |
| 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 (UPDATED)
| |
| ========================= */
| |
| 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>`;
| |
| | |
| const nursingLogic = q.rationale || "Rationale not available.";
| |
| const plainLogic = q.plain || "Plain explanation not available.";
| |
| const mnemonic = q.mnemonic || "No mnemonic for this topic.";
| |
| | |
| 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>
| |
| `;
| |
| }
| |
| | |
| /* =========================
| |
| 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;
| |
| | |
| // Benchmark: passing HESI score is 875
| |
| 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 1 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]] |
| [[Category:HU NSG 520 Pathophysiology and Pharmacology]]
| |