ADR-020: Sistema de diseño Orpyca y asistente conversacional del frontend¶
Estado: Propuesto Fecha: 2026-06-23 Autores: Giampiero (mantenedor principal)
Contexto¶
ADR-011 decidió dónde vive el frontend (carpeta frontend/ del monorepo) y con qué stack (SvelteKit SSR + Bulma/Sass, OAuth2 PKCE vía orpycamcp-frontend), pero dejó abierto cómo se ve y cómo se opera la interfaz. La épica E22 (fase final de presentación) debe resolver tres decisiones de producto/diseño que condicionan toda la UI y que conviene fijar antes de codificar:
-
Identidad visual y consistencia. Las implementaciones legadas de Orfeo (ICETEX, BogotáLimpia, 9342, …) sufren de pantallas heterogéneas, flujos partidos en múltiples pestañas y formularios densos que penalizan a quien trabaja jornadas largas en ventanilla. Hace falta una plantilla única, minimalista y con una paleta de marca consistente, no un tema por módulo.
-
Personalización por tenant y por rol. OrpycaMCP es multi-tenant (ADR-002). Cada institución necesita su marca (logo/colores) sin recompilar, y cada rol (operador de ventanilla, gestor, archivista, administrador) debe ver solo su superficie de trabajo.
-
Asistente conversacional (texto y voz). El requisito de producto es operar el sistema "conversando": radicar, buscar, consultar expedientes, adjuntar anexos y automatizar tareas por lenguaje natural, sin que esa comodidad abra un agujero de seguridad ni filtre datos a terceros (instituciones públicas, Ley 1581/2012 de datos personales).
La pregunta de esta ADR: qué sistema de diseño adopta el frontend y cómo se integra el asistente sin romper las garantías de la plataforma (Keycloak, multi-tenant, RBAC, auditoría, soberanía del dato).
Decisión¶
El frontend adopta un sistema de diseño único "Orpyca" basado en tokens (Sass + CSS custom properties para branding en runtime por tenant), una plantilla de aplicación única con UI condicionada por rol, y un asistente conversacional (texto + voz) que NO ejecuta acciones por sí mismo: traduce lenguaje natural a tools de la capa MCP existente (ADR-019), que pasan por el gateway y heredan toda la seguridad. La inferencia (LLM y STT) corre con proveedores locales pluggable, desactivados por defecto mediante perfiles de Docker Compose.
1. Sistema de diseño por tokens (paleta Orpyca)¶
- Doble expresión de tokens: la paleta se define como variables Sass (alimentan a Bulma en build) y como CSS custom properties
--op-*en:root. Las variables Sass dan el tema base; las custom properties permiten rebrandear por tenant en runtime (inyectando un<style>con overrides de--op-*desde los claims/config del tenant) sin recompilar. - Paleta Orpyca — la tabla siguiente refleja el Sistema de Diseño OrpycaMCP v1.0 (2026-07), que reemplazó los valores provisionales con los que se escribió originalmente esta ADR (
#459940/#305336/#94BC2F/#212421/#515551/#D8DDD6/#F5F7F4, y los estados#D8A32E/#D9534F/#3E7FA6). Los nombres de token no cambiaron, solo sus valores, por lo que ninguna pantalla tuvo que tocarse. La fuente única esfrontend/src/styles/_tokens.scss:
| Token | Valor | Uso |
|---|---|---|
--op-primary |
#2A8C3A |
marca 500 — acción principal. Solo usos NO textuales (ver nota de contraste) |
--op-primary-dark |
#1C6B33 |
marca 600 — hover/activo y color AA-seguro para TEXTO |
--op-primary-light |
#DCEED7 |
fondos sutiles, selección |
--op-on-primary |
#FFFFFF |
texto sobre superficie primaria oscura |
--op-accent |
#8DBF3C |
marca lime 300 — acentos puntuales. NUNCA como texto sobre claro (2.18:1) |
$op-brand-700 / $op-brand-400 |
#173A22 / #4FA84B |
extensión de marca para degradados profundos (hero, panel de login) |
--op-text |
#232624 |
ink — texto primario |
--op-text-secondary |
#6E7370 |
muted — texto secundario |
--op-border |
#E7EAE2 |
hairline — bordes/divisores |
--op-bg |
#FBFCF8 |
canvas — fondo de página |
--op-surface |
#FFFFFF |
tarjetas/superficies |
--op-success / --op-warning / --op-error / --op-info |
#2A8C3A / #C8881C / #C4452C / #2B6CB0 |
estados — color de borde/icono (≥3:1) |
--op-*-bg / --op-*-fg |
ver _tokens.scss |
pares verificados ≥4.5:1 AA para chips, toasts y semáforos |
- Contraste: el primario está partido por rol. El verde de marca v1.0 (
#2A8C3A) mide 4.28:1 sobre blanco — cumple el umbral no-textual de 3:1 pero falla el 4.5:1 que WCAG AA exige a texto normal. Por eso--op-primaryconserva el valor exacto de v1.0 y se reserva a bordes, iconos y anillos de foco, mientras que todo uso de texto —botones, tags, enlaces,.text-primary, skip-link, y las variables Sass$primary/$linkque alimentan las clases autogeneradas de Bulma— consume--op-primary-dark(6.56:1) o el par--op-*-fgcorrespondiente. Los ratios están calculados con la fórmula de luminancia relativa WCAG (no estimados) y anotados junto a cada token en_tokens.scss. - Tipografía: Space Grotesk (display, títulos, cifras destacadas) + Public Sans (UI y cuerpo), self-hosted vía
@fontsource— sin CDN externo, tanto por soberanía como para no filtrar la navegación de los usuarios a un tercero. Escala v1.0: Display 46/700 · H1 28/600 · H2 22/600 · Body 15/400 · Small 13/500 · Mono 12/500. - Movimiento: los tokens de duración y easing viven también en
_tokens.scss, que declara además una regla globalprefers-reduced-motionneutralizando toda transición y animación del sitio; ninguna pantalla necesita repetirla. - Regla de proporción 60/30/10 (neutro/primario/acento) para evitar saturación de verde en pantallas de uso prolongado.
- Plantilla única:
AppLayout(sidebar + topbar + breadcrumb + área de contenido), reutilizando la estructura validada en~/Documents/sgdINTI/frontend(AppLayout/SidebarNav/BreadcrumbBar,api.service.js, storesauth/permissions/roles/ui), repintada con la paleta Orpyca.
2. Vistas consolidadas y UI por rol¶
- Anti multi-pestaña: cada caso de uso del legado que se repartía en varias pestañas se rediseña como una vista con pasos guiados o paneles contextuales (detalle de radicado con acciones de operador en sitio, flujo/historial inline). La matriz de cobertura capacidad→superficie UI vive en
E22-frontend/spec.md §4.1. - UI por rol: la navegación y las acciones se filtran por
roles[]/permissions[]del JWT. Cuatro roles base: operador de ventanilla, gestor/funcionario, archivista, administrador de tenant. La UI nunca ofrece lo que la API no autorizaría — es espejo del RBAC del backend (ADR-013), no una segunda fuente de verdad. - Tenant resuelto del claim del JWT: una sola URL; el subdominio por tenant queda como evolución futura sin coste de rediseño.
3. Asistente conversacional como traductor a tools MCP¶
- No ejecuta, traduce: el asistente convierte lenguaje natural en invocaciones de las tools del
mcp-server(ADR-019), que ya están mapeadas 1:1 a endpoints REST, filtradas por permisos y con gate de escritura. El asistente no accede a BD ni a servicios; toda acción viajafrontend → gateway → mcp-server/servicio, propagando el token Keycloak yX-Tenant-Slug, y queda auditada como cualquier otra llamada. - Contratos (consumidos siempre por el gateway):
POST /api/v1/assistant/message— entrada NL → respuesta + tool calls resueltas; subida de anexos al chat reutiliza E07.POST /api/v1/assistant/transcribe— audio → texto (STT) para los comandos de voz.- Inferencia local y pluggable (soberanía del dato, alineado con ADR-006):
- LLM local (Ollama / llama.cpp) por defecto; proveedor configurable, sin API externa obligatoria.
- STT en servidor con Whisper (proveedor pluggable,
stubpor defecto); el audio no sale a terceros. - Opt-in por perfiles de Compose: los servicios pesados de IA (Ollama, Whisper) y de edición en línea (OnlyOffice, ver E07 §9) se declaran con
profiles: ["assistant"]/["editor"]y arrancan desactivados. Una instalación mínima corre sin GPU ni contenedores de inferencia; quien quiera el asistente activa el perfil.
4. Entrega gradual (5 fases de E22)¶
(1) plantilla base + tokens + login PKCE; (2) vistas consolidadas anti-multipestaña + editor TipTap (borradores de Salida); (3) asistente + voz; (4) personalización tenant + roles; (5) pruebas. Cada fase se cierra antes de abrir la siguiente. Esta ADR fija las decisiones transversales; el detalle de tareas vive en E22-frontend/{spec,plan,tasks}.md.
Consecuencias¶
Positivas:
- Una sola identidad visual y un único AppLayout → coherencia, menor carga cognitiva en jornadas largas, mantenimiento barato.
- Rebranding por tenant sin recompilar gracias a las custom properties --op-* en runtime.
- El asistente no amplía la superficie de ataque: al delegar en tools MCP que pasan por el gateway, hereda autenticación, RBAC, multi-tenant y auditoría; no puede saltárselos aunque el LLM "alucine" una acción no permitida (la API la rechaza).
- Soberanía del dato: LLM y STT locales; nada del contenido documental ni de la voz sale a un proveedor externo por defecto.
- Coste de operación opcional: sin el perfil assistant, no hay contenedores de inferencia; el sistema corre en hardware modesto.
Negativas / límites: - Mantener tokens en dos formatos (Sass + custom properties) exige disciplina para no divergir; se mitiga con un único archivo fuente de tokens y linql del tema. - El asistente queda acoplado a la calidad del catálogo de tools y del LLM local; un modelo pequeño puede fallar en intención compleja (mitigable: confirmación explícita antes de toda tool de escritura). - La inferencia local añade latencia y consumo (RAM/CPU/GPU) cuando el perfil está activo; es coste asumido a cambio de privacidad y aceptable para una capa de asistencia, no de alto rendimiento. - SSR + branding por tenant en runtime obliga a inyectar el tema en cada render del tenant; coste menor frente a recompilar por institución.
Alternativas consideradas¶
- Tema por módulo / CSS ad hoc por pantalla (statu quo del legado): descartado — produce la heterogeneidad que esta ADR busca eliminar.
- Solo variables Sass (sin custom properties): descartado — obligaría a recompilar el frontend por cada tenant; las custom properties habilitan el rebranding en runtime.
- Asistente que ejecuta acciones contra los servicios directamente: descartado — duplicaría RBAC/tenant/auditoría y rompería el respeto de capas; el camino correcto es reutilizar la capa MCP (ADR-019).
- LLM/STT en la nube (OpenAI, etc.): descartado como opción por defecto — instituciones públicas con datos personales; choca con la soberanía del dato. Sigue siendo posible como proveedor pluggable para quien lo decida y asuma.
- Inferencia siempre encendida: descartado — encarece el despliegue mínimo; los perfiles de Compose la dejan opt-in.
- Otro framework de UI / design system de terceros (Material, etc.): descartado — se mantiene Svelte/Bulma de ADR-011 y se construye un sistema de tokens propio ligero en vez de adoptar una librería pesada.
Relacionados¶
- ADR-011 — ubicación y stack del frontend; esta ADR define su diseño y asistente.
- ADR-019 — capa MCP; el asistente es un cliente NL de esas tools, no una vía paralela.
- ADR-006 — proveedor de IA configurable; el LLM/STT local sigue ese patrón pluggable.
- ADR-013 — la UI por rol espeja el RBAC resuelto en la BD; no es fuente de verdad.
- ADR-002 — el branding y el scope por tenant se resuelven con el claim del JWT.
documentos/specDrive/epics/E22-frontend/{spec,plan,tasks}.md— detalle de requisitos (RF-UI-01…21), plan técnico y tareas (gitignored).