feat(studio): tarjetas de agente con señales de estado

La lista de agentes era un directorio: nombre y activo/inactivo. No decía
si el agente estaba realmente listo para atender.

- Tarjetas en grid con el color de marca del agente (el mismo que ve el
  visitante en el widget) y sus iniciales.
- Tres datos por agente: conversaciones de los últimos 7 días, fuentes de
  conocimiento y canales activos. Salen de tres consultas agrupadas, no de
  tres por agente: el N+1 en un listado es el que después no se saca.
- Badge de estado accionable. "sin conocimiento" cuando el agente está
  activo pero no tiene ninguna fuente — ese agente responde de memoria e
  inventa datos, que es exactamente el problema de la URL falsa, y hasta
  ahora no se veía hasta que un cliente recibía la respuesta inventada.
- Esqueleto de carga: la pantalla hace 3 llamadas y quedaba en blanco el
  tiempo suficiente como para parecer rota.

Arregla además el fondo en modo oscuro: index.html traía
<body class="bg-gray-50">, y esa utilidad le ganaba a la regla de body de
la capa base, así que el área principal quedaba clara con el tema oscuro
puesto (medido: --fondo resolvía a 9 13 22 pero el body pintaba 249 250 251).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-14 22:09:32 -05:00
co-authored by Claude Sonnet 5
parent 45f773fb62
commit 5182c45d11
9 changed files with 193 additions and 54 deletions
+9 -1
View File
@@ -176,7 +176,15 @@ func GetUmindAgentesHandler(c *fiber.Ctx) error {
// en vez de dejar que el límite aparezca recién como un error al guardar.
// Sin plan no hay límite (ver verificarCupoAgentes), y eso también hay que
// poder verlo: si no, un tenant sin plan parece un límite que no funciona.
resp := fiber.Map{"items": items, "usados": len(items)}
ids := make([]uint, len(items))
for i, a := range items {
ids[i] = a.ID
}
resp := fiber.Map{
"items": items,
"usados": len(items),
"resumen": models.GetResumenAgentes(ids),
}
if plan := models.GetPlanDeTenant(uint(tenantID)); plan != nil {
resp["plan"] = fiber.Map{"nombre": plan.Nombre, "max_agentes": plan.MaxAgentes}
}