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>
|
||||
Reference in New Issue
Block a user