1239 lines
52 KiB
Python
1239 lines
52 KiB
Python
"""Vista principal de la aplicación GUI con `customtkinter`.
|
||
|
||
Expone métodos para enlazar callbacks, leer entradas y escribir salida en un
|
||
área de texto, además de habilitar/deshabilitar controles durante el proceso.
|
||
"""
|
||
import os # Necesario para basename en update_res_table
|
||
import customtkinter as ctk
|
||
|
||
# matplotlib es opcional
|
||
try:
|
||
import matplotlib
|
||
matplotlib.use("Agg") # backend no interactivo para render en canvas
|
||
from matplotlib.figure import Figure
|
||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||
HAS_MPL = True
|
||
except Exception:
|
||
HAS_MPL = False
|
||
|
||
|
||
class InvoiceAppView(ctk.CTk):
|
||
"""Ventana principal con pestañas de Procesar y Configuración."""
|
||
def __init__(self):
|
||
"""Construye la ventana principal y sus pestañas/controles."""
|
||
super().__init__()
|
||
self.title("IPSExtractor - Procesador de Documentos")
|
||
self.geometry("1100x800")
|
||
|
||
ctk.set_appearance_mode("System")
|
||
ctk.set_default_color_theme("blue")
|
||
|
||
# Fuentes personalizadas
|
||
self.font_title = ctk.CTkFont(family="Roboto", size=22, weight="bold")
|
||
self.font_section = ctk.CTkFont(family="Roboto", size=16, weight="bold")
|
||
self.font_ui = ctk.CTkFont(family="Roboto", size=13)
|
||
self.font_mono = ctk.CTkFont(family="Consolas", size=12)
|
||
|
||
# Layout principal
|
||
self.grid_columnconfigure(0, weight=1)
|
||
self.grid_rowconfigure(0, weight=1)
|
||
|
||
# TabView principal con estilo
|
||
self.tabs = ctk.CTkTabview(self, width=1000, height=700)
|
||
self.tabs.grid(row=0, column=0, sticky="nsew", padx=20, pady=20)
|
||
|
||
self.tab_main = self.tabs.add("Procesar Documentos")
|
||
self.tab_preclass = self.tabs.add("Preclasificación")
|
||
self.tab_results = self.tabs.add("Resultados")
|
||
self.tab_settings = self.tabs.add("Configuración")
|
||
self.tab_usage = self.tabs.add("Dashboard & Métricas")
|
||
|
||
# =========================================================================
|
||
# --- TAB: PROCESAR DOCUMENTOS ---
|
||
# =========================================================================
|
||
self.tab_main.grid_columnconfigure(0, weight=1)
|
||
self.tab_main.grid_rowconfigure(0, weight=1)
|
||
|
||
# Contenedor Scrollable Principal (Todo el contenido aquí)
|
||
self.scroll_container = ctk.CTkScrollableFrame(self.tab_main, fg_color="transparent")
|
||
self.scroll_container.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
||
self.scroll_container.grid_columnconfigure(0, weight=1)
|
||
|
||
# --- Sección Rutas ---
|
||
self.frame_paths = ctk.CTkFrame(self.scroll_container)
|
||
self.frame_paths.grid(row=0, column=0, sticky="ew", padx=10, pady=(5, 2))
|
||
self.frame_paths.grid_columnconfigure(1, weight=1)
|
||
|
||
# Título sección
|
||
lbl_paths = ctk.CTkLabel(self.frame_paths, text="Rutas y Archivos", font=self.font_section)
|
||
lbl_paths.grid(row=0, column=0, columnspan=3, sticky="w", padx=15, pady=(5, 2))
|
||
|
||
# Carpeta Raíz
|
||
self.root_label = ctk.CTkLabel(self.frame_paths, text="Carpeta Raíz:", font=self.font_ui)
|
||
self.root_label.grid(row=1, column=0, padx=15, pady=2, sticky="w")
|
||
|
||
self.root_entry = ctk.CTkEntry(self.frame_paths, placeholder_text="Selecciona la carpeta con los documentos...", font=self.font_ui)
|
||
self.root_entry.grid(row=1, column=1, padx=5, pady=2, sticky="ew")
|
||
|
||
self.root_button = ctk.CTkButton(self.frame_paths, text="Explorar...", font=self.font_ui, width=100)
|
||
self.root_button.grid(row=1, column=2, padx=15, pady=2)
|
||
|
||
# CSV Salida
|
||
self.csv_label = ctk.CTkLabel(self.frame_paths, text="Archivo CSV:", font=self.font_ui)
|
||
self.csv_label.grid(row=2, column=0, padx=15, pady=2, sticky="w")
|
||
|
||
self.csv_entry = ctk.CTkEntry(self.frame_paths, placeholder_text="Ruta para guardar el reporte CSV...", font=self.font_ui)
|
||
self.csv_entry.grid(row=2, column=1, padx=5, pady=2, sticky="ew")
|
||
|
||
self.csv_button = ctk.CTkButton(self.frame_paths, text="Guardar en...", font=self.font_ui, width=100, fg_color="transparent", border_width=1)
|
||
self.csv_button.grid(row=2, column=2, padx=15, pady=2)
|
||
|
||
# Selector de Modo (Radicados vs General)
|
||
self.mode_var = ctk.StringVar(value="radicados")
|
||
|
||
lbl_mode = ctk.CTkLabel(self.frame_paths, text="Estructura:", font=self.font_ui)
|
||
lbl_mode.grid(row=3, column=0, padx=15, pady=2, sticky="w")
|
||
|
||
self.frame_radios_main = ctk.CTkFrame(self.frame_paths, fg_color="transparent")
|
||
self.frame_radios_main.grid(row=3, column=1, columnspan=2, sticky="w", padx=5, pady=2)
|
||
|
||
self.radio_radicados_main = ctk.CTkRadioButton(
|
||
self.frame_radios_main,
|
||
text="Por Radicados (Subcarpetas)",
|
||
variable=self.mode_var,
|
||
value="radicados",
|
||
font=self.font_ui,
|
||
command=self._update_ui_for_mode
|
||
)
|
||
self.radio_radicados_main.pack(side="left", padx=(0, 20))
|
||
|
||
self.radio_general_main = ctk.CTkRadioButton(
|
||
self.frame_radios_main,
|
||
text="Carpeta General (Archivos Planos)",
|
||
variable=self.mode_var,
|
||
value="general",
|
||
font=self.font_ui,
|
||
command=self._update_ui_for_mode
|
||
)
|
||
self.radio_general_main.pack(side="left")
|
||
|
||
# --- Sección Configuración ---
|
||
self.frame_config = ctk.CTkFrame(self.scroll_container)
|
||
self.frame_config.grid(row=1, column=0, sticky="ew", padx=10, pady=2)
|
||
self.frame_config.grid_columnconfigure(0, weight=1)
|
||
|
||
# Keywords oculto (legacy)
|
||
self.kw_label = ctk.CTkLabel(self.frame_config, text="Keywords:")
|
||
self.kw_entry = ctk.CTkEntry(self.frame_config)
|
||
self.kw_entry.insert(0, "Factura_O_Documento_Equivalente,factura")
|
||
|
||
# Tipos de Documento
|
||
self.doc_types_container = ctk.CTkFrame(self.frame_config, fg_color="transparent")
|
||
self.doc_types_container.grid(row=0, column=0, sticky="nsew", padx=10, pady=(5, 2))
|
||
self.doc_types_container.grid_columnconfigure(0, weight=1)
|
||
|
||
self.doc_types_header = ctk.CTkFrame(self.doc_types_container, fg_color="transparent")
|
||
self.doc_types_header.grid(row=0, column=0, sticky="ew", pady=(0, 2))
|
||
self.doc_types_header.grid_columnconfigure(0, weight=1)
|
||
|
||
self.doc_types_label = ctk.CTkLabel(self.doc_types_header, text="Definición de Tipos de Documento", font=self.font_section)
|
||
self.doc_types_label.grid(row=0, column=0, sticky="w")
|
||
|
||
self.add_doc_type_btn = ctk.CTkButton(self.doc_types_header, text="+ Añadir Tipo", font=self.font_ui, width=120, height=28)
|
||
self.add_doc_type_btn.grid(row=0, column=1, sticky="e")
|
||
|
||
# Lista de Tipos: Usamos CTkFrame normal para que crezca y active el scroll del contenedor principal
|
||
self.doc_types_list = ctk.CTkFrame(self.doc_types_container, fg_color="transparent")
|
||
self.doc_types_list.grid(row=1, column=0, sticky="ew", pady=(2, 0))
|
||
self.doc_types_list.grid_columnconfigure(0, weight=1)
|
||
|
||
|
||
self._doc_type_rows = []
|
||
|
||
def _on_add_row():
|
||
self._add_doc_type_row()
|
||
self.add_doc_type_btn.configure(command=_on_add_row)
|
||
|
||
# Fila por defecto
|
||
self._add_doc_type_row(name="Factura", keyword="Factura_O_Documento_Equivalente")
|
||
|
||
# Configuración Avanzada (TIFF Range)
|
||
self.frame_advanced = ctk.CTkFrame(self.frame_config, fg_color="transparent")
|
||
self.frame_advanced.grid(row=1, column=0, sticky="ew", padx=10, pady=(0, 2))
|
||
|
||
self.tiff_start_label = ctk.CTkLabel(self.frame_advanced, text="Pág. Inicio TIFF:", font=self.font_ui)
|
||
self.tiff_start_entry = ctk.CTkEntry(self.frame_advanced, width=60)
|
||
self.tiff_start_entry.insert(0, "0")
|
||
|
||
self.tiff_end_label = ctk.CTkLabel(self.frame_advanced, text="Pág. Fin TIFF:", font=self.font_ui)
|
||
self.tiff_end_entry = ctk.CTkEntry(self.frame_advanced, width=60)
|
||
self.tiff_end_entry.insert(0, "0")
|
||
|
||
# Ocultar inicialmente
|
||
self.tiff_start_label.grid_forget()
|
||
self.tiff_start_entry.grid_forget()
|
||
self.tiff_end_label.grid_forget()
|
||
self.tiff_end_entry.grid_forget()
|
||
|
||
# Campos legacy ocultos
|
||
self.fields_entry = ctk.CTkEntry(self.tab_main)
|
||
self.fields_label = ctk.CTkLabel(self.tab_main, text="")
|
||
|
||
# --- Botón de Acción Principal ---
|
||
self.process_button = ctk.CTkButton(
|
||
self.scroll_container,
|
||
text="INICIAR PROCESAMIENTO",
|
||
font=ctk.CTkFont(size=16, weight="bold"),
|
||
height=35,
|
||
fg_color=("#106BA3", "#106BA3"),
|
||
hover_color=("#0D5582", "#0D5582")
|
||
)
|
||
self.process_button.grid(row=2, column=0, padx=15, pady=(10, 15), sticky="ew")
|
||
|
||
# --- Área de Logs / Salida (Integrada en Scroll) ---
|
||
self.log_frame = ctk.CTkFrame(self.scroll_container)
|
||
self.log_frame.grid(row=3, column=0, sticky="ew", padx=10, pady=(0, 15))
|
||
self.log_frame.grid_columnconfigure(0, weight=1)
|
||
|
||
lbl_logs = ctk.CTkLabel(self.log_frame, text="Registro de Actividad:", font=self.font_ui, text_color=("gray50", "gray70"))
|
||
lbl_logs.grid(row=0, column=0, sticky="w", padx=10, pady=(5, 0))
|
||
|
||
self.output_text = ctk.CTkTextbox(self.log_frame, font=self.font_mono, activate_scrollbars=True, height=150)
|
||
self.output_text.grid(row=1, column=0, padx=5, pady=5, sticky="ew")
|
||
|
||
# =========================================================================
|
||
# --- TAB: RESULTADOS ---
|
||
# =========================================================================
|
||
self.tab_results.grid_columnconfigure(0, weight=1)
|
||
self.tab_results.grid_rowconfigure(0, weight=1)
|
||
|
||
self.tabs_res = ctk.CTkTabview(self.tab_results)
|
||
self.tabs_res.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
|
||
|
||
self.tab_res_extractor = self.tabs_res.add("Extractor (Próximamente)")
|
||
self.tab_res_preclass = self.tabs_res.add("Preclasificación")
|
||
|
||
# --- Sub-Tab: Extractor ---
|
||
lbl_const = ctk.CTkLabel(self.tab_res_extractor, text="En construcción...", font=ctk.CTkFont(size=20))
|
||
lbl_const.place(relx=0.5, rely=0.5, anchor="center")
|
||
|
||
# --- Sub-Tab: Preclasificación ---
|
||
self.tab_res_preclass.grid_columnconfigure(0, weight=1)
|
||
self.tab_res_preclass.grid_rowconfigure(1, weight=1)
|
||
|
||
# 1. Filtros
|
||
self.frame_res_filters = ctk.CTkFrame(self.tab_res_preclass)
|
||
self.frame_res_filters.grid(row=0, column=0, sticky="ew", padx=10, pady=5)
|
||
|
||
ctk.CTkLabel(self.frame_res_filters, text="Run ID:").grid(row=0, column=0, padx=5, pady=5)
|
||
self.combo_res_run = ctk.CTkComboBox(self.frame_res_filters, values=["Todos"], width=150)
|
||
self.combo_res_run.grid(row=0, column=1, padx=5, pady=5)
|
||
|
||
ctk.CTkLabel(self.frame_res_filters, text="Estado:").grid(row=0, column=2, padx=5, pady=5)
|
||
self.combo_res_status = ctk.CTkComboBox(self.frame_res_filters, values=["Todos", "MATCH", "NO_MATCH", "ERROR", "NO_FILES"], width=120)
|
||
self.combo_res_status.grid(row=0, column=3, padx=5, pady=5)
|
||
|
||
self.entry_res_search = ctk.CTkEntry(self.frame_res_filters, placeholder_text="Buscar radicado/archivo...", width=200)
|
||
self.entry_res_search.grid(row=0, column=4, padx=5, pady=5)
|
||
|
||
self.btn_res_search = ctk.CTkButton(self.frame_res_filters, text="Buscar", width=100)
|
||
self.btn_res_search.grid(row=0, column=5, padx=10, pady=5)
|
||
|
||
# 2. Tabla (Treeview)
|
||
self.frame_res_table = ctk.CTkFrame(self.tab_res_preclass, fg_color="transparent")
|
||
self.frame_res_table.grid(row=1, column=0, sticky="nsew", padx=10, pady=5)
|
||
self.frame_res_table.grid_columnconfigure(0, weight=1)
|
||
self.frame_res_table.grid_rowconfigure(0, weight=3) # Peso para la tabla principal
|
||
self.frame_res_table.grid_rowconfigure(2, weight=2) # Peso para la tabla detalle
|
||
|
||
import tkinter.ttk as ttk
|
||
# Estilo básico para treeview
|
||
style = ttk.Style()
|
||
style.theme_use("clam")
|
||
style.configure("Treeview", rowheight=25)
|
||
|
||
# --- Tabla Principal ---
|
||
cols = ("ID", "Run", "Fecha", "Radicado", "Estado", "Archivo")
|
||
self.tree_results = ttk.Treeview(self.frame_res_table, columns=cols, show="headings", selectmode="browse")
|
||
|
||
for c in cols:
|
||
self.tree_results.heading(c, text=c)
|
||
self.tree_results.column(c, width=100)
|
||
self.tree_results.column("Archivo", width=300)
|
||
self.tree_results.column("ID", width=50)
|
||
self.tree_results.column("Run", width=50)
|
||
|
||
# Scrollbars principal
|
||
sb_y = ttk.Scrollbar(self.frame_res_table, orient="vertical", command=self.tree_results.yview)
|
||
sb_x = ttk.Scrollbar(self.frame_res_table, orient="horizontal", command=self.tree_results.xview)
|
||
self.tree_results.configure(yscrollcommand=sb_y.set, xscrollcommand=sb_x.set)
|
||
|
||
self.tree_results.grid(row=0, column=0, sticky="nsew")
|
||
sb_y.grid(row=0, column=1, sticky="ns")
|
||
sb_x.grid(row=1, column=0, sticky="ew")
|
||
|
||
# Separador / Título Detalle
|
||
lbl_detail = ctk.CTkLabel(self.frame_res_table, text="Detalle de Matches (Seleccionar fila arriba)", font=ctk.CTkFont(size=12, weight="bold"))
|
||
lbl_detail.grid(row=1, column=0, sticky="w", pady=(10, 5))
|
||
|
||
# --- Tabla Detalle ---
|
||
cols_detail = ("Key", "Valor Encontrado", "Existe", "Valor Esperado")
|
||
self.tree_matches = ttk.Treeview(self.frame_res_table, columns=cols_detail, show="headings", selectmode="browse", height=5)
|
||
|
||
for c in cols_detail:
|
||
self.tree_matches.heading(c, text=c)
|
||
self.tree_matches.column(c, width=150)
|
||
|
||
sb_detail_y = ttk.Scrollbar(self.frame_res_table, orient="vertical", command=self.tree_matches.yview)
|
||
self.tree_matches.configure(yscrollcommand=sb_detail_y.set)
|
||
|
||
self.tree_matches.grid(row=2, column=0, sticky="nsew")
|
||
sb_detail_y.grid(row=2, column=1, sticky="ns")
|
||
|
||
# 3. Acciones
|
||
self.frame_res_actions = ctk.CTkFrame(self.tab_res_preclass)
|
||
self.frame_res_actions.grid(row=2, column=0, sticky="ew", padx=10, pady=10)
|
||
|
||
self.btn_res_export = ctk.CTkButton(self.frame_res_actions, text="Exportar Vista a Excel", fg_color="green")
|
||
self.btn_res_export.pack(side="right", padx=10, pady=5)
|
||
|
||
self.btn_res_organize = ctk.CTkButton(self.frame_res_actions, text="Organizar Carpetas (Run)", fg_color="#D35400", hover_color="#A04000")
|
||
self.btn_res_organize.pack(side="right", padx=10, pady=5)
|
||
|
||
self.btn_res_refresh = ctk.CTkButton(self.frame_res_actions, text="Refrescar Runs")
|
||
self.btn_res_refresh.pack(side="left", padx=10, pady=5)
|
||
|
||
|
||
# =========================================================================
|
||
# --- TAB: CONFIGURACIÓN ---
|
||
# =========================================================================
|
||
self.tab_settings.grid_columnconfigure(0, weight=1)
|
||
self.tab_settings.grid_rowconfigure(0, weight=1)
|
||
|
||
self.settings_container = ctk.CTkFrame(self.tab_settings, fg_color="transparent")
|
||
self.settings_container.grid(row=0, column=0, sticky="nsew", padx=20, pady=20)
|
||
self.settings_container.grid_columnconfigure(0, weight=1)
|
||
|
||
# --- Sección API / Modelo ---
|
||
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)
|
||
|
||
lbl_api_title = ctk.CTkLabel(self.frame_api, text="Configuración del Modelo LLM", font=self.font_section)
|
||
lbl_api_title.grid(row=0, column=0, columnspan=2, sticky="w", padx=15, pady=10)
|
||
|
||
self.lbl_model = ctk.CTkLabel(self.frame_api, text="Modelo (Ollama):", font=self.font_ui)
|
||
self.lbl_model.grid(row=1, column=0, padx=15, pady=10, sticky="w")
|
||
|
||
self.combo_model = ctk.CTkComboBox(
|
||
self.frame_api,
|
||
values=["5.1", "4.1", "4.1 mini", "4.1 nano"],
|
||
width=250,
|
||
font=self.font_ui
|
||
)
|
||
self.combo_model.grid(row=1, column=1, padx=15, pady=10, sticky="w")
|
||
self.combo_model.set("4.1")
|
||
|
||
# Texto de ayuda para modelos
|
||
lbl_help = ctk.CTkLabel(
|
||
self.frame_api,
|
||
text="ℹ 4.1 mini: Muy rápido (recomendado)\nℹ 4.1: Estándar (balanceado)\nℹ 5.1: Lento pero mayor razonamiento",
|
||
font=ctk.CTkFont(size=12, slant="italic"),
|
||
text_color="gray60",
|
||
justify="left"
|
||
)
|
||
lbl_help.grid(row=2, column=1, padx=15, pady=(0, 10), sticky="w")
|
||
|
||
# --- Sección Procesamiento ---
|
||
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)
|
||
|
||
lbl_proc_title = ctk.CTkLabel(self.frame_proc, text="Procesamiento y Rendimiento", font=self.font_section)
|
||
lbl_proc_title.grid(row=0, column=0, columnspan=2, sticky="w", padx=15, pady=10)
|
||
|
||
lbl_conc = ctk.CTkLabel(self.frame_proc, text="Límite de Concurrencia:", font=self.font_ui)
|
||
lbl_conc.grid(row=1, column=0, padx=15, pady=10, sticky="w")
|
||
|
||
self.concurrency_var = ctk.StringVar(value="20")
|
||
self.concurrency_entry = ctk.CTkEntry(self.frame_proc, textvariable=self.concurrency_var, width=100, font=self.font_ui)
|
||
self.concurrency_entry.grid(row=1, column=1, padx=15, pady=10, sticky="w")
|
||
|
||
lbl_conc_warn = ctk.CTkLabel(
|
||
self.frame_proc,
|
||
text="⚠️ Advertencia: Entre mayor el número, mayor la probabilidad de Rate Limits (429).",
|
||
font=ctk.CTkFont(size=12, slant="italic"),
|
||
text_color="orange"
|
||
)
|
||
lbl_conc_warn.grid(row=2, column=0, columnspan=2, padx=15, pady=(0, 10), sticky="w")
|
||
|
||
# Botón Guardar (Fuera de los frames específicos, al final)
|
||
self.btn_save_settings = ctk.CTkButton(
|
||
self.settings_container,
|
||
text="Aplicar Configuración",
|
||
font=self.font_ui,
|
||
height=35
|
||
)
|
||
self.btn_save_settings.grid(row=2, column=0, padx=15, pady=15, sticky="ew")
|
||
|
||
# =========================================================================
|
||
# --- TAB: PRECLASIFICACIÓN ---
|
||
# =========================================================================
|
||
self.tab_preclass.grid_columnconfigure(0, weight=1)
|
||
self.tab_preclass.grid_rowconfigure(0, weight=1)
|
||
|
||
self.preclass_container = ctk.CTkScrollableFrame(self.tab_preclass, fg_color="transparent")
|
||
self.preclass_container.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
||
self.preclass_container.grid_columnconfigure(0, weight=1)
|
||
# En un ScrollableFrame, grid_rowconfigure con weight=1 hace que esa fila intente ocupar
|
||
# el espacio vertical disponible en el viewport.
|
||
# Row 3 es patterns, Row 5 es logs.
|
||
self.preclass_container.grid_rowconfigure(3, weight=1)
|
||
self.preclass_container.grid_rowconfigure(5, weight=1)
|
||
|
||
# --- Sección: Carpeta Raíz ---
|
||
self.preclass_frame_root = ctk.CTkFrame(self.preclass_container)
|
||
self.preclass_frame_root.grid(row=0, column=0, sticky="ew", padx=5, pady=(5, 10))
|
||
self.preclass_frame_root.grid_columnconfigure(1, weight=1)
|
||
|
||
lbl_pre_root_title = ctk.CTkLabel(self.preclass_frame_root, text="Carpeta Raíz (Preclasificación)", font=self.font_section)
|
||
lbl_pre_root_title.grid(row=0, column=0, columnspan=3, sticky="w", padx=15, pady=(10, 5))
|
||
|
||
self.preclass_root_label = ctk.CTkLabel(self.preclass_frame_root, text="Carpeta Raíz:", font=self.font_ui)
|
||
self.preclass_root_label.grid(row=1, column=0, padx=15, pady=(0, 10), sticky="w")
|
||
|
||
self.preclass_root_entry = ctk.CTkEntry(
|
||
self.preclass_frame_root,
|
||
placeholder_text="Selecciona la carpeta raíz para preclasificar...",
|
||
font=self.font_ui
|
||
)
|
||
self.preclass_root_entry.grid(row=1, column=1, padx=5, pady=(0, 10), sticky="ew")
|
||
|
||
self.preclass_root_button = ctk.CTkButton(self.preclass_frame_root, text="Explorar...", font=self.font_ui, width=100)
|
||
self.preclass_root_button.grid(row=1, column=2, padx=15, pady=(0, 10))
|
||
|
||
# Selector de Modo (Radicados vs General)
|
||
self.preclass_mode_var = ctk.StringVar(value="radicados")
|
||
|
||
lbl_mode = ctk.CTkLabel(self.preclass_frame_root, text="Estructura:", font=self.font_ui)
|
||
lbl_mode.grid(row=2, column=0, padx=15, pady=(0, 10), sticky="w")
|
||
|
||
self.frame_radios = ctk.CTkFrame(self.preclass_frame_root, fg_color="transparent")
|
||
self.frame_radios.grid(row=2, column=1, columnspan=2, sticky="w", padx=5, pady=(0, 10))
|
||
|
||
self.radio_radicados = ctk.CTkRadioButton(
|
||
self.frame_radios,
|
||
text="Por Radicados (Subcarpetas)",
|
||
variable=self.preclass_mode_var,
|
||
value="radicados",
|
||
font=self.font_ui
|
||
)
|
||
self.radio_radicados.pack(side="left", padx=(0, 20))
|
||
|
||
self.radio_general = ctk.CTkRadioButton(
|
||
self.frame_radios,
|
||
text="Carpeta General (Archivos Planos)",
|
||
variable=self.preclass_mode_var,
|
||
value="general",
|
||
font=self.font_ui
|
||
)
|
||
self.radio_general.pack(side="left")
|
||
|
||
# --- Sección: Opciones de Rango (Preclasificación) ---
|
||
self.preclass_frame_options = ctk.CTkFrame(self.preclass_container)
|
||
self.preclass_frame_options.grid(row=1, column=0, sticky="ew", padx=5, pady=(0, 10))
|
||
self.preclass_frame_options.grid_columnconfigure(1, weight=1)
|
||
|
||
self.preclass_range_var = ctk.BooleanVar(value=False)
|
||
self.preclass_range_cb = ctk.CTkCheckBox(
|
||
self.preclass_frame_options,
|
||
text="Habilitar rangos de página",
|
||
variable=self.preclass_range_var,
|
||
font=self.font_ui,
|
||
command=self._on_toggle_preclass_range
|
||
)
|
||
self.preclass_range_cb.grid(row=0, column=0, padx=15, pady=10, sticky="w")
|
||
|
||
self.preclass_range_inputs_frame = ctk.CTkFrame(self.preclass_frame_options, fg_color="transparent")
|
||
# Se mostrará solo si se habilita
|
||
|
||
lbl_desde = ctk.CTkLabel(self.preclass_range_inputs_frame, text="Desde:", font=self.font_ui)
|
||
lbl_desde.grid(row=0, column=0, padx=(10, 5), pady=5)
|
||
self.preclass_range_start_entry = ctk.CTkEntry(self.preclass_range_inputs_frame, width=60, font=self.font_ui)
|
||
self.preclass_range_start_entry.insert(0, "1")
|
||
self.preclass_range_start_entry.grid(row=0, column=1, padx=5, pady=5)
|
||
|
||
lbl_hasta = ctk.CTkLabel(self.preclass_range_inputs_frame, text="Hasta:", font=self.font_ui)
|
||
lbl_hasta.grid(row=0, column=2, padx=(10, 5), pady=5)
|
||
self.preclass_range_end_entry = ctk.CTkEntry(self.preclass_range_inputs_frame, width=60, font=self.font_ui)
|
||
self.preclass_range_end_entry.insert(0, "1")
|
||
self.preclass_range_end_entry.grid(row=0, column=3, padx=5, pady=5)
|
||
|
||
# --- Sección: Opciones de Archivo Objetivo (Preclasificación) ---
|
||
self.preclass_frame_target = ctk.CTkFrame(self.preclass_container)
|
||
self.preclass_frame_target.grid(row=2, column=0, sticky="ew", padx=5, pady=(0, 10))
|
||
self.preclass_frame_target.grid_columnconfigure(0, weight=1)
|
||
|
||
self.preclass_target_var = ctk.BooleanVar(value=False)
|
||
self.preclass_target_cb = ctk.CTkCheckBox(
|
||
self.preclass_frame_target,
|
||
text="Habilitar archivo objetivo",
|
||
variable=self.preclass_target_var,
|
||
font=self.font_ui,
|
||
command=self._on_toggle_preclass_target
|
||
)
|
||
self.preclass_target_cb.grid(row=0, column=0, padx=15, pady=10, sticky="w")
|
||
|
||
self.preclass_target_inputs_frame = ctk.CTkFrame(self.preclass_frame_target, fg_color="transparent")
|
||
# Se mostrará solo si se habilita (en row 1 para que aparezca ABAJO del checkbox)
|
||
|
||
lbl_target_def = ctk.CTkLabel(self.preclass_target_inputs_frame, text="Def. Tipo Doc:", font=self.font_ui)
|
||
lbl_target_def.grid(row=0, column=0, padx=(15, 5), pady=5, sticky="w")
|
||
self.preclass_target_def_entry = ctk.CTkEntry(self.preclass_target_inputs_frame, width=250, font=self.font_ui, placeholder_text="Ej: Factura de Venta")
|
||
self.preclass_target_def_entry.grid(row=0, column=1, padx=5, pady=5, sticky="w")
|
||
|
||
lbl_target_pattern = ctk.CTkLabel(self.preclass_target_inputs_frame, text="Patrón:", font=self.font_ui)
|
||
lbl_target_pattern.grid(row=0, column=2, padx=(20, 5), pady=5, sticky="w")
|
||
self.preclass_target_pattern_entry = ctk.CTkEntry(self.preclass_target_inputs_frame, width=200, font=self.font_ui, placeholder_text="Ej: *_FAC_*.pdf")
|
||
self.preclass_target_pattern_entry.grid(row=0, column=3, padx=5, pady=5, sticky="w")
|
||
|
||
# --- Sección: Patrones ---
|
||
self.preclass_frame_patterns = ctk.CTkFrame(self.preclass_container)
|
||
self.preclass_frame_patterns.grid(row=3, column=0, sticky="nsew", padx=5, pady=5)
|
||
self.preclass_frame_patterns.grid_columnconfigure(0, weight=1)
|
||
|
||
self.preclass_patterns_header = ctk.CTkFrame(self.preclass_frame_patterns, fg_color="transparent")
|
||
self.preclass_patterns_header.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 2))
|
||
self.preclass_patterns_header.grid_columnconfigure(0, weight=1)
|
||
|
||
self.preclass_patterns_label = ctk.CTkLabel(self.preclass_patterns_header, text="Patrones", font=self.font_section)
|
||
self.preclass_patterns_label.grid(row=0, column=0, sticky="w")
|
||
|
||
self.add_preclass_pattern_btn = ctk.CTkButton(self.preclass_patterns_header, text="+ Añadir Patrón", font=self.font_ui, width=140, height=28)
|
||
self.add_preclass_pattern_btn.grid(row=0, column=1, sticky="e")
|
||
|
||
self.preclass_patterns_hint = ctk.CTkLabel(
|
||
self.preclass_frame_patterns,
|
||
text="Agrega patrones de texto. Cada patrón puede ser solo 'key' o 'key + value'.",
|
||
font=ctk.CTkFont(size=12, slant="italic"),
|
||
text_color="gray60",
|
||
justify="left"
|
||
)
|
||
self.preclass_patterns_hint.grid(row=1, column=0, sticky="w", padx=15, pady=(0, 5))
|
||
|
||
self.preclass_patterns_list = ctk.CTkFrame(self.preclass_frame_patterns, fg_color="transparent")
|
||
self.preclass_patterns_list.grid(row=2, column=0, sticky="ew", padx=10, pady=(2, 10))
|
||
self.preclass_patterns_list.grid_columnconfigure(0, weight=1)
|
||
|
||
self._preclass_pattern_rows = []
|
||
|
||
def _on_add_preclass_row():
|
||
self._add_preclass_pattern_row()
|
||
self.add_preclass_pattern_btn.configure(command=_on_add_preclass_row)
|
||
|
||
# Fila por defecto (vacía)
|
||
self._add_preclass_pattern_row()
|
||
|
||
# --- Sección: Botón de Ejecución y Logs ---
|
||
self.preclass_action_frame = ctk.CTkFrame(self.preclass_container, fg_color="transparent")
|
||
self.preclass_action_frame.grid(row=4, column=0, sticky="ew", padx=5, pady=10)
|
||
self.preclass_action_frame.grid_columnconfigure(0, weight=1)
|
||
|
||
self.preclass_run_btn = ctk.CTkButton(
|
||
self.preclass_action_frame,
|
||
text="EJECUTAR PRECLASIFICACIÓN",
|
||
font=ctk.CTkFont(size=16, weight="bold"),
|
||
height=40,
|
||
fg_color=("#2E86C1", "#2E86C1"),
|
||
hover_color=("#21618C", "#21618C")
|
||
)
|
||
self.preclass_run_btn.pack(side="left", fill="x", expand=True, padx=10)
|
||
|
||
self.preclass_export_btn = ctk.CTkButton(
|
||
self.preclass_action_frame,
|
||
text="Exportar Excel",
|
||
font=self.font_ui,
|
||
height=40,
|
||
state="disabled",
|
||
fg_color=("#27AE60", "#27AE60"),
|
||
hover_color=("#1E8449", "#1E8449")
|
||
)
|
||
self.preclass_export_btn.pack(side="left", padx=(0, 10))
|
||
|
||
self.preclass_organize_btn = ctk.CTkButton(
|
||
self.preclass_action_frame,
|
||
text="Organizar Carpetas",
|
||
font=self.font_ui,
|
||
height=40,
|
||
state="disabled",
|
||
fg_color=("#D35400", "#D35400"),
|
||
hover_color=("#A04000", "#A04000")
|
||
)
|
||
self.preclass_organize_btn.pack(side="left", padx=(0, 10))
|
||
|
||
self.preclass_log_frame = ctk.CTkFrame(self.preclass_container)
|
||
self.preclass_log_frame.grid(row=5, column=0, sticky="nsew", padx=5, pady=(0, 10))
|
||
self.preclass_log_frame.grid_columnconfigure(0, weight=1)
|
||
self.preclass_log_frame.grid_rowconfigure(1, weight=1)
|
||
|
||
# Ajustar pesos del contenedor principal para que el log expanda
|
||
self.preclass_container.grid_rowconfigure(5, weight=1)
|
||
# Quitar peso a la fila de patrones si queremos que el log ocupe más espacio,
|
||
# o compartir. Actualmente row 3 (patrones) tiene weight.
|
||
# Vamos a dejar patterns con weight 1 y logs con weight 1 para compartir espacio.
|
||
self.preclass_container.grid_rowconfigure(3, weight=1)
|
||
|
||
lbl_pre_logs = ctk.CTkLabel(self.preclass_log_frame, text="Resultados:", font=self.font_ui, text_color=("gray50", "gray70"))
|
||
lbl_pre_logs.grid(row=0, column=0, sticky="w", padx=10, pady=(5, 0))
|
||
|
||
self.preclass_output = ctk.CTkTextbox(self.preclass_log_frame, font=self.font_mono, activate_scrollbars=True)
|
||
self.preclass_output.grid(row=1, column=0, padx=5, pady=5, sticky="nsew")
|
||
|
||
|
||
# =========================================================================
|
||
# --- TAB: DASHBOARD & MÉTRICAS ---
|
||
# =========================================================================
|
||
self.tab_usage.grid_columnconfigure(0, weight=1)
|
||
self.tab_usage.grid_rowconfigure(1, weight=1)
|
||
self.tab_usage.grid_rowconfigure(3, weight=1)
|
||
|
||
# Panel de KPIs (Top)
|
||
self.kpi_frame = ctk.CTkFrame(self.tab_usage)
|
||
self.kpi_frame.grid(row=0, column=0, sticky="ew", padx=15, pady=15)
|
||
self.kpi_frame.grid_columnconfigure((0, 1, 2), weight=1)
|
||
|
||
# Helper para crear cards KPI
|
||
def create_kpi_card(parent, title, row, col):
|
||
card = ctk.CTkFrame(parent)
|
||
card.grid(row=row, column=col, sticky="nsew", padx=5, pady=5)
|
||
lbl_title = ctk.CTkLabel(card, text=title, font=ctk.CTkFont(size=14, weight="bold"))
|
||
lbl_title.pack(pady=(10, 5), padx=10, anchor="w")
|
||
lbl_content = ctk.CTkLabel(card, text="...", font=self.font_mono, justify="left")
|
||
lbl_content.pack(pady=(0, 10), padx=10, anchor="w")
|
||
return lbl_content
|
||
|
||
self.kpi_overall = create_kpi_card(self.kpi_frame, "Total General", 0, 0)
|
||
self.kpi_extract = create_kpi_card(self.kpi_frame, "Fase: Extracción", 0, 1)
|
||
self.kpi_normalize = create_kpi_card(self.kpi_frame, "Fase: Normalización", 0, 2)
|
||
|
||
self.kpi_overall.configure(text="input: 0\ncached: 0\noutput: 0\nUSD: $0.00")
|
||
self.kpi_extract.configure(text="input: 0\ncached: 0\noutput: 0\nUSD: $0.00")
|
||
self.kpi_normalize.configure(text="input: 0\ncached: 0\noutput: 0\nUSD: $0.00")
|
||
|
||
self.usage_totals = ctk.CTkLabel(self.tab_usage, text="") # Legacy label hidden
|
||
|
||
# Panel Central (Gráficos)
|
||
self.charts_frame = ctk.CTkFrame(self.tab_usage)
|
||
self.charts_frame.grid(row=1, column=0, sticky="nsew", padx=15, pady=5)
|
||
self.charts_frame.grid_columnconfigure((0, 1), weight=1)
|
||
self.charts_frame.grid_rowconfigure(0, weight=1)
|
||
|
||
# Gráfico Línea
|
||
self._cost_line_points_x = []
|
||
self._cost_line_points_y = []
|
||
|
||
if HAS_MPL:
|
||
self.fig_line = Figure(figsize=(4, 3), dpi=100)
|
||
self.fig_line.patch.set_facecolor('#f0f0f0') # Match light theme approx or transparent
|
||
self.ax_line = self.fig_line.add_subplot(111)
|
||
self.ax_line.set_title("Costo Acumulado (USD)", fontsize=10)
|
||
self.ax_line.tick_params(labelsize=8)
|
||
self._line_plot, = self.ax_line.plot([], [], marker="o", color="#1f77b4")
|
||
|
||
self.canvas_line = FigureCanvasTkAgg(self.fig_line, master=self.charts_frame)
|
||
self.canvas_line.get_tk_widget().grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
|
||
else:
|
||
ctk.CTkLabel(self.charts_frame, text="Gráficos no disponibles").grid(row=0, column=0)
|
||
|
||
# Gráfico Barras
|
||
if HAS_MPL:
|
||
self.fig_bars = Figure(figsize=(4, 3), dpi=100)
|
||
self.fig_bars.patch.set_facecolor('#f0f0f0')
|
||
self.ax_bars = self.fig_bars.add_subplot(111)
|
||
self.ax_bars.set_title("Consumo de Tokens", fontsize=10)
|
||
self.ax_bars.tick_params(labelsize=8)
|
||
|
||
self.canvas_bars = FigureCanvasTkAgg(self.fig_bars, master=self.charts_frame)
|
||
self.canvas_bars.get_tk_widget().grid(row=0, column=1, padx=10, pady=10, sticky="nsew")
|
||
|
||
# Panel Comparativo de Modelos
|
||
self.comparison_frame = ctk.CTkFrame(self.tab_usage)
|
||
self.comparison_frame.grid(row=2, column=0, sticky="ew", padx=15, pady=5)
|
||
self.comparison_frame.grid_columnconfigure(0, weight=1)
|
||
|
||
lbl_comp_title = ctk.CTkLabel(self.comparison_frame, text="Tarifas de Modelos (USD por 1M tokens)", font=ctk.CTkFont(size=14, weight="bold"))
|
||
lbl_comp_title.grid(row=0, column=0, sticky="w", padx=10, pady=5)
|
||
|
||
# Tabla de comparación
|
||
from tkinter import ttk
|
||
self.tree_comparison = ttk.Treeview(
|
||
self.comparison_frame,
|
||
columns=("model", "input", "cached", "output"),
|
||
show="headings",
|
||
height=4
|
||
)
|
||
self.tree_comparison.heading("model", text="Modelo")
|
||
self.tree_comparison.heading("input", text="Input / 1M")
|
||
self.tree_comparison.heading("cached", text="Cached / 1M")
|
||
self.tree_comparison.heading("output", text="Output / 1M")
|
||
|
||
self.tree_comparison.column("model", width=150, anchor="w")
|
||
self.tree_comparison.column("input", width=100, anchor="e")
|
||
self.tree_comparison.column("cached", width=100, anchor="e")
|
||
self.tree_comparison.column("output", width=100, anchor="e")
|
||
|
||
self.tree_comparison.grid(row=1, column=0, sticky="ew", padx=10, pady=(0, 10))
|
||
|
||
# Panel Inferior (Detalles Texto)
|
||
self.usage_text = ctk.CTkTextbox(self.tab_usage, height=150, font=self.font_mono)
|
||
self.usage_text.grid(row=3, column=0, sticky="ew", padx=15, pady=15)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# MÉTODOS PÚBLICOS (API COMPATIBLE)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def set_on_select_root(self, callback):
|
||
self.root_button.configure(command=callback)
|
||
|
||
def set_on_select_preclass_root(self, callback):
|
||
self.preclass_root_button.configure(command=callback)
|
||
|
||
def set_on_process(self, callback):
|
||
self.process_button.configure(command=callback)
|
||
|
||
def set_on_select_csv(self, callback):
|
||
self.csv_button.configure(command=callback)
|
||
|
||
def set_on_save_settings(self, callback):
|
||
self.btn_save_settings.configure(command=callback)
|
||
|
||
def set_on_run_preclassification(self, callback):
|
||
self.preclass_run_btn.configure(command=callback)
|
||
|
||
def set_on_export_excel(self, callback):
|
||
self.preclass_export_btn.configure(command=callback)
|
||
|
||
def set_on_organize_files(self, callback):
|
||
self.preclass_organize_btn.configure(command=callback)
|
||
|
||
def enable_preclass_actions(self, enabled: bool):
|
||
state = "normal" if enabled else "disabled"
|
||
self.preclass_export_btn.configure(state=state)
|
||
self.preclass_organize_btn.configure(state=state)
|
||
|
||
# --- Métodos Resultados ---
|
||
def set_on_res_search(self, callback):
|
||
self.btn_res_search.configure(command=callback)
|
||
|
||
def set_on_res_export(self, callback):
|
||
self.btn_res_export.configure(command=callback)
|
||
|
||
def set_on_res_refresh(self, callback):
|
||
self.btn_res_refresh.configure(command=callback)
|
||
|
||
def set_on_res_organize(self, callback):
|
||
self.btn_res_organize.configure(command=callback)
|
||
|
||
def set_on_res_select(self, callback):
|
||
# Bind TreeviewSelect event
|
||
self.tree_results.bind("<<TreeviewSelect>>", callback)
|
||
|
||
def update_res_matches_table(self, rows):
|
||
# Limpiar
|
||
for item in self.tree_matches.get_children():
|
||
self.tree_matches.delete(item)
|
||
# Llenar
|
||
for r in rows:
|
||
# r: key, expected_value, found_value, exists_bool
|
||
exists_str = "✅" if r.get("exists_bool") else "❌"
|
||
vals = (r.get("key"), r.get("found_value"), exists_str, r.get("expected_value") or "-")
|
||
self.tree_matches.insert("", "end", values=vals)
|
||
|
||
def update_res_runs_combo(self, runs: list):
|
||
vals = ["Todos"] + [str(r) for r in runs]
|
||
self.combo_res_run.configure(values=vals)
|
||
self.combo_res_run.set("Todos")
|
||
|
||
def set_res_run(self, run_id: int | str):
|
||
try:
|
||
self.combo_res_run.set(str(run_id))
|
||
except Exception:
|
||
pass
|
||
|
||
def get_res_filters(self):
|
||
return {
|
||
"run_id": self.combo_res_run.get(),
|
||
"status": self.combo_res_status.get(),
|
||
"text": self.entry_res_search.get()
|
||
}
|
||
|
||
def update_res_table(self, rows):
|
||
# Limpiar
|
||
for item in self.tree_results.get_children():
|
||
self.tree_results.delete(item)
|
||
# Llenar
|
||
for r in rows:
|
||
# r dict: id, run_id, radicado, file_path, status, timestamp
|
||
vals = (r.get("id"), r.get("run_id"), r.get("timestamp"), r.get("radicado"), r.get("status"), os.path.basename(r.get("file_path") or ""))
|
||
self.tree_results.insert("", "end", values=vals)
|
||
|
||
def append_preclass_output(self, text: str) -> None:
|
||
self.preclass_output.insert("end", text + "\n")
|
||
self.preclass_output.see("end")
|
||
|
||
def append_preclass_output_safe(self, text: str) -> None:
|
||
self.after(0, lambda: self.append_preclass_output(text))
|
||
|
||
def get_selected_model(self) -> str:
|
||
return self.combo_model.get()
|
||
|
||
def set_selected_model(self, model: str) -> None:
|
||
self.combo_model.set(model)
|
||
|
||
def get_concurrency_limit(self) -> int:
|
||
try:
|
||
return int(self.concurrency_var.get())
|
||
except Exception:
|
||
return 20
|
||
|
||
def set_concurrency_limit(self, limit: int) -> None:
|
||
self.concurrency_var.set(str(limit))
|
||
|
||
def _update_ui_for_mode(self):
|
||
"""Actualiza la visibilidad de componentes según el modo seleccionado."""
|
||
mode = self.get_process_mode()
|
||
if mode == "general":
|
||
# Ocultar Tipos de Documento
|
||
self.doc_types_container.grid_forget()
|
||
# Mostrar Keywords (opcional, para filtrar)
|
||
self.kw_label.grid(row=1, column=0, padx=15, pady=2, sticky="w")
|
||
self.kw_entry.grid(row=1, column=1, padx=5, pady=2, sticky="ew")
|
||
|
||
# Si tiene el valor por defecto de facturas, limpiarlo para sugerir "todos"
|
||
curr = self.kw_entry.get()
|
||
if "Factura_O_Documento_Equivalente" in curr:
|
||
self.kw_entry.delete(0, "end")
|
||
|
||
else:
|
||
# Mostrar Tipos de Documento
|
||
self.doc_types_container.grid(row=0, column=0, sticky="nsew", padx=10, pady=(5, 2))
|
||
# Ocultar Keywords
|
||
self.kw_label.grid_forget()
|
||
self.kw_entry.grid_forget()
|
||
|
||
def get_root_path(self) -> str:
|
||
return self.root_entry.get().strip()
|
||
|
||
def get_process_mode(self) -> str:
|
||
"""Devuelve el modo de procesamiento: 'radicados' o 'general'."""
|
||
try:
|
||
v = (self.mode_var.get() or "").strip().lower()
|
||
return v if v in ("radicados", "general") else "radicados"
|
||
except Exception:
|
||
return "radicados"
|
||
|
||
def get_preclass_root_path(self) -> str:
|
||
return self.preclass_root_entry.get().strip()
|
||
|
||
def get_preclass_mode(self) -> str:
|
||
"""Devuelve el modo de preclasificación: 'radicados' o 'general'."""
|
||
try:
|
||
v = (self.preclass_mode_var.get() or "").strip().lower()
|
||
return v if v in ("radicados", "general") else "radicados"
|
||
except Exception:
|
||
return "radicados"
|
||
|
||
def get_preclass_range(self) -> tuple[bool, int, int]:
|
||
"""Devuelve (habilitado, desde, hasta) para preclasificación."""
|
||
enabled = self.preclass_range_var.get()
|
||
try:
|
||
start = int(self.preclass_range_start_entry.get().strip() or "1")
|
||
end = int(self.preclass_range_end_entry.get().strip() or "1")
|
||
except Exception:
|
||
start, end = 1, 1
|
||
return enabled, start, end
|
||
|
||
def get_preclass_target_info(self) -> dict:
|
||
"""Devuelve la configuración del archivo objetivo."""
|
||
return {
|
||
"enabled": self.preclass_target_var.get(),
|
||
"definition": self.preclass_target_def_entry.get().strip(),
|
||
"pattern": self.preclass_target_pattern_entry.get().strip()
|
||
}
|
||
|
||
def _on_toggle_preclass_range(self):
|
||
"""Muestra u oculta los inputs de rango según el checkbox."""
|
||
if self.preclass_range_var.get():
|
||
self.preclass_range_inputs_frame.grid(row=0, column=1, padx=10, pady=0, sticky="w")
|
||
else:
|
||
self.preclass_range_inputs_frame.grid_forget()
|
||
|
||
def _on_toggle_preclass_target(self):
|
||
"""Muestra u oculta los inputs de archivo objetivo según el checkbox."""
|
||
if self.preclass_target_var.get():
|
||
self.preclass_target_inputs_frame.grid(row=1, column=0, padx=10, pady=(0, 10), sticky="w")
|
||
else:
|
||
self.preclass_target_inputs_frame.grid_forget()
|
||
|
||
def get_keywords(self) -> str:
|
||
return self.kw_entry.get().strip()
|
||
|
||
def get_fields(self) -> str:
|
||
return self.fields_entry.get().strip()
|
||
|
||
def get_csv_path(self) -> str:
|
||
return self.csv_entry.get().strip()
|
||
|
||
def show_tiff_range_controls(self, show: bool) -> None:
|
||
if show:
|
||
self.tiff_start_label.grid(row=0, column=0, padx=5, pady=5)
|
||
self.tiff_start_entry.grid(row=0, column=1, padx=5, pady=5)
|
||
self.tiff_end_label.grid(row=0, column=2, padx=5, pady=5)
|
||
self.tiff_end_entry.grid(row=0, column=3, padx=5, pady=5)
|
||
else:
|
||
self.tiff_start_label.grid_forget()
|
||
self.tiff_start_entry.grid_forget()
|
||
self.tiff_end_label.grid_forget()
|
||
self.tiff_end_entry.grid_forget()
|
||
|
||
def get_tiff_start_page(self) -> int:
|
||
try:
|
||
value = self.tiff_start_entry.get().strip()
|
||
return int(value) if value else 0
|
||
except Exception:
|
||
return 0
|
||
|
||
def get_tiff_end_page(self) -> int:
|
||
try:
|
||
value = self.tiff_end_entry.get().strip()
|
||
return int(value) if value else 0
|
||
except Exception:
|
||
return 0
|
||
|
||
def append_output(self, text: str) -> None:
|
||
self.output_text.insert("end", text + "\n")
|
||
self.output_text.see("end")
|
||
|
||
def append_output_safe(self, text: str) -> None:
|
||
self.after(0, lambda: self.append_output(text))
|
||
|
||
def set_processing(self, processing: bool) -> None:
|
||
state = "disabled" if processing else "normal"
|
||
|
||
self.process_button.configure(state=state, text="PROCESANDO..." if processing else "INICIAR PROCESAMIENTO")
|
||
if processing:
|
||
self.process_button.configure(fg_color="gray")
|
||
else:
|
||
self.process_button.configure(fg_color=("#106BA3", "#106BA3"))
|
||
|
||
self.root_button.configure(state=state)
|
||
self.root_entry.configure(state=state)
|
||
self.csv_button.configure(state=state)
|
||
self.csv_entry.configure(state=state)
|
||
self.add_doc_type_btn.configure(state=state)
|
||
|
||
for row in self._doc_type_rows:
|
||
row["name_entry"].configure(state=state)
|
||
row["kw_entry"].configure(state=state)
|
||
row["remove_btn"].configure(state=state)
|
||
|
||
# --- TAB PRECLASIFICACIÓN ---
|
||
try:
|
||
self.preclass_root_button.configure(state=state)
|
||
self.preclass_root_entry.configure(state=state)
|
||
self.preclass_range_cb.configure(state=state)
|
||
self.preclass_range_start_entry.configure(state=state)
|
||
self.preclass_range_end_entry.configure(state=state)
|
||
self.preclass_target_cb.configure(state=state)
|
||
self.preclass_target_def_entry.configure(state=state)
|
||
self.preclass_target_pattern_entry.configure(state=state)
|
||
self.radio_radicados.configure(state=state)
|
||
self.radio_general.configure(state=state)
|
||
self.radio_radicados_main.configure(state=state)
|
||
self.radio_general_main.configure(state=state)
|
||
self.add_preclass_pattern_btn.configure(state=state)
|
||
self.preclass_run_btn.configure(state=state)
|
||
# No deshabilitamos los de exportación aquí para permitir exportar mientras se procesa otro (opcional),
|
||
# o mejor sí bloquear todo.
|
||
if processing:
|
||
self.preclass_export_btn.configure(state="disabled")
|
||
self.preclass_organize_btn.configure(state="disabled")
|
||
|
||
for row in self._preclass_pattern_rows:
|
||
row["key_entry"].configure(state=state)
|
||
row["mode_combo"].configure(state=state)
|
||
row["value_entry"].configure(state=state)
|
||
row["remove_btn"].configure(state=state)
|
||
except Exception:
|
||
pass
|
||
|
||
# --- TAB USO ---
|
||
|
||
def append_usage(self, text: str) -> None:
|
||
self.usage_text.insert("end", text + "\n")
|
||
self.usage_text.see("end")
|
||
|
||
def append_usage_safe(self, text: str) -> None:
|
||
self.after(0, lambda: self.append_usage(text))
|
||
|
||
def clear_usage(self) -> None:
|
||
self.usage_text.delete("1.0", "end")
|
||
try:
|
||
for item in self.tree_comparison.get_children():
|
||
self.tree_comparison.delete(item)
|
||
except Exception:
|
||
pass
|
||
# Reset KPIs
|
||
zero_txt = "input: 0\ncached: 0\noutput: 0\nUSD: $0.0000"
|
||
self.kpi_overall.configure(text=zero_txt)
|
||
self.kpi_extract.configure(text=zero_txt)
|
||
self.kpi_normalize.configure(text=zero_txt)
|
||
|
||
# Reset Charts
|
||
if HAS_MPL:
|
||
self._cost_line_points_x.clear()
|
||
self._cost_line_points_y.clear()
|
||
self._line_plot.set_data([], [])
|
||
self.ax_line.relim()
|
||
self.ax_line.autoscale_view()
|
||
self.canvas_line.draw_idle()
|
||
|
||
self.ax_bars.clear()
|
||
self.ax_bars.set_title("Consumo de Tokens")
|
||
self.canvas_bars.draw_idle()
|
||
|
||
def set_usage_totals(self, **kwargs):
|
||
pass # Legacy, handled by KPIs now
|
||
|
||
def update_kpis(self, overall: dict, extract_phase: dict, normalize_phase: dict) -> None:
|
||
def fmt(d):
|
||
return (
|
||
f"input: {d.get('input',0)}\n"
|
||
f"cached: {d.get('cached',0)}\n"
|
||
f"output: {d.get('output',0)}\n"
|
||
f"USD: ${d.get('usd',0.0):.4f}"
|
||
)
|
||
self.kpi_overall.configure(text=fmt(overall))
|
||
self.kpi_extract.configure(text=fmt(extract_phase))
|
||
self.kpi_normalize.configure(text=fmt(normalize_phase))
|
||
|
||
def update_cost_line(self, doc_index: int, total_cost_usd: float) -> None:
|
||
if not HAS_MPL: return
|
||
try:
|
||
self._cost_line_points_x.append(doc_index)
|
||
self._cost_line_points_y.append(total_cost_usd)
|
||
self._line_plot.set_data(self._cost_line_points_x, self._cost_line_points_y)
|
||
self.ax_line.relim()
|
||
self.ax_line.autoscale_view()
|
||
self.canvas_line.draw_idle()
|
||
except Exception:
|
||
pass
|
||
|
||
def update_token_bars(self, input_extract: int, cached_extract: int, output_extract: int, input_norm: int, cached_norm: int, output_norm: int) -> None:
|
||
if not HAS_MPL: return
|
||
try:
|
||
self.ax_bars.clear()
|
||
self.ax_bars.set_title("Tokens por Fase")
|
||
labels = ["Input", "Cached", "Output"]
|
||
extract_vals = [input_extract, cached_extract, output_extract]
|
||
norm_vals = [input_norm, cached_norm, output_norm]
|
||
|
||
import numpy as np
|
||
x = np.arange(len(labels))
|
||
width = 0.35
|
||
|
||
self.ax_bars.bar(x - width/2, extract_vals, width, label="Extracción", color="#1f77b4")
|
||
self.ax_bars.bar(x + width/2, norm_vals, width, label="Normalización", color="#ff7f0e")
|
||
|
||
self.ax_bars.set_xticks(x, labels)
|
||
self.ax_bars.legend()
|
||
self.canvas_bars.draw_idle()
|
||
except Exception:
|
||
pass
|
||
|
||
def update_comparison_table(self, comparisons: list[dict]) -> None:
|
||
"""Actualiza la tabla de tarifas de modelos con los datos proporcionados."""
|
||
try:
|
||
# Limpiar tabla
|
||
for item in self.tree_comparison.get_children():
|
||
self.tree_comparison.delete(item)
|
||
|
||
# Insertar filas
|
||
for c in comparisons:
|
||
self.tree_comparison.insert("", "end", values=(
|
||
c.get("model", "N/D"),
|
||
f"${c.get('input', 0.0):.2f}",
|
||
f"${c.get('cached_input', 0.0):.2f}",
|
||
f"${c.get('output', 0.0):.2f}"
|
||
))
|
||
except Exception:
|
||
pass
|
||
|
||
def append_usage_row(self, phase: str, radicado: str, usage: dict | None, cost: dict | None) -> None:
|
||
try:
|
||
usd = (cost or {}).get('total_usd', 0.0)
|
||
self.append_usage(f"[{phase:<10}] {radicado:<20} | cost=${usd:.4f} | usage={usage}")
|
||
except Exception:
|
||
pass
|
||
|
||
# --- HELPERS INTERNOS ---
|
||
|
||
def _add_doc_type_row(self, name: str = "", keyword: str = "") -> None:
|
||
row = ctk.CTkFrame(self.doc_types_list, fg_color="transparent")
|
||
row.pack(fill="x", pady=2)
|
||
|
||
grid_inner = ctk.CTkFrame(row, fg_color="transparent")
|
||
grid_inner.pack(fill="x", padx=5, pady=2)
|
||
grid_inner.grid_columnconfigure(0, weight=1)
|
||
grid_inner.grid_columnconfigure(1, weight=1)
|
||
|
||
name_entry = ctk.CTkEntry(grid_inner, placeholder_text="Nombre Lógico", height=28, font=self.font_ui)
|
||
if name: name_entry.insert(0, name)
|
||
name_entry.grid(row=0, column=0, padx=(0, 5), sticky="ew")
|
||
|
||
kw_entry = ctk.CTkEntry(grid_inner, placeholder_text="Patrón / Keyword", height=28, font=self.font_ui)
|
||
if keyword: kw_entry.insert(0, keyword)
|
||
kw_entry.grid(row=0, column=1, padx=(0, 5), sticky="ew")
|
||
|
||
def _remove():
|
||
if row in [r["frame"] for r in self._doc_type_rows]:
|
||
# Buscar y eliminar de la lista interna
|
||
for i, r_data in enumerate(self._doc_type_rows):
|
||
if r_data["frame"] == row:
|
||
self._doc_type_rows.pop(i)
|
||
break
|
||
row.destroy()
|
||
|
||
remove_btn = ctk.CTkButton(grid_inner, text="×", width=30, height=28, fg_color="#C0392B", hover_color="#A93226", command=_remove)
|
||
remove_btn.grid(row=0, column=2, sticky="e")
|
||
|
||
self._doc_type_rows.append({
|
||
"frame": row,
|
||
"name_entry": name_entry,
|
||
"kw_entry": kw_entry,
|
||
"remove_btn": remove_btn
|
||
})
|
||
|
||
def _add_preclass_pattern_row(self, key: str = "", mode: str = "key", value: str = "") -> None:
|
||
"""Añade una fila editable para un patrón de preclasificación."""
|
||
row = ctk.CTkFrame(self.preclass_patterns_list, fg_color="transparent")
|
||
row.pack(fill="x", pady=2)
|
||
|
||
grid_inner = ctk.CTkFrame(row, fg_color="transparent")
|
||
grid_inner.pack(fill="x", padx=5, pady=2)
|
||
grid_inner.grid_columnconfigure(0, weight=1)
|
||
grid_inner.grid_columnconfigure(2, weight=1)
|
||
|
||
key_entry = ctk.CTkEntry(grid_inner, placeholder_text="Patrón / Key", height=28, font=self.font_ui)
|
||
if key:
|
||
key_entry.insert(0, key)
|
||
key_entry.grid(row=0, column=0, padx=(0, 5), sticky="ew")
|
||
|
||
mode_combo = ctk.CTkComboBox(
|
||
grid_inner,
|
||
values=["key", "key+value"],
|
||
width=110,
|
||
font=self.font_ui
|
||
)
|
||
mode_combo.grid(row=0, column=1, padx=(0, 5), sticky="w")
|
||
|
||
value_entry = ctk.CTkEntry(grid_inner, placeholder_text="Value (si aplica)", height=28, font=self.font_ui)
|
||
if value:
|
||
value_entry.insert(0, value)
|
||
value_entry.grid(row=0, column=2, padx=(0, 5), sticky="ew")
|
||
|
||
def _apply_mode(m: str):
|
||
m_norm = (m or "").strip().lower()
|
||
if m_norm in ("key+value", "key + value", "key_value", "key-value"):
|
||
value_entry.configure(state="normal", placeholder_text="Value (requerido)")
|
||
else:
|
||
# modo key: el value no aplica
|
||
value_entry.delete(0, "end")
|
||
value_entry.configure(state="disabled", placeholder_text="Value (no aplica)")
|
||
|
||
def _on_mode_change(choice: str):
|
||
_apply_mode(choice)
|
||
|
||
# set inicial
|
||
try:
|
||
mode_combo.set(mode if mode in ("key", "key+value") else "key")
|
||
except Exception:
|
||
mode_combo.set("key")
|
||
mode_combo.configure(command=_on_mode_change)
|
||
_apply_mode(mode_combo.get())
|
||
|
||
def _remove():
|
||
try:
|
||
for i, r_data in enumerate(list(self._preclass_pattern_rows)):
|
||
if r_data.get("frame") == row:
|
||
self._preclass_pattern_rows.pop(i)
|
||
break
|
||
except Exception:
|
||
pass
|
||
row.destroy()
|
||
|
||
remove_btn = ctk.CTkButton(grid_inner, text="×", width=30, height=28, fg_color="#C0392B", hover_color="#A93226", command=_remove)
|
||
remove_btn.grid(row=0, column=3, sticky="e")
|
||
|
||
self._preclass_pattern_rows.append({
|
||
"frame": row,
|
||
"key_entry": key_entry,
|
||
"mode_combo": mode_combo,
|
||
"value_entry": value_entry,
|
||
"remove_btn": remove_btn,
|
||
})
|
||
|
||
def get_preclass_patterns(self) -> list[dict]:
|
||
"""Devuelve la lista de patrones de preclasificación definida en la pestaña."""
|
||
out = []
|
||
for it in getattr(self, "_preclass_pattern_rows", []):
|
||
try:
|
||
key = it["key_entry"].get().strip()
|
||
mode = (it["mode_combo"].get() or "").strip()
|
||
value = it["value_entry"].get().strip()
|
||
if not key:
|
||
continue
|
||
if mode == "key+value":
|
||
out.append({"key": key, "mode": "key+value", "value": value})
|
||
else:
|
||
out.append({"key": key, "mode": "key"})
|
||
except Exception:
|
||
pass
|
||
return out
|
||
|
||
def get_doc_types(self) -> list[dict]:
|
||
out = []
|
||
for it in self._doc_type_rows:
|
||
try:
|
||
name = it["name_entry"].get().strip()
|
||
kw = it["kw_entry"].get().strip()
|
||
if name and kw:
|
||
out.append({"name": name, "keyword": kw})
|
||
except: pass
|
||
return out
|
||
|
||
def set_doc_types(self, doc_types: list[dict]) -> None:
|
||
# Limpiar actuales
|
||
for it in self._doc_type_rows:
|
||
it["frame"].destroy()
|
||
self._doc_type_rows = []
|
||
|
||
if not doc_types:
|
||
self._add_doc_type_row("Factura", "Factura_O_Documento_Equivalente")
|
||
return
|
||
|
||
for dt in doc_types:
|
||
self._add_doc_type_row(str(dt.get("name","")), str(dt.get("keyword","")))
|
||
|
||
def validate_doc_types(self) -> tuple[bool, str | None]:
|
||
seen = set()
|
||
for dt in self.get_doc_types():
|
||
name = dt.get("name")
|
||
if name in seen:
|
||
return False, f"Nombre duplicado: {name}"
|
||
seen.add(name)
|
||
if not seen:
|
||
return False, "Debe haber al menos un tipo de documento."
|
||
return True, None
|