Compare commits

..

No commits in common. "fd1ff0a421caf2aaa5aef3b73a291adf4688c5b3" and "01651fc4548b013db30bdbc26b0e1a8cea8ff22c" have entirely different histories.

17 changed files with 271 additions and 2190 deletions

View File

@ -1,6 +0,0 @@
---
alwaysApply: true
---
Use MemoriaDeEvolucion.md in the .doc folder to access the latest modifications and concepts for this project. For each modification, add the important knowledge and decisions to the file, keeping the style simple with plain text, minimal code, and summarized content. Also, include a summarized version of the user's prompt each time to maintain context of the request.
The aplication comments and variables must be in english.

View File

@ -1,49 +1,14 @@
# Italiano
total_italiano = 72
hechas_italiano = 29
faltan_italiano = total_italiano - hechas_italiano
# Matematica
total_mate = 40
hechas_mate = 8
faltan_mate = total_mate - hechas_mate
salario=horas*tarifa
salario=800
tarifa=36
horas=?
# Matematica Libro
total__mate2 = 26
hechas_mate2 = 12
faltan_mate2 = total__mate2 - hechas_mate2
# Ingles
total__english = 48
hechas_english = 35
faltan_english = total__english - hechas_english
# Detective ITA
total__detective_ita = 52
hechas_detective_ita = 52
faltan_detective_ita = total__detective_ita - hechas_detective_ita
# Estado actual de dias
dias_pasados = Today()-Date('06/06/25')
dias_faltan = Date('14/9/2025')-Today()
dias_totales = Date('14/9/2025')-Date('06/06/25')
# Resumen
total = total_italiano+total_mate+total__mate2+total__english + total__detective_ita
hecho = hechas_italiano + hechas_mate + hechas_mate2 + hechas_english + hechas_detective_ita
faltan = total - hecho
porcentaje_hecho = (hecho/total)*100
# Porcentaje de Vaciones ya pasada
100*dias_pasados/dias_totales
# Promedio actual
hecho/dias_pasados
# Minimo para terminar
hacer_por_dia = faltan/dias_faltan

2
.gitignore vendored
View File

@ -8,8 +8,6 @@ __pycache__/
*.json
.mathjax/
# Distribution / packaging
.Python
build/

View File

@ -407,13 +407,6 @@ class PureAlgebraicEngine:
eval_context = self._get_complete_context()
result_obj = sympify(expression_str, locals=eval_context)
# NUEVA FUNCIONALIDAD: Aplicar simplificación simbólica automática en asignaciones
if hasattr(result_obj, 'free_symbols') and result_obj.free_symbols and self.equations:
simplified_obj = self._apply_symbolic_simplification(result_obj)
if simplified_obj != result_obj:
result_obj = simplified_obj
self.logger.debug(f"Simplificación simbólica aplicada en asignación: {expression_str}{result_obj}")
# 1. ASIGNACIÓN AL CONTEXTO (para evaluación directa)
self.symbol_table[var_name] = result_obj
self.variables.add(var_name)
@ -516,13 +509,6 @@ class PureAlgebraicEngine:
eval_context = self._get_complete_context()
result_obj = sympify(line, locals=eval_context)
# NUEVA FUNCIONALIDAD: Aplicar simplificación simbólica automática
if hasattr(result_obj, 'free_symbols') and result_obj.free_symbols and self.equations:
simplified_obj = self._apply_symbolic_simplification(result_obj)
if simplified_obj != result_obj:
result_obj = simplified_obj
self.logger.debug(f"Simplificación simbólica aplicada: {line}{result_obj}")
# Si es un PlotResult, asegurar que tenga la línea original
if isinstance(result_obj, PlotResult) and not result_obj.original_expression:
result_obj.original_expression = line
@ -614,33 +600,15 @@ class PureAlgebraicEngine:
# Verificar que el resultado no sea problemático
if final_value != var_symbol and not isinstance(final_value, (sp.logic.boolalg.BooleanTrue, sp.logic.boolalg.BooleanFalse)):
# NUEVA FUNCIONALIDAD: Aplicar simplificación simbólica al resultado final
if hasattr(final_value, 'free_symbols') and final_value.free_symbols and self.equations:
simplified_final = self._apply_symbolic_simplification(final_value)
if simplified_final != final_value:
self.logger.debug(f"Simplificación aplicada en resultado final: {final_value}{simplified_final}")
final_value = simplified_final
return Eq(var_symbol, final_value)
else:
# Si hay problema con la resolución, devolver simbólico
# Aplicar simplificación también al valor simbólico
if hasattr(solution_value, 'free_symbols') and solution_value.free_symbols and self.equations:
simplified_solution = self._apply_symbolic_simplification(solution_value)
if simplified_solution != solution_value:
self.logger.debug(f"Simplificación aplicada en solución simbólica: {solution_value}{simplified_solution}")
solution_value = simplified_solution
return Eq(var_symbol, solution_value)
else:
# Hay variables con valores simbólicos, devolver ecuación simbólica
return Eq(var_symbol, solution_value)
else:
# No hay variables sin resolver, es ya un valor final
# NUEVA FUNCIONALIDAD: Aplicar simplificación simbólica al valor final
if hasattr(solution_value, 'free_symbols') and solution_value.free_symbols and self.equations:
simplified_solution = self._apply_symbolic_simplification(solution_value)
if simplified_solution != solution_value:
self.logger.debug(f"Simplificación aplicada en valor final: {solution_value}{simplified_solution}")
solution_value = simplified_solution
return Eq(var_symbol, solution_value)
else:
# Si no hay solución en las ecuaciones, verificar en symbol_table
@ -649,12 +617,6 @@ class PureAlgebraicEngine:
value = self.symbol_table[var_name]
# Si el valor en symbol_table es diferente de la variable, devolverlo
if value != var_symbol:
# NUEVA FUNCIONALIDAD: Aplicar simplificación simbólica también aquí
if hasattr(value, 'free_symbols') and value.free_symbols and self.equations:
simplified_value = self._apply_symbolic_simplification(value)
if simplified_value != value:
self.logger.debug(f"Simplificación aplicada en solve: {value}{simplified_value}")
value = simplified_value
return Eq(var_symbol, value)
# Si no hay información, devolver ecuación con la variable sin resolver
@ -753,85 +715,6 @@ class PureAlgebraicEngine:
self.logger.debug(f"Error resolviendo {var_symbol}: {e}")
return var_symbol
def _apply_symbolic_simplification(self, expression):
"""
Aplica simplificación simbólica automática usando ecuaciones definidas
Esta función sustituye símbolos por sus definiciones de ecuaciones cuando es posible,
permitiendo simplificaciones como: mS = mmS/1000, entonces 50000*Hz*mS/mmS 50*Hz
"""
try:
current_expr = expression
max_iterations = 5 # Evitar bucles infinitos
for iteration in range(max_iterations):
substituted = current_expr
substitution_made = False
# Obtener símbolos libres de la expresión actual
free_symbols = substituted.free_symbols
# Para cada símbolo en la expresión, buscar si hay una ecuación que lo defina
for symbol in free_symbols:
symbol_definition = self._find_symbol_definition(symbol)
if symbol_definition is not None and symbol_definition != symbol:
# Aplicar la sustitución
new_expr = substituted.subs(symbol, symbol_definition)
# Simplificar después de la sustitución
simplified = sp.simplify(new_expr)
if simplified != substituted:
substituted = simplified
substitution_made = True
self.logger.debug(f"Símbolo {symbol} sustituido por {symbol_definition}")
# Si no se hicieron sustituciones, terminar
if not substitution_made:
break
current_expr = substituted
return current_expr
except Exception as e:
self.logger.debug(f"Error en simplificación simbólica: {e}")
return expression
def _find_symbol_definition(self, symbol):
"""
Busca la definición de un símbolo en las ecuaciones del sistema
Returns:
La expresión que define al símbolo, o None si no se encuentra
"""
try:
# Buscar en ecuaciones donde el símbolo aparece solo en el lado izquierdo
for eq in self.equations:
if hasattr(eq, 'lhs') and hasattr(eq, 'rhs'):
# Caso 1: símbolo = expresión
if eq.lhs == symbol:
return eq.rhs
# Caso 2: expresión = símbolo
elif eq.rhs == symbol:
return eq.lhs
# Buscar en symbol_table como respaldo
symbol_name = str(symbol)
if symbol_name in self.symbol_table:
value = self.symbol_table[symbol_name]
# Solo usar si el valor es diferente del símbolo mismo
if value != symbol:
return value
return None
except Exception as e:
self.logger.debug(f"Error buscando definición de {symbol}: {e}")
return None
def _resolve_iteratively(self, expression, max_iterations=10):
"""Resuelve una expresión iterativamente sustituyendo valores conocidos"""
try:

View File

