# Feature Specification: Asistente de Chat Integrado (n8n)

**Feature Branch**: `011-asistente-chat-n8n`

**Created**: 2026-07-06

**Status**: Draft (clarificado)

**Input**: ... webhook n8n con payload `{ "action": "sendMessage", "sessionId": "...", "chatInput": "..." }`.

> **Constitution (I. Spec-First)**: Este spec MUST ser agnóstico de stack.
> No mencionar Laravel, React, Inertia.js ni rutas de código. El CÓMO va en `plan.md`.

---

## Contrato de integración (webhook n8n)

### Petición (envío de mensaje)

El sistema MUST enviar al servicio externo un objeto JSON con **únicamente** estos campos (sin datos adicionales del usuario autenticado):

| Campo | Tipo | Descripción |
|-------|------|-------------|
| `action` | string | Valor fijo: `"sendMessage"` |
| `sessionId` | string | Identificador opaco de la conversación activa en la pestaña |
| `chatInput` | string | Texto del mensaje del usuario |

**Endpoint destino (servicio externo):** `http://172.233.98.190:8680/webhook/4093f79d-6fcd-4fe4-b147-019ee1ede065/chat`

Las peticiones desde la interfaz de usuario MUST enrutarse a través de un **proxy intermedio** de la aplicación; el navegador no MUST invocar el endpoint externo directamente.

### Respuesta (contrato esperado)

El servicio externo responde con un objeto JSON con la siguiente estructura:

```json
{
  "status": "success",
  "meta": { "intent": "string descriptivo de la acción" },
  "message": {
    "text": "Respuesta en lenguaje natural en español",
    "type": "text"
  },
  "data": [],
  "error": null
}
```

| Campo | Tipo | Descripción |
|-------|------|-------------|
| `status` | `"success"` \| `"error"` | Resultado de la operación |
| `meta.intent` | string | Descripción de la intención detectada o acción ejecutada |
| `message.text` | string | Respuesta en lenguaje natural dirigida al usuario (español) |
| `message.type` | `"text"` \| `"table"` \| `"card"` | Formato de presentación del mensaje en la interfaz |
| `data` | array | Objetos normalizados recuperados de herramientas de datos (p. ej. `id`, `name`, `cif`, `email`); vacío si no aplica |
| `error` | `null` \| object | Si `status` es `"error"`: `{ "code": number, "message": "Descripción del error en español" }` |

**Reglas de interpretación:**

- Si `status` es `"success"`, mostrar `message.text` según `message.type` (`text`, `table` o `card`) y, si `data` no está vacío, renderizar los registros de forma legible.
- Si `status` es `"error"`, mostrar `error.message` al usuario; no tratar la respuesta como mensaje del asistente.
- Si la respuesta no cumple este esquema, tratar como error de integración.

---

## User Scenarios & Testing *(mandatory)*

### User Story 1 — Acceso al asistente desde cualquier pantalla (Priority: P1)

Como usuario autenticado del backoffice, necesito un botón fijo en la parte inferior del layout principal que me permita abrir y cerrar un asistente de chat en cualquier momento, sin abandonar la página en la que estoy trabajando.

**Why this priority**: Sin un punto de acceso persistente y visible, el asistente no aporta valor; es el requisito mínimo para cualquier interacción posterior.

**Independent Test**: Iniciar sesión, navegar a cualquier sección del sistema y verificar que el botón aparece en la misma posición relativa al layout. Al pulsarlo, el panel de chat se abre; al pulsarlo de nuevo (o cerrar), el panel se oculta y el contenido principal recupera su espacio.

**Acceptance Scenarios**:

1. **Given** un usuario autenticado en cualquier pantalla del backoffice en escritorio, **When** observa el layout principal, **Then** existe un botón de acceso al asistente ubicado en la zona inferior del layout, visible y accesible mediante teclado.

2. **Given** el asistente cerrado, **When** el usuario pulsa el botón de acceso, **Then** el panel de chat se abre sin recargar la página ni perder el contexto de la pantalla actual.

3. **Given** el asistente abierto, **When** el usuario cierra el panel (botón de cierre o acción equivalente), **Then** el panel se oculta y el usuario puede continuar trabajando en la misma pantalla.

