/* quiz.jsx — QuizMode, QuizResults */ // Feature flag — AI tutor (Claude) is disabled in self-hosted builds. // To enable as a Pro/paid feature, see docs/AI_TUTOR_PAID_FEATURE.md const AI_TUTOR_ENABLED = false; function shuffle(arr){return[...arr].sort(()=>Math.random()-0.5);} function normCmd(s){return s.trim().toLowerCase().replace(/\s+/g,' ');} function DomTag({d}){const dom=(window.DOMAINS||[]).find(x=>x.id===d);return {dom?.short||`D${d}`};} function DiffTag({diff,ccnp}){if(ccnp)return CCNP;return {diff===1?'Easy':diff===2?'Med':'Hard'};} function Exhibit({text}){ return(
IOS Terminal Output
{text}
); } function OptBtn({opt,state,onClick}){ const cls=['q-opt',...(state?state.split(' '):[])].filter(Boolean).join(' '); return(); } function MCQ({q,selected,onSelect,submitted}){ return(
{q.opts.map(o=>{ let s=''; if(submitted){s='disabled';if(q.ans.includes(o.id))s='correct disabled';else if(selected.includes(o.id))s='wrong disabled';} else if(selected.includes(o.id))s='selected'; return !submitted&&onSelect(o.id)}/>; })}
); } function MSQ({q,selected,onToggle,submitted}){ return(

▲ Select all that apply

{q.opts.map(o=>{ let s=''; if(submitted){const isA=q.ans.includes(o.id),isSel=selected.includes(o.id);s='disabled';if(isA)s='correct disabled';else if(isSel&&!isA)s='wrong disabled';else if(!isSel&&isA)s='missed disabled';} else if(selected.includes(o.id))s='selected'; return !submitted&&onToggle(o.id)}/>; })}
); } function FITBQ({q,vals,onChange,submitted,correctness}){ return(
{(q.blanks||[]).map((blank,i)=>{ const prompt=i===0?q.fitbPrompt:(q.fitbPrompt2||q.fitbPrompt); let cls='fitb-input';if(submitted)cls+=correctness[i]?' ok':' err'; return(
{prompt} onChange(i,e.target.value)} placeholder="type command…" disabled={submitted} spellCheck={false} autoComplete="off"/>
); })} {submitted&&
{(q.blanks||[]).map((b,i)=>!correctness[i]&&
Line {i+1}: {b}
)}
}
); } // Short-answer (type:"sa"). User types a free-text answer; graded // case-insensitively against q.acceptable (falling back to q.ans). Reuses the // .fitb-input styling so it matches the fill-in-the-blank look. function SAQ({q,val,onChange,submitted,correct,onEnter}){ let cls='fitb-input sa-input';if(submitted)cls+=correct?' ok':' err'; const canonical=(q.ans&&q.ans[0])||(q.acceptable&&q.acceptable[0])||''; return(
onChange(e.target.value)} onKeyDown={e=>{if(e.key==='Enter'){e.preventDefault();if(!submitted&&onEnter)onEnter();}}} placeholder="type your answer…" disabled={submitted} spellCheck={false} autoComplete="off" autoFocus/>
{submitted&&!correct&&
Answer: {canonical}
}
); } function MatchQ({q,termSel,defSel,pairs,onTerm,onDef,defs,submitted}){ return(
Terms — click to select
{q.pairs.map((p,i)=>{const pa=pairs.find(x=>x.ti===i);let cls='match-item';if(termSel===i)cls+=' active';if(pa)cls+=' paired';return( ); })}
Definitions — click to pair
{defs.map((def,i)=>{const pa=pairs.find(x=>x.di===i);let cls='match-item';if(defSel===i)cls+=' active';if(pa)cls+=' paired';return( ); })}
{pairs.length===q.pairs.length&&!submitted&&

✓ All matched — click Submit

} {submitted&&
Match Results
{q.pairs.map((p,i)=>{const pa=pairs.find(x=>x.ti===i);const ok=pa&&defs[pa.di]===p.def;return(
{ok?'✓':'✗'}{p.term} → {p.def}
); })}
}
); } // ===== Report-a-question modal ===== // Used in Feedback (practice mode, post-answer) and QuizResults (final exam review). // Requires the user to be logged in — modal short-circuits to a sign-in prompt // when AuthStore reports no user. Posts to /api/exam-feedback. function ReportQuestionModal({q, source, certHint, onClose}){ const [issueType, setIssueType] = React.useState('wrong_answer'); const [description, setDescription] = React.useState(''); const [status, setStatus] = React.useState('idle'); // idle | sending | sent | error const [errorMsg, setErrorMsg] = React.useState(''); const user = (window.AuthStore && window.AuthStore.user) ? window.AuthStore.user : null; const isLoggedIn = !!user; async function submit(){ if (!description.trim()) return; setStatus('sending'); setErrorMsg(''); try { const api = window.AuthStore && window.AuthStore.api; if (!api) { setErrorMsg('Auth not loaded — refresh the page and try again.'); setStatus('error'); return; } const stem = (q.stem || q.question || '').slice(0, 1900); // AuthStore.api returns the parsed JSON body directly (and throws on // non-2xx). The backend always returns {ok: bool, message: str} for // this endpoint, even on send failure. const data = await api('/api/exam-feedback', { method: 'POST', body: JSON.stringify({ cert: certHint || q.exam || 'unknown', question_id: String(q.id), source: source || 'practice', question_stem: stem, issue_type: issueType, description: description.trim(), }), }); if (data && data.ok) { setStatus('sent'); setTimeout(onClose, 1600); } else { setErrorMsg((data && data.message) || 'Send failed.'); setStatus('error'); } } catch (e) { setErrorMsg(e.message || 'Network error.'); setStatus('error'); } } return (
e.stopPropagation()}>
Report a problem with this question
{!isLoggedIn ? (

You need to be signed in to report a question. Sign in and try again.

) : (
Question ID: {q.id}
Cert: {certHint || q.exam || 'unknown'}