/* ================================================================== Engine Config modal — curated subset (timer / navigation / scoring / atomic_block_shuffle). Loads current config from the test detail endpoint, PATCHes back to /api/tests/:display_id/engine-config. ================================================================== */ const EngineConfigModal = ({ test, onClose, onSaved }) => { const [cfg, setCfg] = React.useState(null); const [loading, setLoading] = React.useState(false); const [saving, setSaving] = React.useState(false); const [err, setErr] = React.useState(""); React.useEffect(() => { if (!test) { setCfg(null); setErr(""); return; } let cancelled = false; setLoading(true); window.AppApi.get(`/tests/${test.id}`) .then(d => { if (!cancelled) setCfg(d.engine_config); }) .catch(e => { if (!cancelled) setErr(e.message || "failed to load config"); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [test]); if (!test) return null; const set = (path, value) => { setCfg(prev => { const next = JSON.parse(JSON.stringify(prev)); const keys = path.split("."); let node = next; for (let i = 0; i < keys.length - 1; i++) node = node[keys[i]]; node[keys[keys.length - 1]] = value; return next; }); }; const handleSave = async () => { setSaving(true); setErr(""); try { const r = await window.AppApi.patch(`/tests/${test.id}/engine-config`, cfg); if (onSaved) onSaved(r.engine_config); onClose(); } catch (e) { setErr(e.message || "failed to save"); } finally { setSaving(false); } }; const SectionTitle = ({ children }) => (
{children}
); const Row = ({ label, children }) => ( ); const Num = ({ value, onChange, min, step = 1 }) => ( onChange(Number(e.target.value))} style={{ width: 110, padding: "4px 8px", background: "var(--bg-0)", border: "1px solid var(--line)", borderRadius: 4, color: "var(--ink-0)", fontSize: 12 }}/> ); const Bool = ({ value, onChange }) => ( onChange(e.target.checked)}/> ); return (
e.stopPropagation()} style={{ background: "var(--bg-1)", border: "1px solid var(--line-strong)", borderRadius: 10, width: 620, maxWidth: "100%", maxHeight: "88vh", display: "flex", flexDirection: "column" }}>
{test.id} · Engine config
{test.title}
{loading &&
Loading…
} {!loading && cfg && ( <> Timer {cfg.timing.type !== "none" && cfg.timing.type !== "per_question" && ( set("timing.duration_seconds", v)}/> )} {cfg.timing.type === "per_question" && ( <> set("timing.per_question_duration", v)}/> set("timing.hard_lock_on_timeout", v)}/> )} {cfg.timing.type !== "none" && ( <> set("timing.show_timer", v)}/> set("timing.auto_submit_on_expiry", v)}/> )} Navigation set("navigation.allow_back_tracking", v)}/> set("navigation.allow_jumping", v)}/> set("navigation.show_question_palette", v)}/> Scoring set("scoring.correct_marks", v)}/> set("scoring.wrong_marks", v)}/> set("scoring.unattempted_marks", v)}/> Logic set("logic.atomic_block_shuffle", v)}/>
Keeps grouped questions (RC / DI passages) together when shuffling.
)} {err &&
{err}
}
); }; window.EngineConfigModal = EngineConfigModal;