@ -40,7 +40,7 @@ class EvaluationManager:
'custom_type': self._create_format("#4ec9b0"),
'plot': self._create_format("#569cd6", underline=True),
'type_indicator': self._create_format("#808080"),
'clickable': self._create_format("#4fc3f7", underline=True, bold=True),
'clickable': self._create_format("#4fc3f7", underline=True),
'helper': self._create_format("#ffd700", italic=True)
}
@ -254,7 +254,6 @@ class EvaluationManager:
cursor.insertText(display_text, self.output_formats.get('clickable'))
end_pos = cursor.position()
self.main_window.output_text.clickable_links[(start_pos, end_pos)] = (link_id, result_object)
self.logger.debug(f"🔗 Link creado: {display_text} en posición {start_pos}-{end_pos}, ID: {link_id}")
elif len(part_data) >= 2:
tag, content = part_data[0], part_data[1]
@ -333,10 +332,3 @@ class EvaluationManager:
"""Programa una evaluación con debounce"""
self._debounce_timer.stop()
self._debounce_timer.start(300) # 300ms de debounce
def handle_output_link_click(self, link_id: str, result_object):
"""Maneja clicks en links del output delegando al interactive manager"""
if self.main_window.interactive_manager:
self.main_window.interactive_manager.handle_interactive_click(result_object)
else:
self.logger.warning("Interactive manager no disponible para manejar click")

View File

@ -10,7 +10,6 @@ from PySide6.QtWebEngineWidgets import QWebEngineView
import logging
from typing import List, Dict, Optional
from .evaluation import EvaluationResult
from .mathjax_manager import MathJaxManager
import sympy
@ -25,12 +24,6 @@ class LatexPanel(QWidget):
self._pending_equations = []
self._parent_calculator = parent
self._webview_initialized = False
self._mathjax_timeout_count = 0
self._max_mathjax_timeout = 20 # 10 segundos (20 * 500ms)
# Manager de MathJax local
self.mathjax_manager = MathJaxManager()
self._setup_ui()
# Timer para verificar si MathJax está listo (inicializado bajo demanda)
@ -114,131 +107,93 @@ class LatexPanel(QWidget):
""")
def _generate_mathjax_html(self):
"""Genera HTML base con MathJax configurado para SVG (local o CDN)"""
# Obtener URL de MathJax (local si está disponible, CDN como fallback)
mathjax_url = self.mathjax_manager.get_mathjax_url()
return f"""<!DOCTYPE html>
"""Genera HTML base con MathJax configurado para SVG"""
return """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script>
<script>
// Intentar cargar MathJax local primero, luego CDN como fallback
window.mathjaxLoadAttempts = 0;
window.mathJaxReady = false;
function loadMathJax(url, isFallback = false) {{
return new Promise((resolve, reject) => {{
const script = document.createElement('script');
script.id = 'MathJax-script';
script.async = true;
script.src = url;
script.onload = () => {{
console.log('MathJax cargado desde:', url);
resolve();
}};
script.onerror = () => {{
console.error('Error cargando MathJax desde:', url);
reject(new Error('MathJax load failed'));
}};
document.head.appendChild(script);
}});
}}
// Configuración de MathJax
window.MathJax = {{
tex: {{
inlineMath: [['$', '$'], ['\\\\(', '\\\\)']],
displayMath: [['$$', '$$'], ['\\\\[', '\\\\]']],
window.MathJax = {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']],
displayMath: [['$$', '$$'], ['\\[', '\\]']],
processEscapes: true
}},
svg: {{
},
svg: {
scale: 0.9,
minScale: 0.5,
fontCache: 'global'
}},
startup: {{
ready: function () {{
},
startup: {
ready: function () {
MathJax.startup.defaultReady();
// Notificar que MathJax está listo
window.mathJaxReady = true;
console.log('MathJax SVG completamente listo');
}}
}}
}};
// Intentar cargar MathJax local, luego CDN
loadMathJax('{mathjax_url}').catch(() => {{
console.log('Fallback a CDN de MathJax...');
return loadMathJax('https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-svg.js', true);
}}).catch(() => {{
console.error('No se pudo cargar MathJax desde ninguna fuente');
// Marcar como listo para mostrar contenido sin renderizado
setTimeout(() => {{
window.mathJaxReady = true;
console.log('Modo fallback activado - sin renderizado LaTeX');
}}, 2000);
}});
console.log('MathJax SVG completamente cargado');
}
}
};
</script>
<style>
body {{
body {
background-color: #1a1a1a;
color: #d4d4d4;
font-family: 'Segoe UI', Arial, sans-serif;
margin: 0;
padding: 8px;
line-height: 1.2;
}}
.equation-block {{
}
.equation-block {
background: rgba(45, 45, 48, 0.8);
border-left: 3px solid;
margin: 4px 0;
padding: 6px 12px;
border-radius: 4px;
transition: all 0.1s ease;
}}
.equation-block:hover {{
}
.equation-block:hover {
background: rgba(45, 45, 48, 0.9);
}}
.comment {{ border-left-color: #6a9955; }}
.assignment {{ border-left-color: #dcdcaa; }}
.equation {{ border-left-color: #c586c0; }}
.symbolic {{ border-left-color: #9cdcfe; }}
}
.comment { border-left-color: #6a9955; }
.assignment { border-left-color: #dcdcaa; }
.equation { border-left-color: #c586c0; }
.symbolic { border-left-color: #9cdcfe; }
.math-content {{
.math-content {
margin: 2px 0;
font-size: 14px;
}}
}
.comment-text {{
.comment-text {
font-style: italic;
color: #6a9955;
font-size: 12px;
margin: 0;
}}
}
.info-message {{
.info-message {
text-align: center;
padding: 20px;
color: #666;
font-size: 12px;
}}
}
/* Optimizaciones para SVG rendering */
mjx-container[jax="SVG"] {{
mjx-container[jax="SVG"] {
margin: 2px 0 !important;
}}
}
mjx-container[jax="SVG"] svg {{
mjx-container[jax="SVG"] svg {
vertical-align: middle;
}}
}
mjx-container[jax="SVG"] svg text {{
/* Mejorar contraste del SVG */
mjx-container[jax="SVG"] svg text {
fill: #d4d4d4 !important;
}}
}
</style>
</head>
<body>
@ -249,13 +204,10 @@ class LatexPanel(QWidget):
</div>
<script>
function addEquation(type, content) {{
try {{
window.mathJaxReady = false;
function addEquation(type, content) {
var container = document.getElementById('equations-container');
if (!container) {{
console.error('Container no encontrado');
return;
}}
// Limpiar mensaje inicial si existe
var infoMsg = container.querySelector('.info-message');
@ -267,72 +219,38 @@ class LatexPanel(QWidget):
var mathContent = document.createElement('div');
mathContent.className = 'math-content';
if (type === 'comment') {{
if (type === 'comment') {
mathContent.className = 'comment-text';
mathContent.textContent = content;
console.log('Comentario agregado:', content);
}} else {{
} else {
mathContent.innerHTML = '$$' + content + '$$';
console.log('Ecuación agregada:', content);
}}
}
equation.appendChild(mathContent);
container.appendChild(equation);
// Re-renderizar MathJax solo si está listo
if (window.MathJax && window.mathJaxReady && type !== 'comment') {{
console.log('Renderizando con MathJax...');
MathJax.typesetPromise([equation]).then(() => {{
console.log('Ecuación renderizada exitosamente');
}}).catch(function(err) {{
console.error('Error renderizando con MathJax:', err);
// Mostrar como texto plano si falla
mathContent.innerHTML = content;
}});
}} else if (type !== 'comment') {{
console.log('MathJax no listo, mostrando como texto:', window.mathJaxReady);
// Mostrar como texto plano si MathJax no está listo
mathContent.innerHTML = content;
}}
if (window.MathJax && window.mathJaxReady && type !== 'comment') {
MathJax.typesetPromise([equation]).catch(function(err) {
console.error('MathJax SVG error:', err);
});
}
}
}} catch (e) {{
console.error('Error en addEquation:', e);
}}
}}
function clearEquations() {{
try {{
function clearEquations() {
var container = document.getElementById('equations-container');
if (container) {{
container.innerHTML = '<div class="info-message">Panel de Ecuaciones LaTeX (SVG)</div>';
console.log('Ecuaciones limpiadas');
}}
}} catch (e) {{
console.error('Error en clearEquations:', e);
}}
}}
}
// Función para notificar que está listo para renderizar
function triggerInitialRender() {{
try {{
console.log('Verificando estado MathJax:', window.mathJaxReady);
if (window.mathJaxReady) {{
function triggerInitialRender() {
if (window.mathJaxReady) {
// Notificar a Python que MathJax está listo
console.log('MathJax SVG listo para renderizado inicial');
return true;
}}
}
return false;
}} catch (e) {{
console.error('Error en triggerInitialRender:', e);
return false;
}}
}}
// Monitor de estado para debug
setInterval(() => {{
if (window.mathJaxReady) {{
console.log('🎯 MathJax listo detectado');
}}
}}, 3000);
}
</script>
</body>
</html>"""
@ -363,67 +281,27 @@ class LatexPanel(QWidget):
if not self._webview_available:
return
# Verificar múltiples condiciones de MathJax
js_check = """
(function() {
try {
// Verificar si window.mathJaxReady está definido
if (window.mathJaxReady === true) return true;
// Verificar si MathJax está disponible y listo
if (window.MathJax && window.MathJax.startup && window.MathJax.startup.document && window.MathJax.startup.document.state() >= 8) {
window.mathJaxReady = true;
return true;
}
// Verificar si existen las funciones necesarias
if (typeof addEquation === 'function' && typeof clearEquations === 'function') {
window.mathJaxReady = true;
return true;
}
return false;
} catch(e) {
console.error('Error verificando MathJax:', e);
return false;
}
})();
"""
self.webview.page().runJavaScript(js_check, self._on_mathjax_ready_check)
# Verificar si MathJax está listo
self.webview.page().runJavaScript(
"window.mathJaxReady || false;",
self._on_mathjax_ready_check
)
def _on_mathjax_ready_check(self, ready):
"""Callback cuando se verifica el estado de MathJax"""
logging.debug(f"🔍 MathJax ready check: {ready}")
if ready and not self._mathjax_ready:
self._mathjax_ready = True
self._mathjax_check_timer.stop()
logging.debug(f"✅ MathJax listo, procesando {len(self._pending_equations)} ecuaciones pendientes")
logging.debug("✅ MathJax listo, procesando ecuaciones pendientes")
# Renderizar ecuaciones pendientes
for eq in self._pending_equations:
self._add_equation_to_webview(eq['type'], eq['content'])
logging.debug(f"📊 Ecuación agregada al WebView: {eq['type']}")
self._pending_equations.clear()
# Trigger initial render si hay un calculador padre
if self._parent_calculator and hasattr(self._parent_calculator, '_trigger_initial_latex_render'):
self._parent_calculator._trigger_initial_latex_render()
elif not ready:
self._mathjax_timeout_count += 1
if self._mathjax_timeout_count >= self._max_mathjax_timeout:
# Timeout alcanzado, forzar como listo para mostrar contenido en texto plano
logging.warning("⚠️ MathJax timeout - cambiando a modo fallback")
self._mathjax_ready = True
self._mathjax_check_timer.stop()
# Procesar ecuaciones pendientes como texto plano
for eq in self._pending_equations:
self._add_equation_as_text(eq['type'], eq['content'])
self._pending_equations.clear()
else:
logging.debug(f"⏳ MathJax aún no está listo ({self._mathjax_timeout_count}/{self._max_mathjax_timeout}), continuando verificación...")
def _add_equation_to_webview(self, eq_type: str, content: str):
"""Añade una ecuación directamente al webview"""
@ -432,22 +310,6 @@ class LatexPanel(QWidget):
js_code = f"addEquation('{eq_type}', '{escaped_content}');"
self.webview.page().runJavaScript(js_code)
def _add_equation_as_text(self, eq_type: str, content: str):
"""Añade una ecuación como texto plano cuando MathJax falla"""
if self._webview_available:
escaped_content = content.replace('\\', '\\\\').replace("'", "\\'").replace('"', '\\"')
# Agregar ecuación como texto plano sin MathJax
js_code = f"""
var container = document.getElementById('equations-container');
if (container) {{
var equation = document.createElement('div');
equation.className = 'equation-block {eq_type}';
equation.innerHTML = '<div class="math-content" style="font-family: monospace;">{escaped_content}</div>';
container.appendChild(equation);
}}
"""
self.webview.page().runJavaScript(js_code)
def add_equation(self, eq_type: str, content: str):
"""Añade una ecuación al panel"""
self.equations.append({'type': eq_type, 'content': content})

