315 lines
12 KiB
Python
315 lines
12 KiB
Python
"""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)
|