Game:520 Unit 1 Dosage Calc Study Buddy/Raw
Jump to navigation
Jump to search
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Unit 1 Dosage Calc</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>Dosage Calculation Study Bot</h1>
<div class="sub">
Unit 1 Dosage Calculation ONLY
<br/>No Patho/Pharm 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,
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)
========================= */
const QUESTIONS = [
// =========================
// MCQ (foundation + common HESI/NCLEX styles)
// =========================
{
id: 1, type: "mcq", topic: "Dose by Supply", difficulty: 2,
stem: "Order: Morphine 4 mg IV now. Available: 10 mg/5 mL. How many mL will the nurse administer?",
options: ["1 mL", "2 mL", "4 mL", "5 mL"],
correct: [1],
rationale: "Dimensional analysis: (4 mg ÷ 10 mg) × 5 mL = 2 mL.",
plain: "You need 4 mg. Your vial has 10 mg in every 5 mL. Since 4 is less than half of 10, you need less than half the liquid (2 mL).",
mnemonic: "D / H x Q (Desired / Have x Quantity)"
},
{
id: 2, type: "mcq", topic: "Tablet Calculation", difficulty: 1,
stem: "Order: Amoxicillin 500 mg PO now. Available: 250 mg/tablet. How many tablets will the nurse give?",
options: ["1", "2", "3", "4"],
correct: [1],
rationale: "500 ÷ 250 = 2 tablets.",
plain: "You need 500 total. Each pill is 250. 250 + 250 = 500.",
mnemonic: "Want / Got (What you want / What you got)"
},
{
id: 3, type: "mcq", topic: "IV Pump Rate (mL/hr)", difficulty: 1,
stem: "An IV order reads: 1000 mL to infuse over 8 hours. What is the pump rate?",
options: ["100 mL/hr", "125 mL/hr", "150 mL/hr", "175 mL/hr"],
correct: [1],
rationale: "1000 mL ÷ 8 hr = 125 mL/hr.",
plain: "The pump only speaks 'mL per hour'. Simply take the total bag size and divide by total hours.",
mnemonic: "Pump Rate = Total Volume / Total Time (Hours)"
},
{
id: 4, type: "mcq", topic: "gtt/min (Gravity)", difficulty: 2,
stem: "Infuse 500 mL over 4 hours. Tubing drop factor: 15 gtt/mL. What is the flow rate in gtt/min?",
options: ["20 gtt/min", "25 gtt/min", "31 gtt/min", "40 gtt/min"],
correct: [2],
rationale: "mL/min = 500 ÷ 240 = 2.083; gtt/min = 2.083 × 15 = 31.25 ≈ 31.",
plain: "Gravity drips are calculated in MINUTES. First convert 4 hours to 240 minutes. Then do (Vol x DF) / Minutes.",
mnemonic: "The Video Doctor Films Minutes (Total Vol x Drop Factor / Minutes)"
},
{
id: 5, type: "mcq", topic: "Weight-Based Dose (kg)", difficulty: 2,
stem: "Client weighs 154 lb. Order: enoxaparin 1 mg/kg. Available: 100 mg/mL. How many mL will the nurse administer?",
options: ["0.35 mL", "0.50 mL", "0.70 mL", "1.4 mL"],
correct: [2],
rationale: "154 lb ÷ 2.2 = 70 kg. Dose = 70 mg. Volume = 70 mg ÷ (100 mg/mL) = 0.70 mL.",
plain: "Step 1: Pounds to Kg (make it smaller). Step 2: Multiply Kg by dose. Step 3: Divide by concentration.",
mnemonic: "2.2 is the key (Lbs / 2.2 = Kg)"
},
{
id: 6, type: "mcq", topic: "Safe Dose Range (Peds)", difficulty: 3,
stem: "A child weighs 22 lb. Safe dose is 2–4 mg/kg/day. Provider orders 50 mg/day. What is the nurse’s priority action?",
options: [
"Administer as ordered",
"Clarify the order before giving",
"Split the dose into two doses",
"Give with food to reduce GI upset"
],
correct: [1],
rationale: "22 lb ÷ 2.2 = 10 kg. Safe range = 20–40 mg/day. Ordered 50 mg/day exceeds safe range.",
plain: "The baby is 10kg. The max safe dose is 4 mg x 10kg = 40mg. The doctor ordered 50mg. That's an overdose.",
mnemonic: "Safety First: Calculate the range BEFORE checking the order."
},
// =========================
// SATA (MCMA partial credit style)
// =========================
{
id: 7, type: "sata", topic: "Error Prevention", difficulty: 2,
stem: "Which actions reduce dosage calculation errors? (Select all that apply)",
options: [
"Convert units to one system before solving",
"Estimate whether the final answer is reasonable",
"Skip unit labels to save time",
"Double-check decimal placement",
"Use an independent double-check for high-alert meds"
],
correct: [0, 1, 3, 4],
rationale: "Unit consistency, reasonableness checks, decimal safety, and double-checks reduce medication errors.",
plain: "Make sure apples match apples (units), and ask 'Does this answer make sense?' before giving it.",
mnemonic: "The 6 Rights + 'Does it make sense?'"
},
{
id: 8, type: "sata", topic: "High-Alert Meds Safety", difficulty: 2,
stem: "Which medications are commonly treated as high-alert and require extra safeguards? (Select all that apply)",
options: ["Insulin", "Heparin", "Potassium chloride IV", "Acetaminophen", "Warfarin"],
correct: [0, 1, 2, 4],
rationale: "Insulin, heparin, IV KCl, and warfarin are commonly high-alert due to serious harm risk if misdosed.",
plain: "Think: Which drugs can kill a patient instantly if the math is wrong? Blood thinners, insulin, and IV potassium are top offenders.",
mnemonic: "PINCH (Potassium, Insulin, Narcotics, Chemo, Heparin)"
},
{
id: 9, type: "sata", topic: "Unit Conversions", difficulty: 2,
stem: "Which conversions are correct? (Select all that apply)",
options: [
"1 g = 1000 mg",
"1 mg = 1000 mcg",
"1 L = 100 mL",
"2.2 lb = 1 kg",
"1 tsp = 10 mL"
],
correct: [0, 1, 3],
rationale: "Correct: 1 g=1000 mg; 1 mg=1000 mcg; 2.2 lb=1 kg. 1 L=1000 mL; 1 tsp=5 mL.",
plain: "Remember your 1000s rule. Grams to milligrams is 1000. Milligrams to micrograms is 1000. But teaspoons are only 5.",
mnemonic: "King Henry Died By Drinking Chocolate Milk (Kilo, Hecto, Deka, Base, Deci, Centi, Milli)"
},
// =========================
// Dropdowns
// =========================
{
id: 10, type: "dropdown", topic: "Dose by Supply", difficulty: 1,
stem: "Complete the calculation.",
blanks: [
{ text: "Order: furosemide 40 mg IV. Available: 20 mg/2 mL. Give: ", options: ["2 mL", "4 mL", "6 mL", "8 mL"], correct: "4 mL" }
],
rationale: "20 mg in 2 mL → 10 mg/mL. Need 40 mg → 4 mL.",
plain: "If 2mL holds 20mg, then 1mL holds 10mg. You need 40mg, so you need 4mL.",
mnemonic: "Double the dose = Double the volume (if concentration stays same)"
},
{
id: 11, type: "dropdown", topic: "IV Pump Rate (mL/hr)", difficulty: 1,
stem: "Complete the calculation.",
blanks: [
{ text: "Infuse 250 mL over 2 hours. Pump rate: ", options: ["100", "125", "150", "175"], correct: "125" }
],
rationale: "250 ÷ 2 = 125 mL/hr.",
plain: "The pump needs to know 'how much per ONE hour'. Divide total (250) by hours (2).",
mnemonic: "Total Vol / Total Hours"
},
{
id: 12, type: "dropdown", topic: "Reconstitution/Concentration", difficulty: 2,
stem: "A vial is reconstituted to a total volume of 10 mL and contains 1 g total drug. Select the final concentration.",
blanks: [
{ text: "Concentration (mg/mL): ", options: ["10", "50", "100", "200"], correct: "100" }
],
rationale: "1 g = 1000 mg. 1000 mg ÷ 10 mL = 100 mg/mL.",
plain: "Always convert grams to mg first. 1 gram is huge (1000mg). Spread 1000mg across 10mL = 100 per mL.",
mnemonic: "1 g = 1 paperclip = 1000 tiny grains of sand (mg)"
},
// =========================
// MATRIX / GRID (2)
// =========================
{
id: 13, type: "matrix", topic: "Method Selection", difficulty: 1,
stem: "Match each scenario to the best calculation approach.",
rows: [
{ row: "IV gravity rate ordered in gtt/min", options: ["mL/hr pump", "gtt/min formula", "tablet calculation"], correct: "gtt/min formula" },
{ row: "Oral tablets available as mg/tablet", options: ["tablet calculation", "gtt/min formula", "titration protocol"], correct: "tablet calculation" },
{ row: "Weight-based medication order (mg/kg)", options: ["weight-based dosing", "tablet calculation", "unitless ratio only"], correct: "weight-based dosing" }
],
rationale: "Picking the right method prevents setup errors before the math even starts.",
plain: "Don't overcomplicate it. If it asks for drops, use the drop formula. If it involves weight, start with kg.",
mnemonic: "Match the formula to the 'Ask' (Unit)"
},
{
id: 14, type: "matrix", topic: "Dose Forms", difficulty: 2,
stem: "Match medication form to what you calculate directly.",
rows: [
{ row: "Liquid PO", options: ["mL to administer", "gtt/min only", "tablets only"], correct: "mL to administer" },
{ row: "IV pump infusion", options: ["mL/hr", "tablets", "mcg/min without conversion"], correct: "mL/hr" },
{ row: "Tablets/capsules", options: ["number of tablets", "gtt/min only", "mL/hr only"], correct: "number of tablets" }
],
rationale: "Dose form tells you what the final ‘answer unit’ must be.",
plain: "Pumps speak mL/hr. Syringes speak mL. Pill cups speak tablets. Know your output unit.",
mnemonic: "Pump = Hour. Gravity = Minute. Syringe = mL."
},
// =========================
// BOWTIE (2)
// =========================
{
id: 15, type: "bowtie", topic: "NGN Bowtie – Overdose Risk", difficulty: 3,
stem: "Bowtie: Identify the risk, contributing factors, and nursing actions.",
center: {
label: "Primary risk",
options: ["Medication overdose", "Medication underdose", "Allergic reaction", "Therapeutic effect only"],
correct: "Medication overdose"
},
left: {
label: "Contributing factors (pick 2)", pick: 2,
options: ["Decimal misplacement", "Incorrect unit conversion", "Patient education provided", "Medication reconciliation completed"],
correct: ["Decimal misplacement", "Incorrect unit conversion"]
},
right: {
label: "Priority actions (pick 2)", pick: 2,
options: ["Recalculate with units and estimate reasonableness", "Use independent double-check when indicated", "Administer quickly to avoid delay", "Skip labels to reduce clutter"],
correct: ["Recalculate with units and estimate reasonableness", "Use independent double-check when indicated"]
},
rationale: "Most critical dosing errors come from decimals and unit conversions; recalculation + double-checks prevent harm.",
plain: "A misplaced decimal can turn a dose of 1.0 into 10.0. That's 10x the dose. Always estimate: 'Does this look right?'",
mnemonic: "Leading Zero = Hero (0.5). Trailing Zero = No (5.0 -> 5)."
},
{
id: 16, type: "bowtie", topic: "NGN Bowtie – Heparin Safety", difficulty: 3,
stem: "Bowtie: Identify the risk, contributing factors, and nursing actions.",
center: {
label: "Primary risk",
options: ["Bleeding complication", "Dehydration", "Hypoglycemia", "Pain escalation only"],
correct: "Bleeding complication"
},
left: {
label: "Contributing factors (pick 2)", pick: 2,
options: ["Dose miscalculation", "No baseline labs reviewed", "Client prefers morning meds", "IV site is patent"],
correct: ["Dose miscalculation", "No baseline labs reviewed"]
},
right: {
label: "Priority actions (pick 2)", pick: 2,
options: ["Verify weight-based order and concentration", "Review relevant labs per protocol", "Increase rate without verification", "Document later to save time"],
correct: ["Verify weight-based order and concentration", "Review relevant labs per protocol"]
},
rationale: "Heparin errors can cause bleeding. Verify math + concentration and check protocol labs.",
plain: "Heparin is high stakes. Wrong math = bleeding out. Always check the PTT/labs and weight first.",
mnemonic: "Heparin = Hemorrhage Risk (H & H)"
},
// =========================
// TRENDS (2)
// =========================
{
id: 17, type: "trends", topic: "NGN Trends – Insulin Correction", difficulty: 2,
stem: "Trends: A client is on a sliding-scale insulin protocol. Identify the best action based on trend + protocol.",
table: {
headers: ["Time", "BG (mg/dL)", "Symptoms"],
rows: [
["0800", "310", "Thirsty"],
["1200", "260", "No symptoms"],
["1600", "220", "No symptoms"],
["2000", "180", "No symptoms"]
]
},
parts: [
{
type: "mcq",
prompt: "The trend indicates the client is moving toward:",
options: ["Worsening hyperglycemia", "Improving glycemic control", "Hypoglycemia risk", "No meaningful change"],
correct: [1]
},
{
type: "sata",
prompt: "Which nursing checks remain priority while insulin is being adjusted? (Select all that apply)",
options: ["Verify meal timing relative to insulin", "Monitor for hypoglycemia symptoms", "Hold all BG checks if improving", "Confirm insulin type/dose drawn up", "Encourage skipping meals"],
correct: [0, 1, 3]
}
],
rationale: "BG is trending down (improving). Still verify timing, dose/type, and monitor for hypoglycemia.",
plain: "Numbers are going down, which is good. But don't get lazy—insulin is dangerous. Confirm the dose and feed the patient.",
mnemonic: "Cold & Clammy = Need some Candy. Hot & Dry = Sugar High."
},
{
id: 18, type: "trends", topic: "NGN Trends – IV Fluids + Output", difficulty: 2,
stem: "Trends: A client is ordered IV fluids. Use the trend to identify the best interpretation and action.",
table: {
headers: ["Hour", "IV Rate (mL/hr)", "Urine Output (mL/hr)", "BP"],
rows: [
["0–1", "125", "15", "92/58"],
["1–2", "125", "20", "94/60"],
["2–3", "125", "28", "98/62"],
["3–4", "125", "35", "104/66"]
]
},
parts: [
{
type: "mcq",
prompt: "The trend most strongly suggests:",
options: ["Worsening hypoperfusion", "Improving perfusion response", "Fluid overload", "Medication error"],
correct: [1]
},
{
type: "dropdown",
prompt: "Best next nursing action:",
blanks: [
{ text: "Action: ", options: ["Stop fluids immediately", "Continue per order and reassess", "Increase rate without order", "Remove IV"], correct: "Continue per order and reassess" }
]
}
],
rationale: "BP and UO are improving at the current ordered rate; continue and reassess per protocol.",
plain: "Kidneys are waking up (output > 30 is the goal) and BP is rising. The fluids are working. Don't stop now.",
mnemonic: "Urine Output Goal > 30 mL/hr"
},
// =========================
// CASE STUDIES (4) – unfolding (3 steps each)
// =========================
{
id: 19, type: "case", topic: "Unfolding Case – Pediatric Acetaminophen", difficulty: 2,
stem: "A child weighs 18 kg. Provider orders acetaminophen 15 mg/kg/dose PO. Available: 160 mg/5 mL.",
steps: [
{
title: "Step 1 — Calculate Dose",
type: "mcq",
prompt: "What dose (mg) should the nurse give?",
options: ["180 mg", "240 mg", "270 mg", "300 mg"],
correct: [2],
rationale: "18 kg × 15 mg/kg = 270 mg.",
plain: "18 kg times 15 mg each = 270 total mg needed.",
mnemonic: "Kg x Dose/Kg = Total Dose"
},
{
title: "Step 2 — Convert to mL",
type: "mcq",
prompt: "How many mL will the nurse administer?",
options: ["5 mL", "6 mL", "7.5 mL", "8.5 mL"],
correct: [2],
rationale: "160 mg/5 mL = 32 mg/mL. 270 mg ÷ 32 = 8.4375 mL (not listed). Using ratio: (270/160)*5 = 8.4375.",
plain: "You need 270. Concentration is 32mg/mL. 270 / 32 is roughly 8.4. Closest safe option is 8.5 mL.",
mnemonic: "D/H x V"
},
{
title: "Step 3 — Safety Priority",
type: "mcq",
prompt: "Which check is most important before giving this dose?",
options: ["Last dose timing and total daily limit", "Ask the parent to administer at home", "Skip weight verification if charted", "Give an extra dose for faster relief"],
correct: [0],
rationale: "Acetaminophen toxicity risk requires timing + total daily dose limit checks.",
plain: "Tylenol hurts the liver if given too much/often. Check when the last dose was!",
mnemonic: "Liver Lover? Watch the Tylenol total."
}
]
},
{
id: 20, type: "case", topic: "Unfolding Case – Heparin Infusion", difficulty: 3,
stem: "Heparin infusion order: 18 units/kg/hr. Client weight: 80 kg. Bag: 25,000 units in 500 mL.",
steps: [
{
title: "Step 1 — Units per hour",
type: "mcq",
prompt: "How many units/hr should the client receive?",
options: ["960 units/hr", "1200 units/hr", "1440 units/hr", "1800 units/hr"],
correct: [2],
rationale: "80 × 18 = 1440 units/hr.",
plain: "Weight (80) x Rate (18) = 1440 units needed per hour.",
mnemonic: "Kg x Units/Kg = Total Units"
},
{
title: "Step 2 — Convert to mL/hr",
type: "mcq",
prompt: "What rate in mL/hr should the pump be set to?",
options: ["14.4 mL/hr", "28.8 mL/hr", "36 mL/hr", "57.6 mL/hr"],
correct: [1],
rationale: "Concentration: 25,000 ÷ 500 = 50 units/mL. Rate: 1440 ÷ 50 = 28.8 mL/hr.",
plain: "Bag concentration is 50 units in every mL. You need 1440 units. 1440 / 50 = 28.8.",
mnemonic: "Total Units / Concentration = Rate"
},
{
title: "Step 3 — Safety Check",
type: "sata",
prompt: "Which actions are priority for safe heparin administration? (Select all that apply)",
options: ["Confirm concentration on bag", "Use independent double-check if policy requires", "Increase rate if patient reports pain", "Monitor for bleeding", "Skip protocol labs if stable"],
correct: [0, 1, 3],
rationale: "Verify concentration, double-check, and monitor bleeding per protocol. Pain doesn’t justify rate changes; labs are not optional.",
plain: "Heparin is a high-alert med. Double check everything. Watch for blood.",
mnemonic: "High Alert = Double Check"
}
]
},
{
id: 21, type: "case", topic: "Unfolding Case – IV Antibiotic Volume", difficulty: 2,
stem: "Order: ceftriaxone 1 g IV. Vial: 1 g. After reconstitution, concentration is 100 mg/mL. IV push max volume per policy is 10 mL per syringe.",
steps: [
{
title: "Step 1 — Convert Dose",
type: "mcq",
prompt: "1 g equals how many mg?",
options: ["10 mg", "100 mg", "1000 mg", "10,000 mg"],
correct: [2],
rationale: "1 g = 1000 mg.",
plain: "Grams are big. Milligrams are small. 1 big gram = 1000 small mg.",
mnemonic: "G to mg = x1000"
},
{
title: "Step 2 — Calculate Volume",
type: "mcq",
prompt: "How many mL contains 1 g at 100 mg/mL?",
options: ["5 mL", "10 mL", "15 mL", "20 mL"],
correct: [1],
rationale: "1000 mg ÷ 100 mg/mL = 10 mL.",
plain: "You have 1000mg total. Each mL fits 100mg. You need 10 mL to hold it all.",
mnemonic: "Total / Concentration = Volume"
},
{
title: "Step 3 — Safety Decision",
type: "mcq",
prompt: "Given the policy limit of 10 mL per syringe, the nurse should:",
options: ["Proceed with 10 mL in one syringe", "Split into two syringes automatically", "Hold medication without notifying anyone", "Give IM instead"],
correct: [0],
rationale: "10 mL meets the max per syringe; proceed per policy and administration guidelines.",
plain: "Policy says max is 10mL. You have exactly 10mL. You are safe to proceed.",
mnemonic: "Know your max limits."
}
]
},
{
id: 22, type: "case", topic: "Unfolding Case – Gravity Drip Check", difficulty: 2,
stem: "Order: 1000 mL over 10 hours. Tubing: 20 gtt/mL. The nurse is using gravity tubing (no pump).",
steps: [
{
title: "Step 1 — mL/min",
type: "mcq",
prompt: "What is the rate in mL/min?",
options: ["1.0", "1.5", "1.67", "2.0"],
correct: [2],
rationale: "10 hr = 600 min. 1000 ÷ 600 = 1.67 mL/min.",
plain: "Convert 10 hours to 600 minutes. 1000 mL / 600 min = 1.67 mL/min.",
mnemonic: "Hours x 60 = Minutes"
},
{
title: "Step 2 — gtt/min",
type: "mcq",
prompt: "What is the flow rate in gtt/min?",
options: ["20", "28", "33", "40"],
correct: [2],
rationale: "1.67 × 20 = 33.4 ≈ 33 gtt/min.",
plain: "Take mL/min (1.67) and multiply by the drop factor (20). ~33 drops per minute.",
mnemonic: "mL/min x DF = gtt/min"
},
{
title: "Step 3 — Safety Habit",
type: "mcq",
prompt: "Best practice after setting the gtt/min is to:",
options: ["Recheck in 15 minutes and with any position change", "Assume it stays accurate all shift", "Turn the roller clamp fully open", "Silence alarms to prevent interruptions"],
correct: [0],
rationale: "Gravity flow rates drift with position and venous pressure; recheck is a safety standard.",
plain: "Gravity drips aren't robots. If the patient moves their arm, the rate changes. Recheck often.",
mnemonic: "Gravity is unreliable. Trust but verify."
}
]
},
// =========================
// More MCQ / SATA / Dropdown to reach 30 total
// =========================
{
id: 23, type: "mcq", topic: "mcg ↔ mg Conversion", difficulty: 2,
stem: "Order: levothyroxine 75 mcg PO. Available: 0.1 mg tablets. How many tablets will the nurse administer?",
options: ["0.5 tablet", "0.75 tablet", "1 tablet", "1.5 tablets"],
correct: [1],
rationale: "0.1 mg = 100 mcg. Need 75 mcg → 75/100 = 0.75 tablet.",
plain: "First, convert mg to mcg. 0.1 mg is 100 mcg. You need 75. 75 is three-quarters of 100.",
mnemonic: "0.1 mg = 100 mcg"
},
{
id: 24, type: "mcq", topic: "mEq Calculation (Basic)", difficulty: 3,
stem: "Order: potassium chloride 20 mEq IV. Available: 10 mEq/5 mL. How many mL will the nurse administer?",
options: ["5 mL", "10 mL", "15 mL", "20 mL"],
correct: [1],
rationale: "10 mEq in 5 mL → 2 mEq/mL. Need 20 mEq → 10 mL.",
plain: "You need 20 mEq. You have 10 mEq in every 5mL scoop. You need two scoops. 5 + 5 = 10 mL.",
mnemonic: "D/H x Q"
},
{
id: 25, type: "sata", topic: "Rounding Rules & Policy", difficulty: 2,
stem: "Which statements about rounding are safest for NCLEX/HESI-style dosing? (Select all that apply)",
options: [
"Follow facility policy for rounding (especially pediatrics/IV)",
"Round only at the end of the problem",
"Round aggressively early to save time",
"If options don’t match, recheck math and units first",
"Document your rounding method in high-risk situations"
],
correct: [0, 1, 3, 4],
rationale: "Don’t round early; confirm units; follow policy; document when needed. If options don’t match, it’s a red flag to recheck.",
plain: "Rounding early introduces error. Keep the long decimals in your calculator until the very final step.",
mnemonic: "Round at the End, not the Trend"
},
{
id: 26, type: "dropdown", topic: "Ratio/Proportion Setup", difficulty: 1,
stem: "Complete the statement.",
blanks: [
{ text: "If 5 mL contains 250 mg, then 10 mL contains: ", options: ["250 mg", "500 mg", "750 mg", "1000 mg"], correct: "500 mg" }
],
rationale: "Doubling the volume doubles the dose: 250 → 500 mg.",
plain: "If you double the liquid (5 to 10), you double the drug (250 to 500).",
mnemonic: "Direct Proportion: Double one, double the other."
},
{
id: 27, type: "mcq", topic: "IVPB Rate (mL/hr)", difficulty: 1,
stem: "An IVPB antibiotic is 100 mL to infuse over 30 minutes. What pump rate is needed?",
options: ["100 mL/hr", "150 mL/hr", "200 mL/hr", "250 mL/hr"],
correct: [2],
rationale: "30 minutes = 0.5 hr. 100 ÷ 0.5 = 200 mL/hr.",
plain: "You need 100 mL in half an hour. That means in a full hour, you'd need double that amount (200).",
mnemonic: "Half hour run? Double the rate."
},
{
id: 28, type: "mcq", topic: "Dose by Supply (Liquid)", difficulty: 2,
stem: "Order: diphenhydramine 25 mg PO. Available: 12.5 mg/5 mL. How many mL will the nurse administer?",
options: ["5 mL", "10 mL", "12 mL", "20 mL"],
correct: [1],
rationale: "25 mg is double 12.5 mg, so volume doubles: 5 mL → 10 mL.",
plain: "You need 25. You have 12.5. 12.5 is half of 25. So you need two doses. 5mL x 2 = 10mL.",
mnemonic: "D/H x V"
},
{
id: 29, type: "sata", topic: "Clinical Reasonableness Check", difficulty: 2,
stem: "A nurse calculates a dose volume of 25 mL for an IV push medication. Which actions are appropriate? (Select all that apply)",
options: [
"Recheck the concentration and units",
"Confirm route and whether dilution/IVPB is required",
"Administer anyway if patient is in pain",
"Consult policy/charge nurse if volume seems unsafe for IV push",
"Assess if the ordered dose exceeds safe range"
],
correct: [0, 1, 3, 4],
rationale: "Large IV push volumes are a red flag. Verify units/concentration, route/policy, and safe range before giving.",
plain: "Imagine pushing 25 mL into an IV line by hand. That's a huge syringe. Red flag! Stop and recheck.",
mnemonic: "Big Volume IV Push? Stop & Shush (Check it)"
},
{
id: 30, type: "mcq", topic: "Time to Infuse", difficulty: 2,
stem: "An IV is running at 75 mL/hr. How long will it take to infuse 450 mL?",
options: ["4 hours", "5 hours", "6 hours", "7 hours"],
correct: [2],
rationale: "Time = volume ÷ rate = 450 ÷ 75 = 6 hours.",
plain: "You have 450 total. You use 75 every hour. How many 75s fit into 450? (6).",
mnemonic: "Total / Rate = Time"
}
];
/* =========================
State + Storage (MOVED UP 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();
}
// DEFINING 'el' HERE SO IT EXISTS BEFORE ANY RENDER FUNCTION USES IT
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 TO USE DISTINCT 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>`;
// UPDATED: Now uses direct fields for distinct logic
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>
`;
}
/* =========================
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>