View File

@ -124,40 +124,26 @@ class HybridCalculatorPySide6(QMainWindow):
# Sincronizar scroll
self._setup_scroll_sync()
# Crear splitter secundario para LaTeX
self.secondary_splitter = QSplitter(Qt.Horizontal)
self.secondary_splitter.addWidget(self.main_splitter)
# Añadir splitter al layout
main_layout.addWidget(self.main_splitter)
# Botón expandible para LaTeX
self.latex_button = ExpandableLatexButton()
self.latex_button.clicked.connect(self._toggle_latex_panel)
main_layout.addWidget(self.latex_button)
# Panel LaTeX con splitter
# Panel LaTeX (inicialmente oculto)
self.latex_panel = LatexPanel(self)
self.latex_panel.setMinimumWidth(200)
self.latex_panel.setMinimumWidth(300)
self.latex_panel.setMaximumWidth(500)
if self.latex_panel_visible:
self.secondary_splitter.addWidget(self.latex_panel)
main_layout.addWidget(self.latex_panel)
self.latex_button.setChecked(True)
# Configurar tamaños: 70% para main_splitter, 30% para latex_panel
latex_width = self.settings.get("latex_panel_width", 300)
total_width = 1000 # Ancho por defecto de la ventana
main_width = total_width - latex_width
self.secondary_splitter.setSizes([main_width, latex_width])
else:
# Asegurar que el panel está oculto por defecto
self.latex_panel.hide()
self.latex_button.setChecked(False)
self.secondary_splitter.setSizes([1000])
# Layout principal con splitter secundario y botón
splitter_layout = QHBoxLayout()
splitter_layout.setContentsMargins(0, 0, 0, 0)
splitter_layout.setSpacing(0)
splitter_layout.addWidget(self.secondary_splitter)
splitter_layout.addWidget(self.latex_button)
main_layout.addLayout(splitter_layout)
# Los formatos de salida se configuran automáticamente en EvaluationManager
@ -169,9 +155,6 @@ class HybridCalculatorPySide6(QMainWindow):
# Aplicar tema oscuro
self._apply_dark_theme()
# Aplicar tamaño de fuente guardado
self._apply_saved_font_size()
def _setup_scroll_sync(self):
"""Sincroniza el scroll entre entrada y salida"""
def sync_input_to_output():
@ -259,22 +242,6 @@ class HybridCalculatorPySide6(QMainWindow):
"""
self.setStyleSheet(dark_style)
def _apply_saved_font_size(self):
"""Aplica el tamaño de fuente guardado"""
is_large = self.settings_manager.get_setting("font_size_large", False)
font_size = 14 if is_large else 11
# Aplicar al estilo actual
current_style = self.styleSheet()
import re
# Reemplazar font-size en QPlainTextEdit y QTextEdit
pattern = r'(QPlainTextEdit, QTextEdit\s*\{[^}]*?)font-size:\s*\d+px;([^}]*\})'
replacement = rf'\1font-size: {font_size}px;\2'
new_style = re.sub(pattern, replacement, current_style)
self.setStyleSheet(new_style)
def _on_input_changed(self):
"""Maneja cambios en el texto de entrada"""
# Delegar al autocomplete manager
@ -293,45 +260,26 @@ class HybridCalculatorPySide6(QMainWindow):
def _handle_output_link_click(self, link_id: str, result_object):
"""Maneja clicks en links del output"""
self.logger.debug(f"🖱️ Click en link recibido: {link_id}, objeto: {type(result_object).__name__}")
# Delegar al evaluation manager
self.evaluation_manager.handle_output_link_click(link_id, result_object)
def _toggle_latex_panel(self):
"""Togglea la visibilidad del panel LaTeX"""
main_layout = self.centralWidget().layout()
if self.latex_panel_visible:
# Ocultar panel
self.secondary_splitter.widget(1).hide() if self.secondary_splitter.count() > 1 else None
main_layout.removeWidget(self.latex_panel)
self.latex_panel.hide()
self.latex_button.setChecked(False)
self.latex_panel_visible = False
self.secondary_splitter.setSizes([1000])
else:
# Mostrar panel
if self.secondary_splitter.count() == 1:
self.secondary_splitter.addWidget(self.latex_panel)
main_layout.addWidget(self.latex_panel)
self.latex_panel.show()
self.latex_button.setChecked(True)
self.latex_panel_visible = True
# Configurar tamaños con límites
latex_width = self.settings.get("latex_panel_width", 300)
total_width = self.width()
# CORRECCIÓN: Aplicar límites para evitar crecimiento descontrolado
min_latex_width = 200
max_latex_width = min(600, total_width * 0.5) # Máximo 50% del ancho total
latex_width = max(min_latex_width, min(latex_width, max_latex_width))
main_width = total_width - latex_width - 50 # 50 para el botón
# Asegurar que main_width no sea negativo
if main_width < 200:
main_width = 200
latex_width = total_width - main_width - 50
self.secondary_splitter.setSizes([main_width, latex_width])
# Actualizar panel con ecuaciones pendientes
self._sync_latex_equations_on_show()
@ -357,19 +305,6 @@ class HybridCalculatorPySide6(QMainWindow):
def closeEvent(self, event):
"""Maneja el cierre de la aplicación"""
# Guardar ancho del panel LaTeX si está visible con límites de validación
if self.latex_panel_visible and self.secondary_splitter.count() > 1:
sizes = self.secondary_splitter.sizes()
if len(sizes) > 1:
latex_width = sizes[1]
# CORRECCIÓN: Validar y limitar el ancho antes de guardarlo
min_latex_width = 200
max_latex_width = 600
latex_width = max(min_latex_width, min(latex_width, max_latex_width))
self.settings_manager.set_setting("latex_panel_width", latex_width)
# Delegar al settings manager
self.settings_manager.save_all()
event.accept()

View File

@ -1,11 +1,12 @@
"""
Sistema de Menús y Diálogos para la Calculadora MAV CAS Híbrida
"""
import os
import time
from pathlib import Path
from PySide6.QtWidgets import QMenuBar, QMenu, QMessageBox, QFileDialog
from PySide6.QtWidgets import (
QMenuBar, QMenu, QMessageBox, QFileDialog
)
from PySide6.QtGui import QAction, QKeySequence
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication
@ -42,10 +43,6 @@ class MenuManager:
save_action.triggered.connect(self.save_file)
file_menu.addAction(save_action)
# Menú de archivos recientes
self.recent_files_menu = file_menu.addMenu("Archivos recientes")
self._update_recent_files_menu()
file_menu.addSeparator()
exit_action = QAction("Salir", self.main_window)
@ -79,33 +76,6 @@ class MenuManager:
view_menu.addSeparator()
# Submenu de fuentes
font_menu = view_menu.addMenu("Tamaño de fuente")
font_small_action = QAction("Pequeña (11px)", self.main_window)
font_small_action.setCheckable(True)
font_small_action.triggered.connect(lambda: self.set_font_size(False))
font_menu.addAction(font_small_action)
font_large_action = QAction("Grande (14px)", self.main_window)
font_large_action.setCheckable(True)
font_large_action.triggered.connect(lambda: self.set_font_size(True))
font_menu.addAction(font_large_action)
# Marcar el tamaño actual
is_large = self.main_window.settings_manager.get_setting(
"font_size_large", False
)
if is_large:
font_large_action.setChecked(True)
else:
font_small_action.setChecked(True)
self.font_small_action = font_small_action
self.font_large_action = font_large_action
view_menu.addSeparator()
system_info_action = QAction("Información del sistema", self.main_window)
system_info_action.triggered.connect(self.show_types_info)
view_menu.addAction(system_info_action)
@ -130,10 +100,6 @@ class MenuManager:
latex_status_action.triggered.connect(self._show_latex_panel_status)
diag_menu.addAction(latex_status_action)
save_html_action = QAction("💾 Guardar HTML del Panel LaTeX", self.main_window)
save_html_action.triggered.connect(self._save_latex_html)
diag_menu.addAction(save_html_action)
diag_menu.addSeparator()
copy_debug_action = QAction("📋 Copiar Debug al Portapapeles", self.main_window)
@ -182,50 +148,34 @@ class MenuManager:
self.clear_input()
self.clear_output()
self.main_window.latex_panel.clear_equations()
if hasattr(self.main_window, "_latex_equations"):
if hasattr(self.main_window, '_latex_equations'):
self.main_window._latex_equations.clear()
self.main_window._update_status("✨ Nueva sesión iniciada")
def load_file(self):
"""Carga archivo en el editor"""
filepath, _ = QFileDialog.getOpenFileName(
self.main_window,
"Cargar archivo",
"",
"Archivos de texto (*.txt);;Archivos Python (*.py);;Todos los archivos (*.*)",
self.main_window, "Cargar archivo", "",
"Archivos de texto (*.txt);;Archivos Python (*.py);;Todos los archivos (*.*)"
)
if filepath:
self._load_file_content(filepath)
def _load_file_content(self, filepath: str):
"""Carga el contenido de un archivo en el editor."""
try:
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
self.main_window.input_text.setPlainText(content)
self.main_window._evaluate_and_update()
self.main_window._update_status(
f"📁 Archivo cargado: {Path(filepath).name}"
)
# Añadir a recientes y actualizar menú
self.main_window.settings_manager.add_recent_file(filepath)
self._update_recent_files_menu()
self.main_window._update_status(f"📁 Archivo cargado: {Path(filepath).name}")
except Exception as e:
QMessageBox.critical(
self.main_window, "Error", f"No se pudo cargar el archivo:\n{e}"
)
QMessageBox.critical(self.main_window, "Error", f"No se pudo cargar el archivo:\n{e}")
def save_file(self):
"""Guarda contenido del editor"""
filepath, _ = QFileDialog.getSaveFileName(
self.main_window,
"Guardar archivo",
"",
"Archivos de texto (*.txt);;Archivos Python (*.py);;Todos los archivos (*.*)",
self.main_window, "Guardar archivo", "",
"Archivos de texto (*.txt);;Archivos Python (*.py);;Todos los archivos (*.*)"
)
if filepath:
@ -234,36 +184,10 @@ class MenuManager:
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
self.main_window._update_status(
f"💾 Archivo guardado: {Path(filepath).name}"
)
# Añadir a recientes y actualizar menú
self.main_window.settings_manager.add_recent_file(filepath)
self._update_recent_files_menu()
self.main_window._update_status(f"💾 Archivo guardado: {Path(filepath).name}")
except Exception as e:
QMessageBox.critical(
self.main_window, "Error", f"No se pudo guardar el archivo:\n{e}"
)
def _update_recent_files_menu(self):
"""Actualiza el menú de archivos recientes"""
self.recent_files_menu.clear()
recent_files = self.main_window.settings_manager.get_setting("recent_files", [])
if not recent_files:
empty_action = QAction("(Vacío)", self.main_window)
empty_action.setEnabled(False)
self.recent_files_menu.addAction(empty_action)
return
for filepath in recent_files:
action = QAction(filepath, self.main_window)
action.triggered.connect(
lambda checked=False, path=filepath: self._load_file_content(path)
)
self.recent_files_menu.addAction(action)
QMessageBox.critical(self.main_window, "Error", f"No se pudo guardar el archivo:\n{e}")
# ========== FUNCIONES DE MENÚ EDITAR ==========
@ -276,7 +200,7 @@ class MenuManager:
"""Limpia panel de salida y LaTeX"""
self.main_window._clear_output()
self.main_window.latex_panel.clear_equations()
if hasattr(self.main_window, "_latex_equations"):
if hasattr(self.main_window, '_latex_equations'):
self.main_window._latex_equations.clear()
def clear_history(self):
@ -286,50 +210,7 @@ class MenuManager:
os.remove(self.main_window.HISTORY_FILE)
self.main_window._update_status("✓ Historial limpiado")
except Exception as e:
QMessageBox.critical(
self.main_window, "Error", f"No se pudo limpiar el historial:\n{e}"
)
def set_font_size(self, is_large: bool):
"""Cambia el tamaño de fuente de los paneles"""
# Guardar configuración
self.main_window.settings_manager.set_setting("font_size_large", is_large)
# Actualizar checkboxes
self.font_small_action.setChecked(not is_large)
self.font_large_action.setChecked(is_large)
# Aplicar nuevo tamaño
font_size = 14 if is_large else 11
self._apply_font_size_to_panels(font_size)
status_text = f"🔤 Fuente cambiada a {'grande' if is_large else 'pequeña'} ({font_size}px)"
self.main_window._update_status(status_text)
def _apply_font_size_to_panels(self, font_size: int):
"""Aplica el tamaño de fuente a los paneles de texto"""
style_update = f"""
QPlainTextEdit, QTextEdit {{
font-size: {font_size}px;
}}
"""
# Actualizar estilo de los paneles principales
current_style = self.main_window.styleSheet()
# Reemplazar solo la parte de font-size en QPlainTextEdit y QTextEdit
import re
# Patrón para encontrar y reemplazar font-size en QPlainTextEdit y QTextEdit
pattern = r"(QPlainTextEdit, QTextEdit\s*\{[^}]*?)font-size:\s*\d+px;([^}]*\})"
replacement = rf"\1font-size: {font_size}px;\2"
new_style = re.sub(pattern, replacement, current_style)
# Si no se encontró el patrón, agregar el estilo
if new_style == current_style:
new_style += style_update
self.main_window.setStyleSheet(new_style)
QMessageBox.critical(self.main_window, "Error", f"No se pudo limpiar el historial:\n{e}")
# ========== FUNCIONES DE MENÚ HERRAMIENTAS ==========
@ -342,9 +223,7 @@ class MenuManager:
self.main_window._update_status("✓ Sistema de tipos recargado")
except Exception as e:
self.logger.error(f"Error recargando tipos: {e}")
QMessageBox.critical(
self.main_window, "Error", f"Error recargando tipos:\n{e}"
)
QMessageBox.critical(self.main_window, "Error", f"Error recargando tipos:\n{e}")
def show_types_info(self):
"""Muestra información sobre tipos disponibles"""
@ -368,9 +247,7 @@ CARACTERÍSTICAS:
self._show_info_dialog("Información del Sistema", info_text)
except Exception as e:
QMessageBox.critical(
self.main_window, "Error", f"Error obteniendo información:\n{e}"
)
QMessageBox.critical(self.main_window, "Error", f"Error obteniendo información:\n{e}")
def show_types_syntax(self):
"""Muestra sintaxis de tipos disponibles"""
@ -384,37 +261,20 @@ CARACTERÍSTICAS:
self._show_info_dialog("Sintaxis de Tipos", syntax_text)
except Exception as e:
QMessageBox.critical(
self.main_window, "Error", f"Error obteniendo sintaxis:\n{e}"
)
QMessageBox.critical(self.main_window, "Error", f"Error obteniendo sintaxis:\n{e}")
# ========== FUNCIONES DE DIAGNÓSTICO ==========
def _diagnose_mathjax(self):
"""Ejecuta diagnóstico de MathJax"""
if (
not hasattr(self.main_window.latex_panel, "_webview_available")
or not self.main_window.latex_panel._webview_available
):
QMessageBox.warning(
self.main_window,
"Diagnóstico",
"Panel LaTeX no usa WebEngine (usando fallback)",
)
if not hasattr(self.main_window.latex_panel, '_webview_available') or not self.main_window.latex_panel._webview_available:
QMessageBox.warning(self.main_window, "Diagnóstico", "Panel LaTeX no usa WebEngine (usando fallback)")
return
# Aquí iría el código de diagnóstico
# Por ahora solo mostrar estado
status = (
"WebEngine disponible"
if self.main_window.latex_panel._webview_available
else "Usando fallback HTML"
)
equations = (
len(self.main_window._latex_equations)
if hasattr(self.main_window, "_latex_equations")
else 0
)
status = "WebEngine disponible" if self.main_window.latex_panel._webview_available else "Usando fallback HTML"
equations = len(self.main_window._latex_equations) if hasattr(self.main_window, '_latex_equations') else 0
info = f"""DIAGNÓSTICO MATHJAX
@ -430,16 +290,10 @@ en el WebEngineView.
def _show_latex_panel_status(self):
"""Muestra estado del panel LaTeX"""
panel_exists = hasattr(self.main_window, "latex_panel")
panel_exists = hasattr(self.main_window, 'latex_panel')
panel_visible = self.main_window.latex_panel_visible if panel_exists else False
webview_available = (
self.main_window.latex_panel._webview_available if panel_exists else False
)
equations_count = (
len(self.main_window._latex_equations)
if hasattr(self.main_window, "_latex_equations")
else 0
)
webview_available = self.main_window.latex_panel._webview_available if panel_exists else False
equations_count = len(self.main_window._latex_equations) if hasattr(self.main_window, '_latex_equations') else 0
status_message = f"""ESTADO DEL PANEL LATEX
@ -460,73 +314,6 @@ PARA SOLUCIONAR:
self._show_info_dialog("Estado Panel LaTeX", status_message)
def _save_latex_html(self):
"""Guarda el HTML del panel LaTeX para diagnóstico"""
try:
# Obtener HTML del panel LaTeX
latex_panel = self.main_window.latex_panel
if (
hasattr(latex_panel, "_webview_available")
and latex_panel._webview_available
):
# Panel con WebEngine
if hasattr(latex_panel, "webview") and latex_panel._webview_initialized:
# Obtener HTML actual del WebView
def on_html_received(html_content):
self._save_html_to_file(html_content, "webview_current")
latex_panel.webview.page().toHtml(on_html_received)
# También guardar el HTML base generado
base_html = latex_panel._generate_mathjax_html()
self._save_html_to_file(base_html, "webview_base")
else:
# WebEngine no inicializado, solo HTML base
base_html = latex_panel._generate_mathjax_html()
self._save_html_to_file(base_html, "webview_not_initialized")
else:
# Panel con fallback text browser
if hasattr(latex_panel, "text_browser"):
fallback_html = latex_panel.text_browser.toHtml()
self._save_html_to_file(fallback_html, "fallback_textbrowser")
else:
self._save_html_to_file(
"No hay contenido HTML disponible", "no_content"
)
QMessageBox.information(
self.main_window,
"HTML Guardado",
"El HTML del panel LaTeX ha sido guardado en ./debug_html/ para diagnóstico.",
)
except Exception as e:
self.logger.error(f"Error guardando HTML del panel LaTeX: {e}")
QMessageBox.critical(
self.main_window, "Error", f"Error guardando HTML: {e}"
)
def _save_html_to_file(self, html_content, file_suffix):
"""Guarda contenido HTML a un archivo"""
# Crear directorio de debug si no existe
debug_dir = Path("./debug_html")
debug_dir.mkdir(exist_ok=True)
# Generar nombre de archivo con timestamp
timestamp = time.strftime("%Y%m%d_%H%M%S")
filename = f"latex_panel_{file_suffix}_{timestamp}.html"
filepath = debug_dir / filename
try:
with open(filepath, "w", encoding="utf-8") as f:
f.write(html_content)
self.logger.info(f"HTML guardado en: {filepath}")
except Exception as e:
self.logger.error(f"Error escribiendo archivo HTML: {e}")
def _copy_debug_to_clipboard(self):
"""Copia información de debug completa al portapapeles"""
try:
@ -541,16 +328,11 @@ PARA SOLUCIONAR:
# Obtener ecuaciones LaTeX si están disponibles
latex_equations = ""
if (
hasattr(self.main_window, "_latex_equations")
and self.main_window._latex_equations
):
latex_equations = "\\n".join(
[
if hasattr(self.main_window, '_latex_equations') and self.main_window._latex_equations:
latex_equations = "\\n".join([
f"[{eq['type']}] {eq['content']}"
for eq in self.main_window._latex_equations
]
)
])
# Crear reporte de debug completo
debug_report = f"""=== REPORTE DEBUG CALCULADORA MAV ===
@ -583,17 +365,11 @@ Panel LaTeX visible: {self.main_window.latex_panel_visible}
clipboard.setText(debug_report)
# Mostrar confirmación
self.main_window._update_status(
"📋 Información de debug copiada al portapapeles", 3000
)
self.main_window._update_status("📋 Información de debug copiada al portapapeles", 3000)
except Exception as e:
self.logger.error(f"Error copiando debug: {e}")
QMessageBox.critical(
self.main_window,
"Error",
f"Error copiando debug al portapapeles:\\n{e}",
)
QMessageBox.critical(self.main_window, "Error", f"Error copiando debug al portapapeles:\\n{e}")
# ========== FUNCIONES DE MENÚ AYUDA ==========

