> ## Documentation Index
> Fetch the complete documentation index at: https://wondeya.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Widget

> Cómo poner un agente de wondeya en un sitio que ya tienes

El widget ejecuta uno de tus **agentes** dentro de un sitio que ya es tuyo, como un **componente en línea**: colocas un contenedor en tu página, el componente se renderiza ahí y crece con la conversación como cualquier otro contenido. Es parte del flujo de tu página: nunca se superpone a tu página ni flota en una esquina de ella.

Por debajo es un **iframe en un origen dedicado**, montado por un pequeño loader en JavaScript puro, sin dependencias, de menos de 5 KB comprimido con gzip. Esa forma es el modelo de seguridad, no un detalle de implementación: el agente corre en su propio origen, con su propio almacenamiento, sin acceso a tu página, y tu página no tiene acceso a él. Lo único que cruza la frontera es un canal `postMessage` versionado con un destino explícito en ambos lados.

El widget incrusta al agente directamente: el cerebro, no una página. No necesita **ni proyecto ni página publicada**: un agente con conocimiento y una site key es todo lo que un embed requiere.

## Lo que ve el visitante

El componente es el **ask-block** de wondeya, exactamente como se ve en una página de wondeya: un encabezado que invita a preguntar, un campo de texto redondeado con un botón de enviar, preguntas sugeridas para empezar y el aviso de divulgación de IA. Cuando el visitante pregunta, el agente responde con **generative UI** (texto junto a componentes renderizados), usando el mismo renderizador que las páginas de wondeya. La conversación se extiende hacia abajo en tu página y tu página se desplaza; el componente nunca se desplaza dentro de sí mismo.

## Instálalo

<Note>
  El CDN del loader aún no está en vivo: `cdn.wondeya.com` empieza a servir con el lanzamiento 0.1.0 del SDK, así que puedes crear tu site key y colocar el snippet ahora, y el widget aparece en el momento en que el loader se publique.
</Note>

Dos etiquetas: un contenedor donde el componente debe renderizarse, y el script del loader con la site key del agente que quieres incrustar.

```html theme={null}
<div data-wondeya></div>
<script
  async
  src="https://cdn.wondeya.com/widget/v0.1/loader.js"
  data-agent="wdy_site_REPLACE_WITH_YOUR_KEY"
></script>
```

Pon el `div` donde el componente deba aparecer: la mitad de una sección de la landing, el final de una página de producto, una columna lateral. La etiqueta `script` puede ir en cualquier parte; antes del cierre de `body` es el lugar clásico, y `head` también es seguro, porque si el loader corre antes de que tu contenedor se haya parseado, espera a `DOMContentLoaded` y busca una vez más. Si `[data-wondeya]` no encaja con tu marcado, apunta el loader a cualquier elemento con `data-container` (mira las opciones más abajo).

Esa es toda la instalación, con una condición: **el origen de tu sitio debe estar registrado en esa site key**. Hasta que lo esté, el frame se niega a cargar. Eso es lo que impide que alguien que copió tu clave ponga tu agente en una página que no controlas. Un dominio no registrado es un frame en blanco, por diseño.

## Obtener una site key

Una site key identifica a un agente y **no autoriza nada**. Es pública por diseño: viaja dentro de tu HTML, donde cualquiera puede leerla. Lo que protege el endpoint es la lista de orígenes permitidos más la infraestructura anti-abuso de wondeya, no el secreto de la clave.

Creas una para un agente junto con los orígenes exactos autorizados a incrustarlo. Hoy eso ocurre por [MCP](/docs/es/mcp/introduction): la herramienta `create_site_key` recibe el agente y la lista de permitidos,

```json theme={null}
{
  "agentId": "YOUR_AGENT_ID",
  "allowedOrigins": ["https://panaderialaflor.cl"]
}
```

y su respuesta trae la clave en sí (`wdy_site_…`), el único lugar donde aparece el valor completo: los listados muestran únicamente un prefijo de referencia. `list_site_keys` y `rotate_site_key` cubren el resto del ciclo de vida; [la referencia de herramientas](/docs/es/mcp/tools) documenta las tres.

Reglas para `allowedOrigins`:

