Calc/app/gui_evaluation.py

334 lines
14 KiB
Python

"""
Sistema de Evaluación y Procesamiento para la Calculadora MAV CAS Híbrida
"""
import logging
from typing import List, Dict, Any, Optional, Tuple
from PySide6.QtGui import QTextCursor, QTextCharFormat, QColor, QFont
from .evaluation import EvaluationResult
from .gui_latex import LatexProcessor
import sympy
class EvaluationManager:
"""Gestor del sistema de evaluación y procesamiento de resultados"""
def __init__(self, main_window):
self.main_window = main_window
self.logger = logging.getLogger(__name__)
self._latex_equations = []
# Timer para debounce de evaluación
from PySide6.QtCore import QTimer
self._debounce_timer = QTimer()
self._debounce_timer.setSingleShot(True)
self._debounce_timer.timeout.connect(self.evaluate_and_update)
# Configurar formatos de salida
self._setup_output_formats()
def _setup_output_formats(self):
"""Configura los formatos de texto para la salida"""
self.output_formats = {
'error': self._create_format("#f44747", bold=True),
'comment': self._create_format("#6a9955", italic=True),
'assignment': self._create_format("#dcdcaa"),
'equation': self._create_format("#c586c0"),
'symbolic': self._create_format("#9cdcfe"),
'numeric': self._create_format("#b5cea8"),
'boolean': self._create_format("#569cd6"),
'string': self._create_format("#ce9178"),
'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),
'helper': self._create_format("#ffd700", italic=True)
}
def _create_format(self, color: str, bold: bool = False, italic: bool = False, underline: bool = False) -> QTextCharFormat:
"""Crea un formato de texto"""
fmt = QTextCharFormat()
fmt.setForeground(QColor(color))
if bold:
fmt.setFontWeight(QFont.Bold)
if italic:
fmt.setFontItalic(True)
if underline:
fmt.setFontUnderline(True)
return fmt
def evaluate_and_update(self):
"""Evalúa todas las líneas y actualiza la salida"""
try:
input_content = self.main_window.input_text.toPlainText()
if not input_content.strip():
self._clear_output()
return
# Limpiar contexto del motor
self.main_window.engine.equations.clear()
self.main_window.engine.symbol_table.clear()
self.main_window.engine.variables.clear()
self.logger.debug("Contexto del motor limpiado")
# Limpiar ecuaciones LaTeX en memoria
self._latex_equations.clear()
# Solo limpiar panel visual si está visible
if self.main_window.latex_panel_visible:
self.main_window.latex_panel.clear_equations()
lines = input_content.splitlines()
self._evaluate_lines(lines)
except Exception as e:
self._show_error(f"Error durante evaluación: {e}")
def _evaluate_lines(self, lines: List[str]):
"""Evalúa múltiples líneas de código"""
output_data = []
for line_num, line in enumerate(lines, 1):
line_stripped = line.strip()
# Líneas vacías o comentarios
if not line_stripped or line_stripped.startswith('#'):
if line_stripped:
output_data.append([("comment", line_stripped)])
# Añadir comentario al panel LaTeX
if line_stripped.startswith('#'):
comment_text = line_stripped[1:].strip()
self._add_to_latex_panel("comment", comment_text)
else:
output_data.append([("", "")])
continue
# Evaluar línea
result = self.main_window.engine.evaluate_line(line_stripped)
line_output = self._process_evaluation_result(result)
output_data.append(line_output)
self._display_output(output_data)
def _process_evaluation_result(self, result: EvaluationResult) -> List[tuple]:
"""Procesa el resultado de evaluación para display"""
output_parts = []
indicator_text: Optional[str] = None
# Añadir al panel LaTeX si es aplicable
self._add_to_latex_panel_if_applicable(result)
if result.result_type == "comment":
output_parts.append(("comment", result.output if result.output is not None else ""))
return output_parts
if not result.success:
# Manejo de errores con ayuda contextual
error_msg = f"Error: {result.error_message}"
# Intentar obtener ayuda
ayuda_text = self._obtener_ayuda(result.input_line)
if ayuda_text:
ayuda_linea = ayuda_text.replace("\n", " ").strip()
if len(ayuda_linea) > 120:
ayuda_linea = ayuda_linea[:117] + "..."
output_parts.append(("error", error_msg))
output_parts.append(("\n", "\n"))
output_parts.append(("helper", f"Sugerencia: {ayuda_linea}"))
else:
output_parts.append(("error", error_msg))
else:
# Intentar crear tag interactivo
if self.main_window.interactive_manager:
interactive_info = self.main_window.interactive_manager.create_interactive_link(
result.actual_result_object
)
if interactive_info:
link_id, display_text, result_object = interactive_info
output_parts.append(("clickable", display_text, link_id, result_object))
# Añadir indicador de tipo algebraico
if result.algebraic_type:
indicator_text = f"[{result.algebraic_type}]"
output_parts.append((" ", " "))
output_parts.append(("type_indicator", indicator_text))
return output_parts
# Si no es interactivo, usar formato normal
main_output_tag = "base"
if result.is_assignment:
main_output_tag = "assignment"
indicator_text = "[=]"
elif result.is_equation:
main_output_tag = "equation"
indicator_text = "[eq]"
elif result.result_type == "plot":
main_output_tag = "plot"
else:
# Determinar tag según tipo algebraico
if result.algebraic_type:
type_lower = result.algebraic_type.lower()
if isinstance(result.actual_result_object, sympy.Basic):
main_output_tag = "symbolic"
elif type_lower in ["int", "float", "complex"]:
main_output_tag = "numeric"
elif type_lower == "bool":
main_output_tag = "boolean"
elif type_lower == "str":
main_output_tag = "string"
else:
main_output_tag = "custom_type"
if result.algebraic_type:
is_collection = any(kw in result.algebraic_type.lower()
for kw in ["matrix", "list", "dict", "tuple", "vector", "array"])
if is_collection or isinstance(result.actual_result_object, sympy.Basic):
indicator_text = f"[{result.algebraic_type}]"
output_parts.append((main_output_tag, result.output if result.output is not None else ""))
if indicator_text:
output_parts.append((" ", " "))
output_parts.append(("type_indicator", indicator_text))
return output_parts
def _add_to_latex_panel_if_applicable(self, result: EvaluationResult):
"""Agrega resultado al panel LaTeX si es aplicable"""
try:
should_add, equation_type = LatexProcessor.should_add_to_latex(result)
if should_add:
# Generar contenido LaTeX que incluya toda la información del output
latex_content = LatexProcessor.generate_complete_latex_content(result)
self._add_to_latex_panel(equation_type, latex_content)
except Exception as e:
self.logger.error(f"Error procesando para panel LaTeX: {e}")
def _add_to_latex_panel(self, equation_type: str, latex_content: str):
"""Añade una ecuación al panel LaTeX"""
self._latex_equations.append({
'type': equation_type,
'content': latex_content
})
# Solo actualizar el panel si está visible/activo
if self.main_window.latex_panel_visible:
self.main_window.latex_panel.add_equation(equation_type, latex_content)
# Actualizar indicador visual
self._update_latex_indicator()
def _update_latex_indicator(self):
"""Actualiza el indicador visual de contenido LaTeX"""
equation_count = len(self._latex_equations)
if equation_count > 0:
self.main_window.latex_button.setToolTip(f"📐 Panel LaTeX ({equation_count} ecuaciones)")
else:
self.main_window.latex_button.setToolTip("📐 Panel LaTeX (sin ecuaciones)")
def _display_output(self, output_data: List[List[tuple]]):
"""Muestra los datos de salida en el widget"""
self.main_window.output_text.clear()
self.main_window.output_text.clickable_links.clear()
cursor = self.main_window.output_text.textCursor()
for line_idx, line_parts in enumerate(output_data):
if line_idx > 0:
cursor.insertText("\n")
if not line_parts or (len(line_parts) == 1 and line_parts[0][0] == "" and line_parts[0][1] == ""):
continue
for part_idx, part_data in enumerate(line_parts):
if len(part_data) >= 4 and part_data[0] == "clickable":
# Link clickeable
_, display_text, link_id, result_object = part_data
start_pos = cursor.position()
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)
elif len(part_data) >= 2:
tag, content = part_data[0], part_data[1]
if not content:
continue
if part_idx > 0:
prev_tag = line_parts[part_idx-1][0] if part_idx > 0 else None
if tag not in ["type_indicator"] and prev_tag:
cursor.insertText(" ; ")
elif tag == "type_indicator" and prev_tag:
cursor.insertText(" ")
format_obj = self.output_formats.get(tag, None)
if format_obj:
cursor.insertText(str(content), format_obj)
else:
cursor.insertText(str(content))
def _clear_output(self):
"""Limpia el panel de salida"""
self.main_window.output_text.clear()
def _show_error(self, error_msg: str):
"""Muestra un error en el panel de salida"""
self.main_window.output_text.clear()
cursor = self.main_window.output_text.textCursor()
cursor.insertText(error_msg, self.output_formats['error'])
# Intentar obtener ayuda para el error
try:
input_content = self.main_window.input_text.toPlainText()
last_line = input_content.strip().split('\n')[-1] if input_content.strip() else ""
if last_line:
ayuda = self._obtener_ayuda(last_line)
if ayuda:
cursor.insertText("\n\n💡 Ayuda:\n", self.output_formats['helper'])
cursor.insertText(ayuda, self.output_formats['helper'])
except Exception as e:
self.logger.debug(f"Error obteniendo ayuda: {e}")
def _obtener_ayuda(self, input_str: str) -> Optional[str]:
"""Obtiene ayuda usando helpers dinámicos"""
for helper in self.main_window.HELPERS:
try:
ayuda = helper(input_str)
if ayuda:
return ayuda
except Exception as e:
self.logger.debug(f"Error en helper: {e}")
return None
def trigger_initial_latex_render(self):
"""Activa el renderizado inicial del panel LaTeX cuando MathJax está listo"""
try:
# Solo hacer evaluación inicial si hay contenido en el input
input_content = self.main_window.input_text.toPlainText()
if input_content.strip():
logging.debug("🎯 Activando renderizado inicial de LaTeX")
self.evaluate_and_update()
except Exception as e:
logging.error(f"Error en renderizado inicial de LaTeX: {e}")
def clear_latex_equations(self):
"""Limpia las ecuaciones LaTeX"""
self._latex_equations.clear()
self._update_latex_indicator()
def get_latex_equations(self):
"""Obtiene las ecuaciones LaTeX actuales"""
return self._latex_equations.copy() if self._latex_equations else []
def schedule_evaluation(self):
"""Programa una evaluación con debounce"""
self._debounce_timer.stop()
self._debounce_timer.start(300) # 300ms de debounce