4. **Given** el asistente abierto, **When** el usuario navega a otra sección del backoffice en la misma pestaña, **Then** el botón de acceso permanece disponible y el estado abierto/cerrado del panel se conserva; el historial visible en la interfaz se mantiene solo mientras la pestaña del navegador permanezca abierta.

5. **Given** un usuario en dispositivo móvil o tablet (viewport por debajo del umbral de escritorio), **When** observa el layout principal, **Then** el botón del asistente y el panel de chat no están disponibles (ocultos).

6. **Given** un usuario que cierra la pestaña o el navegador, **When** vuelve a acceder al sistema (incluso tras logout/login), **Then** el historial visible del chat en la interfaz no se restaura; la persistencia del contexto conversacional queda a cargo del servicio n8n vía `sessionId` en peticiones futuras con nueva sesión de pestaña.

---

### User Story 2 — Enviar consultas y recibir respuestas del asistente (Priority: P1)

Como usuario autenticado, necesito escribir preguntas en lenguaje natural dentro del panel de chat y recibir respuestas del servicio externo conectado vía webhook, de forma similar a un chat de soporte.

**Why this priority**: Es la funcionalidad central del asistente; sin intercambio de mensajes no hay producto utilizable.

**Independent Test**: Abrir el asistente, escribir un mensaje de prueba y enviarlo. Verificar que la petición al proxy incluye únicamente `action`, `sessionId` y `chatInput`, y que la respuesta JSON se interpreta y muestra según el contrato definido.

**Acceptance Scenarios**:

1. **Given** el panel de chat abierto, **When** el usuario escribe un mensaje y lo envía, **Then** el mensaje aparece en el hilo como mensaje del usuario y el sistema envía una petición con `action: "sendMessage"`, un `sessionId` válido y el texto en `chatInput`, sin campos adicionales.

2. **Given** un mensaje enviado correctamente, **When** el servicio responde con `status: "success"`, **Then** `message.text` se muestra en el hilo según `message.type` (`text`, `table` o `card`) y, si `data` contiene registros, se presentan de forma legible.

3. **Given** un mensaje enviado, **When** el servicio responde con `status: "error"`, **Then** se muestra `error.message` al usuario y no se presenta como respuesta normal del asistente.

4. **Given** el panel de chat abierto, **When** el usuario intenta enviar un mensaje vacío o solo espacios en blanco, **Then** el envío se bloquea y se indica al usuario que debe escribir contenido.

5. **Given** un mensaje en curso de envío, **When** la petición está pendiente, **Then** el usuario ve un indicador de carga y no puede enviar mensajes duplicados hasta que finalice la operación.

---

### User Story 3 — Panel lateral incrustado estilo asistente contextual (Priority: P2)

Como usuario autenticado, necesito que el asistente se despliegue como panel lateral incrustado que ocupe aproximadamente el 25% del ancho de la pantalla y el 100% de su alto, de modo que el contenido principal siga visible en el resto del espacio, similar a un asistente contextual tipo Copilot.

**Why this priority**: Es el modo de presentación preferido descrito por el usuario para uso productivo sin perder contexto de la pantalla principal.

**Independent Test**: Abrir el asistente en una pantalla de escritorio con resolución estándar (≥1280 px). Medir que el panel ocupa ~25% del ancho y todo el alto visible del viewport, y que el contenido principal se redimensiona o desplaza sin quedar oculto de forma permanente.

**Acceptance Scenarios**:

1. **Given** una ventana de escritorio con ancho suficiente, **When** el usuario abre el asistente en modo panel incrustado, **Then** el panel ocupa aproximadamente el 25% del ancho de la pantalla y el 100% del alto del viewport.

2. **Given** el panel incrustado abierto, **When** el usuario interactúa con el contenido principal (fuera del panel), **Then** puede seguir operando en la pantalla sin que el panel bloquee la interacción en el área restante.

3. **Given** una ventana de escritorio, **When** el usuario redimensiona el navegador hacia viewports de tablet o móvil, **Then** el asistente se oculta por completo (botón y panel no disponibles).

---

