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

# Eventos de actividad (onEvent)

> Suscríbete a lo que ocurre dentro del widget —grabación, actividad del médico, generación del informe— para detectar inactividad y mantener viva la sesión de tu sistema

Los callbacks `handleReport`, `onReportApply` y `handleExtras` se disparan al **final** de un flujo. No te dicen nada de lo que pasa mientras tanto: si el médico está grabando, si está escribiendo, si SofIA está generando la nota. `onEvent` cubre ese hueco con un flujo de **eventos tipados y sin datos clínicos**.

El caso de uso que lo motivó: un HIS que cierra la sesión por inactividad no puede distinguir "el médico lleva veinte minutos grabando una consulta sin tocar el teclado" de "el médico se fue hace veinte minutos". Con el micrófono activo y sin otra actividad, deslogueaba al médico a mitad de consulta.

<Info>
  Los eventos **nunca** llevan texto clínico, contenido del informe, lo que se escribió ni identificadores de paciente o médico. Cada campo del payload es un enumerado, un booleano o un número. Si necesitas el contenido clínico, ya tienes canales dedicados para ello (`handleReport`, `onReportApply`, `handleExtras`).
</Info>

## Las dos props

| Propiedad              | Tipo       | Cómo se asigna                                                                                                                                 |
| ---------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| **onEvent**            | `function` | **Solo propiedad JS** (`el.onEvent = fn`). En React, la prop `onEvent`                                                                         |
| **eventSubscriptions** | `string[]` | Propiedad JS `el.eventSubscriptions = [...]` o atributo del web component `event-subscriptions` (JSON). En React, la prop `eventSubscriptions` |

```typescript theme={null}
onEvent: (event: SdkEvent) => void
eventSubscriptions: Array<'recording.*' | 'activity.*' | 'report.*' | 'lifecycle.*' | '*' | 'recording.started' | /* …cualquier nombre exacto */>
```

La suscripción es **explícita**: sin `eventSubscriptions` (o con un array vacío) no se entrega ningún evento, y el SDK registra un aviso en consola si pasas `onEvent` sin ella. Admite nombres exactos (`"recording.started"`), comodines de familia (`"recording.*"`) o todo (`"*"`).

<Tabs>
  <Tab title="React">
    ```tsx theme={null}
    <Omniscribe
      apikey={apiKey}
      userid={userId}
      patientid={patientId}
      eventSubscriptions={['recording.*', 'activity.*', 'report.*']}
      onEvent={event => {
        if (event.name === 'recording.started') idleTimer.cancel();
        if (event.name === 'recording.stopped') idleTimer.start();
        if (event.family === 'activity') idleTimer.reset();
      }}
    />
    ```
  </Tab>

  <Tab title="Web Component">
    ```html theme={null}
    <sofia-sdk
      id="sofia"
      apikey="your-api-key"
      userid="user_12345"
      patientid="patient_67890"
      event-subscriptions='["recording.*", "activity.*", "report.*"]'
    ></sofia-sdk>

    <script>
      customElements.whenDefined('sofia-sdk').then(() => {
        const sofia = document.getElementById('sofia');
        sofia.onEvent = (event) => {
          if (event.name === 'recording.started') idleTimer.cancel();
          if (event.name === 'recording.stopped') idleTimer.start();
          if (event.family === 'activity') idleTimer.reset();
        };
      });
    </script>
    ```
  </Tab>
</Tabs>

## Forma de un evento

```typescript theme={null}
interface SdkEvent {
  name: string;          // 'recording.started', 'activity.typing', …
  family: 'recording' | 'activity' | 'report' | 'lifecycle';
  level: 'info' | 'warn' | 'error';
  ts: string;            // ISO-8601
  seq: number;           // monotónico por carga de página; garantiza el orden
  sdkVersion: string;
  sessionId: string | null;  // UUID aleatorio por consulta; no identifica a nadie
  payload: Record<string, unknown>;  // depende del evento, ver tabla
}
```

`sessionId` es el mismo identificador aleatorio que el SDK usa para su analítica interna. Cambia con cada paciente, así que te sirve para correlacionar una ráfaga de eventos con una consulta y para descartar los que lleguen tras un cambio de paciente.

## Catálogo de eventos