* Hasta 20 entradas, cada una un origen exacto: esquema, host, puerto opcional. Sin ruta, sin barra final, **sin comodines**. `https://*.example.com` se rechaza: de lo contrario, el takeover de un subdominio olvidado de tu lado se convertiría en el derecho a enmarcar tu agente.
* `http://` se acepta solo en loopback (`http://localhost:3000`), para desarrollo. Un sitio en producción sobre HTTP plano entregaría la decisión de "quién puede enmarcar esto" a quien controle la red.
* Un origen que nunca podría funcionar se rechaza con un error que lo nombra, para que puedas corregir lo que escribiste.
* La lista queda fija al crear la clave y no se puede editar después: para agregar o cambiar un origen, crea una clave nueva con la lista completa y cambia el snippet.

Rotar una site key emite una nueva de inmediato y mantiene la anterior funcionando por 24 horas, para que las páginas ya desplegadas no se rompan mientras las actualizas. La rotación conserva el agente y los orígenes tal como están.

<Note>
  La pantalla de la consola para site keys y orígenes de embed está en camino; hasta que llegue, las herramientas MCP son la forma de crearlas y gestionarlas.
</Note>

## Llamar a la API antes de que llegue el loader

`async` significa que el loader puede no haberse parseado cuando corre tu propio código. Agrega el stub de cola encima del snippet y llama a `wondeya(...)` cuando quieras; las llamadas se reproducen en orden una vez que el widget existe:

```html theme={null}
<script>
  window.wondeya =
    window.wondeya ||
    function () {
      (window.wondeya.q = window.wondeya.q || []).push(arguments);
    };
  wondeya('identify', { id: 'user_42', traits: { name: 'Ana', plan: 'pro' } });
  wondeya('on', 'ready', () => console.log('widget ready'));
</script>
<div data-wondeya></div>
<script
  async
  src="https://cdn.wondeya.com/widget/v0.1/loader.js"
  data-agent="wdy_site_REPLACE_WITH_YOUR_KEY"
></script>
```

## Opciones

Todas opcionales, todas se definen como atributos en la etiqueta `script`.

| Atributo | Predeterminado | Qué hace |
| - | - | - |
| `data-agent` | (ninguno) | **Obligatorio.** La site key (`wdy_site_…`). |
| `data-container` | el primer `[data-wondeya]` | Selector CSS del elemento donde se monta el componente. Sin definir, el loader se monta en el primer elemento que coincida con `[data-wondeya]`. |
| `data-locale` | el propio del agente | Etiqueta BCP-47 (`es`, `en`, `es-419`). Define el idioma de la interfaz propia del widget. |
| `data-origin` | `https://widget.wondeya.com` | Solo para desarrollo local. |

## Apariencia

El widget viene con la apariencia de wondeya; estos atributos la reemplazan para este embed. Viajan al frame en la URL del iframe y se validan en **ambos** lados: un valor inválido es una sola advertencia en la consola del navegador y esa clave cayendo a su valor predeterminado, nunca un widget roto.

| Atributo | Valores | Qué hace |
| - | - | - |
| `data-primary` | `#rrggbb` (o `rrggbb`) | Color primario de la marca. Pinta el botón de enviar, entre otras cosas. |
| `data-accent` | `#rrggbb` | Resaltados y enlaces dentro de la conversación. |
| `data-surface` | `#rrggbb` | Fondo de las superficies del componente. |
| `data-ink` | `#rrggbb` | Color del texto. |
| `data-muted` | `#rrggbb` | Color del texto secundario. |
| `data-radius` | `none` `sm` `md` `lg` `full` | Redondeo de esquinas de la interfaz del componente. Predeterminado `lg`. |
| `data-font` | `montserrat` `inter` `source-serif` `inherit` | La tipografía del widget. `inherit` adopta la de tu página; mira más abajo. |

Los colores son hex de 6 dígitos y nada más; los radios y las tipografías son listas cerradas. No hay forma de pasar CSS libre al widget, a propósito: el mismo vocabulario cerrado que protege una página de wondeya protege el embed.

**`data-font="inherit"` no puede usar tus fuentes web.** El widget es un iframe, un documento separado, y las fuentes que carga tu página nunca cruzan hacia él. `inherit` lee el font stack calculado de tu página desde `body` y lo reenvía, así que el widget coincide con tu sitio allí donde ese stack se resuelve en fuentes del sistema o instaladas localmente, y cae a la tipografía predeterminada del widget donde no.

## Cómo se dimensiona

El componente se comporta como un bloque de tu propia página, y las dos dimensiones vienen de dos lugares distintos:

