feat(umind): logo propio, plantillas de agente por rubro y página promocional

Logo. Hasta ahora no había: era el texto "uM" en un cuadrado, con tamaño y
esquinas distintas en cada pantalla. Ahora hay un mark — la "u" del nombre con
el punto de pulso integrado — probado a 16px, 20px y 32px sobre claro y oscuro,
que es donde se cae la mayoría de los logos. Reemplaza el texto en el portal,
el header, el sidebar del Studio y la promo, y va también de favicon.

Plantillas por rubro. Un agente recién creado no sabía nada, y escribir el
conocimiento desde cero frente a un campo en blanco es donde la mayoría
abandona. Al crear uno se puede elegir un punto de partida —tienda,
restaurante, consultorio, servicios, peluquería o la base genérica— y arranca
con sus notas ya escritas, listas para editar.

Los valores de ejemplo van entre corchetes a propósito, para que se vea que hay
que reemplazarlos: una nota que parezca dato real termina en boca del asistente.
Un test lo verifica nota por nota. Y la plantilla de salud abre con el límite —
el asistente no da consejo médico ni interpreta síntomas — porque es el único
rubro donde una respuesta inventada hace daño de verdad.

El catálogo vive en código, no en base: es contenido editorial que mejoramos
nosotros, no configuración que cada instalación toca por su lado. Las notas se
cargan en segundo plano porque cada una necesita sus embeddings, y hacer
esperar el alta sería castigar justo al que eligió plantilla.

Con las opciones nuevas el formulario pasaba de largo la pantalla y el botón de
guardar quedaba abajo del borde: el modal ahora tiene su propio scroll.

Y la página promocional de uMind queda servida en /umind.html.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-19 18:21:46 -05:00
co-authored by Claude Opus 5
parent 3ea17b0980
commit af71b66bb8
17 changed files with 1797 additions and 38 deletions
+9 -3
View File
@@ -140,9 +140,15 @@ onMounted(() => {
md:static md:h-screen md:sticky md:top-0 md:translate-x-0"
:class="menuAbierto ? 'translate-x-0' : '-translate-x-full'"
>
<div class="h-14 px-4 flex items-center border-b border-borde">
<router-link to="/" class="text-base font-semibold text-texto">
uMind <span class="text-brand">Studio</span>
<div class="h-14 px-4 flex items-center gap-2 border-b border-borde">
<router-link to="/" class="flex items-center gap-2 text-base font-semibold text-texto">
<svg viewBox="0 0 96 96" class="w-6 h-6 shrink-0" aria-hidden="true">
<rect width="96" height="96" rx="22" fill="#8eb02f" />
<path d="M32,42 V58 A14,14 0 0 0 60,58 V42" fill="none" stroke="#fff" stroke-width="10" stroke-linecap="round" stroke-linejoin="round" />
<path d="M60,58 V64" fill="none" stroke="#fff" stroke-width="10" stroke-linecap="round" />
<circle cx="60" cy="28" r="7" fill="#fff" />
</svg>
<span>uMind <span class="text-brand">Studio</span></span>
</router-link>
</div>
+41 -3
View File
@@ -62,23 +62,30 @@ const cupo = computed(() => {
})
function vacio() {
return { nombre: '', ai_config_id: null, tono: '', mensaje_bienvenida: '', color: '#8eb02f', activo: true }
return { nombre: '', ai_config_id: null, tono: '', mensaje_bienvenida: '', color: '#8eb02f', activo: true, plantilla_rubro: '' }
}
// Puntos de partida por rubro. Un agente vacío no sirve el primer día, y
// escribir el conocimiento desde cero frente a un campo en blanco es donde la
// mayoría abandona.
const plantillas = ref([])
async function cargar() {
error.value = ''
cargando.value = true
try {
const [t, a, ai] = await Promise.all([
const [t, a, ai, pl] = await Promise.all([
api.get(apiUmind('/umind/tenants')),
api.get(apiUmind(`/umind/agentes?tenant_id=${props.id}`)),
api.get(apiUmind('/umind/ai-configs')),
api.get(apiUmind('/umind/plantillas-rubro')),
])
tenant.value = (t.items || []).find((x) => String(x.ID) === props.id) || null
agentes.value = a.items || []
plan.value = a.plan || null
resumen.value = a.resumen || {}
aiConfigs.value = ai.items || []
plantillas.value = pl.items || []
} catch (e) {
error.value = e.message
} finally {
@@ -231,13 +238,44 @@ watch(() => props.id, cargar, { immediate: true })
</div>
<div v-if="showForm" class="fixed inset-0 bg-black/40 flex items-center justify-center p-4 z-50" @click.self="showForm = false">
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6 w-full max-w-lg">
<!-- max-h + scroll propio: con las opciones de plantilla el formulario
pasa de largo la pantalla, y sin esto el botón de guardar queda
abajo del borde inferior, inalcanzable. -->
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6 w-full max-w-lg max-h-[calc(100vh-2rem)] overflow-y-auto">
<h2 class="font-semibold text-gray-800 dark:text-gray-100 mb-4">{{ editing ? 'Editar agente' : 'Nuevo agente' }}</h2>
<form class="space-y-3" @submit.prevent="guardar">
<div>
<label class="label">Nombre</label>
<input v-model="form.nombre" required placeholder="ej: Ventas, Soporte" class="input" />
</div>
<!-- Solo al crear: en un agente que ya existe, precargar notas
pisaría el conocimiento que el dueño ya escribió. -->
<div v-if="!editing && plantillas.length">
<label class="label">Arrancar con</label>
<div class="grid gap-1.5">
<label
v-for="p in [{ clave: '', nombre: 'Agente en blanco', descripcion: 'Sin conocimiento cargado. Lo escribís vos desde cero.' }, ...plantillas]"
:key="p.clave"
class="flex gap-2.5 p-2.5 rounded-lg border cursor-pointer transition-colors"
:class="form.plantilla_rubro === p.clave
? 'border-brand bg-brand/5'
: 'border-borde hover:border-brand/40'"
>
<input v-model="form.plantilla_rubro" type="radio" :value="p.clave" class="mt-1 text-brand focus:ring-brand" />
<span class="min-w-0">
<span class="block text-sm text-texto">{{ p.nombre }}</span>
<span class="block text-xs text-tenue">{{ p.descripcion }}</span>
<span v-if="p.notas" class="block text-xs text-tenue mt-0.5">
{{ p.notas }} notas listas para editar · {{ p.resumen }}
</span>
</span>
</label>
</div>
<p class="text-xs text-tenue mt-1.5">
Las notas vienen con ejemplos entre corchetes abrilas y reemplazalas por tus datos reales.
</p>
</div>
<div>
<label class="label">Config de IA</label>
<select v-model="form.ai_config_id" class="input">