feat: interactive linear algebra practice web app
- 68 exercises from UBA FCE chapters 1-3 - Step-by-step solutions with KaTeX rendering - Theory panels (26 topics) expandable per exercise - Matrix builder (2x2/3x3/4x4) with 7 operations - System solver (Gauss, Gauss-Jordan, Cramer, Rouché-Frobenius) - Glassmorphism UI with dark mode - Canvas particle background - ARIA accessibility (keyboard nav, screen reader) - Zero build step - open index.html directly
This commit is contained in:
200
js/components/exercise-browser.js
Normal file
200
js/components/exercise-browser.js
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* ExerciseBrowser — Topic filter tabs, exercise list, search, navigation to viewer
|
||||
*/
|
||||
const ExerciseBrowser = (() => {
|
||||
let container = null;
|
||||
let exercises = [];
|
||||
let filteredExercises = [];
|
||||
let activeTopic = 'all';
|
||||
let searchText = '';
|
||||
let exerciseDataLoaded = {};
|
||||
let ac = null;
|
||||
let searchDebounceTimer = null;
|
||||
|
||||
const TOPICS = [
|
||||
{ key: 'all', label: 'Todos', icon: '📚' },
|
||||
{ key: 'vector-ops', label: 'Vectores', icon: '➡️' },
|
||||
{ key: 'line-eq', label: 'Rectas', icon: '📏' },
|
||||
{ key: 'plane-eq', label: 'Planos', icon: '📐' },
|
||||
{ key: 'matrix-ops', label: 'Matrices', icon: '🔲' },
|
||||
{ key: 'determinants', label: 'Determinantes', icon: 'det' },
|
||||
{ key: 'matrix-inverse', label: 'Inversa', icon: '🔄' },
|
||||
{ key: 'systems', label: 'Sistemas', icon: '⚖️' }
|
||||
];
|
||||
|
||||
const CHAPTER_NAMES = {
|
||||
1: 'Cap. 1 — Vectores, Recta y Plano',
|
||||
2: 'Cap. 2 — Matrices y Determinantes',
|
||||
3: 'Cap. 3 — Sistemas de Ecuaciones'
|
||||
};
|
||||
|
||||
async function loadExerciseData() {
|
||||
if (Object.keys(exerciseDataLoaded).length > 0) return exerciseDataLoaded;
|
||||
const chapters = [1, 2, 3];
|
||||
const promises = chapters.map(async (ch) => {
|
||||
try {
|
||||
const resp = await fetch(`data/exercises-cap0${ch}.json`);
|
||||
exerciseDataLoaded[ch] = await resp.json();
|
||||
} catch (e) {
|
||||
console.warn(`[ExerciseBrowser] Failed to load cap${ch}:`, e);
|
||||
exerciseDataLoaded[ch] = [];
|
||||
}
|
||||
});
|
||||
await Promise.all(promises);
|
||||
exercises = Object.values(exerciseDataLoaded).flat();
|
||||
return exerciseDataLoaded;
|
||||
}
|
||||
|
||||
function getTopicCounts() {
|
||||
const counts = { all: exercises.length };
|
||||
exercises.forEach(ex => {
|
||||
counts[ex.topic] = (counts[ex.topic] || 0) + 1;
|
||||
});
|
||||
return counts;
|
||||
}
|
||||
|
||||
function filterExercises() {
|
||||
filteredExercises = exercises.filter(ex => {
|
||||
const topicMatch = activeTopic === 'all' || ex.topic === activeTopic;
|
||||
if (!searchText) return topicMatch;
|
||||
const search = searchText.toLowerCase();
|
||||
const textMatch =
|
||||
(ex.statement || '').toLowerCase().includes(search) ||
|
||||
(ex.id || '').toLowerCase().includes(search) ||
|
||||
(ex.subtopic || '').toLowerCase().includes(search);
|
||||
return topicMatch && textMatch;
|
||||
});
|
||||
}
|
||||
|
||||
function renderTopicTabs() {
|
||||
const counts = getTopicCounts();
|
||||
return `<div class="topic-tabs">${TOPICS.map(t => {
|
||||
const isActive = t.key === activeTopic ? ' topic-tab--active' : '';
|
||||
const count = counts[t.key] || 0;
|
||||
return `<button class="topic-tab${isActive}" data-topic="${t.key}">
|
||||
<span>${t.icon}</span> ${t.label} <span class="topic-tab__count">${count}</span>
|
||||
</button>`;
|
||||
}).join('')}</div>`;
|
||||
}
|
||||
|
||||
function renderExerciseCards() {
|
||||
if (filteredExercises.length === 0) {
|
||||
return `<div class="exercise-browser__empty"><p>No se encontraron ejercicios</p></div>`;
|
||||
}
|
||||
return `<div class="exercise-grid">${filteredExercises.map(ex => {
|
||||
const topicInfo = TOPICS.find(t => t.key === ex.topic) || { label: ex.topic };
|
||||
const difficultyClass = `difficulty--${ex.difficulty || 'basic'}`;
|
||||
const diffLabel = { basic: 'Básico', intermediate: 'Intermedio', advanced: 'Avanzado' };
|
||||
return `<div class="exercise-card" data-exercise-id="${ex.id}" tabindex="0" role="button" aria-label="Ver ejercicio ${ex.id}">
|
||||
<div class="exercise-card__id">${ex.id}</div>
|
||||
<span class="exercise-card__topic">${topicInfo.icon || ''} ${topicInfo.label}</span>
|
||||
<div class="exercise-card__statement">${ex.statement}</div>
|
||||
<div class="exercise-card__difficulty ${difficultyClass}">${diffLabel[ex.difficulty] || ex.difficulty}</div>
|
||||
</div>`;
|
||||
}).join('')}</div>`;
|
||||
}
|
||||
|
||||
function escapeAttr(str) {
|
||||
return (str || '').replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!container) return;
|
||||
filterExercises();
|
||||
container.innerHTML = `
|
||||
<div class="exercise-browser">
|
||||
<div class="exercise-browser__header">
|
||||
<h2>Ejercicios</h2>
|
||||
<p>Seleccioná un tema para filtrar los ejercicios</p>
|
||||
</div>
|
||||
<input type="text" class="exercise-browser__search" placeholder="Buscar ejercicios..." value="${searchText}">
|
||||
${renderTopicTabs()}
|
||||
${renderExerciseCards()}
|
||||
</div>
|
||||
`;
|
||||
// Skip KaTeX on cards — text+math mixing causes spacing loss in math mode.
|
||||
// Full KaTeX rendering happens in exercise-viewer when user opens the exercise.
|
||||
}
|
||||
|
||||
function renderMathInElements() {
|
||||
// Disabled: KaTeX rendering on cards removed due to text spacing issues.
|
||||
// The statement is shown as plain text; full rendering in exercise-viewer.
|
||||
return;
|
||||
}
|
||||
|
||||
function handleTopicClick(e) {
|
||||
const tab = e.target.closest('.topic-tab');
|
||||
if (!tab) return;
|
||||
activeTopic = tab.dataset.topic;
|
||||
render();
|
||||
}
|
||||
|
||||
function handleCardClick(e) {
|
||||
const card = e.target.closest('.exercise-card');
|
||||
if (!card) return;
|
||||
const id = card.dataset.exerciseId;
|
||||
if (id && typeof App !== 'undefined') {
|
||||
App.navigate(`/exercise/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch(e) {
|
||||
clearTimeout(searchDebounceTimer);
|
||||
searchDebounceTimer = setTimeout(() => {
|
||||
searchText = e.target.value;
|
||||
filterExercises();
|
||||
// Re-render only the grid, not the tabs
|
||||
const grid = container.querySelector('.exercise-grid');
|
||||
const empty = container.querySelector('.exercise-browser__empty');
|
||||
if (grid || empty) {
|
||||
const parent = grid ? grid.parentElement : empty.parentElement;
|
||||
const targetEl = grid || empty;
|
||||
const temp = document.createElement('div');
|
||||
temp.innerHTML = renderExerciseCards();
|
||||
targetEl.replaceWith(temp.firstElementChild);
|
||||
renderMathInElements();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function init(el) {
|
||||
container = el;
|
||||
ac = new AbortController();
|
||||
loadExerciseData().then(() => {
|
||||
render();
|
||||
container.addEventListener('click', handleTopicClick, { signal: ac.signal });
|
||||
container.addEventListener('click', handleCardClick, { signal: ac.signal });
|
||||
container.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
const card = e.target.closest('.exercise-card');
|
||||
if (card) { e.preventDefault(); card.click(); }
|
||||
}
|
||||
}, { signal: ac.signal });
|
||||
container.addEventListener('input', (e) => {
|
||||
if (e.target.classList.contains('exercise-browser__search')) {
|
||||
handleSearch(e);
|
||||
}
|
||||
}, { signal: ac.signal });
|
||||
});
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (ac) {
|
||||
ac.abort();
|
||||
ac = null;
|
||||
}
|
||||
clearTimeout(searchDebounceTimer);
|
||||
if (container) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
container = null;
|
||||
activeTopic = 'all';
|
||||
searchText = '';
|
||||
}
|
||||
|
||||
function getExercises() {
|
||||
return exercises;
|
||||
}
|
||||
|
||||
return { init, destroy, getExercises, loadExerciseData };
|
||||
})();
|
||||
330
js/components/exercise-viewer.js
Normal file
330
js/components/exercise-viewer.js
Normal file
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* 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, '<').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 `
|
||||
<div class="exercise-viewer__problem">
|
||||
<div class="exercise-viewer__problem-header">
|
||||
<span class="exercise-viewer__problem-id">${ex.id}</span>
|
||||
<span class="exercise-viewer__problem-topic">${topicLabels[ex.topic] || ex.topic} — ${diffLabel[ex.difficulty] || ex.difficulty}</span>
|
||||
</div>
|
||||
<div class="exercise-viewer__statement" id="exercise-statement"></div>
|
||||
${ex.hint ? `<button class="exercise-viewer__hint-btn" id="hintBtn">💡 Ver pista</button>
|
||||
<div class="exercise-viewer__hint" id="hintBox">${escapeHtml(ex.hint)}</div>` : ''}
|
||||
${ex.theoryKey ? `<button class="exercise-viewer__theory-btn" id="theoryBtn">📖 Ver teoría</button>
|
||||
<div class="exercise-viewer__theory theory-panel" id="theoryBox"></div>` : ''}
|
||||
</div>
|
||||
|
||||
<div class="exercise-viewer__attempt">
|
||||
<h3>Tu respuesta</h3>
|
||||
<div class="attempt-input">
|
||||
<input type="text" class="attempt-input__field" id="attemptInput" placeholder="Ingresá tu respuesta...">
|
||||
<button class="attempt-input__submit" id="attemptSubmit">Verificar</button>
|
||||
</div>
|
||||
<div class="attempt-feedback" id="attemptFeedback" role="status" aria-live="polite"></div>
|
||||
</div>
|
||||
|
||||
<div class="exercise-viewer__solution">
|
||||
<h3>Solución paso a paso</h3>
|
||||
<ol class="solution-steps" id="solutionSteps"></ol>
|
||||
<button class="reveal-btn" id="revealBtn">Mostrar siguiente paso ▼</button>
|
||||
<div class="solution-answer" id="solutionAnswer">
|
||||
<div class="solution-answer__label">Respuesta final</div>
|
||||
<div id="answerContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 = `<div class="theory-panel"><h4>${title}</h4><div class="theory-content" id="theoryContent"></div></div>`;
|
||||
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 = `
|
||||
<div class="solution-step__desc">${escapeHtml(step.desc)}</div>
|
||||
<div class="solution-step__expression" data-step="${revealedSteps}"></div>
|
||||
`;
|
||||
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 = '<p>Teoría no disponible.</p>';
|
||||
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 = '<p>Ejercicio no especificado.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const ex = await findExercise(params.id);
|
||||
if (!ex) {
|
||||
container.innerHTML = `<p>Ejercicio "${escapeHtml(params.id)}" no encontrado.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
currentExercise = ex;
|
||||
container.innerHTML = `
|
||||
<div class="exercise-viewer">
|
||||
<button class="exercise-viewer__back">← Volver a ejercicios</button>
|
||||
${renderProblem(ex)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
renderStatement(ex);
|
||||
setupEvents();
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (ac) {
|
||||
ac.abort();
|
||||
ac = null;
|
||||
}
|
||||
if (container) container.innerHTML = '';
|
||||
container = null;
|
||||
currentExercise = null;
|
||||
revealedSteps = 0;
|
||||
}
|
||||
|
||||
return { init, destroy };
|
||||
})();
|
||||
286
js/components/matrix-builder.js
Normal file
286
js/components/matrix-builder.js
Normal file
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* MatrixBuilder — Size selector (2x2/3x3/4x4), input grid, operation buttons
|
||||
*/
|
||||
const MatrixBuilder = (() => {
|
||||
let container = null;
|
||||
let currentSize = 3;
|
||||
|
||||
function render() {
|
||||
container.innerHTML = `
|
||||
<div class="matrix-builder">
|
||||
<div class="matrix-builder__controls">
|
||||
<label>Tamaño:</label>
|
||||
<select class="matrix-builder__size-select" id="matrixSize">
|
||||
<option value="2" ${currentSize === 2 ? 'selected' : ''}>2×2</option>
|
||||
<option value="3" ${currentSize === 3 ? 'selected' : ''}>3×3</option>
|
||||
<option value="4" ${currentSize === 4 ? 'selected' : ''}>4×4</option>
|
||||
</select>
|
||||
<button class="matrix-ops__btn" data-action="clear">Limpiar</button>
|
||||
<button class="matrix-ops__btn" data-action="example">Ejemplo</button>
|
||||
</div>
|
||||
|
||||
<div id="matrixGridContainer"></div>
|
||||
|
||||
<div class="matrix-ops">
|
||||
<button class="matrix-ops__btn" data-op="determinant">Determinante</button>
|
||||
<button class="matrix-ops__btn" data-op="transpose">Traspuesta</button>
|
||||
<button class="matrix-ops__btn" data-op="trace">Traza</button>
|
||||
<button class="matrix-ops__btn" data-op="rank">Rango</button>
|
||||
<button class="matrix-ops__btn" data-op="inverse">Inversa</button>
|
||||
<button class="matrix-ops__btn" data-op="det-sarrus">Sarrus (3×3)</button>
|
||||
<button class="matrix-ops__btn" data-op="det-triangular">Triangularización</button>
|
||||
</div>
|
||||
|
||||
<div class="error-message" id="matrixError"></div>
|
||||
<div class="matrix-result" id="matrixResult">
|
||||
<div class="matrix-result__title" id="resultTitle"></div>
|
||||
<div class="matrix-result__value" id="resultValue"></div>
|
||||
<div id="resultSteps"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
renderGrid();
|
||||
}
|
||||
|
||||
function renderGrid() {
|
||||
const gridContainer = document.getElementById('matrixGridContainer');
|
||||
if (!gridContainer) return;
|
||||
let html = `<div class="matrix-grid" style="grid-template-columns: repeat(${currentSize}, 1fr); padding-left: 20px; padding-right: 20px;">`;
|
||||
for (let i = 0; i < currentSize; i++) {
|
||||
for (let j = 0; j < currentSize; j++) {
|
||||
html += `<input type="text" class="matrix-grid__cell" id="cell-${i}-${j}" data-row="${i}" data-col="${j}">`;
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
gridContainer.innerHTML = html;
|
||||
}
|
||||
|
||||
function getMatrix() {
|
||||
const M = [];
|
||||
let hasEmpty = false;
|
||||
for (let i = 0; i < currentSize; i++) {
|
||||
const row = [];
|
||||
for (let j = 0; j < currentSize; j++) {
|
||||
const cell = document.getElementById(`cell-${i}-${j}`);
|
||||
if (!cell) return null;
|
||||
const val = cell.value.trim();
|
||||
if (val === '') {
|
||||
cell.classList.add('matrix-grid__cell--error');
|
||||
hasEmpty = true;
|
||||
} else {
|
||||
cell.classList.remove('matrix-grid__cell--error');
|
||||
}
|
||||
const num = parseFloat(val);
|
||||
if (isNaN(num)) {
|
||||
cell.classList.add('matrix-grid__cell--error');
|
||||
hasEmpty = true;
|
||||
}
|
||||
row.push(num || 0);
|
||||
}
|
||||
M.push(row);
|
||||
}
|
||||
if (hasEmpty) {
|
||||
showError('Completá todas las celdas con valores numéricos.');
|
||||
return null;
|
||||
}
|
||||
hideError();
|
||||
return M;
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
const el = document.getElementById('matrixError');
|
||||
if (el) {
|
||||
el.textContent = msg;
|
||||
el.classList.add('error-message--visible');
|
||||
}
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
const el = document.getElementById('matrixError');
|
||||
if (el) el.classList.remove('error-message--visible');
|
||||
}
|
||||
|
||||
function showResult(title, value, steps) {
|
||||
const resultEl = document.getElementById('matrixResult');
|
||||
const titleEl = document.getElementById('resultTitle');
|
||||
const valueEl = document.getElementById('resultValue');
|
||||
const stepsEl = document.getElementById('resultSteps');
|
||||
|
||||
titleEl.textContent = title;
|
||||
resultEl.classList.add('matrix-result--visible');
|
||||
|
||||
// Render value
|
||||
if (typeof value === 'number') {
|
||||
if (typeof KatexRenderer !== 'undefined') {
|
||||
KatexRenderer.renderDisplay(valueEl, `\\text{${title}} = ${MathEngine.fmtNum(value)}`);
|
||||
} else {
|
||||
valueEl.textContent = `${title} = ${value}`;
|
||||
}
|
||||
} else if (Array.isArray(value) && Array.isArray(value[0])) {
|
||||
if (typeof KatexRenderer !== 'undefined') {
|
||||
KatexRenderer.renderDisplay(valueEl, MathEngine.matToLatex(value));
|
||||
} else {
|
||||
valueEl.textContent = JSON.stringify(value);
|
||||
}
|
||||
} else if (Array.isArray(value)) {
|
||||
if (typeof KatexRenderer !== 'undefined') {
|
||||
KatexRenderer.renderDisplay(valueEl, MathEngine.vecToLatex(value));
|
||||
} else {
|
||||
valueEl.textContent = JSON.stringify(value);
|
||||
}
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
valueEl.textContent = JSON.stringify(value);
|
||||
} else if (typeof KatexRenderer !== 'undefined') {
|
||||
KatexRenderer.renderDisplay(valueEl, String(value));
|
||||
} else {
|
||||
valueEl.textContent = String(value);
|
||||
}
|
||||
|
||||
// Render steps
|
||||
stepsEl.innerHTML = '';
|
||||
if (steps && steps.length > 0) {
|
||||
const ol = document.createElement('ol');
|
||||
ol.className = 'matrix-result__steps';
|
||||
steps.forEach((step, idx) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'matrix-result__step';
|
||||
const descDiv = document.createElement('div');
|
||||
descDiv.textContent = step.desc;
|
||||
const exprDiv = document.createElement('div');
|
||||
if (typeof KatexRenderer !== 'undefined') {
|
||||
KatexRenderer.renderDisplay(exprDiv, step.latex || step.expression || '');
|
||||
} else {
|
||||
exprDiv.textContent = step.latex || step.expression || '';
|
||||
}
|
||||
li.appendChild(descDiv);
|
||||
li.appendChild(exprDiv);
|
||||
ol.appendChild(li);
|
||||
});
|
||||
stepsEl.appendChild(ol);
|
||||
}
|
||||
}
|
||||
|
||||
function executeOperation(op) {
|
||||
const M = getMatrix();
|
||||
if (!M) return;
|
||||
|
||||
let result;
|
||||
try {
|
||||
const ME = MathEngine;
|
||||
switch (op) {
|
||||
case 'determinant':
|
||||
if (currentSize === 2) result = ME.determinant.det2x2(M);
|
||||
else if (currentSize === 3) result = ME.determinant.det3x3Sarrus(M);
|
||||
else result = ME.determinant.det(M);
|
||||
break;
|
||||
case 'det-sarrus':
|
||||
if (currentSize !== 3) { showError('Sarrus solo aplica a matrices 3×3.'); return; }
|
||||
result = ME.determinant.det3x3Sarrus(M);
|
||||
break;
|
||||
case 'det-triangular':
|
||||
result = ME.determinant.detByTriangularization(M);
|
||||
break;
|
||||
case 'transpose':
|
||||
result = ME.matrix.transpose(M);
|
||||
break;
|
||||
case 'trace':
|
||||
result = ME.matrix.trace(M);
|
||||
break;
|
||||
case 'rank':
|
||||
result = ME.system.rank(M);
|
||||
break;
|
||||
case 'inverse':
|
||||
if (currentSize === 2) result = ME.inverse.inverse2x2(M);
|
||||
else result = ME.inverse.inverse(M);
|
||||
break;
|
||||
default:
|
||||
showError('Operación no reconocida.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
showError(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const titles = {
|
||||
'determinant': 'Determinante',
|
||||
'det-sarrus': 'Determinante (Sarrus)',
|
||||
'det-triangular': 'Determinante (Triangularización)',
|
||||
'transpose': 'Traspuesta',
|
||||
'trace': 'Traza',
|
||||
'rank': 'Rango',
|
||||
'inverse': 'Inversa'
|
||||
};
|
||||
showResult(titles[op] || op, result.value, result.steps);
|
||||
} catch (e) {
|
||||
showError('Error: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function clearGrid() {
|
||||
for (let i = 0; i < currentSize; i++)
|
||||
for (let j = 0; j < currentSize; j++) {
|
||||
const cell = document.getElementById(`cell-${i}-${j}`);
|
||||
if (cell) { cell.value = ''; cell.classList.remove('matrix-grid__cell--error'); }
|
||||
}
|
||||
const resultEl = document.getElementById('matrixResult');
|
||||
if (resultEl) resultEl.classList.remove('matrix-result--visible');
|
||||
hideError();
|
||||
}
|
||||
|
||||
function fillExample() {
|
||||
const examples = {
|
||||
2: [[1, 2], [3, 4]],
|
||||
3: [[1, 2, 3], [4, 5, 2], [6, 3, 1]],
|
||||
4: [[1, 2, 0, 1], [3, 0, 1, 2], [1, 1, 2, 0], [2, 3, 1, 1]]
|
||||
};
|
||||
const ex = examples[currentSize];
|
||||
for (let i = 0; i < currentSize; i++)
|
||||
for (let j = 0; j < currentSize; j++) {
|
||||
const cell = document.getElementById(`cell-${i}-${j}`);
|
||||
if (cell) cell.value = ex[i][j];
|
||||
}
|
||||
}
|
||||
|
||||
function handleSizeChange(e) {
|
||||
currentSize = parseInt(e.target.value);
|
||||
renderGrid();
|
||||
const resultEl = document.getElementById('matrixResult');
|
||||
if (resultEl) resultEl.classList.remove('matrix-result--visible');
|
||||
hideError();
|
||||
}
|
||||
|
||||
function handleOpClick(e) {
|
||||
const btn = e.target.closest('[data-op]');
|
||||
if (!btn) return;
|
||||
executeOperation(btn.dataset.op);
|
||||
}
|
||||
|
||||
function handleActionClick(e) {
|
||||
const btn = e.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
const action = btn.dataset.action;
|
||||
if (action === 'clear') clearGrid();
|
||||
else if (action === 'example') fillExample();
|
||||
}
|
||||
|
||||
function init(el) {
|
||||
container = el;
|
||||
render();
|
||||
container.addEventListener('change', (e) => {
|
||||
if (e.target.id === 'matrixSize') handleSizeChange(e);
|
||||
});
|
||||
container.addEventListener('click', handleOpClick);
|
||||
container.addEventListener('click', handleActionClick);
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (container) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
container = null;
|
||||
}
|
||||
|
||||
return { init, destroy, clearGrid, fillExample };
|
||||
})();
|
||||
310
js/components/system-solver.js
Normal file
310
js/components/system-solver.js
Normal file
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* SystemSolver — System input, method selector, step-by-step solution display
|
||||
*/
|
||||
const SystemSolver = (() => {
|
||||
let container = null;
|
||||
let currentSize = 3;
|
||||
let currentMethod = 'gauss';
|
||||
|
||||
function render() {
|
||||
container.innerHTML = `
|
||||
<div class="system-solver">
|
||||
<div class="system-solver__controls">
|
||||
<label>Incógnitas:</label>
|
||||
<select class="system-solver__size-select" id="systemSize">
|
||||
<option value="2" ${currentSize === 2 ? 'selected' : ''}>2</option>
|
||||
<option value="3" ${currentSize === 3 ? 'selected' : ''}>3</option>
|
||||
</select>
|
||||
|
||||
<label>Método:</label>
|
||||
<select class="system-solver__method-select" id="systemMethod">
|
||||
<option value="gauss" ${currentMethod === 'gauss' ? 'selected' : ''}>Gauss</option>
|
||||
<option value="gauss-jordan" ${currentMethod === 'gauss-jordan' ? 'selected' : ''}>Gauss-Jordan</option>
|
||||
<option value="cramer" ${currentMethod === 'cramer' ? 'selected' : ''}>Cramer</option>
|
||||
<option value="rouche-frobenius" ${currentMethod === 'rouche-frobenius' ? 'selected' : ''}>Rouché-Frobenius</option>
|
||||
</select>
|
||||
|
||||
<button class="matrix-ops__btn" data-action="clear">Limpiar</button>
|
||||
<button class="matrix-ops__btn" data-action="example">Ejemplo</button>
|
||||
</div>
|
||||
|
||||
<p class="system-solver__matrix-label">Matriz de coeficientes | Términos independientes</p>
|
||||
<div id="systemGridContainer"></div>
|
||||
|
||||
<button class="system-solver__solve-btn" id="solveBtn">Resolver</button>
|
||||
|
||||
<div class="error-message" id="systemError"></div>
|
||||
<div class="matrix-result" id="systemResult">
|
||||
<div class="matrix-result__title" id="resultTitle"></div>
|
||||
<div class="matrix-result__value" id="resultValue"></div>
|
||||
<div id="resultSteps"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
renderGrid();
|
||||
}
|
||||
|
||||
function renderGrid() {
|
||||
const gridContainer = document.getElementById('systemGridContainer');
|
||||
if (!gridContainer) return;
|
||||
const n = currentSize;
|
||||
// n columns for coefficients + divider + 1 column for constants
|
||||
let html = `<div class="augmented-grid" style="grid-template-columns: repeat(${n}, 1fr) auto 1fr; padding-left: 24px; padding-right: 24px; gap: 4px;">`;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
for (let j = 0; j < n; j++) {
|
||||
html += `<input type="text" class="matrix-grid__cell" id="sys-coeff-${i}-${j}" data-row="${i}" data-col="${j}" placeholder="a${i + 1}${j + 1}">`;
|
||||
}
|
||||
// Divider column (one per row, but we only need 1 actual divider)
|
||||
if (i === 0) {
|
||||
html += `<div class="augmented-grid__divider" style="grid-row: span ${n};"></div>`;
|
||||
} else {
|
||||
// Empty cell for grid alignment — no, the divider spans all rows
|
||||
}
|
||||
html += `<input type="text" class="matrix-grid__cell" id="sys-const-${i}" data-row="${i}" placeholder="b${i + 1}" style="border-color: var(--color-accent);">`;
|
||||
}
|
||||
html += '</div>';
|
||||
gridContainer.innerHTML = html;
|
||||
}
|
||||
|
||||
function getSystem() {
|
||||
const n = currentSize;
|
||||
const A = [];
|
||||
const b = [];
|
||||
let hasError = false;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const row = [];
|
||||
for (let j = 0; j < n; j++) {
|
||||
const cell = document.getElementById(`sys-coeff-${i}-${j}`);
|
||||
if (!cell) return null;
|
||||
const val = cell.value.trim();
|
||||
if (val === '') {
|
||||
cell.classList.add('matrix-grid__cell--error');
|
||||
hasError = true;
|
||||
} else {
|
||||
cell.classList.remove('matrix-grid__cell--error');
|
||||
}
|
||||
const num = parseFloat(val);
|
||||
if (isNaN(num)) {
|
||||
cell.classList.add('matrix-grid__cell--error');
|
||||
hasError = true;
|
||||
}
|
||||
row.push(num || 0);
|
||||
}
|
||||
A.push(row);
|
||||
|
||||
const bCell = document.getElementById(`sys-const-${i}`);
|
||||
if (!bCell) return null;
|
||||
const bVal = bCell.value.trim();
|
||||
if (bVal === '') {
|
||||
bCell.classList.add('matrix-grid__cell--error');
|
||||
hasError = true;
|
||||
} else {
|
||||
bCell.classList.remove('matrix-grid__cell--error');
|
||||
}
|
||||
b.push(parseFloat(bVal) || 0);
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
showError('Completá todas las celdas con valores numéricos.');
|
||||
return null;
|
||||
}
|
||||
hideError();
|
||||
return { A, b };
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
const el = document.getElementById('systemError');
|
||||
if (el) {
|
||||
el.textContent = msg;
|
||||
el.classList.add('error-message--visible');
|
||||
}
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
const el = document.getElementById('systemError');
|
||||
if (el) el.classList.remove('error-message--visible');
|
||||
}
|
||||
|
||||
function showResult(title, value, steps) {
|
||||
const resultEl = document.getElementById('systemResult');
|
||||
const titleEl = document.getElementById('resultTitle');
|
||||
const valueEl = document.getElementById('resultValue');
|
||||
const stepsEl = document.getElementById('resultSteps');
|
||||
|
||||
titleEl.textContent = title;
|
||||
resultEl.classList.add('matrix-result--visible');
|
||||
|
||||
// Render value
|
||||
if (typeof value === 'object' && value !== null && value.type) {
|
||||
// Classification result
|
||||
let text = '';
|
||||
if (value.type === 'CD') text = 'Compatible Determinado';
|
||||
else if (value.type === 'CI') text = `Compatible Indeterminado (${value.freeVars || '?'} parámetros libres)`;
|
||||
else if (value.type === 'SI') text = 'Sistema Incompatible';
|
||||
else if (value.type === 'indeterminate') text = 'Compatible Indeterminado';
|
||||
else if (value.type === 'trivial') text = 'Solución trivial';
|
||||
valueEl.textContent = text;
|
||||
|
||||
if (value.solution) {
|
||||
const solDiv = document.createElement('div');
|
||||
solDiv.style.marginTop = '8px';
|
||||
if (typeof KatexRenderer !== 'undefined') {
|
||||
KatexRenderer.renderDisplay(solDiv, 'x = ' + MathEngine.vecToLatex(value.solution));
|
||||
} else {
|
||||
solDiv.textContent = 'x = ' + JSON.stringify(value.solution);
|
||||
}
|
||||
valueEl.appendChild(solDiv);
|
||||
}
|
||||
} else if (Array.isArray(value)) {
|
||||
if (typeof KatexRenderer !== 'undefined') {
|
||||
KatexRenderer.renderDisplay(valueEl, 'x = ' + MathEngine.vecToLatex(value));
|
||||
} else {
|
||||
valueEl.textContent = 'x = ' + JSON.stringify(value);
|
||||
}
|
||||
} else {
|
||||
valueEl.textContent = String(value);
|
||||
}
|
||||
|
||||
// Render steps
|
||||
stepsEl.innerHTML = '';
|
||||
if (steps && steps.length > 0) {
|
||||
const ol = document.createElement('ol');
|
||||
ol.className = 'matrix-result__steps';
|
||||
steps.forEach((step) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'matrix-result__step';
|
||||
const descDiv = document.createElement('div');
|
||||
descDiv.textContent = step.desc;
|
||||
const exprDiv = document.createElement('div');
|
||||
if (typeof KatexRenderer !== 'undefined' && (step.latex || step.expression)) {
|
||||
KatexRenderer.renderDisplay(exprDiv, step.latex || step.expression);
|
||||
} else {
|
||||
exprDiv.textContent = step.latex || step.expression || '';
|
||||
}
|
||||
li.appendChild(descDiv);
|
||||
li.appendChild(exprDiv);
|
||||
ol.appendChild(li);
|
||||
});
|
||||
stepsEl.appendChild(ol);
|
||||
}
|
||||
}
|
||||
|
||||
function solve() {
|
||||
const system = getSystem();
|
||||
if (!system) return;
|
||||
|
||||
const { A, b } = system;
|
||||
let result;
|
||||
|
||||
try {
|
||||
const ME = MathEngine;
|
||||
switch (currentMethod) {
|
||||
case 'gauss':
|
||||
result = ME.system.gaussElimination(A, b);
|
||||
break;
|
||||
case 'gauss-jordan':
|
||||
result = ME.system.gaussJordan(A, b);
|
||||
break;
|
||||
case 'cramer':
|
||||
result = ME.system.cramer(A, b);
|
||||
break;
|
||||
case 'rouche-frobenius':
|
||||
result = ME.system.roucheFrobenius(A, b);
|
||||
break;
|
||||
default:
|
||||
showError('Método no reconocido.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
showError(result.error);
|
||||
// Still show steps
|
||||
if (result.steps && result.steps.length > 0) {
|
||||
showResult('Clasificación', null, result.steps);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const titles = {
|
||||
'gauss': 'Eliminación de Gauss',
|
||||
'gauss-jordan': 'Gauss-Jordan',
|
||||
'cramer': 'Regla de Cramer',
|
||||
'rouche-frobenius': 'Rouché-Frobenius'
|
||||
};
|
||||
showResult(titles[currentMethod] || currentMethod, result.value, result.steps);
|
||||
} catch (e) {
|
||||
showError('Error: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function clearGrid() {
|
||||
const n = currentSize;
|
||||
for (let i = 0; i < n; i++) {
|
||||
for (let j = 0; j < n; j++) {
|
||||
const cell = document.getElementById(`sys-coeff-${i}-${j}`);
|
||||
if (cell) { cell.value = ''; cell.classList.remove('matrix-grid__cell--error'); }
|
||||
}
|
||||
const bCell = document.getElementById(`sys-const-${i}`);
|
||||
if (bCell) { bCell.value = ''; bCell.classList.remove('matrix-grid__cell--error'); }
|
||||
}
|
||||
const resultEl = document.getElementById('systemResult');
|
||||
if (resultEl) resultEl.classList.remove('matrix-result--visible');
|
||||
hideError();
|
||||
}
|
||||
|
||||
function fillExample() {
|
||||
const examples = {
|
||||
2: { A: [[2, 1], [1, 1]], b: [7, 4] },
|
||||
3: { A: [[2, 1, -1], [-3, -1, 2], [-2, 1, 2]], b: [8, -11, -3] }
|
||||
};
|
||||
const ex = examples[currentSize];
|
||||
const n = currentSize;
|
||||
for (let i = 0; i < n; i++) {
|
||||
for (let j = 0; j < n; j++) {
|
||||
const cell = document.getElementById(`sys-coeff-${i}-${j}`);
|
||||
if (cell) cell.value = ex.A[i][j];
|
||||
}
|
||||
const bCell = document.getElementById(`sys-const-${i}`);
|
||||
if (bCell) bCell.value = ex.b[i];
|
||||
}
|
||||
}
|
||||
|
||||
function init(el) {
|
||||
container = el;
|
||||
render();
|
||||
|
||||
container.addEventListener('change', (e) => {
|
||||
if (e.target.id === 'systemSize') {
|
||||
currentSize = parseInt(e.target.value);
|
||||
renderGrid();
|
||||
const resultEl = document.getElementById('systemResult');
|
||||
if (resultEl) resultEl.classList.remove('matrix-result--visible');
|
||||
hideError();
|
||||
}
|
||||
if (e.target.id === 'systemMethod') {
|
||||
currentMethod = e.target.value;
|
||||
}
|
||||
});
|
||||
|
||||
container.addEventListener('click', (e) => {
|
||||
if (e.target.id === 'solveBtn') solve();
|
||||
});
|
||||
|
||||
container.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
const action = btn.dataset.action;
|
||||
if (action === 'clear') clearGrid();
|
||||
else if (action === 'example') fillExample();
|
||||
});
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (container) container.innerHTML = '';
|
||||
container = null;
|
||||
}
|
||||
|
||||
return { init, destroy, clearGrid, fillExample };
|
||||
})();
|
||||
Reference in New Issue
Block a user