* **El ancho sigue al contenedor.** El iframe siempre ocupa el 100% del elemento donde se montó, así que controlas el ancho como controlas cualquier layout: dimensiona el contenedor. Una columna de 360px te da un componente angosto; una sección de ancho completo te da uno ancho.
* **La altura sigue a la conversación.** El frame mide su propio documento y reporta la altura por el canal `postMessage`; el loader la aplica al iframe. A medida que llegan respuestas y se renderizan componentes, el componente crece y el contenido que está debajo baja: tu página se desplaza, el componente no. No hay nada que configurar. El loader verifica la sensatez de cada altura reportada antes de aplicarla, así que ni siquiera un frame comprometido podría convertir tu página en un iframe de un kilómetro de largo.

El iframe lleva `loading="lazy"`: un componente que queda fuera de la primera pantalla no le cuesta nada a tu página hasta que el visitante se desplaza cerca de él.

## La API de JavaScript

```js theme={null}
wondeya('init', { agent: 'wdy_site_…' }); // only if you skipped data-agent
wondeya('identify', { id: 'user_42', traits: { plan: 'pro' } });
wondeya('on', 'ready', handler);
wondeya('destroy');                       // removes everything it added
```

`init` acepta todas las opciones de las dos tablas de arriba, con los mismos nombres sin el prefijo `data-` y con la misma validación; `container` puede ser una cadena con un selector CSS o el propio elemento del DOM. Cualquier cosa que el loader no reconozca (un comando desconocido, un argumento malformado) es un único `console.warn` con el prefijo `[wondeya]` y una operación nula, nunca una excepción en tu página.

### Eventos

| Evento | Cuándo se dispara |
| - | - |
| `ready` | El frame terminó su handshake. El frame carga con tu página; con `loading="lazy"`, un componente que queda fuera de la primera pantalla carga cuando el visitante se desplaza cerca de él, y `ready` se dispara entonces. |
| `turn-started` / `turn-completed` | Se está respondiendo una pregunta. `turn-completed` se dispara cuando el turno termina, incluso tras un error; `turn-failed` está reservado en el protocolo y hoy nunca se dispara. |

Si quieres saber si el loader en sí ya está activo antes de todo eso, lee `window.wondeya.loaded`.

### `identify` no es autenticación

Son datos de visualización. Cualquier script de tu página, como una etiqueta de analítica o una extensión del navegador, puede enviarlo, así que nada se autoriza jamás con él y nunca debe llevar un secreto. Los traits están limitados a 12 claves y 120 caracteres cada una, y todo lo que no sea una cadena corta se descarta antes de salir de tu página.

## React

```bash theme={null}
npm install @wondeya/react
```

`@wondeya/react` aún no está en el registro de npm; se publica con el mismo lanzamiento 0.1.0 del SDK que el loader.

```tsx theme={null}
import { WondeyaWidget } from '@wondeya/react';

export const AskSection = () => (
  <section>
    <h2>Ask us anything</h2>
    <WondeyaWidget
      siteKey="wdy_site_…"
      identify={{ traits: { plan: 'pro' } }}
      appearance={{ primary: '#0f172a', radius: 'full', font: 'inherit' }}
      onEvent={(event) => console.log('[widget]', event)}
      className="ask-block"
    />
  </section>
);
```

El componente renderiza un `div` simple en su posición dentro de tu árbol y monta el widget adentro. `className` y `style` caen en ese `div`; así controlas el ancho y la ubicación, exactamente como con cualquier otro bloque. `appearance` toma los mismos valores que los atributos `data-*` de arriba, con la misma validación y la misma limitación de `inherit`; `onEvent` recibe todos los eventos de la tabla de arriba. Al desmontarse elimina todo lo que agregó, que es la parte que una etiqueta script simple no puede hacer en una single-page app.

El envoltorio es deliberadamente delgado: todo el comportamiento vive en el loader, así que una app React y una etiqueta script reciben el mismo widget en lugar de dos que divergen.

## Content-Security-Policy

Si tu sitio envía una CSP (y debería), agrega estas tres fuentes:

```
script-src  https://cdn.wondeya.com;
frame-src   https://widget.wondeya.com;
connect-src https://api.wondeya.com;
```

* **`script-src`**: el loader en sí. Con una política basada en nonce, pon tu nonce en la etiqueta `script`; el loader no agrega scripts inline propios.
* **`frame-src`**: el iframe del widget. Sin él, el frame queda bloqueado y tu contenedor se queda vacío.
* **`connect-src`**: la solicitud del chat. La hace **el iframe, desde su propio origen**, así que en rigor la gobierna la política del iframe y no la tuya. Algunos linters de CSP y navegadores antiguos igual la marcan; agregarla no cuesta nada y evita un reporte confuso.