View File

@ -8,13 +8,8 @@ from PySide6.QtGui import QFont, QTextCursor, QTextCharFormat, QColor
from PySide6.QtWebEngineWidgets import QWebEngineView
import sympy
from typing import Any, Optional, Dict, List, Tuple
try:
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
MATPLOTLIB_AVAILABLE = True
except ImportError:
print("⚠️ Matplotlib no disponible - plots no funcionarán")
MATPLOTLIB_AVAILABLE = False
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
import numpy as np
@ -132,8 +127,11 @@ class InteractiveResultManager(QWidget):
def handle_interactive_click(self, result: Any, is_mathjax_click: bool = False):
"""Maneja clicks en elementos interactivos"""
if isinstance(result, PlotResult):
# Abrir ventana de plot directamente
print(f"🖱️ Abriendo ventana de plot para: {result.plot_type}")
if not is_mathjax_click:
# Primera vez: mostrar en MathJax
self.plot_requested.emit(result)
else:
# Click en MathJax: abrir ventana emergente
self._show_plot_window(result)
else:
# Otros tipos siempre abren ventana
@ -305,18 +303,7 @@ class InteractiveResultManager(QWidget):
if not parent_frame.layout():
parent_frame.setLayout(QVBoxLayout())
if not MATPLOTLIB_AVAILABLE:
error_label = QLabel("Matplotlib no está disponible.\nInstala con: pip install matplotlib")
error_label.setStyleSheet("color: #f44747; font-size: 12px;")
error_label.setWordWrap(True)
parent_frame.layout().addWidget(error_label)
return
print(f"📊 Creando plot {plot_result.plot_type} con args: {plot_result.args}")
fig, ax = plt.subplots(figsize=(8, 6))
fig.patch.set_facecolor('#1a1a1a') # Fondo oscuro
ax.set_facecolor('#1a1a1a')
if plot_result.plot_type == "plot":
self._create_2d_plot(fig, ax, plot_result.args, plot_result.kwargs)
@ -327,13 +314,7 @@ class InteractiveResultManager(QWidget):
canvas = FigureCanvasQTAgg(fig)
parent_frame.layout().addWidget(canvas)
print("✅ Plot creado exitosamente")
except Exception as e:
print(f"❌ Error creando plot: {e}")
import traceback
traceback.print_exc()
if not parent_frame.layout():
parent_frame.setLayout(QVBoxLayout())

