/**
* 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.hint ? `
${escapeHtml(ex.hint)}
` : ''}
${ex.theoryKey ? `
` : ''}
Solución paso a paso
`;
}
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 = ``;
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 };
})();