### `recording` — grabación

| Evento                              | Payload                                                      | Cuándo                                                                                                                                                                                                                              |
| ----------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `recording.started`                 | `{ mode: 'consultation' \| 'dictation' }`                    | El micrófono se ha abierto y la grabación arranca                                                                                                                                                                                   |
| `recording.stopped`                 | `{ mode, durationSeconds: number }`                          | La grabación termina, por cualquier causa (parada manual, pérdida del servidor, timeout de arranque)                                                                                                                                |
| `recording.heartbeat`               | `{}`                                                         | **Hay audio fluyendo de verdad** hacia el transcriptor. Late con el primer paquete de audio y después cada \~30 s mientras sigan enviándose. Ver [mantener viva una sesión externa](#mantener-viva-una-sesión-externa)              |
| `recording.audio_lost`              | `{ cause: 'mic' \| 'network' \| 'server', durationSeconds }` | *(nivel `warn`)* Se perdió audio durante una grabación. Para `mic`/`network` se emite al recuperarse e indica cuánto duró el hueco; para `server` se emite al instante e indica en qué segundo de la grabación se cerró la conexión |
| `recording.microphone_disconnected` | `{}`                                                         | *(nivel `warn`)* La pista del micrófono terminó o se silenció a nivel de sistema mientras se grababa                                                                                                                                |

### `activity` — actividad del médico

| Evento                 | Payload                                                                                              | Cuándo                                                                                                                                                                                                                                    |
| ---------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `activity.interaction` | `{ kind: 'recording' \| 'chat' \| 'settings' \| 'report' \| 'transcript' \| 'history' \| 'widget' }` | El médico pulsó un control del widget. `kind` es la zona aproximada, nunca qué botón concreto                                                                                                                                             |
| `activity.typing`      | `{ surface: 'chat' \| 'note' }`                                                                      | El médico tecleó en el chat o editó la nota en la [vista previa de inserción](/sofia/es/sdk/insertion-preview). Solo la superficie: **ni un carácter de lo escrito**. El texto que el dictado inserta en el chat no cuenta como escritura |

Ambos se limitan a **un evento cada 5 segundos por tipo/superficie**, en el flanco de subida: la primera pulsación o tecla se emite al instante y las siguientes se silencian durante la ventana. Para detectar inactividad, emitir de más es inocuo (tu temporizador se reinicia); emitir de menos cerraría la app con el médico a mitad de frase.

### `report` — generación del informe

| Evento                      | Payload           | Cuándo                                                                                                                                    |
| --------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `report.generation_started` | `{}`              | El médico pulsó **Generar** o **Regenerar** y la petición está en vuelo                                                                   |
| `report.settled`            | `{ ok: boolean }` | La generación terminó. `ok: true` solo si se entregó un informe; `false` en cualquier otra salida (error, cancelación, borrador en caché) |

<Warning>
  **Suprime tu temporizador de inactividad entre `report.generation_started` y `report.settled`.** La generación tarda decenas de segundos sin que el médico toque nada —se parece exactamente a la inactividad— y cerrar el widget ahí pierde la nota. Los dos eventos van **siempre en pareja**: el SDK garantiza el `settled` desde todas las salidas del código, no desde nombres de eventos.
</Warning>

### `lifecycle` — ciclo de vida

| Evento             | Payload | Cuándo                                                                                    |
| ------------------ | ------- | ----------------------------------------------------------------------------------------- |
| `lifecycle.ready`  | `{}`    | El widget terminó de cargar su configuración y es utilizable. Una vez por carga de página |
| `lifecycle.closed` | `{}`    | El médico pulsó el botón de cerrar del widget                                             |

## Mantener viva una sesión externa

Si tu sistema cierra la sesión por inactividad, lo que necesita son **pings periódicos de actividad**, no los flancos `started`/`stopped`. Para eso existe `recording.heartbeat`.

```typescript theme={null}
onEvent={event => {
  if (event.name === 'recording.heartbeat') session.keepAlive();
  if (event.family === 'activity') session.keepAlive();
}}
```

Con eso tu integración no guarda ningún estado. Y **falla en cerrado**: si el audio deja de fluir por la razón que sea —micrófono desconectado, red caída, conexión zombi, pestaña congelada— los latidos cesan y tu propio timeout actúa.

<Tip>
  ¿Por qué no basarlo en `recording.started` / `recording.stopped`? Porque entre los dos puede haber veinte minutos sin ningún otro evento, así que tendrías que mantener un `setInterval` propio entre ambos. Y si te pierdes el `stopped` (recarga a mitad de grabación, pestaña colgada, un `throw` en tu handler), ese intervalo mantiene la sesión viva para siempre: justo el fallo que un cierre por inactividad existe para evitar.
</Tip>

El latido sale del mismo punto del código que el SDK usa internamente para saber que una grabación está sana: el envío de cada paquete de audio, que solo ocurre con el transcriptor listo, la conexión abierta y el micrófono captando. Sigue latiendo aunque el médico esté en silencio, porque los paquetes de audio se envían igualmente.

## Receta completa para un temporizador de inactividad

```typescript theme={null}
let generating = false;

onEvent={event => {
  switch (event.family) {
    case 'recording':
    case 'activity':
      idleTimer.reset();           // cualquier cosa aquí es el médico trabajando
      break;
    case 'report':
      generating = event.name === 'report.generation_started';
      if (generating) idleTimer.pause(); else idleTimer.resume();
      break;
  }
}}
```

Con `eventSubscriptions={['recording.*', 'activity.*', 'report.*']}`. Un `recording.audio_lost` o `recording.microphone_disconnected` también reinicia el temporizador aquí —es razonable: el médico sigue delante de la pantalla viendo el aviso—, pero si prefieres tratarlos como el fin de la actividad, compáralos por `event.name`.

## Garantías de entrega

* **Asíncrona.** Cada evento se entrega en un microtask posterior, nunca dentro del render de React ni en la ruta de audio. Tu handler puede hacer `setState` sin problema, y un handler lento no retrasa la grabación.
* **Ordenada.** `seq` es monotónico y los eventos llegan en ese orden.
* **Aislada.** Si tu handler lanza una excepción, el SDK la captura y sigue funcionando; registra un único aviso en consola por carga de página.
* **Estable entre pacientes.** Cambiar `patientid` o `userid` remonta el widget por dentro, pero tu suscripción y tu handler sobreviven sin que tengas que volver a asignarlos. Pasar una función inline en cada render tampoco re-suscribe.
* **Un widget por página.** El SDK admite una sola instancia de `<Omniscribe>`/`<sofia-sdk>`. Si se registrara un segundo `onEvent`, gana el último y se avisa en consola.
* **Nombres abiertos.** Pueden añadirse eventos nuevos en versiones menores. Compara por `event.name` o `event.family` y trata los nombres desconocidos como "algo pasó"; no asumas un conjunto cerrado.

<Info>
  Si en TypeScript quieres un `switch` exhaustivo, estrecha `event.name` a `KnownSdkEventName`. Los tipos `SdkEvent`, `SdkEventName`, `KnownSdkEventName`, `SdkEventFamily`, `SdkEventHandler`, `SdkEventPayloadMap` y `SdkEventSubscription` se exportan desde `@omniloy/sofia-sdk`.
</Info>

## Privacidad

El diseño parte de una invariante que el propio compilador del SDK hace cumplir: **ningún campo de ningún payload es una cadena libre**. Los errores viajan como códigos, los informes como booleanos, la escritura como el nombre de la superficie. El SDK tiene un test que lee la definición de tipos y falla la build si aparece un `string` sin acotar.

Por eso `onEvent` **no** es un volcado del log interno del SDK: los mensajes de log son prosa escrita a mano que interpola identificadores y objetos de error completos, y eso no se puede hacer seguro con un filtro. Si necesitas depurar, usa la prop `debug` y la consola; si necesitas contenido clínico, usa los callbacks dedicados.

## Próximos pasos

<CardGroup cols={2}>
  <Card title="Propiedades opcionales" icon="sliders" href="/sofia/es/sdk/optional-properties">
    Todas las props y callbacks del componente
  </Card>

  <Card title="Vista previa de inserción" icon="eye" href="/sofia/es/sdk/insertion-preview">
    De dónde sale `activity.typing` con `surface: 'note'`
  </Card>
</CardGroup>
