267 lines
11 KiB
Python
267 lines
11 KiB
Python
"""Pestaña Dashboard & Métricas de la aplicación GUI."""
|
|
import customtkinter as ctk
|
|
import tkinter.ttk as ttk
|
|
from gui.styles import AppStyles
|
|
|
|
# matplotlib es opcional
|
|
try:
|
|
from matplotlib.figure import Figure
|
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|
HAS_MPL = True
|
|
except Exception:
|
|
HAS_MPL = False
|
|
|
|
|
|
class MetricsTab:
|
|
"""Encapsula la UI de la pestaña Dashboard & Métricas."""
|
|
|
|
def __init__(self, parent_tab):
|
|
"""Inicializa la pestaña de Métricas.
|
|
|
|
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()
|
|
|
|
# Variables internas para gráficos
|
|
self._cost_line_points_x = []
|
|
self._cost_line_points_y = []
|
|
|
|
self._setup_layout()
|
|
self._create_kpi_section()
|
|
self._create_charts_section()
|
|
self._create_comparison_section()
|
|
self._create_usage_section()
|
|
|
|
def _setup_layout(self):
|
|
"""Configura el layout base de la pestaña."""
|
|
self.tab.grid_columnconfigure(0, weight=1)
|
|
self.tab.grid_rowconfigure(1, weight=1)
|
|
self.tab.grid_rowconfigure(3, weight=1)
|
|
|
|
def _create_kpi_section(self):
|
|
"""Crea la sección de KPIs."""
|
|
self.kpi_frame = ctk.CTkFrame(self.tab)
|
|
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.fonts["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")
|
|
|
|
def _create_charts_section(self):
|
|
"""Crea la sección de gráficos."""
|
|
self.charts_frame = ctk.CTkFrame(self.tab)
|
|
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
|
|
if HAS_MPL:
|
|
self.fig_line = Figure(figsize=(4, 3), dpi=100)
|
|
self.fig_line.patch.set_facecolor('#f0f0f0')
|
|
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")
|
|
|
|
def _create_comparison_section(self):
|
|
"""Crea la sección de comparación de tarifas."""
|
|
self.comparison_frame = ctk.CTkFrame(self.tab)
|
|
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
|
|
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))
|
|
|
|
def _create_usage_section(self):
|
|
"""Crea la sección de detalle de uso."""
|
|
self.usage_text = ctk.CTkTextbox(self.tab, height=150, font=self.fonts["mono"])
|
|
self.usage_text.grid(row=3, column=0, sticky="ew", padx=15, pady=15)
|
|
|
|
# --- API Pública ---
|
|
|
|
def append_usage(self, text: str) -> None:
|
|
"""Añade texto al área de uso."""
|
|
self.usage_text.insert("end", text + "\n")
|
|
self.usage_text.see("end")
|
|
|
|
def append_usage_safe(self, text: str) -> None:
|
|
"""Añade texto al área de uso de forma thread-safe."""
|
|
self.append_usage(text)
|
|
|
|
def clear(self) -> None:
|
|
"""Limpia todos los datos de la pestaña."""
|
|
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):
|
|
"""Legacy method, handled by KPIs now."""
|
|
pass
|
|
|
|
def update_kpis(self, overall: dict, extract_phase: dict, normalize_phase: dict) -> None:
|
|
"""Actualiza los KPIs con los datos proporcionados."""
|
|
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:
|
|
"""Actualiza el gráfico de línea de costo acumulado."""
|
|
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:
|
|
"""Actualiza el gráfico de barras de tokens."""
|
|
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 comparación de tarifas."""
|
|
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:
|
|
"""Añade una fila de detalle de uso."""
|
|
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
|