View File

@ -1,7 +1,6 @@
"""
Sistema de Configuración y Persistencia para la Calculadora MAV CAS Híbrida
"""
import os
import json
import logging
@ -32,10 +31,7 @@ class SettingsManager:
"window_geometry": None,
"splitter_sizes": None,
"debug_mode": False,
"latex_panel_visible": False,
"latex_panel_width": 300,
"font_size_large": False, # False = pequeña (11px), True = grande (14px)
"recent_files": [],
"latex_panel_visible": False
}
def _save_settings(self):
@ -50,15 +46,15 @@ class SettingsManager:
"x": geometry.x(),
"y": geometry.y(),
"width": geometry.width(),
"height": geometry.height(),
"height": geometry.height()
}
# Guardar tamaños del splitter
if hasattr(self.main_window, "main_splitter"):
if hasattr(self.main_window, 'main_splitter'):
self.settings["splitter_sizes"] = self.main_window.main_splitter.sizes()
self.settings["latex_panel_visible"] = self.main_window.latex_panel_visible
self.settings["debug_mode"] = getattr(self.main_window, "debug", False)
self.settings["debug_mode"] = getattr(self.main_window, 'debug', False)
with open(self.SETTINGS_FILE, "w", encoding="utf-8") as f:
json.dump(self.settings, f, indent=4, ensure_ascii=False)
@ -99,13 +95,11 @@ class SettingsManager:
try:
geom = self.settings.get("window_geometry")
if geom and isinstance(geom, dict):
self.main_window.setGeometry(
geom["x"], geom["y"], geom["width"], geom["height"]
)
self.main_window.setGeometry(geom["x"], geom["y"], geom["width"], geom["height"])
# Restaurar splitter
sizes = self.settings.get("splitter_sizes")
if sizes and hasattr(self.main_window, "main_splitter"):
if sizes and hasattr(self.main_window, 'main_splitter'):
self.main_window.main_splitter.setSizes(sizes)
except Exception as e:
@ -128,21 +122,3 @@ class SettingsManager:
def set_setting(self, key: str, value):
"""Establece un valor de configuración"""
self.settings[key] = value
def add_recent_file(self, filepath: str):
"""Añade un archivo a la lista de recientes y guarda."""
try:
recent_files = self.get_setting("recent_files", [])
# Si el archivo ya está, lo eliminamos para volver a añadirlo al principio
if filepath in recent_files:
recent_files.remove(filepath)
# Añadir al principio de la lista
recent_files.insert(0, filepath)
# Limitar la lista a 10 elementos
self.set_setting("recent_files", recent_files[:10])
except Exception as e:
self.logger.error(f"Error añadiendo archivo reciente: {e}")