**No** necesitas `style-src 'unsafe-inline'`. El loader da estilo solo a los elementos que él creó, usando atributos `style` de elemento, que una CSP no gobierna. No carga fuentes, ni imágenes, ni analítica desde tu página.

### La parte del trato que pone wondeya

El documento del embed trae su propia política, construida por site key en el servidor antes de que el documento exista:

* `default-src 'none'`, y luego solo lo que la página genuinamente necesita;
* `connect-src` es el propio origen del frame más la API de wondeya;
* `img-src` es el propio origen del frame, URIs `data:` y el CDN de wondeya;
* `object-src 'none'` y `base-uri 'none'`;
* cuando la protección anti-bots está activada, el origen del desafío de Cloudflare Turnstile (`https://challenges.cloudflare.com`) se suma a `script-src` y `frame-src`;
* **`frame-ancestors` lista solo los orígenes que registraste.** No se aceptan comodines, y una lista de permitidos vacía se convierte en `'none'` en lugar de `*`. Si algún origen registrado no pasa la validación, la lista entera colapsa a `'none'` en vez de servir en silencio una política más débil que la que configuraste.

Así que ni siquiera alguien con tu site key en la mano puede enmarcar tu agente en su propio sitio.

## Versionado y el compromiso con SRI

Cada lanzamiento se publica en dos URLs:

| URL | Caché | Úsala cuando |
| - | - | - |
| `…/widget/v0.1/loader.js` | 5 minutos | Quieres recibir las correcciones automáticamente. **El predeterminado, y lo que usa el snippet de arriba.** |
| `…/widget/0.1.0/loader.js` | inmutable, 1 año | Fijas versiones y actualizas deliberadamente. |

**Subresource Integrity es incompatible con el canal de actualización automática, y eso no es un defecto de ninguno de los dos.** `integrity="sha384-…"` le dice al navegador que rechace un archivo cuyos bytes cambiaron, que es exactamente lo que hace el canal de actualización automática cuando sale una corrección. Elige uno:

* **actualización automática, sin SRI**: recibes correcciones de seguridad sin tocar tu sitio, y confías en el CDN de wondeya. El predeterminado correcto para casi todos.
* **versión fija más SRI**: una garantía byte a byte, de modo que un CDN comprometido no puede alcanzar a tus visitantes. A cambio, una corrección te llega cuando despliegas, no antes.

```html theme={null}
<script
  async
  src="https://cdn.wondeya.com/widget/0.1.0/loader.js"
  integrity="sha384-…"
  crossorigin="anonymous"
  data-agent="wdy_site_…"
></script>
```

Cada lanzamiento publica su hash junto al artefacto. Una URL de canal nunca cambia lo que promete: mientras wondeya está antes de la 1.0, el canal es `v0.1` y el siguiente lanzamiento con cambios incompatibles abre `v0.2`, manteniendo vivo el anterior. Desde la 1.0.0 en adelante es `v1`, y toda corrección `1.x` llega por ahí.

## Solución de problemas

El loader **nunca lanza excepciones hacia tu página**: un widget no debe poder romper el sitio donde está. Todo aquello de lo que se queja es un `console.warn` con el prefijo `[wondeya]`.

| Síntoma | Causa |
| - | - |
| `[wondeya] missing or malformed data-agent` | La site key está mal o truncada. Cópiala de nuevo. |
| El componente no aparece y la consola del navegador muestra una advertencia `[wondeya]` sobre el contenedor | No hay ningún `<div data-wondeya></div>` en la página, o tu selector `data-container` no coincidió con nada. Agrega el contenedor donde el componente deba renderizarse. |
| El espacio del componente se queda en blanco, sin advertencia de contenedor | Tu origen no está registrado en esa site key, o la clave fue revocada. |
| Nada en absoluto, sin advertencia | El script nunca cargó. Revisa `script-src` y tu bloqueador de anuncios. |
| `[wondeya] … was called before init` | Llamaste a la API sin el stub de cola. Agrégalo, o llama primero a `wondeya('init', …)`. |
| `[wondeya] already initialized` | Dos snippets en una misma página. Elimina uno, o llama primero a `wondeya('destroy')`. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.