feat(studio): rediseño de la pantalla del agente — estado arriba, tres zonas, probar al lado
La pantalla estaba organizada por tabla de base de datos: siete pestañas que eran nuestras siete entidades —documentos, herramientas, canales, conexiones, eventos— y que se desbordaban a lo ancho. Un dueño de negocio nunca piensa "voy a Conexiones": piensa "¿por qué contestó mal?" o "quiero que sepa mis precios nuevos". Tres cambios. Un estado arriba que responde "¿está bien mi asistente?" sin un clic: "Atendiendo en WhatsApp · Sabe de 2 cosas · 1 conversación", y en ámbar cuando algo falta — "No está atendiendo en ningún lado", "Todavía no sabe nada de tu negocio". Antes eso había que deducirlo entrando pestaña por pestaña. Cada señal es un botón que lleva a donde se arregla, y si el destino está en Avanzado, lo despliega. Tres zonas en lugar de siete pestañas: Conversaciones, Lo que sabe, Dónde atiende. Herramientas, Cuentas conectadas y Problemas pasan a "Avanzado", plegado. No se sacan —hacen falta— pero dejan de competir todos los días con lo que se mira todos los días. Y el conocimiento con la prueba lado a lado. El bucle real es leer lo que sabe → probar → corregir → probar de nuevo; en dos pestañas separadas eso eran seis clics por corrección. Desde 1024px van en dos columnas, con el chat fijo al hacer scroll; abajo de eso se apilan, que es el orden en que igual se trabaja. El vocabulario deja de ser el nuestro: "Base de conocimiento" es "Lo que sabe", "Auditoría" es "Problemas", "Conexiones" es "Cuentas conectadas". Y la site_key sale del encabezado: se usa una vez, al instalar el widget, y vive donde se instala. No se tocó ni el color ni la tipografía ni Umi. Esto es estructura, no pintura: cambiando las dos cosas a la vez no se sabría cuál mejoró qué. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c78fcce89b
commit
22158a10a7
@@ -11,6 +11,7 @@ import TabChat from './agente/TabChat.vue'
|
||||
import TabConversaciones from './agente/TabConversaciones.vue'
|
||||
import TabHerramientas from './agente/TabHerramientas.vue'
|
||||
import TabCanales from './agente/TabCanales.vue'
|
||||
import EstadoAgente from './agente/EstadoAgente.vue'
|
||||
|
||||
const props = defineProps({
|
||||
tenantId: { type: String, required: true },
|
||||
@@ -234,27 +235,36 @@ async function cargarConexiones() {
|
||||
conexiones.value = r.items || []
|
||||
}
|
||||
|
||||
const tabs = computed(() =>
|
||||
contexto.esPortal
|
||||
? [
|
||||
['conversaciones', 'Conversaciones'],
|
||||
['chat', 'Probar'],
|
||||
['conocimiento', 'Base de conocimiento'],
|
||||
['herramientas', 'Herramientas'],
|
||||
['canales', 'Canales'],
|
||||
['conexiones', 'Conexiones'],
|
||||
['auditoria', 'Auditoría'],
|
||||
]
|
||||
: [
|
||||
['conocimiento', 'Base de conocimiento'],
|
||||
['herramientas', 'Herramientas'],
|
||||
['canales', 'Canales'],
|
||||
['conexiones', 'Conexiones'],
|
||||
['chat', 'Chat de prueba'],
|
||||
['conversaciones', 'Conversaciones'],
|
||||
['auditoria', 'Auditoría'],
|
||||
],
|
||||
)
|
||||
// Tres zonas en vez de siete pestañas. Las siete eran nuestras siete tablas
|
||||
// —documentos, tools, canales, conexiones, eventos— y un dueño de negocio no
|
||||
// piensa "voy a Conexiones": piensa "¿por qué contestó mal?" o "quiero que
|
||||
// sepa mis precios nuevos".
|
||||
//
|
||||
// Lo que se usa todos los días queda arriba; lo que se toca una vez y se
|
||||
// olvida, en Avanzado.
|
||||
const ZONAS = [
|
||||
['conversaciones', 'Conversaciones'],
|
||||
['conocimiento', 'Lo que sabe'],
|
||||
['canales', 'Dónde atiende'],
|
||||
]
|
||||
|
||||
const AVANZADO = [
|
||||
['herramientas', 'Herramientas'],
|
||||
['conexiones', 'Cuentas conectadas'],
|
||||
['auditoria', 'Problemas'],
|
||||
]
|
||||
|
||||
const mostrarAvanzado = ref(false)
|
||||
|
||||
// Si se entra directo a una pestaña avanzada (por la URL o desde el estado de
|
||||
// arriba), la sección tiene que verse abierta o el botón activo queda
|
||||
// escondido detrás de un "Avanzado" cerrado.
|
||||
if (AVANZADO.some(([k]) => k === tab.value)) mostrarAvanzado.value = true
|
||||
|
||||
function irA(destino) {
|
||||
tab.value = destino
|
||||
if (AVANZADO.some(([k]) => k === destino)) mostrarAvanzado.value = true
|
||||
}
|
||||
|
||||
// ─── Auditoría ────────────────────────────────────────────────────────────────
|
||||
const eventos = ref([])
|
||||
@@ -294,13 +304,8 @@ watch(
|
||||
<div>
|
||||
<router-link :to="`/tenants/${tenantId}`" class="label hover:text-brand">← Agentes</router-link>
|
||||
|
||||
<div v-if="agente" class="mb-6 mt-1 flex items-start justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
<h1 class="text-xl font-semibold text-texto">{{ agente.nombre }}</h1>
|
||||
<p class="text-xs text-tenue mt-1">
|
||||
site_key: <code class="bg-elevado px-1.5 py-0.5 rounded">{{ agente.site_key }}</code>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="agente" class="mb-4 mt-1 flex items-start justify-between gap-4">
|
||||
<h1 class="text-xl font-semibold text-texto min-w-0 truncate">{{ agente.nombre }}</h1>
|
||||
<!-- Un archivo que se reenvía sirve para justificar el gasto puertas
|
||||
adentro; un panel al que hay que entrar, no. -->
|
||||
<a :href="urlReporte" class="btn-ghost shrink-0" title="Conversaciones y consumo del mes en Excel">
|
||||
@@ -308,13 +313,24 @@ watch(
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- El estado va antes que la navegación: es lo primero que se quiere
|
||||
saber y, además, lleva directo a lo que haya que arreglar. -->
|
||||
<EstadoAgente
|
||||
v-if="agente"
|
||||
:canales="canales"
|
||||
:documentos="documentos"
|
||||
:sesiones="sesiones"
|
||||
:eventos="eventos"
|
||||
@ir="irA"
|
||||
/>
|
||||
|
||||
<p v-if="error" class="text-sm text-red-600 dark:text-red-400 mb-4">{{ error }}</p>
|
||||
|
||||
<div class="flex gap-1.5 mb-6 overflow-x-auto sin-barra pb-1">
|
||||
<div class="flex items-center gap-1.5 mb-6 flex-wrap">
|
||||
<button
|
||||
v-for="[key, label] in tabs"
|
||||
v-for="[key, label] in ZONAS"
|
||||
:key="key"
|
||||
class="px-3 py-1.5 rounded-full text-sm whitespace-nowrap transition-colors"
|
||||
class="px-3.5 py-1.5 rounded-full text-sm whitespace-nowrap transition-colors"
|
||||
:class="tab === key
|
||||
? 'bg-brand text-white font-medium'
|
||||
: 'bg-white dark:bg-gray-900 text-gray-600 dark:text-gray-400 border border-borde hover:border-brand/50'"
|
||||
@@ -322,10 +338,38 @@ watch(
|
||||
>
|
||||
{{ label }}
|
||||
</button>
|
||||
|
||||
<!-- Lo que se configura una vez no puede competir todos los días con lo
|
||||
que se mira todos los días. -->
|
||||
<button
|
||||
class="ml-auto text-xs text-tenue hover:text-texto inline-flex items-center gap-1"
|
||||
@click="mostrarAvanzado = !mostrarAvanzado"
|
||||
>
|
||||
Avanzado
|
||||
<span class="text-[10px]">{{ mostrarAvanzado ? '▲' : '▼' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Base de conocimiento -->
|
||||
<div v-if="tab === 'conocimiento'">
|
||||
<div v-if="mostrarAvanzado" class="flex gap-1.5 mb-6 flex-wrap">
|
||||
<button
|
||||
v-for="[key, label] in AVANZADO"
|
||||
:key="key"
|
||||
class="px-3 py-1 rounded-full text-xs whitespace-nowrap transition-colors"
|
||||
:class="tab === key
|
||||
? 'bg-texto text-white dark:bg-gray-200 dark:text-gray-900 font-medium'
|
||||
: 'bg-white dark:bg-gray-900 text-tenue border border-borde hover:border-brand/50'"
|
||||
@click="tab = key"
|
||||
>
|
||||
{{ label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Lo que sabe: el conocimiento y la prueba, lado a lado.
|
||||
El bucle real es leer lo que sabe → probar → corregir → probar de
|
||||
nuevo. Con las dos cosas en pestañas separadas, eso eran seis clics
|
||||
por corrección; acá es escribir y ver. -->
|
||||
<div v-if="tab === 'conocimiento'" class="grid gap-4 lg:grid-cols-[1fr_21rem] items-start">
|
||||
<div>
|
||||
<div class="card p-4 mb-4">
|
||||
<div class="flex gap-1 mb-3">
|
||||
<button
|
||||
@@ -432,6 +476,16 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- La prueba, al lado y siempre visible mientras se edita el
|
||||
conocimiento. En pantallas angostas cae abajo, que es el orden en
|
||||
que igual se trabaja. -->
|
||||
<div class="lg:sticky lg:top-4">
|
||||
<p class="label mb-2">Probalo</p>
|
||||
<TabChat :agente-id="agenteIdNum" />
|
||||
</div>
|
||||
|
||||
<!-- Editar una nota o el texto extraído de un archivo -->
|
||||
<div
|
||||
v-if="editandoDoc"
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import UiIcono from '../../components/ui/UiIcono.vue'
|
||||
|
||||
// Responde "¿está bien mi asistente?" sin hacer un solo clic.
|
||||
//
|
||||
// Antes había que entrar pestaña por pestaña para saberlo: si estaba atendiendo
|
||||
// en algún lado, si sabía algo, si alguien le había escrito. Tres datos que el
|
||||
// dueño quiere de un vistazo y que estaban repartidos en tres pantallas.
|
||||
const props = defineProps({
|
||||
canales: { type: Array, default: () => [] },
|
||||
documentos: { type: Array, default: () => [] },
|
||||
sesiones: { type: Array, default: () => [] },
|
||||
eventos: { type: Array, default: () => [] },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['ir'])
|
||||
|
||||
const canalesActivos = computed(() => props.canales.filter((c) => c.activo))
|
||||
|
||||
// Los tipos vienen en minúscula del backend; "Atendiendo en whatsapp" se lee
|
||||
// como un error de tipeo.
|
||||
const NOMBRES_CANAL = { whatsapp: 'WhatsApp', telegram: 'Telegram', web: 'tu web' }
|
||||
const nombreCanal = (t) => NOMBRES_CANAL[t] || t
|
||||
|
||||
const fuentesListas = computed(() => props.documentos.filter((d) => d.estado === 'listo'))
|
||||
|
||||
// Dos meses es cuando una fuente empieza a ser sospechosa de estar vieja: es el
|
||||
// mismo corte que usa la lista de conocimiento, para que no digan cosas
|
||||
// distintas.
|
||||
const fuentesViejas = computed(() =>
|
||||
fuentesListas.value.filter(
|
||||
(d) => d.procesado_at && Date.now() - new Date(d.procesado_at) > 60 * 86400000,
|
||||
),
|
||||
)
|
||||
|
||||
const problemas = computed(() => props.eventos.filter((e) => e.nivel === 'error').length)
|
||||
|
||||
// Cada señal dice qué pasa y, cuando algo anda mal, a dónde ir a arreglarlo.
|
||||
const señales = computed(() => [
|
||||
{
|
||||
clave: 'canales',
|
||||
icono: 'mensaje',
|
||||
bien: canalesActivos.value.length > 0,
|
||||
texto: canalesActivos.value.length
|
||||
? `Atendiendo en ${canalesActivos.value.map((c) => nombreCanal(c.tipo)).join(' y ')}`
|
||||
: 'No está atendiendo en ningún lado',
|
||||
destino: 'canales',
|
||||
},
|
||||
{
|
||||
clave: 'conocimiento',
|
||||
icono: 'escribir',
|
||||
bien: fuentesListas.value.length > 0 && fuentesViejas.value.length === 0,
|
||||
texto: !fuentesListas.value.length
|
||||
? 'Todavía no sabe nada de tu negocio'
|
||||
: fuentesViejas.value.length
|
||||
? `${fuentesViejas.value.length} de ${fuentesListas.value.length} fuentes están viejas`
|
||||
: `Sabe de ${fuentesListas.value.length} ${fuentesListas.value.length === 1 ? 'cosa' : 'cosas'}`,
|
||||
destino: 'conocimiento',
|
||||
},
|
||||
{
|
||||
clave: 'conversaciones',
|
||||
icono: 'grafico',
|
||||
// Que nadie haya escrito todavía no es un problema: es un dato.
|
||||
bien: true,
|
||||
neutro: props.sesiones.length === 0,
|
||||
texto: props.sesiones.length
|
||||
? `${props.sesiones.length} ${props.sesiones.length === 1 ? 'conversación' : 'conversaciones'}`
|
||||
: 'Nadie escribió todavía',
|
||||
destino: 'conversaciones',
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card p-1 mb-6 flex flex-wrap">
|
||||
<button
|
||||
v-for="s in señales"
|
||||
:key="s.clave"
|
||||
class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm hover:bg-elevado transition-colors text-left"
|
||||
@click="emit('ir', s.destino)"
|
||||
>
|
||||
<UiIcono
|
||||
:nombre="s.bien ? s.icono : 'alerta'"
|
||||
:tam="16"
|
||||
:class="s.neutro ? 'text-tenue' : s.bien ? 'text-brand' : 'text-amber-600 dark:text-amber-400'"
|
||||
/>
|
||||
<span :class="s.neutro ? 'text-tenue' : s.bien ? 'text-texto' : 'text-amber-700 dark:text-amber-400 font-medium'">
|
||||
{{ s.texto }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="problemas"
|
||||
class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm hover:bg-elevado transition-colors ml-auto"
|
||||
@click="emit('ir', 'auditoria')"
|
||||
>
|
||||
<UiIcono nombre="alerta" :tam="16" class="text-red-600 dark:text-red-400" />
|
||||
<span class="text-red-700 dark:text-red-400 font-medium">
|
||||
{{ problemas }} {{ problemas === 1 ? 'problema' : 'problemas' }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user