/* global React, ReactDOM */ // ============================================================== // app.jsx — main controller + Tweaks // ============================================================== var { useState, useEffect, useCallback } = React; const TWEAKS = /*EDITMODE-BEGIN*/{ "theme": "light", "fontSize": "medium", "questionLayout": "standard", "feedbackMode": "auto" }/*EDITMODE-END*/; const App = () => { const [screen, setScreen] = useState('landing'); // landing | exam | results | review const [exam, setExam] = useState(null); const [state, setState] = useState(null); const [history, setHistory] = useState(() => window.loadHistory()); const [tweaks, setTweaks] = useState(TWEAKS); const [tweaksOpen, setTweaksOpen] = useState(false); // Apply theme/font tweaks useEffect(() => { document.documentElement.dataset.theme = tweaks.theme; const map = { small: '14px', medium: '15px', large: '17px' }; document.body.style.fontSize = map[tweaks.fontSize] || '15px'; document.documentElement.dataset.layout = tweaks.questionLayout; }, [tweaks]); // Persist state on change during exam useEffect(() => { if (screen === 'exam' && exam && state) { window.saveState({ exam, state }); } }, [exam, state, screen]); // Edit mode protocol useEffect(() => { const handler = (e) => { if (e.data?.type === '__activate_edit_mode') setTweaksOpen(true); if (e.data?.type === '__deactivate_edit_mode') setTweaksOpen(false); }; window.addEventListener('message', handler); window.parent.postMessage({ type: '__edit_mode_available' }, '*'); return () => window.removeEventListener('message', handler); }, []); const updateTweak = (key, value) => { setTweaks((t) => { const next = { ...t, [key]: value }; window.parent.postMessage({ type: '__edit_mode_set_keys', edits: { [key]: value } }, '*'); return next; }); }; const loadExam = (data) => { const finalFeedbackMode = tweaks.feedbackMode === 'auto' ? (data.feedbackMode || 'end') : tweaks.feedbackMode; const initialState = { currentIdx: 0, answers: {}, flags: {}, revealed: {}, startedAt: Date.now(), feedbackMode: finalFeedbackMode, }; setExam(data); setState(initialState); setScreen('exam'); }; const resume = () => { const saved = window.loadState(); if (saved?.exam && saved?.state) { setExam(saved.exam); setState(saved.state); setScreen('exam'); } }; const submit = useCallback(() => { if (!exam || !state) return; const completedAt = Date.now(); const elapsedMs = completedAt - state.startedAt; const graded = exam.questions.map((q) => window.gradeQuestion(q, state.answers[q.id])); const gradedCount = graded.filter((r) => r.status !== 'ungraded').length; const correctCount = graded.filter((r) => r.correct).length; const totalPoints = exam.questions.reduce((a, q) => a + (q.points || 1), 0); const scorePoints = graded.reduce((a, r) => a + r.points, 0); const historyEntry = { id: `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`, title: exam.title, completedAt, elapsedMs, gradedCount, correctCount, totalPoints, scorePoints, passingScore: exam.passingScore || 60, exam, // full snapshot so we can re-review state: { ...state, completedAt }, }; window.appendHistory(historyEntry); setHistory(window.loadHistory()); window.clearState(); setState((s) => ({ ...s, completedAt })); setScreen('results'); }, [exam, state]); const exit = () => { setScreen('landing'); setExam(null); setState(null); }; const restart = () => { if (!exam) return; loadExam(exam); }; // Replay a past attempt window.__replayAttempt = (entry) => { setExam(entry.exam); setState(entry.state); setScreen('review'); }; const clearAllHistory = () => { if (confirm('Clear all past attempts?')) { window.clearHistory(); setHistory([]); } }; return (