157 lines
12 KiB
Markdown
157 lines
12 KiB
Markdown
# Changelog — s2s-client (Azurian Vocero)
|
||
|
||
Todos los cambios relevantes del proyecto están documentados en este archivo.
|
||
Formato basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/).
|
||
|
||
---
|
||
|
||
## [2026-09-07]
|
||
|
||
### Añadido — Soporte `episode_id` (inject de episodio podcast)
|
||
|
||
Nuevo parámetro query string `?episode_id=` junto a `?phoneNumber=` que
|
||
habilita la carga y la inyección automática de un episodio podcast como
|
||
contexto PRIMARIO en el prompt LLM.
|
||
|
||
**URL de ejemplo:**
|
||
```
|
||
/?phoneNumber=%2B447432477433&episode_id=episode-19569f77-a06e-5ce5-a52f-6299dbd67d03
|
||
```
|
||
|
||
**Archivos modificados:**
|
||
|
||
- **`server.py`** ([server.py](file:///home/duxiter/s2s/s2s-client/server.py)):
|
||
1. Añadida la constante `PODCAST_EPISODE_HINT` ([L426-L429](file:///home/duxiter/s2s/s2s-client/server.py#L426-L429)) para el hint semántico en el prompt.
|
||
2. Añadida la función `_resolve_episode_context(raw_value)` ([L475-L504](file:///home/duxiter/s2s/s2s-client/server.py#L475-L504)): normaliza la respuesta API (dict/list/str, posible wrapper `{"result": …}`), la serializa en JSON (tope 24.000 chars) y la envuelve con los marcadores `<<<PODCAST_EPISODE_BEGIN/END>>>`.
|
||
3. Añadida la función asíncrona `_fetch_podcast_episode(episode_id)` ([L1010-L1078](file:///home/duxiter/s2s/s2s-client/server.py#L1010-L1078)): GET directo a
|
||
`https://aquachile.azurianlab.com/api/v1/podcast/episodes/{episode_id}`
|
||
(timeout 15s, follow redirects, manejo de errores tolerante — nunca rompe el flujo, devuelve `None` en caso de fallo).
|
||
4. Extendida `_augment_instructions_with_profile(...)` ([L1137-L1181](file:///home/duxiter/s2s/s2s-client/server.py#L1137-L1181)) con el nuevo parámetro `episode_context_text`; el orden de inject ahora es (más específico → más genérico): **episode → profile → reports → summary**.
|
||
5. Extendida `_patch_client_session_update_with_profile(...)` ([L1184-L1306](file:///home/duxiter/s2s/s2s-client/server.py#L1184-L1306)) con soporte a `episode_context_text` y logging dedicado `[INJECT-EPISODE]` / `[DEBUG-EPISODE]`.
|
||
6. WebSocket connect handler ([L1791-L1959](file:///home/duxiter/s2s/s2s-client/server.py#L1791-L1959)):
|
||
- lee `episode_id` desde query params junto a `phoneNumber`;
|
||
- ejecuta fetch episodio + resolve ANTES de profile/reports/summary;
|
||
- guarda `injected_episode_context` e `inject_episode_id` en el `session_ctx` compartido.
|
||
7. `_client_to_upstream` ([L2649-L2661](file:///home/duxiter/s2s/s2s-client/server.py#L2649-L2661)): pasa `ctx_episode_context` al patch de `session.update`.
|
||
8. Endpoint `POST /api/reasoning`:
|
||
- extendido el modelo `ReasoningRequest` con el campo `episode_id: str | None = None` ([L1309-L1323](file:///home/duxiter/s2s/s2s-client/server.py#L1309-L1323));
|
||
- en el handler ([L1411-L1577](file:///home/duxiter/s2s/s2s-client/server.py#L1411-L1577)) fetch episodio independiente de phone;
|
||
- `inject_episode_text` se hace **prepend** sobre `user_context_parts` (primero de todos);
|
||
- reenvío a NODE_RED / OPENAI con logging extendido (`episodeIdLen`).
|
||
|
||
- **`main.js`** ([main.js](file:///home/duxiter/s2s/s2s-client/main.js)):
|
||
1. Lectura desde query string: `const voceroEpisodeId = pageParams.get("episode_id")` ([L676-L680](file:///home/duxiter/s2s/s2s-client/main.js#L676-L680)).
|
||
2. `fetchOpenAiReasoningAnswer` ([L1337-L1364](file:///home/duxiter/s2s/s2s-client/main.js#L1337-L1364)): campo `episode_id` añadido al body POST `/api/reasoning`, logKey y `console.info` extendidos.
|
||
3. Construcción URL WebSocket ([L2323-L2342](file:///home/duxiter/s2s/s2s-client/main.js#L2323-L2342)): `episode_id` añadido a query params tanto con la API `URL.searchParams` como en el fallback string (concat con `&`).
|
||
|
||
---
|
||
|
||
### Añadido — `S2S_START_SESSION_MODE`: 3 modos de inicio de sesión
|
||
|
||
Variable de entorno leída por el servidor y propagada al cliente vía `GET /api/config.startSessionMode`.
|
||
Objetivo: controlar si y cómo se enciende automáticamente el micrófono y si el modelo saluda
|
||
primero (flujo similar al wake-word). Valores soportados:
|
||
|
||
| Valor | Descripción |
|
||
|---|---|
|
||
| *(vacío) / default* | Inicio manual: solo tap sobre el Orb / botón Start |
|
||
| **`TALK`** | Después de 2s overlay a pantalla completa "Toca en cualquier lugar para empezar" (policy safe para Safari/iOS `getUserMedia`). Click/touch/tecla dentro de la user gesture → `doStart()`. El cliente WS inyecta bootstrap `"hola vocero"` + `requestResponse()`. |
|
||
| **`TALKFIRST`** | Después de 2s **intenta `doStart()` DIRECTAMENTE SIN click**, precargando `pendingWakeWordConfirmationText = "hola vocero"` (misma variable de la wake-word real). Si los permisos de mic ya están "allow always" funciona sin click. Si falla (policy Safari/iOS) → fallback sobre el mismo unlocker de TALK con mensaje "permiso micrófono". |
|
||
|
||
**Archivos modificados:**
|
||
|
||
- **`server.py`** ([server.py](file:///home/duxiter/s2s/s2s-client/server.py)):
|
||
1. Lectura env: `S2S_START_SESSION_MODE = os.environ.get(...)`, `.strip().upper()` ([L150](file:///home/duxiter/s2s/s2s-client/server.py#L150)).
|
||
2. Banner `[STARTUP]` extendido con el campo `startSessionMode=` ([L196-L212](file:///home/duxiter/s2s/s2s-client/server.py#L196-L212)).
|
||
3. Endpoint `GET /api/config` → nuevo campo `startSessionMode` + log `[INTERNAL] /api/config` ([L1383-L1400](file:///home/duxiter/s2s/s2s-client/server.py#L1383-L1400)).
|
||
|
||
- **`index.html`** ([index.html](file:///home/duxiter/s2s/s2s-client/index.html#L32-L39)):
|
||
- Añadido nuevo overlay `#talk-unlocker` a pantalla completa (z-index 9999), `hidden` por defecto, con tarjeta "Preparando Vocero… / Toca en cualquier lugar para empezar a hablar" + spinner degradado.
|
||
|
||
- **`style.css`** ([style.css](file:///home/duxiter/s2s/s2s-client/style.css#L145-L215)):
|
||
- Clases `.talk-unlocker`, `.talk-unlocker-hint`, `.talk-unlocker-spinner`, `.talk-unlocker-title`, `.talk-unlocker-sub` + animaciones fade-in y pop-up de tarjeta.
|
||
|
||
- **`main.js`** ([main.js](file:///home/duxiter/s2s/s2s-client/main.js)):
|
||
1. Variable global `startSessionMode = ""` + comentario ([L628-L632](file:///home/duxiter/s2s/s2s-client/main.js#L628-L632)).
|
||
2. Parse dentro de `fetchConfig()` → uppercase, log en `reportDebug("B",…)` ([L2309-L2320](file:///home/duxiter/s2s/s2s-client/main.js#L2309-L2320)).
|
||
3. Paso a cliente WS → `startWithResponse: startSessionMode === "TALK"` ([L2683-L2692](file:///home/duxiter/s2s/s2s-client/main.js#L2683-L2692)).
|
||
4. Bloque scheduling auto-start ([L2976-L3083](file:///home/duxiter/s2s/s2s-client/main.js#L2976-L3083)):
|
||
- `bootstrapWithAutoStart()`: `await fetchConfig()` → `maybeScheduleAutoStart()`;
|
||
- `setTimeout(2000 ms)` warmup para finalizar fetch de episode/profile/reports;
|
||
- `showTalkUnlocker(fallbackHint)`: gestiona labels personalizadas en caso de fallback TALKFIRST;
|
||
- **`tryTalkFirstDirect()` (TALKFIRST)**: set `pendingWakeWordConfirmationText="hola vocero"` + `doStart()` + try/catch con teardown y fallback unlocker;
|
||
- listener unlocker click/touchstart/keydown → `startTalkFromGesture()` con `createResumedAudioContext()` dentro de la gesture (para iOS silent-free).
|
||
5. `onClientStatus()` ([L2878-L2932](file:///home/duxiter/s2s/s2s-client/main.js#L2878-L2932)) añadido `hideTalkUnlocker()` en `connecting/queued/your-turn/connected`.
|
||
|
||
- **`ws/s2s-ws-client.js`** ([s2s-ws-client.js](file:///home/duxiter/s2s/s2s-client/ws/s2s-ws-client.js)):
|
||
1. Constructor: nuevas propiedades `_startWithResponse`, `_startWithResponseText` (default `"hola vocero"`), `_initialResponseSent` guardia one-shot ([L163-L178](file:///home/duxiter/s2s/s2s-client/ws/s2s-ws-client.js#L163-L178)).
|
||
2. Handler `session.updated` ([L688-L713](file:///home/duxiter/s2s/s2s-client/ws/s2s-ws-client.js#L688-L713)): **Solo para `TALK`** (no TALKFIRST, que en cambio usa `pendingWakeWordConfirmationText`): `this.sendUserText("hola vocero") + this.requestResponse()` — mismo code path identico de la wake-word.
|
||
3. `requestResponse` / `_createResponseNow` restaurados limpios (solo payload opcional `image`; ningún shaped `response.create`).
|
||
|
||
---
|
||
|
||
### Modificado — `#reports-sidebar-toggle` visible solo con `?debug=true`
|
||
|
||
El botón "reports" en la topbar (toggle del panel lateral reports) ahora es
|
||
`data-debug-only="true"` — por lo tanto solo se muestra si la URL contiene
|
||
`?debug=true`, de manera análoga al cluster `.topbar-right`.
|
||
|
||
**Archivos modificados:**
|
||
|
||
- **`index.html`** → `#reports-sidebar-toggle` ([index.html](file:///home/duxiter/s2s/s2s-client/index.html#L47-L50)): añadido `data-debug-only="true"`.
|
||
- **`main.js`** → `applyDebugOnlyVisibilityGate()` ([L48-L109](file:///home/duxiter/s2s/s2s-client/main.js#L48-L109)): extendido para iterar *todos* los elementos `[data-debug-only="true"]` (no solo `.topbar-right`); línea 1227: `reportsSidebarToggle.hidden = false` condicionado a `qs.get("debug") === "true"` ([L1227-L1246](file:///home/duxiter/s2s/s2s-client/main.js#L1227-L1246)) + log rojo `🚫 FORZADO HIDDEN` cuando falta debug=true.
|
||
- **`style.css`** → regla de seguridad ya presente: `[data-debug-only]:not([data-debug-enabled]) { display:none !important; visibility:hidden; opacity:0 }` ([L532-L537](file:///home/duxiter/s2s/s2s-client/style.css#L532-L537)).
|
||
|
||
---
|
||
|
||
## [2026-09-04]
|
||
|
||
### Modificado — Chat siempre visible + integración seamless
|
||
|
||
Reorganizado el layout para mostrar el chat textual de forma permanente e
|
||
integrado visualmente con el Orb en la parte superior del stage.
|
||
|
||
- Eliminado botón toggle chat en la topbar (`#chat-btn`) y relativo overlay.
|
||
- Stage con el Orb en la parte superior; nuevo `#chat-panel` a continuación (historial
|
||
+ form input fijo sobre el footer sticky).
|
||
- Actualizada lógica en `main.js` y `ui/chat.js` para el envío de texto mediante
|
||
`client.sendUserText` y auto-scroll sin controles de visibilidad del panel.
|
||
- Versión banner deploy: **2026-09-03 14:40**.
|
||
|
||
### Resuelto — Estabilidad integración chat mobile
|
||
|
||
- Error `Missing element: #chat-btn` resuelto con selectores non-strict
|
||
(`document.querySelector`) para elementos opcionales eliminados.
|
||
- Refactorización estética "seamless": chat sin marcos, sombras ni fondos
|
||
(`background:transparent`, `box-shadow:none`).
|
||
- **Layout mobile:**
|
||
- padding incrementado en `#app` (56px) y `.chat-history` (96px) para evitar
|
||
superposiciones con el footer sticky;
|
||
- márgenes fijos en las burbujas: `margin-right:22px` (Assistant), `margin-left:6px` (User).
|
||
- **Auto-scroll streaming:** implementado `_scrollToBottom` con cadena de retry
|
||
múltiples (`requestAnimationFrame` + `setTimeout`) para compensar el line-wrap
|
||
asíncrono durante la llegada de los tokens; `overflow-anchor:none` + `scroll-behavior:auto !important`.
|
||
|
||
### Añadido — Avatar usuario con iniciales + modal de perfil
|
||
|
||
- Sustituido el texto del nombre de usuario en el header por avatar circular 40×40px
|
||
(gradient de marca) conteniendo las iniciales calculadas (ej. "AV").
|
||
- Nueva `<dialog id="user-profile-dialog">` en `index.html` que se abre al click
|
||
sobre el avatar: muestra email, teléfono, rol, empresa, con soporte a shapes
|
||
anidadas del payload de perfil.
|
||
- Cierre del modal: tecla ESC, click fuera de la tarjeta, botón `×` dedicado.
|
||
|
||
---
|
||
|
||
## [Notas de formato]
|
||
|
||
- **Tope campo prompt episodio:** 24.000 caracteres (transcript largos).
|
||
- **Tope profile / summary:** 1.600 caracteres cada uno.
|
||
- **Tope reports list:** 12.000 caracteres.
|
||
- **Orden inject prompt LLM (WS session.update + reasoning):**
|
||
1. Episodio podcast actual (PRIMARIO)
|
||
2. Perfil llamador (identidad)
|
||
3. Lista reports / scripts podcast programados
|
||
4. Summary última conversación
|
||
- Todos los bloques están marcados con `<<<NOMBRE_BLOQUE_BEGIN/END>>>` para inspección de logs.
|