feat(umind): Umi, la mascota, y un solo vocabulario en toda la interfaz
La mascota. Umi sale del logo, no es un dibujo aparte: su cuerpo es la "u" del monograma y el punto del logo pasa a ser su antena, así que símbolo y personaje son la misma forma vista dos veces. Por eso puede estar al lado del logo sin competirle. Tiene estados, y no son decoración: cada pantalla vacía dice algo distinto y la cara lo dice antes que el texto. Dormida cuando todavía no pasó nada, buscando cuando falta cargarle información, contenta cuando no hay errores, alerta cuando algo se rompió. Los ojos toman el color de la superficie de atrás en vez de ser blancos, así se apoya sobre cualquier tarjeta y en modo oscuro no quedan dos puntos flotando. Las pantallas vacías. Había una buena y ocho que decían "Sin canales configurados." y nada más — el usuario quedaba resolviendo solo qué significa eso y qué hacer. Ahora las ocho explican qué va ahí y por qué importa: "No está atendiendo en ningún lado", "Todavía no sabe nada de tu negocio". El vocabulario. La misma cosa tenía dos nombres según la pantalla: tenant y espacio, tool y herramienta. Un producto que se llama distinto a sí mismo en cada lugar se siente como varios productos pegados. Queda: espacio, agente, herramienta, fuente. Y "al AI" pasa a "a la IA", que además es el género que ya usaba el resto del panel. Y la barra de scroll de las pestañas, que se veía gris y gruesa sobre el contenido: en escritorio las siete entran holgadas y no hacía falta, en móvil siguen deslizándose pero sin la barra encima. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5086087e1b
commit
3ceb61c035
@@ -112,7 +112,7 @@ async function guardar() {
|
||||
}
|
||||
|
||||
async function eliminar(t) {
|
||||
if (!confirm(`¿Eliminar el tenant "${t.nombre}"? Esto borra también sus agentes. No se puede deshacer.`)) return
|
||||
if (!confirm(`¿Eliminar el espacio "${t.nombre}"? Esto borra también sus agentes. No se puede deshacer.`)) return
|
||||
await api.del(apiUmind(`/umind/tenants/${t.ID}`))
|
||||
if (tenantActivoId.value === String(t.ID)) router.push('/')
|
||||
await cargar()
|
||||
@@ -157,7 +157,7 @@ onMounted(() => {
|
||||
class="btn-primary w-full"
|
||||
@click="nuevoTenant"
|
||||
>
|
||||
+ Nuevo tenant
|
||||
+ Nuevo espacio
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -166,7 +166,7 @@ onMounted(() => {
|
||||
<nav class="flex-1 overflow-y-auto px-2 py-3 space-y-0.5">
|
||||
<p v-if="loading" class="px-2 text-xs text-tenue">Cargando...</p>
|
||||
<p v-else-if="tenants.length === 0" class="px-2 text-xs text-tenue">
|
||||
{{ contexto.esPortal ? 'Todavía no tenés ningún espacio asignado. Escribinos y lo activamos.' : 'Sin tenants todavía.' }}
|
||||
{{ contexto.esPortal ? 'Todavía no tenés ningún espacio asignado. Escribinos y lo activamos.' : 'Sin espacios todavía.' }}
|
||||
</p>
|
||||
<div
|
||||
v-for="t in tenants"
|
||||
@@ -213,10 +213,10 @@ onMounted(() => {
|
||||
>
|
||||
<div class="card w-full max-w-lg p-6 animate-escalar shadow-2xl">
|
||||
<h2 class="font-semibold text-texto mb-4">
|
||||
{{ editing ? 'Editar tenant' : 'Nuevo tenant' }}
|
||||
{{ editing ? 'Editar espacio' : 'Nuevo espacio' }}
|
||||
</h2>
|
||||
<p class="text-xs text-tenue mb-3">
|
||||
Un tenant es el negocio/sitio dueño de los dominios permitidos. La config de IA, tono y demás se configuran por agente, dentro del tenant.
|
||||
Un espacio es el negocio o sitio dueño de los dominios permitidos. La config de IA, tono y demás se configuran por agente, dentro del tenant.
|
||||
</p>
|
||||
<form class="space-y-3" @submit.prevent="guardar">
|
||||
<div>
|
||||
@@ -247,7 +247,7 @@ onMounted(() => {
|
||||
<option v-for="c in clientes" :key="c.ID" :value="c.ID">{{ c.nombre }}</option>
|
||||
</select>
|
||||
<p class="text-[11px] text-tenue mt-1">
|
||||
{{ clientes.length ? 'Define quién ve este tenant desde el portal.' : 'No hay clientes activos — creá uno en Clientes.' }}
|
||||
{{ clientes.length ? 'Define quién ve este espacio desde el portal.' : 'No hay clientes activos — creá uno en Clientes.' }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
<script setup>
|
||||
defineProps({ icono: { type: String, default: '' }, titulo: String, detalle: String })
|
||||
import UiMascota from './UiMascota.vue'
|
||||
|
||||
// Una pantalla vacía es la primera que ve alguien que recién empieza, y la que
|
||||
// más veces ve el que todavía no configuró algo. Decir "Sin canales" y nada
|
||||
// más deja al usuario resolviendo solo qué significa eso y qué hacer.
|
||||
//
|
||||
// La mascota lleva el estado: dormida cuando no pasó nada todavía, buscando
|
||||
// cuando falta cargar información, alerta cuando algo se rompió. Se lee antes
|
||||
// que el texto.
|
||||
defineProps({
|
||||
// Estado de Umi. Sin él, cae en 'durmiendo', que es el caso más común.
|
||||
estado: { type: String, default: 'durmiendo' },
|
||||
titulo: String,
|
||||
detalle: String,
|
||||
// Escape para los pocos casos donde un emoji dice más que la mascota.
|
||||
icono: { type: String, default: '' },
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center text-center py-12 px-6">
|
||||
<div v-if="icono" class="text-3xl mb-3 opacity-70">{{ icono }}</div>
|
||||
<UiMascota v-else :estado="estado" :tam="64" class="mb-3 text-brand" />
|
||||
|
||||
<p class="text-sm font-medium text-texto">{{ titulo }}</p>
|
||||
<p v-if="detalle" class="text-xs text-tenue mt-1 max-w-sm">{{ detalle }}</p>
|
||||
<div class="mt-4"><slot /></div>
|
||||
<p v-if="detalle" class="text-xs text-tenue mt-1.5 max-w-sm leading-relaxed">{{ detalle }}</p>
|
||||
<div v-if="$slots.default" class="mt-4"><slot /></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<script setup>
|
||||
// Umi, la mascota de uMind.
|
||||
//
|
||||
// No es un dibujo suelto: sale del logo. El cuerpo es la "u" del monograma y
|
||||
// el punto del logo pasa a ser su antena, así que el símbolo de la marca y el
|
||||
// personaje son la misma forma vista dos veces. Por eso funciona al lado del
|
||||
// logo sin competirle.
|
||||
//
|
||||
// El estado no es decoración: cada pantalla vacía dice algo distinto, y la
|
||||
// cara lo dice antes que el texto. Dormida cuando no pasó nada todavía,
|
||||
// buscando cuando falta cargarle información, alerta cuando algo se rompió.
|
||||
defineProps({
|
||||
estado: {
|
||||
type: String,
|
||||
default: 'normal', // normal | durmiendo | contenta | pensando | buscando | alerta
|
||||
},
|
||||
tam: { type: [Number, String], default: 72 },
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
:width="tam"
|
||||
:height="tam"
|
||||
viewBox="0 0 120 120"
|
||||
class="shrink-0"
|
||||
role="img"
|
||||
:aria-label="`Umi, la mascota de uMind (${estado})`"
|
||||
>
|
||||
<!-- Antena: el punto del logo. Cambia de color solo cuando algo anda mal,
|
||||
para que el rojo signifique algo. -->
|
||||
<circle
|
||||
cx="60"
|
||||
cy="26"
|
||||
r="6"
|
||||
:fill="estado === 'alerta' ? '#dc2626' : 'currentColor'"
|
||||
:class="estado === 'pensando' ? 'umi-late' : ''"
|
||||
/>
|
||||
<line
|
||||
x1="60" y1="32" x2="60" y2="44"
|
||||
:stroke="estado === 'alerta' ? '#dc2626' : 'currentColor'"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
|
||||
<!-- Cuerpo: la "u" del logo, cerrada. -->
|
||||
<path d="M30,46 V76 A30,30 0 0 0 90,76 V46 Z" fill="currentColor" />
|
||||
|
||||
<!-- Ojos abiertos -->
|
||||
<template v-if="estado === 'normal' || estado === 'contenta' || estado === 'alerta'">
|
||||
<circle cx="47" :cy="estado === 'contenta' ? 64 : 66" r="6" class="umi-ojo" />
|
||||
<circle cx="73" :cy="estado === 'contenta' ? 64 : 66" r="6" class="umi-ojo" />
|
||||
<circle cx="48" :cy="estado === 'contenta' ? 65 : 67" r="3" class="umi-pupila" />
|
||||
<circle cx="74" :cy="estado === 'contenta' ? 65 : 67" r="3" class="umi-pupila" />
|
||||
</template>
|
||||
|
||||
<!-- Ojos cerrados: no pasó nada todavía, no hay nada roto -->
|
||||
<template v-else-if="estado === 'durmiendo'">
|
||||
<path d="M41,66 h12 M67,66 h12" class="umi-linea" stroke-width="5" stroke-linecap="round" fill="none" />
|
||||
</template>
|
||||
|
||||
<!-- Pensando: los tres puntos de "escribiendo…" que ya conoce cualquiera -->
|
||||
<template v-else-if="estado === 'pensando'">
|
||||
<circle cx="45" cy="66" r="4.5" class="umi-ojo umi-p1" />
|
||||
<circle cx="60" cy="66" r="4.5" class="umi-ojo umi-p2" />
|
||||
<circle cx="75" cy="66" r="4.5" class="umi-ojo umi-p3" />
|
||||
</template>
|
||||
|
||||
<!-- Buscando: mira de costado, como quien revisa algo -->
|
||||
<template v-else-if="estado === 'buscando'">
|
||||
<circle cx="47" cy="66" r="6" class="umi-ojo" />
|
||||
<circle cx="73" cy="66" r="6" class="umi-ojo" />
|
||||
<circle cx="50" cy="66" r="3" class="umi-pupila" />
|
||||
<circle cx="76" cy="66" r="3" class="umi-pupila" />
|
||||
</template>
|
||||
|
||||
<!-- Sonrisa solo cuando hay algo que celebrar -->
|
||||
<path
|
||||
v-if="estado === 'contenta'"
|
||||
d="M50,78 q10,8 20,0"
|
||||
class="umi-linea"
|
||||
stroke-width="4"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Los ojos toman el color de la superficie que tienen detrás, no blanco fijo:
|
||||
así la mascota se apoya sobre cualquier tarjeta sin un halo alrededor de
|
||||
cada ojo, y en modo oscuro no quedan dos puntos blancos flotando.
|
||||
Los tokens del proyecto son tripletes RGB, de ahí el rgb(). */
|
||||
.umi-ojo { fill: rgb(var(--superficie)); }
|
||||
.umi-linea { stroke: rgb(var(--superficie)); }
|
||||
|
||||
/* La pupila sí es fija: es lo único que da la sensación de mirada, y tiene
|
||||
que leerse igual sobre el verde en los dos temas. */
|
||||
.umi-pupila { fill: #11150F; }
|
||||
|
||||
.umi-late { animation: umi-latido 2s ease-in-out infinite; }
|
||||
@keyframes umi-latido {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.35; }
|
||||
}
|
||||
|
||||
.umi-p1, .umi-p2, .umi-p3 { animation: umi-punto 1.4s ease-in-out infinite; }
|
||||
.umi-p2 { animation-delay: 0.18s; }
|
||||
.umi-p3 { animation-delay: 0.36s; }
|
||||
@keyframes umi-punto {
|
||||
0%, 60%, 100% { opacity: 0.35; }
|
||||
30% { opacity: 1; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.umi-late, .umi-p1, .umi-p2, .umi-p3 { animation: none; opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
@@ -86,3 +86,16 @@
|
||||
@apply tab text-tenue hover:text-texto hover:bg-elevado;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Deslizable con el dedo, sin la barra gris encima del contenido. El borde
|
||||
cortado de la última pestaña ya avisa que hay más hacia el costado. */
|
||||
.sin-barra {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.sin-barra::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { api } from '../lib/api.js'
|
||||
import { apiUmind, contexto } from '../lib/contexto.js'
|
||||
import UiEmptyState from '../components/ui/UiEmptyState.vue'
|
||||
|
||||
const props = defineProps({
|
||||
tenantId: { type: String, required: true },
|
||||
@@ -248,7 +249,7 @@ async function guardarTool() {
|
||||
}
|
||||
|
||||
async function eliminarTool(t) {
|
||||
if (!confirm(`¿Eliminar la tool "${t.nombre}"?`)) return
|
||||
if (!confirm(`¿Eliminar la herramienta "${t.nombre}"?`)) return
|
||||
await api.del(apiUmind(`/umind/tools/${t.ID}`))
|
||||
await cargarTools()
|
||||
}
|
||||
@@ -482,7 +483,7 @@ watch(
|
||||
|
||||
<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 pb-1">
|
||||
<div class="flex gap-1.5 mb-6 overflow-x-auto sin-barra pb-1">
|
||||
<button
|
||||
v-for="[key, label] in tabs"
|
||||
:key="key"
|
||||
@@ -554,7 +555,12 @@ watch(
|
||||
</form>
|
||||
</div>
|
||||
<div class="card divide-y divide-borde">
|
||||
<div v-if="documentos.length === 0" class="p-6 text-sm text-tenue">Sin fuentes todavía.</div>
|
||||
<UiEmptyState
|
||||
v-if="documentos.length === 0"
|
||||
estado="buscando"
|
||||
titulo="Todavía no sabe nada de tu negocio"
|
||||
detalle="Sin información cargada va a contestar que no sabe. Empezá por escribir tus horarios y lo que te preguntan todos los días — es lo más rápido y lo que más se nota."
|
||||
/>
|
||||
<div v-for="d in documentos" :key="d.ID" class="p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-sm text-texto">{{ d.origen }}</div>
|
||||
@@ -592,13 +598,17 @@ watch(
|
||||
<!-- Herramientas -->
|
||||
<div v-else-if="tab === 'herramientas'">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<p class="label">Máximo 10 tools activas por agente.</p>
|
||||
<p class="label">Máximo 10 herramientas activas por agente.</p>
|
||||
<button class="btn-primary" @click="nuevaTool">
|
||||
+ Nueva tool
|
||||
+ Nueva herramienta
|
||||
</button>
|
||||
</div>
|
||||
<div class="card divide-y divide-borde">
|
||||
<div v-if="tools.length === 0" class="p-6 text-sm text-tenue">Sin tools custom todavía.</div>
|
||||
<UiEmptyState
|
||||
v-if="tools.length === 0"
|
||||
titulo="Sin herramientas conectadas"
|
||||
detalle="Las herramientas le dejan consultar tus sistemas mientras conversa: stock, estado de un pedido, disponibilidad de turnos. Sin ninguna, responde solo con lo que tiene cargado."
|
||||
/>
|
||||
<div v-for="t in tools" :key="t.ID" class="p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-sm text-texto font-mono">{{ t.nombre }}</div>
|
||||
@@ -618,7 +628,7 @@ watch(
|
||||
|
||||
<div v-if="showToolForm" class="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50" @click.self="showToolForm = false">
|
||||
<div class="card p-6 w-full max-w-xl max-h-[85vh] overflow-y-auto">
|
||||
<h2 class="font-semibold text-texto mb-4">{{ editingTool ? 'Editar tool' : 'Nueva tool' }}</h2>
|
||||
<h2 class="font-semibold text-texto mb-4">{{ editingTool ? 'Editar herramienta' : 'Nueva herramienta' }}</h2>
|
||||
<form class="space-y-3" @submit.prevent="guardarTool">
|
||||
<div>
|
||||
<label class="label">Nombre (identificador, ej: consultar_stock)</label>
|
||||
@@ -712,7 +722,11 @@ watch(
|
||||
</button>
|
||||
</div>
|
||||
<div class="card divide-y divide-borde">
|
||||
<div v-if="canales.length === 0" class="p-6 text-sm text-tenue">Sin canales configurados.</div>
|
||||
<UiEmptyState
|
||||
v-if="canales.length === 0"
|
||||
titulo="No está atendiendo en ningún lado"
|
||||
detalle="Conectá WhatsApp o Telegram para que empiece a responderle a tus clientes. El widget de tu web funciona aparte, con la clave de sitio de arriba."
|
||||
/>
|
||||
<div v-for="c in canales" :key="c.ID" class="p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
@@ -825,7 +839,11 @@ watch(
|
||||
</button>
|
||||
</div>
|
||||
<div class="card divide-y divide-borde">
|
||||
<div v-if="conexiones.length === 0" class="p-6 text-sm text-tenue">Sin cuentas conectadas.</div>
|
||||
<UiEmptyState
|
||||
v-if="conexiones.length === 0"
|
||||
titulo="Sin cuentas conectadas"
|
||||
detalle="Conectando una cuenta de correo, el agente puede leer y responder mensajes con tu dirección."
|
||||
/>
|
||||
<div v-for="c in conexiones" :key="c.ID" class="p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<span class="font-medium text-texto capitalize">{{ c.proveedor }}</span>
|
||||
@@ -843,7 +861,7 @@ watch(
|
||||
<div v-else-if="tab === 'chat'" class="card p-4 flex flex-col h-[28rem]">
|
||||
<div class="flex-1 overflow-y-auto space-y-2 mb-3">
|
||||
<p v-if="chatMensajes.length === 0" class="text-sm text-tenue">
|
||||
Probá este agente tal cual lo va a ver un visitante — usa la misma config de IA y las mismas tools/base de conocimiento.
|
||||
Probá este agente tal cual lo va a ver un visitante — usa la misma config de IA, las mismas herramientas y la misma base de conocimiento.
|
||||
</p>
|
||||
<div
|
||||
v-for="(m, i) in chatMensajes"
|
||||
@@ -866,7 +884,11 @@ watch(
|
||||
<!-- Conversaciones -->
|
||||
<div v-else-if="tab === 'conversaciones'" class="grid grid-cols-3 gap-4">
|
||||
<div class="col-span-1 card divide-y divide-borde max-h-[28rem] overflow-y-auto">
|
||||
<div v-if="sesiones.length === 0" class="p-4 text-sm text-tenue">Sin conversaciones.</div>
|
||||
<UiEmptyState
|
||||
v-if="sesiones.length === 0"
|
||||
titulo="Nadie escribió todavía"
|
||||
detalle="Acá vas a ver todo lo que le preguntan y qué contestó."
|
||||
/>
|
||||
<button
|
||||
v-for="s in sesiones"
|
||||
:key="s.session_id"
|
||||
@@ -894,10 +916,15 @@ watch(
|
||||
<!-- Auditoría -->
|
||||
<div v-else>
|
||||
<p class="label mb-4">
|
||||
Errores y eventos técnicos de este agente — fallos al llamar al AI, a tools, a correo o a los canales. Últimos 100.
|
||||
Errores y eventos técnicos de este agente — fallos al llamar a la IA, a una herramienta, al correo o a los canales. Últimos 100.
|
||||
</p>
|
||||
<div class="card divide-y divide-borde max-h-[32rem] overflow-y-auto">
|
||||
<div v-if="eventos.length === 0" class="p-6 text-sm text-tenue">Sin eventos registrados — buena señal.</div>
|
||||
<UiEmptyState
|
||||
v-if="eventos.length === 0"
|
||||
estado="contenta"
|
||||
titulo="Ningún problema registrado"
|
||||
detalle="Acá aparecen los errores: una herramienta que no responde, una fuente que no se pudo leer. Que esté vacío es buena señal."
|
||||
/>
|
||||
<details v-for="e in eventos" :key="e.ID" class="p-3">
|
||||
<summary class="cursor-pointer flex items-center gap-2 text-sm">
|
||||
<span class="px-1.5 py-0.5 rounded text-xs shrink-0" :class="nivelColor(e.nivel)">{{ e.nivel }}</span>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { api } from '../lib/api.js'
|
||||
import { apiUmind, contexto } from '../lib/contexto.js'
|
||||
import UiMascota from '../components/ui/UiMascota.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const cargando = ref(contexto.esPortal)
|
||||
@@ -51,7 +52,7 @@ onMounted(irAlAgenteSiEsUnoSolo)
|
||||
<!-- Sin espacios no hay nada que elegir ni forma de crearlo: mandarlo a
|
||||
"elegí de la izquierda" cuando la izquierda está vacía es un callejón. -->
|
||||
<div v-else-if="sinEspacios" class="max-w-md mx-auto text-center py-20">
|
||||
<div class="text-4xl mb-4">🤖</div>
|
||||
<UiMascota estado="durmiendo" :tam="80" class="mx-auto mb-4 text-brand" />
|
||||
<h1 class="text-lg font-medium text-texto">Todavía no tenés un asistente activo</h1>
|
||||
<p class="text-sm text-tenue mt-2">
|
||||
uMind contesta por vos en WhatsApp y en tu sitio, con la información de tu negocio.
|
||||
@@ -69,7 +70,7 @@ onMounted(irAlAgenteSiEsUnoSolo)
|
||||
<div v-else class="flex flex-col items-center justify-center text-center py-24">
|
||||
<div class="text-4xl mb-4">💬</div>
|
||||
<h1 class="text-lg font-medium text-texto">
|
||||
{{ contexto.esPortal ? 'Elegí tu espacio de la izquierda' : 'Elegí un tenant de la izquierda' }}
|
||||
{{ contexto.esPortal ? 'Elegí tu espacio de la izquierda' : 'Elegí un espacio de la izquierda' }}
|
||||
</h1>
|
||||
<p class="text-sm text-tenue mt-1">
|
||||
{{ contexto.esPortal
|
||||
|
||||
@@ -185,7 +185,7 @@ watch(() => props.id, cargar, { immediate: true })
|
||||
<button class="btn-primary" :disabled="cupo.lleno" @click="nuevoAgente">+ Nuevo agente</button>
|
||||
</div>
|
||||
<p v-if="cupo.sinPlan && !contexto.esPortal" class="text-xs text-tenue -mt-2 mb-4">
|
||||
Este tenant no tiene plan asignado, así que no se le aplica ningún límite de agentes. Asignale uno desde el lápiz del tenant en la barra izquierda.
|
||||
Este espacio no tiene plan asignado, así que no se le aplica ningún límite de agentes. Asignale uno desde el lápiz del espacio, en la barra izquierda.
|
||||
</p>
|
||||
<p v-else-if="cupo.lleno" class="text-xs text-tenue -mt-2 mb-4">
|
||||
Alcanzaste el máximo de agentes de tu plan.
|
||||
@@ -209,7 +209,7 @@ watch(() => props.id, cargar, { immediate: true })
|
||||
<UiEmptyState
|
||||
v-else-if="agentes.length === 0"
|
||||
class="card"
|
||||
icono="🤖"
|
||||
estado="durmiendo"
|
||||
titulo="Todavía no hay agentes"
|
||||
detalle="Creá el primero y cargale su base de conocimiento para que empiece a responder."
|
||||
>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 96'%3E%3Crect width='96' height='96' rx='22' fill='%238eb02f'/%3E%3Cpath d='M32,42 V58 A14,14 0 0 0 60,58 V42' fill='none' stroke='%23fff' stroke-width='10' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M60,58 V64' fill='none' stroke='%23fff' stroke-width='10' stroke-linecap='round'/%3E%3Ccircle cx='60' cy='28' r='7' fill='%23fff'/%3E%3C/svg%3E" />
|
||||
<title>uMind Studio</title>
|
||||
<script type="module" crossorigin src="/orchestrator/assets/index-CRnmvnlL.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/orchestrator/assets/index-Culm0hM7.css">
|
||||
<script type="module" crossorigin src="/orchestrator/assets/index-Da7ZrNMD.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/orchestrator/assets/index-DNRtUHD7.css">
|
||||
</head>
|
||||
<!-- Sin clase de fondo: el color lo pone body en style.css desde los tokens,
|
||||
que son los que cambian con el tema. Una utilidad acá le ganaba a la
|
||||
|
||||
+26
-1
@@ -387,6 +387,14 @@ a:focus-visible, button:focus-visible {
|
||||
}
|
||||
|
||||
.salida.on { opacity: 1; transform: none; transition: opacity .4s ease, transform .4s ease; }
|
||||
.umi-mini {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
vertical-align: -3px;
|
||||
margin-right: 5px;
|
||||
color: var(--lime);
|
||||
}
|
||||
|
||||
.salida-rotulo {
|
||||
display: block;
|
||||
font-family: var(--mono);
|
||||
@@ -569,6 +577,20 @@ footer a:hover { color: var(--cyan); }
|
||||
<div class="marco">
|
||||
|
||||
<div class="escenario" id="escenario">
|
||||
<!-- Umi vive en el mismo archivo que la usa: una promo tiene que poder
|
||||
copiarse a cualquier lado sin arrastrar dependencias. -->
|
||||
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
|
||||
<symbol id="umi" viewBox="0 0 120 120">
|
||||
<circle cx="60" cy="26" r="6" fill="currentColor" />
|
||||
<line x1="60" y1="32" x2="60" y2="44" stroke="currentColor" stroke-width="4" stroke-linecap="round" />
|
||||
<path d="M30,46 V76 A30,30 0 0 0 90,76 V46 Z" fill="currentColor" />
|
||||
<circle cx="47" cy="66" r="6" fill="var(--panel)" />
|
||||
<circle cx="73" cy="66" r="6" fill="var(--panel)" />
|
||||
<circle cx="48" cy="67" r="3" fill="#11150F" />
|
||||
<circle cx="74" cy="67" r="3" fill="#11150F" />
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<canvas id="campo" class="campo" aria-hidden="true"></canvas>
|
||||
<div class="foco" id="foco" aria-hidden="true"></div>
|
||||
|
||||
@@ -641,7 +663,10 @@ footer a:hover { color: var(--cyan); }
|
||||
</div>
|
||||
|
||||
<div class="salida" id="salida">
|
||||
<span class="salida-rotulo">Lo que recibe tu cliente</span>
|
||||
<span class="salida-rotulo">
|
||||
<svg class="umi-mini" viewBox="0 0 120 120" aria-hidden="true"><use href="#umi" /></svg>
|
||||
Lo que recibe tu cliente
|
||||
</span>
|
||||
<span id="texto-salida"></span><span class="cursor" id="cursor"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user