Commit Graph
12 Commits
Author SHA1 Message Date
Lizandro GuarnizoandClaude Opus 5 ee9bb2cfb7 feat(studio): un solo set de iconos en vez de emojis mezclados con símbolos
En la misma pantalla convivían dos lenguajes: emojis a color (🌙 📊 🧠 ✍️ 📄
🌐 🔁) y símbolos tipográficos monocromos (✎ ✕ ← ↻ ⚠ ⬇ ✓). Eso se lee como
descuido, y encima los emojis los dibuja el sistema operativo — el mismo panel
se ve distinto en Mac, en Windows y en Android, sin que podamos hacer nada.

Ahora hay un set propio de iconos SVG de trazo: mismo grosor de línea, misma
caja, y heredan currentColor, así que toman el color del texto que los rodea y
funcionan igual en tema claro y oscuro. Al lado uno de otro tienen el mismo
peso visual, que es lo que hacía falta.

El emoji suelto de la pantalla de inicio pasa a ser Umi, que ya es el personaje
del producto.

Un detalle de implementación que costó dos intentos: los iconos NO pueden
declararse como cadenas de HTML para v-html. Dentro de un <svg>, v-html parsea
como HTML y los <path> quedan sin el namespace de SVG: aparecen en el DOM pero
no dibujan nada, así que la primera versión se veía perfecta en el código y en
blanco en pantalla. Van declarados como pares [etiqueta, atributos] para que
Vue los cree con el namespace correcto.

Y AgenteDetail usaba cinco iconos sin importar el componente. Verificado ahora
sobre todos los archivos: cada componente usado está importado donde se usa.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 10:45:43 -05:00
Lizandro GuarnizoandClaude Opus 5 002b75e71b feat(studio): que se note que hay una IA atrás, sin fondo animado permanente
Un fondo animado corriendo todo el día en una herramienta de trabajo se ve bien
diez segundos y molesta las ocho horas siguientes, además de tener un canvas
comiéndose la batería. Así que el movimiento va donde hay algo pasando de
verdad, y el fondo queda quieto.

Al entrar, una vez por sesión: el campo de puntos conectándose durante poco más
de un segundo, y se destruye. Es el mismo lenguaje visual de la página pública
—puntos que se enlazan, como el espacio vectorial donde vive el conocimiento—
así que refuerza la marca en vez de inventar algo para adentro.

En los momentos de espera, que es donde el producto se veía muerto: el chat de
prueba decía "Pensando..." en gris y ahora muestra a Umi con los tres puntos,
el mismo gesto que ve el visitante en el widget. Y una fuente en "procesando"
tenía la etiqueta quieta, sin decir si seguía avanzando o se había colgado:
ahora late.

El fondo es una retícula de plano técnico, tenue y estática, que se desvanece
hacia abajo para no competir con las tablas y los formularios.

Dos cosas que aparecieron al verificar:

El apagado de la animación de entrada estaba dentro del requestAnimationFrame,
que el navegador no corre en pestañas de fondo — abrir el Studio en una pestaña
que no se está mirando dejaba el canvas puesto para siempre, tapando la
interfaz con una capa invisible. Ahora el temporizador se programa aparte.

Y una fuente sin procesar mostraba "leído sin procesar", que no quiere decir
nada: si no hay fecha de lectura, no se escribe la frase.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 20:44:40 -05:00
Lizandro GuarnizoandClaude Opus 5 3ceb61c035 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>
2026-08-19 19:19:01 -05:00
Lizandro GuarnizoandClaude Opus 5 af71b66bb8 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>
2026-08-19 18:21:46 -05:00
Lizandro GuarnizoandClaude Sonnet 5 ce5956878d fix(studio): uMind era inusable en móvil
El SPA no tenía ningún tratamiento responsive: un sidebar fijo de 256px
siempre visible. En un teléfono de 375px eso deja 119px para el contenido.

Ahora en móvil el sidebar sale de flujo y entra deslizándose desde la
izquierda, con botón en el header y fondo oscuro para cerrarlo. Se cierra
solo al elegir un tenant: dejarlo abierto tapa justo la pantalla a la que
se acaba de entrar. En escritorio (md+) no cambia nada — sigue fijo y sin
botón.

El estado vive en lib/ui.js porque lo tocan dos componentes que no son
padre/hijo: el botón del header y el propio sidebar.

También se achica el padding del contenido en pantallas chicas y las tabs
del agente pasan a scrollear en su propia caja en vez de desbordar.

Medido a 492px de ancho: scrollWidth igual a innerWidth y ningún elemento
sobresaliendo. Escritorio verificado sin regresión.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 18:16:34 -05:00
Lizandro GuarnizoandClaude Sonnet 5 115f1675ad fix(studio): los campos Cliente y Plan no aparecían al crear un tenant
/app/api/clientes/select devuelve el array pelado y /app/umind-planes/list
lo envuelve en {items}. El código leía solo .registros/.items, así que con
el array quedaba vacío — y el bloque estaba detrás de v-if="clientes.length",
o sea que Cliente y Plan simplemente no se veían y no había forma de
asignarlos.

