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:
co-authored by
Claude Sonnet 5
parent
45f773fb62
commit
5182c45d11
@@ -1,6 +1,8 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -82,3 +84,61 @@ func GetUmindAgentePorSiteKey(siteKey string) (*UmindAgente, error) {
|
||||
}
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
// ResumenAgente son las señales que hacen falta para saber de un vistazo si
|
||||
// un agente está listo o le falta algo, sin entrar a configurarlo.
|
||||
type ResumenAgente struct {
|
||||
AgenteID uint `json:"agente_id"`
|
||||
Documentos int64 `json:"documentos"`
|
||||
Canales int64 `json:"canales"`
|
||||
Conversaciones7d int64 `json:"conversaciones_7d"`
|
||||
}
|
||||
|
||||
// GetResumenAgentes agrega las tres señales en tres consultas agrupadas, no
|
||||
// en tres por agente: con 5 agentes la diferencia no se nota, pero el patrón
|
||||
// N+1 en una pantalla de listado es el que después no se puede sacar.
|
||||
func GetResumenAgentes(agenteIDs []uint) map[uint]*ResumenAgente {
|
||||
out := make(map[uint]*ResumenAgente, len(agenteIDs))
|
||||
for _, id := range agenteIDs {
|
||||
out[id] = &ResumenAgente{AgenteID: id}
|
||||
}
|
||||
if len(agenteIDs) == 0 {
|
||||
return out
|
||||
}
|
||||
db := app.Http.Database.DB
|
||||
|
||||
type fila struct {
|
||||
AgenteID uint
|
||||
Total int64
|
||||
}
|
||||
|
||||
var docs []fila
|
||||
db.Model(&UmindDocumento{}).Select("agente_id, COUNT(*) AS total").
|
||||
Where("agente_id IN ? AND deleted_at IS NULL", agenteIDs).Group("agente_id").Scan(&docs)
|
||||
for _, f := range docs {
|
||||
if r, ok := out[f.AgenteID]; ok {
|
||||
r.Documentos = f.Total
|
||||
}
|
||||
}
|
||||
|
||||
var canales []fila
|
||||
db.Model(&UmindCanal{}).Select("agente_id, COUNT(*) AS total").
|
||||
Where("agente_id IN ? AND activo = ? AND deleted_at IS NULL", agenteIDs, true).Group("agente_id").Scan(&canales)
|
||||
for _, f := range canales {
|
||||
if r, ok := out[f.AgenteID]; ok {
|
||||
r.Canales = f.Total
|
||||
}
|
||||
}
|
||||
|
||||
// Conversaciones, no mensajes: lo que importa es a cuánta gente atendió.
|
||||
var convs []fila
|
||||
db.Model(&UmindMensaje{}).Select("agente_id, COUNT(DISTINCT session_id) AS total").
|
||||
Where("agente_id IN ? AND created_at >= ? AND deleted_at IS NULL", agenteIDs, time.Now().AddDate(0, 0, -7)).
|
||||
Group("agente_id").Scan(&convs)
|
||||
for _, f := range convs {
|
||||
if r, ok := out[f.AgenteID]; ok {
|
||||
r.Conversaciones7d = f.Total
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user