View File

@ -41,7 +41,6 @@ class OutputTextEdit(QTextEdit):
self.setReadOnly(True)
self.setFont(QFont("Consolas", 11))
self.clickable_links = {} # {(start, end): (link_id, object)}
self.setMouseTracking(True) # Habilitar tracking del mouse
def mousePressEvent(self, event):
"""Detecta clicks en links"""
@ -49,38 +48,14 @@ class OutputTextEdit(QTextEdit):
cursor = self.cursorForPosition(event.pos())
pos = cursor.position()
logging.debug(f"🖱️ Click en posición {pos}, links disponibles: {len(self.clickable_links)}")
# Buscar si el click fue en un link
for (start, end), (link_id, obj) in self.clickable_links.items():
logging.debug(f"🔍 Verificando link {start}-{end}: {link_id}")
if start <= pos <= end:
logging.debug(f"✅ Click en link detectado: {link_id}")
self.link_clicked.emit(link_id, obj)
return
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
"""Cambia el cursor cuando está sobre un link"""
cursor = self.cursorForPosition(event.pos())
pos = cursor.position()
# Verificar si el mouse está sobre un link
over_link = False
for (start, end), (link_id, obj) in self.clickable_links.items():
if start <= pos <= end:
over_link = True
break
# Cambiar cursor
if over_link:
self.setCursor(Qt.PointingHandCursor)
else:
self.setCursor(Qt.ArrowCursor)
super().mouseMoveEvent(event)
class ExpandableLatexButton(QPushButton):
"""Botón expandible para mostrar/ocultar panel LaTeX"""

View File

@ -1,120 +0,0 @@
"""
Sistema de gestión de MathJax local con descarga automática
"""
import os
import zipfile
import logging
from pathlib import Path
from urllib.request import urlretrieve
from urllib.error import URLError
class MathJaxManager:
"""Gestor de MathJax local con descarga automática"""
MATHJAX_VERSION = "3.2.2"
MATHJAX_URL = f"https://github.com/mathjax/MathJax/archive/{MATHJAX_VERSION}.zip"
LOCAL_DIR = Path(".mathjax")
def __init__(self):
self.logger = logging.getLogger(__name__)
self.mathjax_dir = self.LOCAL_DIR / f"MathJax-{self.MATHJAX_VERSION}"
self.es5_dir = self.mathjax_dir / "es5"
def get_mathjax_url(self) -> str:
"""
Obtiene la URL de MathJax (local si está disponible, CDN si no)
"""
if self.is_local_available():
# Construir URL local relativa para file://
local_url = f"file:///{self.es5_dir.absolute().as_posix()}/tex-svg.js"
self.logger.debug(f"Usando MathJax local: {local_url}")
return local_url
else:
# Descargar automáticamente si no existe
if self.download_mathjax():
local_url = f"file:///{self.es5_dir.absolute().as_posix()}/tex-svg.js"
self.logger.info(f"MathJax descargado y configurado localmente: {local_url}")
return local_url
else:
# Fallback a CDN
cdn_url = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"
self.logger.warning(f"Fallback a CDN: {cdn_url}")
return cdn_url
def is_local_available(self) -> bool:
"""Verifica si MathJax está disponible localmente"""
tex_svg_file = self.es5_dir / "tex-svg.js"
return tex_svg_file.exists() and tex_svg_file.is_file()
def download_mathjax(self) -> bool:
"""
Descarga MathJax automáticamente si no está disponible
Returns:
True si la descarga fue exitosa, False si falló
"""
try:
self.logger.info(f"Descargando MathJax {self.MATHJAX_VERSION}...")
# Crear directorio si no existe
self.LOCAL_DIR.mkdir(exist_ok=True)
# Archivo temporal para la descarga
zip_path = self.LOCAL_DIR / f"mathjax-{self.MATHJAX_VERSION}.zip"
# Descargar archivo ZIP
self.logger.debug(f"Descargando desde: {self.MATHJAX_URL}")
urlretrieve(self.MATHJAX_URL, zip_path)
# Extraer archivo
self.logger.debug(f"Extrayendo en: {self.LOCAL_DIR}")
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(self.LOCAL_DIR)
# Limpiar archivo ZIP
zip_path.unlink()
# Verificar que se extrajo correctamente
if self.is_local_available():
self.logger.info(f"✅ MathJax {self.MATHJAX_VERSION} descargado correctamente")
return True
else:
self.logger.error("❌ Error: MathJax no se extrajo correctamente")
return False
except URLError as e:
self.logger.error(f"❌ Error de red descargando MathJax: {e}")
return False
except zipfile.BadZipFile as e:
self.logger.error(f"❌ Error: archivo ZIP corrupto: {e}")
# Limpiar archivo corrupto
if zip_path.exists():
zip_path.unlink()
return False
except Exception as e:
self.logger.error(f"❌ Error inesperado descargando MathJax: {e}")
return False
def clean_local_mathjax(self):
"""
Limpia la instalación local de MathJax
(Para forzar redownload en la siguiente inicialización)
"""
try:
if self.LOCAL_DIR.exists():
import shutil
shutil.rmtree(self.LOCAL_DIR)
self.logger.info("🗑️ MathJax local limpiado")
except Exception as e:
self.logger.error(f"Error limpiando MathJax local: {e}")
def get_status_info(self) -> dict:
"""Obtiene información del estado de MathJax"""
return {
"local_available": self.is_local_available(),
"local_dir": str(self.LOCAL_DIR.absolute()),
"mathjax_dir": str(self.mathjax_dir.absolute()) if self.mathjax_dir.exists() else None,
"version": self.MATHJAX_VERSION,
"tex_svg_file": str(self.es5_dir / "tex-svg.js") if self.is_local_available() else None
}

View File

