/* 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(
);
}
function OptBtn({opt,state,onClick}){
const cls=['q-opt',...(state?state.split(' '):[])].filter(Boolean).join(' ');
return({opt.id.toUpperCase()} {opt.t} );
}
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}
)}
}
);
}
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(
!submitted&&!pa&&onTerm(i)} disabled={submitted||!!pa}>
{String.fromCharCode(65+i)}. {p.term}
);
})}
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(
!submitted&&!pa&&onDef(i)} disabled={submitted||!!pa}>{def} );
})}
{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}
);
})}
}
);
}
function Feedback({q,selected,correct}){
return(
{correct?'✓ Correct':'✗ Incorrect'}
{q.exp}
{!correct&&q.wrong&&
{selected.filter(s=>!q.ans.includes(s)).map(s=>q.wrong[s]&&(
{s.toUpperCase()}: {q.wrong[s]}
))}
}
);
}
function HintPanel({q,hintsShown,onHint,chatLog,chatIn,setChatIn,onAsk,loading}){
const logRef=React.useRef(null);
React.useEffect(()=>{if(logRef.current)logRef.current.scrollTop=logRef.current.scrollHeight;},[chatLog,loading]);
const maxHints=q.hints?.length||0;
return(
{AI_TUTOR_ENABLED?'AI Tutor':'Hints'} {AI_TUTOR_ENABLED&&Claude }
{hintsShown>0&&
{q.hints.slice(0,hintsShown).map((h,i)=>
Hint {i+1}/{maxHints} {h}
)}
}
{hintsShown
+ Reveal hint ({hintsShown}/{maxHints})
}
{AI_TUTOR_ENABLED &&
{chatLog.length===0&&
Ask me anything… I won't reveal the answer until you submit.
}
{chatLog.map((m,i)=>
{m.c}
)}
{loading&&
thinking…
}
}
);
}
function QuizMode({session,onComplete,onAnswer,onExit}){
const{questions}=session;const total=questions.length;
const[idx,setIdx]=React.useState(0);
const[selected,setSelected]=React.useState([]);
const[fitbVals,setFitbVals]=React.useState([]);
const[fitbCorrect,setFitbCorrect]=React.useState([]);
const[shuffDefs,setShuffDefs]=React.useState([]);
const[matchPairs,setMatchPairs]=React.useState([]);
const[termSel,setTermSel]=React.useState(null);
const[defSel,setDefSel]=React.useState(null);
const[submitted,setSubmitted]=React.useState(false);
const[answers,setAnswers]=React.useState({});
const[hintsShown,setHintsShown]=React.useState({});
const[chatLogs,setChatLogs]=React.useState({});
const[chatIn,setChatIn]=React.useState('');
const[loading,setLoading]=React.useState(false);
const[timeLeft,setTimeLeft]=React.useState(session.timerEnabled?session.timerDuration*60:null);
const[learnTopicId,setLearnTopicId]=React.useState(null);
const q=questions[idx];
const chatLog=chatLogs[q?.id]||[];
const hShown=hintsShown[q?.id]||0;
React.useEffect(()=>{
if(!session.timerEnabled||timeLeft===null||timeLeft<=0)return;
const t=setTimeout(()=>setTimeLeft(tl=>tl-1),1000);return()=>clearTimeout(t);
},[timeLeft,session.timerEnabled]);
React.useEffect(()=>{
setSelected([]);setFitbVals([]);setFitbCorrect([]);setMatchPairs([]);
setTermSel(null);setDefSel(null);setSubmitted(false);setChatIn('');
if(q?.type==='match')setShuffDefs(shuffle(q.pairs.map(p=>p.def)));
},[idx]);
function toggle(id){setSelected(prev=>q.type==='ms'?(prev.includes(id)?prev.filter(x=>x!==id):[...prev,id]):[id]);}
function handleMatchTerm(ti){if(defSel!==null){setMatchPairs(p=>[...p,{ti,di:defSel}]);setTermSel(null);setDefSel(null);}else setTermSel(ti);}
function handleMatchDef(di){if(termSel!==null){setMatchPairs(p=>[...p,{ti:termSel,di}]);setTermSel(null);setDefSel(null);}else setDefSel(di);}
function canSubmit(){
if(submitted)return false;
if(q.type==='mc'||q.type==='exhibit')return selected.length===1;
if(q.type==='ms')return selected.length>=1;
if(q.type==='fitb')return(q.blanks||[]).every((_,i)=>(fitbVals[i]||'').trim().length>0);
if(q.type==='match')return matchPairs.length===q.pairs.length;
return false;
}
function doSubmit(){
let correct=false;let fc=[];
if(q.type==='mc'||q.type==='exhibit')correct=selected.length===q.ans.length&&selected.every(s=>q.ans.includes(s));
else if(q.type==='ms')correct=selected.length===q.ans.length&&selected.every(s=>q.ans.includes(s));
else if(q.type==='fitb'){fc=(q.blanks||[]).map((b,i)=>normCmd(fitbVals[i]||'')===normCmd(b));correct=fc.every(Boolean);setFitbCorrect(fc);}
else if(q.type==='match')correct=matchPairs.length===q.pairs.length&&matchPairs.every(({ti,di})=>shuffDefs[di]===q.pairs[ti].def);
setSubmitted(true);
setAnswers(prev=>({...prev,[q.id]:{correct,selected:[...selected]}}));
if(onAnswer)onAnswer(q,correct);
}
function doNext(){if(idxi+1);else onComplete(answers);}
function handleSkip(){doNext();}
async function doAsk(){
if(!chatIn.trim()||loading)return;
const msg=chatIn.trim();setChatIn('');
const prev=chatLogs[q.id]||[];
const newLog=[...prev,{r:'user',c:msg}];
setChatLogs(p=>({...p,[q.id]:newLog}));
setLoading(true);
try{
const sys=`You are a concise CCNA/CCNP exam tutor. Topic: ${q.topic}. Question: "${q.stem}".${!submitted?' Do NOT reveal the correct answer option. Give Socratic guidance — ask leading questions, don\'t state the answer directly.':' Student submitted. Explain fully.'} Max 3 sentences.`;
const msgs=newLog.map(m=>({role:m.r==='bot'?'assistant':'user',content:m.c}));
const reply=await window.claude.complete({system:sys,messages:msgs});
setChatLogs(p=>({...p,[q.id]:[...newLog,{r:'bot',c:reply}]}));
}catch(e){setChatLogs(p=>({...p,[q.id]:[...newLog,{r:'bot',c:'Claude unavailable — check your connection.'}]}));}
setLoading(false);
}
const isCorrect=answers[q?.id]?.correct;
const fmtTime=s=>`${Math.floor(s/60)}:${String(s%60).padStart(2,'0')}`;
if(!q)return null;
// ===== CLI QUESTION TYPE =====
if(q.type==='cli'){
return(
CLI {idx+1} / {total}
{session.label}
✕ Exit
{q.topic}
{setAnswers(prev=>({...prev,[q.id]:{correct,selected:[]}}));setSubmitted(true);if(onAnswer)onAnswer(q,correct);}}
submitted={submitted}
onSkip={!submitted?handleSkip:null}
onLearnTopic={q.conceptId ? ()=>setLearnTopicId(q.conceptId) : null}/>
{submitted&&
{idx}
{!submitted&&↑↓ history · ? help
}
{learnTopicId&&setLearnTopicId(null)}/>}
);
}
return(
Q {idx+1} / {total}
{session.label}
{session.timerEnabled&&timeLeft!==null&&{fmtTime(timeLeft)} }
✕ Exit
{q.topic}
{q.stem}
{q.exhibit&&
}
{(q.type==='mc'||q.type==='exhibit')&&}
{q.type==='ms'&&}
{q.type==='fitb'&&setFitbVals(p=>{const n=[...p];n[i]=v;return n;})} submitted={submitted} correctness={fitbCorrect}/>}
{q.type==='match'&&}
{!submitted
?Submit Answer
:{idx}
{!submitted&&setHintsShown(p=>({...p,[q.id]:Math.min((p[q.id]||0)+1,q.hints?.length||0)}))}>Hint }
{!submitted&&Skip → }
{submitted&&}
setHintsShown(p=>({...p,[q.id]:Math.min((p[q.id]||0)+1,q.hints?.length||0)}))}
chatLog={chatLog} chatIn={chatIn} setChatIn={setChatIn} onAsk={doAsk} loading={loading}/>
);
}
function QuizResults({session,finalAnswers,onHome,onRetry}){
const{questions}=session;
const correct=questions.filter(q=>finalAnswers[q.id]?.correct).length;
const answered=questions.filter(q=>finalAnswers[q.id]).length;
const skipped=questions.length-answered;
const pct=answered>0?Math.round((correct/answered)*100):0;
const pass=pct>=70;
const ds={};window.DOMAINS.forEach(d=>ds[d.id]={c:0,t:0});
questions.forEach(q=>{ds[q.d].t++;if(finalAnswers[q.id]?.correct)ds[q.d].c++;});
return(
Session Complete
← Home
Final Score
{pct}%
{correct}/{answered} correct{skipped>0?` · ${skipped} skipped`:''} · {pass?'✓ Pass':'Keep studying'}
{window.DOMAINS.map(d=>{const s=ds[d.id];if(!s.t)return null;const p=Math.round((s.c/s.t)*100);return(
Retry Session
Home
Question Review
{questions.map((q,i)=>{const ans=finalAnswers[q.id];const pipCls=ans?.correct?'ok':ans?'bad':'skip';return(
Q{i+1}. {q.stem.slice(0,68)}{q.stem.length>68?'…':''}
{q.topic.split(' ')[0]}
);})}
);
}
Object.assign(window,{QuizMode,QuizResults});