/** * ExerciseViewer — Problem display with KaTeX, step-by-step solution reveal, interactive attempt */ const ExerciseViewer = (() => { let container = null; let currentExercise = null; let revealedSteps = 0; let ac = null; // AbortController for event listener cleanup function escapeHtml(str) { return (str || '').replace(/&/g, '&').replace(//g, '>'); } async function findExercise(id) { await ExerciseBrowser.loadExerciseData(); const exercises = ExerciseBrowser.getExercises(); return exercises.find(ex => ex.id === id); } function renderProblem(ex) { const diffLabel = { basic: 'Básico', intermediate: 'Intermedio', advanced: 'Avanzado' }; const topicLabels = { 'vector-ops': 'Vectores', 'line-eq': 'Rectas', 'plane-eq': 'Planos', 'matrix-ops': 'Matrices', 'determinants': 'Determinantes', 'matrix-inverse': 'Inversa', 'systems': 'Sistemas' }; return `
${ex.id} ${topicLabels[ex.topic] || ex.topic} — ${diffLabel[ex.difficulty] || ex.difficulty}
${ex.hint ? `
${escapeHtml(ex.hint)}
` : ''} ${ex.theoryKey ? `
` : ''}

Tu respuesta

Solución paso a paso

    Respuesta final
    `; } let theoryCache = {}; function renderStatement(ex) { const el = document.getElementById('exercise-statement'); if (!el) return; const latex = ex.statement; if (!latex) { el.textContent = ''; return; } if (typeof KatexRenderer !== 'undefined' && KatexRenderer.renderInline) { try { KatexRenderer.renderInline(el, latex); } catch (e) { console.warn('[ExerciseViewer] KaTeX render error, falling back to text:', e); el.textContent = latex; } } else { el.textContent = latex; } } async function loadTheory(chapter, theoryKey) { const cacheKey = `${chapter}-${theoryKey}`; if (theoryCache[cacheKey]) return theoryCache[cacheKey]; try { const resp = await fetch(`data/theory-cap0${chapter}.json`); const data = await resp.json(); theoryCache[cacheKey] = data[theoryKey] || null; return theoryCache[cacheKey]; } catch (e) { console.warn('[ExerciseViewer] Failed to load theory:', e); return null; } } function renderTheoryPanel(theory) { const box = document.getElementById('theoryBox'); if (!box) return; const title = escapeHtml(theory.title || ''); const content = theory.content || ''; box.innerHTML = `

    ${title}

    `; const contentEl = document.getElementById('theoryContent'); if (contentEl && typeof KatexRenderer !== 'undefined') { contentEl.textContent = content; KatexRenderer.renderAll(contentEl); } else if (contentEl) { contentEl.textContent = content; } } function checkAnswer(userInput, exercise) { if (!exercise.answer) return { correct: false, message: 'No hay respuesta definida para este ejercicio.' }; const ans = exercise.answer; const input = userInput.trim(); if (exercise.answerType === 'numeric') { const num = parseFloat(input); if (isNaN(num)) return { correct: false, message: 'Ingresá un número válido.' }; const correct = Math.abs(num - ans.value) < 0.01; return { correct, message: correct ? '¡Correcto! 🎉' : 'Incorrecto. Intentá de nuevo.' }; } if (exercise.answerType === 'vector') { // Parse vector format: (1; 2; 3) or (1,2,3) const vecMatch = input.replace(/\s/g, '').match(/[\(-]?([-\d.]+)[;,]([-\d.]+)(?:[;,]([-\d.]+))?[;\)]?/); if (!vecMatch) return { correct: false, message: 'Formato: (1; 2; 3) o (1,2,3)' }; const vals = vecMatch.slice(1).filter(v => v !== undefined).map(Number); const answerVals = ans.value; if (vals.length !== answerVals.length) return { correct: false, message: `Se esperan ${answerVals.length} componentes.` }; const correct = vals.every((v, i) => Math.abs(v - answerVals[i]) < 0.01); return { correct, message: correct ? '¡Correcto! 🎉' : 'Incorrecto. Intentá de nuevo.' }; } if (exercise.answerType === 'expression') { // Normalize and compare expression answers // Handle both text answers and LaTeX answers const normalizeText = (s) => { if (!s) return ''; return s .trim() .toLowerCase() .replace(/\s+/g, ' ') .normalize('NFD') .replace(/[\u0300-\u036f]/g, ''); // Remove diacritics for accent-insensitive comparison }; const normalizeLatex = (s) => { if (!s) return ''; return s .trim() .replace(/\s+/g, ' ') .replace(/\\vec\{([^}]*)\}/g, '\\vec{$1}') .replace(/\\overrightarrow\{([^}]*)\}/g, '\\overrightarrow{$1}') .replace(/\\cdot/g, '\\cdot') .replace(/\\times/g, '\\times'); }; const normInput = normalizeText(input); const normAnswer = normalizeText(ans.value); if (!normAnswer) return { correct: false, message: 'Este ejercicio requiere verificación manual. Mirá la solución.' }; const correct = normInput === normAnswer; return { correct, message: correct ? '¡Correcto! 🎉' : 'Incorrecto. Intentá de nuevo.' }; } return { correct: false, message: 'Tipo de respuesta no soportado.' }; } function revealNextStep() { if (!currentExercise || !currentExercise.solutionSteps) return; const steps = currentExercise.solutionSteps; const stepsEl = document.getElementById('solutionSteps'); const revealBtn = document.getElementById('revealBtn'); if (revealedSteps < steps.length) { const step = steps[revealedSteps]; const li = document.createElement('li'); li.className = 'solution-step'; li.innerHTML = `
    ${escapeHtml(step.desc)}
    `; stepsEl.appendChild(li); // Render math const mathEl = li.querySelector('.solution-step__expression'); if (mathEl && typeof KatexRenderer !== 'undefined') { KatexRenderer.renderDisplay(mathEl, step.expression); } revealedSteps++; // Hide button if all steps revealed if (revealedSteps >= steps.length) { revealBtn.textContent = 'Mostrar respuesta final ▼'; revealBtn.onclick = revealAnswer; } } } function revealAnswer() { if (!currentExercise || !currentExercise.answer) return; const answerEl = document.getElementById('solutionAnswer'); const contentEl = document.getElementById('answerContent'); const revealBtn = document.getElementById('revealBtn'); if (contentEl && typeof KatexRenderer !== 'undefined') { KatexRenderer.renderDisplay(contentEl, currentExercise.answer.latex); } else if (contentEl) { contentEl.textContent = currentExercise.answer.latex; } answerEl.classList.add('solution-answer--visible'); revealBtn.style.display = 'none'; } function setupEvents() { ac = new AbortController(); // Back button const backBtn = container.querySelector('.exercise-viewer__back'); if (backBtn) { backBtn.addEventListener('click', () => { if (typeof App !== 'undefined') App.navigate('/exercises'); }, { signal: ac.signal }); } // Hint button const hintBtn = document.getElementById('hintBtn'); if (hintBtn) { hintBtn.addEventListener('click', () => { const hintBox = document.getElementById('hintBox'); hintBox.classList.toggle('exercise-viewer__hint--visible'); hintBtn.textContent = hintBox.classList.contains('exercise-viewer__hint--visible') ? '🙈 Ocultar pista' : '💡 Ver pista'; }, { signal: ac.signal }); } // Theory button const theoryBtn = document.getElementById('theoryBtn'); if (theoryBtn && currentExercise && currentExercise.theoryKey) { theoryBtn.addEventListener('click', async () => { const theoryBox = document.getElementById('theoryBox'); const isVisible = theoryBox.classList.contains('exercise-viewer__theory--visible'); if (isVisible) { theoryBox.classList.remove('exercise-viewer__theory--visible'); theoryBtn.textContent = '📖 Ver teoría'; } else { const chapter = currentExercise.chapter || 1; const theory = await loadTheory(chapter, currentExercise.theoryKey); if (theory) { renderTheoryPanel(theory); theoryBox.classList.add('exercise-viewer__theory--visible'); theoryBtn.textContent = '📕 Ocultar teoría'; } else { theoryBox.innerHTML = '

    Teoría no disponible.

    '; theoryBox.classList.add('exercise-viewer__theory--visible'); } } }, { signal: ac.signal }); } // Attempt const submitBtn = document.getElementById('attemptSubmit'); const attemptInput = document.getElementById('attemptInput'); const feedback = document.getElementById('attemptFeedback'); if (submitBtn && currentExercise) { const doCheck = () => { const result = checkAnswer(attemptInput.value, currentExercise); feedback.className = 'attempt-feedback'; feedback.classList.add(result.correct ? 'attempt-feedback--correct' : 'attempt-feedback--incorrect'); feedback.textContent = result.message; }; submitBtn.addEventListener('click', doCheck, { signal: ac.signal }); attemptInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') doCheck(); }, { signal: ac.signal }); } // Reveal steps const revealBtn = document.getElementById('revealBtn'); if (revealBtn) { revealBtn.addEventListener('click', revealNextStep, { signal: ac.signal }); } } async function init(el, params) { container = el; revealedSteps = 0; currentExercise = null; if (!params || !params.id) { container.innerHTML = '

    Ejercicio no especificado.

    '; return; } const ex = await findExercise(params.id); if (!ex) { container.innerHTML = `

    Ejercicio "${escapeHtml(params.id)}" no encontrado.

    `; return; } currentExercise = ex; container.innerHTML = `
    ${renderProblem(ex)}
    `; renderStatement(ex); setupEvents(); } function destroy() { if (ac) { ac.abort(); ac = null; } if (container) container.innerHTML = ''; container = null; currentExercise = null; revealedSteps = 0; } return { init, destroy }; })();