- lista() acepta las dos formas (verificado contra ambas y contra
  vacío/null/undefined).
- Se cargan con allSettled: que falle uno ya no vacía el otro.
- El bloque se muestra siempre para el staff, y cada select dice a dónde ir
  si su lista está vacía. Antes un fallo de red se veía exactamente igual
  que "no hay nada configurado", que es lo que enmascaró este bug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 21:49:32 -05:00
Lizandro GuarnizoandClaude Sonnet 5 d8f9acd1f8 feat(studio): rediseño con design tokens, pantalla de consumo y rename a Studio
El SPA pasa a llamarse uMind Studio y vive en /studio (el nombre
"Orquestador" no le decía nada al cliente final, que ahora es quien lo usa).

Diseño:
- Los colores viven una sola vez como variables CSS + clases semánticas
  (.card, .input, .btn-*, .badge-*). Antes cada elemento repetía el par
  claro/oscuro a mano en cientos de lugares y cambiar un tono era buscar
  y reemplazar.
- darkMode pasa de 'media' a 'class' con toggle propio persistido: el
  usuario elige, no el sistema operativo. Se aplica antes de montar la
  app para que no parpadee.
- Componentes compartidos (UiModal, UiBadge, UiEmptyState) donde antes
  había markup duplicado inline.

Nueva pantalla de consumo (/tenants/:id/uso): filtros por fecha con
atajos, total del período, pendiente de facturar, barra contra el tope
del plan, desglose por tipo y gráfico por día. El gráfico son divs con
altura porcentual — no vale traer una librería de charts para esto.

Rename:
- /studio y /portal/studio; /orchestrator redirige 301 conservando la
  ruta interna, así los enlaces guardados siguen funcionando. Los assets
  del bundle quedan exentos (el `base` de Vite sigue en /orchestrator/):
  redirigirlos rompería el SPA.
- El submódulo sembrado migra el mismo registro buscando por las tres
  URLs históricas (/app/umind → /orchestrator → /studio) en vez de
  dejar entradas duplicadas en el menú.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 12:02:54 -05:00
Lizandro GuarnizoandClaude Sonnet 5 5b78f6677c feat(umind): el cliente administra sus agentes desde el portal
Acá es donde uMind deja de ser una herramienta interna: el cliente entra
a /portal/studio con su sesión de portal y gestiona lo suyo.

- UmindScopePortal/UmindScopeStaff es el ÚNICO punto donde se decide el
  alcance. El del cliente sale de GetClienteIDsForPortalUser, el mismo
  que ya autoriza el resto del portal. nil = staff sin restricción,
  slice vacío = no ve nada; una ruta sin scope también cae en "no ve
  nada" para que olvidarse el middleware falle visible y no abra todo.
- Un solo set de handlers montado bajo /app/umind y /portal/umind
  (RegistrarRutasUmind). Duplicarlos sería duplicar las chances de
  olvidar un chequeo.
- Guarda de acceso en TODOS los handlers, incluidos los sub-recursos que
  llegan por :id (documento, tool, canal, conexión): hay que cargarlos
  para saber de quién son, si no un cliente podría borrar el canal de
  otro adivinando el id. Responden 404, no 403: un 403 confirmaría que
  el recurso existe.