### User Story 4 — Modo flotante alternativo (Priority: P3)

Como usuario autenticado, necesito poder usar el asistente en un panel flotante superpuesto (similar a widgets de soporte al cliente), tanto como opción explícita de presentación como fallback automático en ciertos contextos de escritorio, para consultas rápidas sin redimensionar el contenido principal.

**Why this priority**: El usuario requiere ambas modalidades (incrustado y flotante); el flotante complementa el panel lateral en escritorio.

**Independent Test**: Activar el modo flotante manualmente y verificar que el panel aparece superpuesto, no es arrastrable, y mantiene la misma capacidad de envío/recepción. Verificar también el fallback automático cuando aplique en escritorio.

**Acceptance Scenarios**:

1. **Given** el asistente en modo flotante, **When** el usuario lo abre, **Then** aparece un panel superpuesto sobre el contenido principal sin desplazar el layout subyacente.

2. **Given** el panel flotante abierto, **When** el usuario intenta arrastrarlo, **Then** el panel permanece en posición fija (no arrastrable).

3. **Given** el panel flotante abierto, **When** el usuario envía un mensaje, **Then** el comportamiento de envío y recepción es idéntico al del modo panel incrustado.

4. **Given** ambos modos disponibles en escritorio, **When** el usuario elige explícitamente uno, **Then** la preferencia se recuerda durante la sesión activa de la pestaña (no persiste tras cerrar navegador ni entre pestañas).

5. **Given** condiciones de escritorio donde el fallback automático aplique, **When** el sistema determina que el panel incrustado no es adecuado, **Then** activa el modo flotante sin intervención del usuario.

---

### User Story 5 — Gestión de sesión de conversación (Priority: P2)

Como usuario autenticado, necesito que mis consultas dentro del asistente pertenezcan a una misma conversación identificada por un identificador de sesión (`sessionId`), de modo que el servicio externo n8n pueda mantener contexto entre mensajes sucesivos.

**Why this priority**: El contrato del webhook exige `sessionId`; n8n gestiona el historial conversacional en el servicio; la interfaz solo mantiene el hilo visible en memoria de la pestaña.

**Independent Test**: Enviar dos mensajes consecutivos en la misma pestaña. Verificar que ambos usan el mismo `sessionId`. Abrir otra pestaña y comprobar `sessionId` distinto. Iniciar nueva conversación y comprobar nuevo `sessionId`.

**Acceptance Scenarios**:

1. **Given** un usuario que abre el asistente por primera vez en una pestaña, **When** envía su primer mensaje, **Then** el sistema genera un `sessionId` único para esa pestaña y lo reutiliza en todos los mensajes posteriores de esa conversación.

2. **Given** dos pestañas abiertas con el backoffice, **When** el usuario envía mensajes en cada una, **Then** cada pestaña mantiene su propio `sessionId` independiente.

3. **Given** una conversación en curso, **When** el usuario elige iniciar una nueva conversación (acción explícita), **Then** se asigna un nuevo `sessionId`, el hilo visual se reinicia y los mensajes anteriores dejan de mostrarse en el panel activo.

4. **Given** una conversación en curso en la misma pestaña, **When** el usuario cierra y vuelve a abrir el panel sin iniciar nueva conversación, **Then** el historial visible y el `sessionId` activo se mantienen mientras la pestaña siga abierta.

5. **Given** una pestaña cerrada, **When** el usuario abre una nueva pestaña del backoffice, **Then** se inicia una nueva sesión de chat con nuevo `sessionId` y hilo vacío en la interfaz.

---

### Edge Cases

