Files
IPSExtractor/gui/tabs/settings_tab.py
T

315 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Pestaña de Configuración de la aplicación GUI."""
import customtkinter as ctk
from gui.styles import AppStyles
from application.app_prefs import (
format_moderadora_text_from_prefs,
get_allowed_document_types,
load_cop_mod_preferences,
normalize_document_types,
parse_moderadora_text,
default_moderadora_by_year_payload,
)
class SettingsTab:
"""Encapsula la UI de la pestaña Configuración."""
def __init__(self, parent_tab):
"""Inicializa la pestaña de Configuración.
Args:
parent_tab: El CTkFrame que representa la pestaña.
"""
self.tab = parent_tab
self.fonts = AppStyles.get_fonts()
self.colors = AppStyles.get_colors()
self.spacing = AppStyles.get_spacing()
self._setup_layout()
self._create_api_section()
self._create_processing_section()
self._create_document_types_section()
self._create_cop_mod_section()
self._create_save_button()
def _setup_layout(self):
"""Configura el layout: área con scroll y barra inferior fija con Guardar."""
self.tab.grid_columnconfigure(0, weight=1)
self.tab.grid_rowconfigure(0, weight=1)
self.tab.grid_rowconfigure(1, weight=0)
self.settings_container = ctk.CTkScrollableFrame(
self.tab,
fg_color="transparent",
)
self.settings_container.grid(row=0, column=0, sticky="nsew", padx=20, pady=(20, 8))
self.settings_container.grid_columnconfigure(0, weight=1)
self.save_footer = ctk.CTkFrame(self.tab, fg_color="transparent")
self.save_footer.grid(row=1, column=0, sticky="ew", padx=20, pady=(4, 16))
self.save_footer.grid_columnconfigure(0, weight=1)
def _create_api_section(self):
"""Crea la sección de configuración del modelo LLM."""
self.frame_api = ctk.CTkFrame(self.settings_container)
self.frame_api.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
self.frame_api.grid_columnconfigure(1, weight=1)
# Título
lbl_api_title = ctk.CTkLabel(
self.frame_api,
text="Configuración del Modelo LLM",
font=self.fonts["section"]
)
lbl_api_title.grid(row=0, column=0, columnspan=2, sticky="w", padx=15, pady=10)
# Selector de modelo
lbl_model = ctk.CTkLabel(
self.frame_api,
text="Modelo (extracción):",
font=self.fonts["ui"]
)
lbl_model.grid(row=1, column=0, padx=15, pady=10, sticky="w")
self.combo_model = ctk.CTkComboBox(
self.frame_api,
values=[
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.4-nano",
"gpt-4.1-mini",
"grok-4.3",
],
width=250,
font=self.fonts["ui"]
)
self.combo_model.grid(row=1, column=1, padx=15, pady=10, sticky="w")
self.combo_model.set("gpt-5.4")
# Texto de ayuda
help_text = (
"ℹ gpt-5.4: Modelo predeterminado (OpenAI / compatible)\n"
"ℹ gpt-5.4-mini / gpt-4.1-mini: opciones más rápidas\n"
"ℹ gpt-5.4-nano: opción más económica\n"
"ℹ grok-4.3: API xAI (requiere GROK_API_KEY en .env)"
)
lbl_help = ctk.CTkLabel(
self.frame_api,
text=help_text,
font=self.fonts["italic_small"],
text_color=self.colors["gray_light"],
justify="left"
)
lbl_help.grid(row=2, column=1, padx=15, pady=(0, 10), sticky="w")
def _create_processing_section(self):
"""Crea la sección de procesamiento y rendimiento."""
self.frame_proc = ctk.CTkFrame(self.settings_container)
self.frame_proc.grid(row=1, column=0, sticky="ew", padx=5, pady=5)
self.frame_proc.grid_columnconfigure(1, weight=1)
# Título
lbl_proc_title = ctk.CTkLabel(
self.frame_proc,
text="Procesamiento y Rendimiento",
font=self.fonts["section"]
)
lbl_proc_title.grid(row=0, column=0, columnspan=2, sticky="w", padx=15, pady=10)
# Límite de concurrencia
lbl_conc = ctk.CTkLabel(
self.frame_proc,
text="Límite de Concurrencia:",
font=self.fonts["ui"]
)
lbl_conc.grid(row=1, column=0, padx=15, pady=10, sticky="w")
self.concurrency_var = ctk.StringVar(value="100")
self.concurrency_entry = ctk.CTkEntry(
self.frame_proc,
textvariable=self.concurrency_var,
width=100,
font=self.fonts["ui"]
)
self.concurrency_entry.grid(row=1, column=1, padx=15, pady=10, sticky="w")
# Advertencia
warning_text = (
"⚠️ Advertencia: Entre mayor el número, mayor la probabilidad de Rate Limits (429)."
)
lbl_conc_warn = ctk.CTkLabel(
self.frame_proc,
text=warning_text,
font=self.fonts["italic_small"],
text_color="orange"
)
lbl_conc_warn.grid(row=2, column=0, columnspan=2, padx=15, pady=(0, 10), sticky="w")
def _create_document_types_section(self):
"""Crea la sección editable de tipos documentales permitidos."""
self.frame_doc_types = ctk.CTkFrame(self.settings_container)
self.frame_doc_types.grid(row=2, column=0, sticky="ew", padx=5, pady=5)
self.frame_doc_types.grid_columnconfigure(0, weight=1)
lbl_title = ctk.CTkLabel(
self.frame_doc_types,
text="Tipos de Documento Permitidos",
font=self.fonts["section"]
)
lbl_title.grid(row=0, column=0, sticky="w", padx=15, pady=(10, 4))
lbl_help = ctk.CTkLabel(
self.frame_doc_types,
text="Escríbelos separados por comas. Se usarán en documento_forzate. NIT se ignorará automáticamente.",
font=self.fonts["italic_small"],
text_color=self.colors["gray_light"],
justify="left"
)
lbl_help.grid(row=1, column=0, sticky="w", padx=15, pady=(0, 6))
self.allowed_doc_types_entry = ctk.CTkEntry(
self.frame_doc_types,
font=self.fonts["ui"],
placeholder_text="CC, TI, CE, RC, PA, PPT, PEP, EPT, NUIP"
)
self.allowed_doc_types_entry.grid(row=2, column=0, sticky="ew", padx=15, pady=(0, 10))
self.set_allowed_document_types(list(get_allowed_document_types()))
def _create_cop_mod_section(self):
"""Cuotas moderadora por año (regla Separa COP/MOD)."""
self.frame_cop_mod = ctk.CTkFrame(self.settings_container)
self.frame_cop_mod.grid(row=3, column=0, sticky="ew", padx=5, pady=5)
self.frame_cop_mod.grid_columnconfigure(0, weight=1)
lbl_title = ctk.CTkLabel(
self.frame_cop_mod,
text="Cuota moderadora (COP/MOD)",
font=self.fonts["section"],
)
lbl_title.grid(row=0, column=0, sticky="w", padx=15, pady=(10, 4))
lbl_help = ctk.CTkLabel(
self.frame_cop_mod,
text=(
"Una línea por año: 2024: 4500, 18200, 47700. "
"La columna «Fecha Servicio» se elige en Proceso de Limpieza al activar la regla."
),
font=self.fonts["italic_small"],
text_color=self.colors["gray_light"],
justify="left",
)
lbl_help.grid(row=1, column=0, sticky="w", padx=15, pady=(0, 6))
merged, fecha_def = load_cop_mod_preferences()
initial_txt = format_moderadora_text_from_prefs(merged)
self.moderadora_text = ctk.CTkTextbox(
self.frame_cop_mod,
font=self.fonts["mono_small"],
height=140,
)
self.moderadora_text.grid(row=2, column=0, sticky="ew", padx=15, pady=(0, 8))
self.moderadora_text.insert("1.0", initial_txt)
row_fe = ctk.CTkFrame(self.frame_cop_mod, fg_color="transparent")
row_fe.grid(row=3, column=0, sticky="ew", padx=15, pady=(0, 6))
row_fe.grid_columnconfigure(1, weight=1)
ctk.CTkLabel(
row_fe,
text="Sugerencia nombre columna fecha (opcional):",
font=self.fonts["ui"],
).grid(row=0, column=0, padx=(0, 10), sticky="w")
self.fecha_column_default_var = ctk.StringVar(value=fecha_def)
self.fecha_column_default_entry = ctk.CTkEntry(
row_fe,
textvariable=self.fecha_column_default_var,
font=self.fonts["ui"],
placeholder_text="Fecha Servicio",
)
self.fecha_column_default_entry.grid(row=0, column=1, sticky="ew")
self.btn_moderadora_defaults = ctk.CTkButton(
self.frame_cop_mod,
text="Restaurar valores por defecto",
font=self.fonts["ui"],
command=self._on_restore_moderadora_defaults,
)
self.btn_moderadora_defaults.grid(row=4, column=0, sticky="w", padx=15, pady=(0, 12))
def _on_restore_moderadora_defaults(self) -> None:
txt = format_moderadora_text_from_prefs(default_moderadora_by_year_payload())
self.moderadora_text.delete("1.0", "end")
self.moderadora_text.insert("1.0", txt)
def _create_save_button(self):
"""Barra inferior fija: siempre visible aunque el formulario sea largo."""
self.btn_save_settings = ctk.CTkButton(
self.save_footer,
text="Guardar cambios",
font=self.fonts["ui"],
height=38,
)
self.btn_save_settings.grid(row=0, column=0, sticky="ew")
# --- API Pública ---
def set_on_save(self, callback):
"""Vincula el callback al botón de guardar."""
self.btn_save_settings.configure(command=callback)
def get_selected_model(self) -> str:
"""Devuelve el modelo seleccionado."""
return self.combo_model.get()
def set_selected_model(self, model: str) -> None:
"""Establece el modelo seleccionado."""
self.combo_model.set(model)
def get_concurrency_limit(self) -> int:
"""Devuelve el límite de concurrencia."""
try:
return int(self.concurrency_var.get())
except Exception:
return 20
def set_concurrency_limit(self, limit: int) -> None:
"""Establece el límite de concurrencia."""
self.concurrency_var.set(str(limit))
def get_allowed_document_types(self) -> list[str]:
"""Devuelve la lista editable de tipos documentales válidos."""
raw = self.allowed_doc_types_entry.get().strip()
values = [part.strip() for part in raw.split(",")]
return normalize_document_types(values)
def set_allowed_document_types(self, values: list[str]) -> None:
"""Establece la lista de tipos documentales válidos."""
self.allowed_doc_types_entry.delete(0, "end")
self.allowed_doc_types_entry.insert(0, ", ".join(normalize_document_types(values)))
def get_moderadora_cuotas_text(self) -> str:
"""Texto multilínea año: valores."""
return self.moderadora_text.get("1.0", "end").strip()
def set_moderadora_cuotas_ui(self, payload: dict[str, list[int]], fecha_hint: str) -> None:
"""Establece cuotas y sugerencia de columna fecha desde prefs."""
self.moderadora_text.delete("1.0", "end")
self.moderadora_text.insert("1.0", format_moderadora_text_from_prefs(payload))
self.fecha_column_default_var.set(fecha_hint or "")
def get_fecha_servicio_column_hint(self) -> str:
return str(self.fecha_column_default_var.get() or "").strip()
def enable_controls(self, enabled: bool):
"""Habilita o deshabilita los controles."""
state = "normal" if enabled else "disabled"
self.combo_model.configure(state=state)
self.concurrency_entry.configure(state=state)
self.allowed_doc_types_entry.configure(state=state)
self.moderadora_text.configure(state=state)
self.fecha_column_default_entry.configure(state=state)
self.btn_moderadora_defaults.configure(state=state)
self.btn_save_settings.configure(state=state)