- Cierra un bug preexistente: las lecturas GET /app/umind/* no tenían
  SoloAdmin ni pasaban por MenuMiddleware, así que cualquier usuario de
  staff podía leer los tenants de todos los clientes.
- Límite de agentes por plan (409 con mensaje claro). Un tenant sin plan
  no tiene límite: cortarles de golpe sería peor que dejarlos como estaban.
- /umind/ai-configs reemplaza con alcance a /app/api/ai-config/select,
  que devolvía TODAS las configs del sistema.
- El SPA deduce por la URL si es staff o cliente (base del router,
  prefijo de API y URL de login) y oculta lo que es solo de staff.
- Test de aislamiento entre clientes: 7 casos, incluido que un scope
  vacío no se confunda con staff.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 11:51:12 -05:00
Lizandro GuarnizoandClaude Sonnet 5 c69b904b03 feat(umind): vincula tenants a clientes y agrega planes con precios
Base para que el cliente administre uMind desde su portal y para el cobro
por consumo. Todavía no cambia nada del comportamiento actual.

- UmindTenant gana ClienteID (punteros, sin not null: los tenants creados
  antes del portal quedan sin asignar y el ALTER TABLE no falla sobre
  datos existentes) y PlanID.
- GetUmindTenantsByClientes es fail-closed: sin clientes, no ve nada.
- UmindPlan define el máximo de agentes y los precios por 1k tokens, por
  imagen OCR, por transcripción y la mensualidad, más un tope de consumo
  que solo avisa. CRUD para staff en /app/umind-planes.
- El modelo va en AMBAS listas de AutoMigrate (main.go y migrations),
  no repetir el error de agregarlo solo en una.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 11:32:38 -05:00
Lizandro GuarnizoandClaude Sonnet 5 f3f2f421d6 feat: uMind pasa a multi-agente por tenant
Un tenant (negocio/sitio, dueño de los dominios permitidos) puede tener
varios UmindAgente independientes (ej. "Ventas", "Soporte"), cada uno con
su propia config de IA, tono, base de conocimiento, tools, canales y
conexión de correo. El site_key también pasa a ser por agente, así cada
uno tiene su propio <script> de widget embebible y su propio color.

Backend:
- Nuevo modelo UmindAgente (pkg/models/umind_agente.go), con SiteKey,
  AiConfigID, Tono, MensajeBienvenida y Color — campos que antes vivían en
  UmindTenant y se sacan de ahí (las columnas viejas quedan huérfanas sin
  usar, no se hace DROP COLUMN).
- UmindDocumento, UmindChunk, UmindHerramienta, UmindCanal, UmindConexion y
  UmindMensaje pasan de TenantID a AgenteID. El campo se agrega sin
  "not null" para no romper el ALTER TABLE en Postgres sobre tablas que ya
  tienen filas (ej. emetropolitana).
- migrations.MigrarUmindAgentes(): idempotente, crea un agente "Principal"
  por cada tenant existente heredando lo que ya tenía configurado, y mueve
  sus datos de tenant_id a agente_id. Corre en cada arranque normal, mismo
  criterio que los Seed* — nada se rompe para los tenants ya en producción.
- Motor del agente, widget, canales (Telegram/WhatsApp) y OAuth de correo
  ahora operan sobre UmindAgente; el tenant solo se consulta para el
  chequeo de dominio permitido y el nombre del negocio que ve el visitante.

Frontend: nueva jerarquía de navegación tenant → lista de agentes
(TenantAgentes.vue) → detalle de un agente (AgenteDetail.vue, antes
TenantDetail.vue) con las mismas 6 tabs de siempre, ahora por agente. El
modal de tenant en el sidebar se achica a nombre/dominios/activo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 09:30:06 -05:00
Lizandro GuarnizoandClaude Sonnet 5 8c1e754ab3 feat: rediseña el widget embebible (ícono AI, color configurable, animaciones)
El bubble era un emoji 💬 sobre un color fijo hardcodeado en el JS. Ahora:
- Ícono nuevo (spark/sparkle, el motivo visual estándar de "asistente IA"
  hoy) en vez del emoji, con anillo de pulso al cargar para llamar la
  atención sin ser invasivo.
- Color configurable por tenant (UmindTenant.Color, validado como hex en el
  server) — se aplica en tiempo real vía /widget/:site_key/init, así que
  cambiarlo desde el panel no requiere recopiar el <script>.
- Panel con animación de entrada, botón de cerrar explícito, subtítulo
  "Asistente con IA · uMind" y badge de marca — antes solo tenía el nombre
  del tenant y ningún indicio de qué lo potencia.
- El widget ahora se inicializa al cargar el script (no al primer click), así
  el color/nombre reales ya están listos antes de que el visitante abra el chat.

Selector de color agregado al form de tenant en el orquestador (input
type=color + hex).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 20:03:51 -05:00
Lizandro GuarnizoandClaude Sonnet 5 5ba41786d6 feat: conexiones OAuth (Gmail/Outlook) para el agente + rediseño del orquestador
Backend:
- UmindConexion: cuenta de correo conectada por tenant vía OAuth2
  (golang.org/x/oauth2, promovida de indirecta a directa), tokens cifrados
  en reposo con el mismo AES-GCM+APP_KEY que ya usan tools/canales.
- Flujo completo: /app/umind/conexiones/conectar redirige a Google/Microsoft,
  /callback/:proveedor intercambia el code (state autoverificable por HMAC,
  sin tabla de estados pendientes), refresh on-demand antes de cada uso.
- Dos tools nuevas para el agente (enviar_correo/leer_bandeja) que aparecen
  solo si el tenant tiene una conexión activa, vía Gmail API / Microsoft
  Graph directo (sin el SDK pesado de Google).
- Requiere que el dueño del proyecto cree las apps OAuth en Google Cloud
  Console / Azure y cargue GOOGLE_OAUTH_CLIENT_ID/SECRET y
  MS_OAUTH_CLIENT_ID/SECRET — sin eso los botones de conectar fallan con un
  mensaje claro, no en silencio.

Frontend: rediseño del orquestador — layout de sidebar fijo (reemplaza el
navbar + lista de página completa), modo oscuro vía prefers-color-scheme,
tabs en pill, y la nueva tab "Conexiones".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 10:25:13 -05:00