- ¿Qué ocurre si el webhook no responde, devuelve error HTTP o excede un tiempo de espera razonable? → Mostrar mensaje de error comprensible, permitir reintentar y no perder el texto del mensaje fallido.
- ¿Qué ocurre si el webhook responde con un formato que no cumple el esquema definido? → Mostrar mensaje genérico de error de integración y registrar el incidente para diagnóstico.
- ¿Qué ocurre en viewports móvil/tablet? → El asistente (botón y panel) permanece oculto; no se ofrece modo alternativo en esos dispositivos.
- ¿Qué ocurre si el usuario abre el asistente en múltiples pestañas? → Cada pestaña mantiene su propia sesión de chat con `sessionId` e historial visible independientes.
- ¿Qué ocurre con respuestas `message.type: "table"` o `"card"` con `data` poblado? → Renderizar tablas/tarjetas legibles dentro del hilo, con scroll si el contenido es extenso.
- ¿Qué ocurre si el proxy o el endpoint externo no es accesible? → Informar al usuario de indisponibilidad del servicio; no bloquear el resto de la aplicación.
- ¿Qué ocurre si el usuario no está autenticado? → El botón del asistente no debe estar disponible.
- ¿Qué ocurre tras logout/login en la misma pestaña sin cerrar el navegador? → El historial visible en la interfaz no se restaura; nueva interacción implica nueva sesión de pestaña con nuevo `sessionId`.

---

## Requirements *(mandatory)*

### Functional Requirements

- **FR-001**: El sistema MUST mostrar un botón de acceso al asistente de chat fijo en la zona inferior del layout principal, visible en todas las pantallas autenticadas del backoffice en **escritorio únicamente**.

- **FR-002**: El sistema MUST ocultar por completo el botón y el panel del asistente en viewports móvil y tablet (sin modo alternativo en esos dispositivos).

- **FR-003**: El sistema MUST permitir abrir y cerrar el panel del asistente sin recargar la página ni interrumpir la navegación del usuario.

- **FR-004**: El panel del asistente MUST presentar un área de historial de mensajes, un campo de entrada de texto y un control de envío.

- **FR-005**: Al enviar un mensaje, el sistema MUST enrutar la petición a través de un **proxy intermedio** de la aplicación hacia el webhook externo; el cliente no MUST llamar al endpoint externo directamente.

- **FR-006**: El cuerpo de la petición MUST contener **únicamente** los campos `action` (`"sendMessage"`), `sessionId` y `chatInput`. No MUST incluir datos del usuario autenticado ni campos adicionales.

- **FR-007**: El sistema MUST generar y reutilizar un `sessionId` coherente por pestaña del navegador para todos los mensajes de una misma conversación, hasta que el usuario inicie explícitamente una nueva.

- **FR-008**: Cada pestaña del navegador MUST mantener un `sessionId` e historial visible independientes.

- **FR-009**: El sistema MUST interpretar las respuestas del servicio según el contrato definido en la sección «Contrato de integración»: `status`, `message` (con `text` y `type`), `data` y `error`.

- **FR-010**: El sistema MUST renderizar respuestas según `message.type`: texto plano (`text`), tabla (`table`) o tarjeta (`card`), incluyendo los registros de `data` cuando existan.

- **FR-011**: El sistema MUST impedir el envío de mensajes vacíos o compuestos únicamente por espacios en blanco.

- **FR-012**: El sistema MUST mostrar un estado de carga mientras espera la respuesta y deshabilitar envíos duplicados durante ese periodo.

- **FR-013**: En modo panel incrustado (predeterminado en escritorio), el asistente MUST ocupar aproximadamente el 25% del ancho del viewport y el 100% de su alto, manteniendo el contenido principal accesible en el espacio restante.

- **FR-014**: El sistema MUST ofrecer modo flotante superpuesto como **opción explícita del usuario** y como **fallback automático** en escritorio cuando corresponda.

- **FR-015**: El panel flotante MUST NOT ser arrastrable; permanece en posición fija definida por la interfaz.

- **FR-016**: El sistema MUST permitir al usuario iniciar una nueva conversación, generando un nuevo `sessionId` y limpiando el hilo visible.

- **FR-017**: El sistema MUST manejar errores de comunicación (timeout, errores HTTP, respuestas inválidas, `status: "error"`) mostrando feedback al usuario y permitiendo reintentar el envío.

- **FR-018**: El botón de acceso y el panel del asistente MUST cumplir criterios básicos de accesibilidad: navegación por teclado, etiquetas descriptivas, contraste suficiente y foco visible en controles interactivos.

- **FR-019**: El asistente MUST estar disponible únicamente para usuarios autenticados.