@ -1,377 +0,0 @@
"""
Módulo de manejo de fechas para el sistema de tipos
Permite crear y manipular fechas con aritmética de días y semanas, soporte para
operaciones simbólicas a través de SymPy, y utilidades comunes como cálculo de
fines de semana, días hábiles y diferencias entre fechas.
Ejemplo de uso:
>>> d = Date("10/5/25") # 10 de mayo de 2025
>>> d + 7 # 17/5/25
>>> d.day_of_week() # 'Sábado'
>>> d.is_weekend() # True
>>> d.next_business_day() # 12/5/25
"""
from __future__ import annotations
import datetime
from typing import List, Tuple, Optional, Union
import sympy
from app.class_base import (
ClassBase,
) # noqa: F401 (aún no se usa, pero se importa para coherencia)
from app.sympy_Base import SympyClassBase
__all__ = [
"Class_Date",
"Date", # alias de conveniencia
"Today", # nueva función constructora
]
def _parse_date_str(date_str: str) -> datetime.date:
"""Convierte una cadena a objeto ``datetime.date``.
Acepta los formatos comunes:
- DD/MM/YY (se asume 2000-2099 para YY < 70 y 1900-1999 para el resto)
- DD/MM/YYYY
- YYYY-MM-DD (ISO-8601)
Asimismo se admiten separadores «/», «-» o «.».
"""
date_str = date_str.strip()
# Intentar ISO-8601 directamente
try:
return datetime.date.fromisoformat(date_str)
except ValueError:
pass
# Reemplazar posibles separadores por «/» para simplificar
normalised = date_str.replace("-", "/").replace(".", "/")
parts = normalised.split("/")
if len(parts) != 3:
raise ValueError(f"Formato de fecha desconocido: '{date_str}'")
# Detectar si el primer campo es año (ISO) o día
if len(parts[0]) == 4: # AAAA/MM/DD
year = int(parts[0])
month = int(parts[1])
day = int(parts[2])
else: # DD/MM/AA o DD/MM/YYYY
day = int(parts[0])
month = int(parts[1])
year_part = parts[2]
if len(year_part) == 2:
yy = int(year_part)
year = 2000 + yy if yy < 70 else 1900 + yy
else:
year = int(year_part)
return datetime.date(year, month, day)
class Class_Date(SympyClassBase):
"""Clase de fecha con aritmética de días y semanas.
Internamente la fecha se almacena como el ordinal devuelto por
:py:meth:`datetime.date.toordinal`, lo que facilita la aritmética con
números enteros (p.ej. añadir días).
"""
_op_priority = 20 # preferencia alta en operaciones de SymPy
def __init__(self, value: Union[str, datetime.date, int, sympy.Expr]):
# Manejar expresiones SymPy (p. ej. símbolos o enteros simbólicos)
if isinstance(value, sympy.Expr) and not isinstance(value, sympy.Integer):
# Modo simbólico puro (no intentamos resolver a fecha real)
self._ordinal_expr = value
self._has_symbols = True
date_repr = str(value)
else:
# Convertir todos los casos a «datetime.date» y ordinal entero
if isinstance(value, datetime.date):
date_obj = value
elif isinstance(value, str):
date_obj = _parse_date_str(value)
elif isinstance(value, (int, sympy.Integer)):
# Se interpreta como ordinal absoluto
date_obj = datetime.date.fromordinal(int(value))
else:
raise TypeError(
"value debe ser str, datetime.date o entero (ordinal), "
f"recibido: {type(value)}"
)
self._ordinal = date_obj.toordinal()
self._has_symbols = False
self._date = date_obj
self._ordinal_expr = sympy.Integer(self._ordinal)
date_repr = self._date.isoformat()
super().__init__(self._ordinal_expr, date_repr)
# ------------------------------------------------------------------
# Representación
# ------------------------------------------------------------------
def __repr__(self): # noqa: D401
if self._has_symbols:
return f"Date({sympy.sstr(self._ordinal_expr)})"
return f"Date('{self.__str__()}')"
def __str__(self): # noqa: D401
if self._has_symbols:
return f"Date({sympy.sstr(self._ordinal_expr)})"
return self._date.strftime("%d/%m/%y")
def _sympystr(self, printer): # pragma: no cover
return str(self)
# ------------------------------------------------------------------
# Conversión
# ------------------------------------------------------------------
def to_datetime(self) -> datetime.date:
"""Devuelve la fecha como objeto ``datetime.date``."""
if self._has_symbols:
raise ValueError(
"La fecha es simbólica y no se puede convertir a 'datetime.date'."
)
return self._date
def to_ordinal(self) -> int:
"""Devuelve el ordinal (días desde 01-01-0001)."""
return int(self._ordinal_expr)
# ------------------------------------------------------------------
# Aritmética
# ------------------------------------------------------------------
def __add__(self, other):
"""Añadir días o expresiones simbólicas: ``fecha + n`` o ``fecha + símbolo``."""
# Soporte para expresiones SymPy (símbolos, enteros simbólicos, etc.)
if isinstance(other, sympy.Expr):
new_expr = self._ordinal_expr + other
return Class_Date(new_expr)
if hasattr(other, "__int__"):
return self.add_days(int(other))
return NotImplemented
__radd__ = __add__
def __sub__(self, other):
"""Resta días o devuelve diferencia: ``fecha - n``, ``fecha - símbolo``, o diferencia entre fechas."""
if isinstance(other, sympy.Expr):
# Restar expresión simbólica -> nueva fecha simbólica
new_expr = self._ordinal_expr - other
return Class_Date(new_expr)
if hasattr(other, "__int__"):
return self.add_days(-int(other))
if isinstance(other, Class_Date):
# Diferencia (puede ser simbólica)
return self._ordinal_expr - other._ordinal_expr
return NotImplemented
def add_days(self, n: int) -> "Class_Date":
"""Devuelve una nueva fecha desplazada *n* días."""
if self._has_symbols:
new_expr = self._ordinal_expr + n
return Class_Date(new_expr)
else:
new_date = self._date + datetime.timedelta(days=n)
return Class_Date(new_date)
def add_weeks(self, n: int) -> "Class_Date":
"""Devuelve una nueva fecha desplazada *n* semanas."""
return self.add_days(n * 7)
def next_day(self) -> "Class_Date":
return self.add_days(1)
def previous_day(self) -> "Class_Date":
return self.add_days(-1)
# ------------------------------------------------------------------
# Información de calendario
# ------------------------------------------------------------------
_WEEKDAY_NAMES_ES = [
"Lunes",
"Martes",
"Miércoles",
"Jueves",
"Viernes",
"Sábado",
"Domingo",
]
def day_of_week(self) -> str:
"""Nombre del día de la semana en español (Lunes-Domingo)."""
if self._has_symbols:
raise ValueError(
"La fecha es simbólica; no se puede determinar el día de la semana."
)
weekday = self._date.weekday() # 0 = lunes
return self._WEEKDAY_NAMES_ES[weekday]
def is_weekend(self) -> bool:
if self._has_symbols:
raise ValueError(
"La fecha es simbólica; no se puede determinar si es fin de semana."
)
return self._date.weekday() >= 5
# ------------------------------------------------------------------
# Días hábiles
# ------------------------------------------------------------------
def is_business_day(self, holidays: Optional[List[datetime.date]] = None) -> bool:
"""Indica si es día laborable (L-V) y no festivo.
Args:
holidays: lista opcional de festivos como ``datetime.date``.
"""
if self._has_symbols:
raise ValueError(
"La fecha es simbólica; no se puede evaluar si es laborable."
)
if self.is_weekend():
return False
if holidays and self._date in holidays:
return False
return True
def next_business_day(
self, holidays: Optional[List[datetime.date]] = None
) -> "Class_Date":
"""Devuelve el siguiente día laborable."""
candidate = self.next_day()
while not candidate.is_business_day(holidays):
candidate = candidate.next_day()
return candidate
def business_days_between(
self, other: "Class_Date", holidays: Optional[List[datetime.date]] = None
) -> int:
"""Cuenta días laborables entre dos fechas (excluye la inicial, incluye la final si procede)."""
if not isinstance(other, Class_Date):
raise TypeError("other debe ser Class_Date")
start_ord = min(self.to_ordinal(), other.to_ordinal()) + 1
end_ord = max(self.to_ordinal(), other.to_ordinal())
count = 0
for ord_val in range(start_ord, end_ord + 1):
d = Class_Date(ord_val)
if d.is_business_day(holidays):
count += 1
return count
# ------------------------------------------------------------------
# Diferencias
# ------------------------------------------------------------------
def days_between(self, other: "Class_Date") -> int:
"""Número absoluto de días entre dos fechas."""
if not isinstance(other, Class_Date):
raise TypeError("other debe ser Class_Date")
return abs(self - other)
def weeks_between(self, other: "Class_Date") -> float:
"""Número de semanas (con decimales) entre dos fechas."""
return self.days_between(other) / 7
# ------------------------------------------------------------------
# Comparaciones y ecuaciones simbólicas
# ------------------------------------------------------------------
def __eq__(self, other): # type: ignore[override]
"""Devuelve una ecuación SymPy en vez de comparar booleanamente cuando se usa con objetos Date o expresiones SymPy."""
if isinstance(other, Class_Date):
return sympy.Eq(self._ordinal_expr, other._ordinal_expr)
if isinstance(other, sympy.Expr):
return sympy.Eq(self._ordinal_expr, other)
return NotImplemented
# ------------------------------------------------------------------
# Helper para autocompletado & paneles GUI
# ------------------------------------------------------------------
@staticmethod
def Helper(input_str): # pragma: no cover
if input_str.strip().lower().startswith("date("):
return (
"Ej: date('10/5/25'), date('2025-05-10')\n" # noqa: E501
"Funciones: add_days(), add_weeks(), next_business_day(), day_of_week(), is_weekend(), is_business_day(), days_between()…"
)
return None
@staticmethod
def PopupFunctionList(): # pragma: no cover
return [
("add_days", "Suma N días"),
("add_weeks", "Suma N semanas"),
("next_day", "Día siguiente"),
("previous_day", "Día anterior"),
("next_business_day", "Siguiente día hábil"),
("is_business_day", "¿Es día laborable?"),
("day_of_week", "Nombre del día de la semana"),
("is_weekend", "¿Es fin de semana?"),
("days_between", "Días entre dos fechas"),
("weeks_between", "Semanas entre dos fechas"),
("to_datetime", "Convertir a datetime.date"),
("to_ordinal", "Ordinal (nº de días)"),
]
@classmethod
def today(cls) -> "Class_Date":
"""Fecha actual (según *datetime.date.today()*)."""
return cls(datetime.date.today())
# Alias interno por consistencia con el resto del sistema
Today = today
# ----------------------------------------------------------------------
# Alias de usuario final — ajustado al sistema de tokenización existente
# ----------------------------------------------------------------------
def Date(value: Union[str, datetime.date, int, sympy.Expr]): # noqa: N802
"""Alias que imita la sintaxis ``date('')`` empleada por el usuario."""
return Class_Date(value)
def Today(): # noqa: N802
"""Construye un objeto Date con la fecha actual (hoy)."""
return Class_Date.today()
# ----------------------------------------------------------------------
# Registro para el sistema de tipos dinámico
# ----------------------------------------------------------------------
def register_classes_in_module(): # pragma: no cover
"""Devuelve la lista de clases para el registro automático."""
return [
(
"Date",
Class_Date,
"SympyClassBase",
{
"add_lowercase": True, # permite usar ``date`` minúscula
"supports_brackets": False,
"description": "Fechas con aritmética y utilidades de calendario",
},
),
(
"Today",
Today,
"function",
{
"add_lowercase": True,
"supports_brackets": False,
"description": "Fecha actual (hoy)",
},
),
]

View File