- **FR-020**: El historial visible en la interfaz MUST persistir solo mientras la pestaña del navegador permanezca abierta; al cerrar pestaña o navegador, el hilo visible se descarta. No MUST persistir en almacenamiento local ni restaurarse tras logout/login.

- **FR-021**: La preferencia de modo de presentación (incrustado vs flotante) MUST recordarse solo durante la sesión activa de la pestaña.

### Key Entities

- **Conversación de chat (UI)**: Hilo visible en la pestaña; identificado por `sessionId`; compuesto por mensajes ordenados cronológicamente; efímero (solo vida de la pestaña).

- **Conversación de chat (servicio n8n)**: Contexto conversacional persistido en el servicio externo, asociado al `sessionId` enviado en cada petición.

- **Mensaje**: Unidad de comunicación con origen (usuario o asistente), contenido (`text`/`table`/`card`), datos adjuntos (`data`) y estado (enviado, pendiente, error).

- **Sesión de chat (`sessionId`)**: Identificador opaco único por pestaña que agrupa mensajes para el servicio n8n; independiente entre pestañas.

- **Proxy de chat**: Servicio intermedio de la aplicación que reenvía peticiones al webhook n8n, evitando llamadas directas desde el navegador y problemas de conectividad/CORS.

- **Webhook de chat (n8n)**: Servicio externo que recibe consultas (`sendMessage`) y devuelve respuestas estructuradas según el contrato definido.

---

## Success Criteria *(mandatory)*

### Measurable Outcomes

- **SC-001**: El 100% de los usuarios autenticados en escritorio pueden localizar y abrir el asistente desde cualquier pantalla del backoffice en menos de 3 segundos.

- **SC-002**: El tiempo percibido entre pulsar «Enviar» y ver el indicador de «mensaje enviado» en el hilo es inferior a 500 ms en condiciones normales de red (excluyendo el tiempo de respuesta del servicio externo).

- **SC-003**: En resoluciones de escritorio ≥1280 px, el panel incrustado ocupa entre el 23% y el 27% del ancho del viewport y el 100% de su alto.

- **SC-004**: Al menos el 95% de los mensajes enviados con el servicio operativo reciben respuesta visible en el hilo interpretada correctamente según el contrato JSON.

- **SC-005**: En caso de fallo del servicio, el usuario recibe feedback de error en menos de 10 segundos y puede reintentar sin perder el mensaje escrito.

- **SC-006**: Con el panel abierto en escritorio, el usuario puede completar al menos una acción típica de la pantalla subyacente sin cerrar el chat.

- **SC-007**: Todos los controles del asistente son operables mediante teclado, cumpliendo WCAG 2.1 nivel AA en contraste y foco visible.

- **SC-008**: En viewports móvil/tablet, el 100% de las pantallas autenticadas no muestran botón ni panel del asistente.

- **SC-009**: Peticiones desde la interfaz nunca alcanzan el endpoint externo directamente; el 100% pasan por el proxy de la aplicación (verificable en pruebas de integración).

---

## Assumptions

- El webhook n8n en la URL proporcionada estará operativo y accesible desde el entorno servidor de la aplicación (proxy); la conectividad directa desde el navegador no se asume ni se requiere.

- El payload de envío es un objeto JSON plano con **solo** los campos `action`, `sessionId` y `chatInput`.

- El servicio n8n es responsable de la persistencia del contexto conversacional asociado a cada `sessionId`; la interfaz no almacena ni restaura historial más allá de la vida de la pestaña.

- El modo panel incrustado (25% ancho, 100% alto) es la presentación predeterminada en escritorio; el modo flotante está disponible como opción explícita y como fallback automático.

- El panel flotante no es arrastrable.

- Solo usuarios autenticados del backoffice en escritorio tendrán acceso al asistente.

- El `sessionId` se genera como identificador aleatorio opaco por pestaña; no se vincula al ID de usuario interno en el payload.

- El idioma de la interfaz del asistente (etiquetas, mensajes de error) y las respuestas del servicio (`message.text`, `error.message`) serán español.

- El umbral exacto de viewport escritorio vs móvil/tablet se definirá en `plan.md` (p. ej. breakpoint estándar); el comportamiento funcional (ocultar en móvil/tablet) es invariable.