@ -1,153 +0,0 @@
"""
Clase para símbolos SymPy - Tokenización automática de corchetes
Convierte [texto] automáticamente a symbols('texto')
EJEMPLOS DE USO:
k = 100 * [Hz] k = 100 * symbols('Hz')
E = [m] * [c]**2 E = symbols('m') * symbols('c')**2
f = [omega] / (2*[pi]) f = symbols('omega') / (2*symbols('pi'))
CARACTERÍSTICAS:
- Tokenización automática de [nombre] a symbols('nombre')
- Integración completa con SymPy para álgebra simbólica
- Soporte para operaciones matemáticas completas
- Prioridad alta en el sistema de tokenización
"""
from app.sympy_Base import SympyClassBase
import re
class SymPySymbol(SympyClassBase):
"""
Wrapper para símbolos de SymPy con tokenización automática de corchetes
Esta clase permite usar la sintaxis [nombre] como shortcut para symbols('nombre'),
mejorando significativamente la experiencia de usuario para trabajo algebraico.
Uso: [Hz] se convierte automáticamente a symbols('Hz')
"""
def __init__(self, symbol_name):
"""Inicialización del símbolo SymPy"""
from sympy import symbols
if isinstance(symbol_name, str):
self.symbol_name = symbol_name
self.symbol = symbols(symbol_name)
else:
raise TypeError(f"SymPySymbol requiere un string, recibido: {type(symbol_name)}")
# Llamar al constructor base
super().__init__(self.symbol, str(self.symbol))
def __str__(self):
"""Representación string"""
return str(self.symbol)
def _sympystr(self, printer):
"""Representación SymPy string"""
return str(self.symbol)
def __repr__(self):
return f"Symbol({self.symbol_name})"
# ========== DELEGACIÓN DE OPERADORES A SYMPY ==========
def __add__(self, other):
"""Suma delegada al símbolo SymPy"""
return self.symbol + other
def __radd__(self, other):
return other + self.symbol
def __sub__(self, other):
"""Resta delegada al símbolo SymPy"""
return self.symbol - other
def __rsub__(self, other):
return other - self.symbol
def __mul__(self, other):
"""Multiplicación delegada al símbolo SymPy"""
return self.symbol * other
def __rmul__(self, other):
return other * self.symbol
def __truediv__(self, other):
"""División delegada al símbolo SymPy"""
return self.symbol / other
def __rtruediv__(self, other):
return other / self.symbol
def __pow__(self, other):
"""Potencia delegada al símbolo SymPy"""
return self.symbol ** other
def __rpow__(self, other):
return other ** self.symbol
# ========== MÉTODOS ESTÁTICOS ==========
@staticmethod
def Helper(input_str):
"""Ayuda contextual para símbolos"""
if re.search(r'\[.*\]', input_str):
return ('Uso: [Hz] se convierte automáticamente a symbols("Hz")\n'
'Para unidades físicas, frecuencias, variables simbólicas, etc.\n'
'Ejemplos: [x], [omega], [pi], [Hz], [m], [c]')
return None
@staticmethod
def PopupFunctionList():
"""Lista de métodos sugeridos para símbolos"""
return [
("subs", "Sustituir valores: symbol.subs(x, valor)"),
("expand", "Expandir expresión"),
("simplify", "Simplificar expresión"),
("factor", "Factorizar expresión"),
("solve", "Resolver ecuaciones"),
]
@staticmethod
def get_tokenization_patterns():
"""
Define la regla de tokenización principal: [texto] symbols('texto')
PATRÓN: \[([a-zA-Z_][a-zA-Z0-9_]*)\]
- Captura texto entre corchetes que empiece con letra o _
- Permite números después del primer carácter
- Convierte a llamada symbols() automáticamente
Returns:
List[Dict]: Lista de reglas de tokenización
"""
return [
{
'pattern': r'\[([a-zA-Z_][a-zA-Z0-9_]*)\]',
'replacement': lambda match: f'symbols("{match.group(1)}")',
'priority': 10, # ALTA prioridad - muy específico y común
'description': 'Símbolos con corchetes: [Hz], [x], [omega] → symbols("Hz"), etc.'
}
]
# ========== FUNCIÓN DE REGISTRO ==========
def register_classes_in_module():
"""
Registra la clase SymPySymbol en el sistema de tipos
Configuración:
- add_lowercase: True (permite sympysymbol() además de SymPySymbol())
- supports_brackets: False (los corchetes se manejan vía tokenización)
- Integración automática con el sistema de tokenización distribuida
"""
return [
("SymPySymbol", SymPySymbol, "SympyClassBase", {
"add_lowercase": True,
"supports_brackets": False, # Los corchetes se manejan vía tokenización
"description": "Símbolos SymPy con tokenización automática de corchetes [nombre]"
}),
]

View File

@ -1,218 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script>
// Intentar cargar MathJax local primero, luego CDN como fallback
window.mathjaxLoadAttempts = 0;
window.mathJaxReady = false;
function loadMathJax(url, isFallback = false) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.id = 'MathJax-script';
script.async = true;
script.src = url;
script.onload = () => {
console.log('MathJax cargado desde:', url);
resolve();
};
script.onerror = () => {
console.error('Error cargando MathJax desde:', url);
reject(new Error('MathJax load failed'));
};
document.head.appendChild(script);
});
}
// Configuración de MathJax
window.MathJax = {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']],
displayMath: [['$$', '$$'], ['\\[', '\\]']],
processEscapes: true
},
svg: {
scale: 0.9,
minScale: 0.5,
fontCache: 'global'
},
startup: {
ready: function () {
MathJax.startup.defaultReady();
window.mathJaxReady = true;
console.log('MathJax SVG completamente listo');
}
}
};
// Intentar cargar MathJax local, luego CDN
loadMathJax('file:///D:/Proyectos/Scripts/Calcv2/.mathjax/MathJax-3.2.2/es5/tex-svg.js').catch(() => {
console.log('Fallback a CDN de MathJax...');
return loadMathJax('https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-svg.js', true);
}).catch(() => {
console.error('No se pudo cargar MathJax desde ninguna fuente');
// Marcar como listo para mostrar contenido sin renderizado
setTimeout(() => {
window.mathJaxReady = true;
console.log('Modo fallback activado - sin renderizado LaTeX');
}, 2000);
});
</script>
<style>
body {
background-color: #1a1a1a;
color: #d4d4d4;
font-family: 'Segoe UI', Arial, sans-serif;
margin: 0;
padding: 8px;
line-height: 1.2;
}
.equation-block {
background: rgba(45, 45, 48, 0.8);
border-left: 3px solid;
margin: 4px 0;
padding: 6px 12px;
border-radius: 4px;
transition: all 0.1s ease;
}
.equation-block:hover {
background: rgba(45, 45, 48, 0.9);
}
.comment { border-left-color: #6a9955; }
.assignment { border-left-color: #dcdcaa; }
.equation { border-left-color: #c586c0; }
.symbolic { border-left-color: #9cdcfe; }
.math-content {
margin: 2px 0;
font-size: 14px;
}
.comment-text {
font-style: italic;
color: #6a9955;
font-size: 12px;
margin: 0;
}
.info-message {
text-align: center;
padding: 20px;
color: #666;
font-size: 12px;
}
/* Optimizaciones para SVG rendering */
mjx-container[jax="SVG"] {
margin: 2px 0 !important;
}
mjx-container[jax="SVG"] svg {
vertical-align: middle;
}
mjx-container[jax="SVG"] svg text {
fill: #d4d4d4 !important;
}
</style>
</head>
<body>
<div id="equations-container">
<div class="info-message">
Panel de Ecuaciones LaTeX (SVG)
</div>
</div>
<script>
function addEquation(type, content) {
try {
var container = document.getElementById('equations-container');
if (!container) {
console.error('Container no encontrado');
return;
}
// Limpiar mensaje inicial si existe
var infoMsg = container.querySelector('.info-message');
if (infoMsg) infoMsg.remove();
var equation = document.createElement('div');
equation.className = 'equation-block ' + type;
var mathContent = document.createElement('div');
mathContent.className = 'math-content';
if (type === 'comment') {
mathContent.className = 'comment-text';
mathContent.textContent = content;
console.log('Comentario agregado:', content);
} else {
mathContent.innerHTML = '$$' + content + '$$';
console.log('Ecuación agregada:', content);
}
equation.appendChild(mathContent);
container.appendChild(equation);
// Re-renderizar MathJax solo si está listo
if (window.MathJax && window.mathJaxReady && type !== 'comment') {
console.log('Renderizando con MathJax...');
MathJax.typesetPromise([equation]).then(() => {
console.log('Ecuación renderizada exitosamente');
}).catch(function(err) {
console.error('Error renderizando con MathJax:', err);
// Mostrar como texto plano si falla
mathContent.innerHTML = content;
});
} else if (type !== 'comment') {
console.log('MathJax no listo, mostrando como texto:', window.mathJaxReady);
// Mostrar como texto plano si MathJax no está listo
mathContent.innerHTML = content;
}
} catch (e) {
console.error('Error en addEquation:', e);
}
}
function clearEquations() {
try {
var container = document.getElementById('equations-container');
if (container) {
container.innerHTML = '<div class="info-message">Panel de Ecuaciones LaTeX (SVG)</div>';
console.log('Ecuaciones limpiadas');
}
} catch (e) {
console.error('Error en clearEquations:', e);
}
}
// Función para notificar que está listo para renderizar
function triggerInitialRender() {
try {
console.log('Verificando estado MathJax:', window.mathJaxReady);
if (window.mathJaxReady) {
console.log('MathJax SVG listo para renderizado inicial');
return true;
}
return false;
} catch (e) {
console.error('Error en triggerInitialRender:', e);
return false;
}
}
// Monitor de estado para debug
setInterval(() => {
if (window.mathJaxReady) {
console.log('🎯 MathJax listo detectado');
}
}, 3000);
</script>
</body>
</html>

File diff suppressed because one or more lines are too long