# Conversaciones: voz, subtítulos, feedback y traducciones

## Piezas

- **Navegador**: `views/shared/ChatView.vue` (la pantalla) y
  `components/ElevenLabsChat.vue` (la llamada). La voz va directa entre el
  navegador y ElevenLabs, con el SDK `@elevenlabs/client`
  (`Conversation.startSession` con la URL firmada y `connectionType:
  "websocket"`).
- **Servidor**: un WebSocket propio en `/ws` (`websocketService.ts`), que
  autentica, valida el acceso al auxiliar, pide a ElevenLabs la URL firmada,
  lleva los dos temporizadores (bot y cuota) y traduce los subtítulos. Al
  terminar, `ConversationService.endConversation` anota el consumo y prepara
  el feedback.
- **ElevenLabs**: un agente por auxiliar (`Bot.agentId`). La API key se guarda
  en BD (`elevenlabs_config`) desde Configuración
  (`components/admin/ElevenLabsConfig.vue`).
- **OpenAI** (opcional): hace las traducciones (modelo
  `OPENAI_TRANSLATION_MODEL`, por defecto `gpt-4o-mini`) y el feedback
  estructurado, que necesita además un Assistant ID (opcional). La API key
  también se configura desde Configuración.

## Una llamada, paso a paso

1. El navegador abre `/ws`. El servidor identifica al usuario por la cookie
   `accessToken`; si no la trae, responde `auth_required`.
2. El navegador envía `init_conversation` con el auxiliar y, si se reanuda, la
   conversación. Entonces el servidor:
   - comprueba que el alumno tiene asignado ese auxiliar (`StudentBotAccess`),
     también al reanudar. Si el auxiliar pedido no coincide con el de la
     conversación, no la reanuda: la resuelve por nombre, comprobando el
     acceso de nuevo;
   - si la cuota está agotada, responde `quota_exhausted` y no sigue;
   - crea o reutiliza la conversación en BD, junto con una sesión
     (`ConversationSession`). Solo para alumnos y admins: las conversaciones
     de los profesores no se guardan;
   - pide a ElevenLabs la URL firmada (y un token, si puede) y el idioma del
     agente;
   - responde `elevenlabs_ready` con todo eso, el `transcriptionLanguage` del
     bot y, si el bot tiene temporizador, `timerSettings`.
3. El navegador abre la sesión con ElevenLabs. Al conectar envía
   `elevenlabs_conversation_started` con el id de la conversación en
   ElevenLabs, y va guardando los mensajes con `save_elevenlabs_message`.
4. Durante la llamada, el servidor envía `timer_update` (tiempo del bot) y
   `quota_update` (cuota) cada segundo, y `time_warning` cuando faltan 40 s.
   Cuando se agota, espera 10 s, envía `time_expired` o `quota_exhausted` y
   cierra. Detalle en [quotas.md](quotas.md).
5. Al recibir `time_warning`, el navegador pasa al agente una instrucción de
   despedida con `sendContextualUpdate`, que no aparece en la transcripción.
   El texto está en `FAREWELL_INSTRUCTION` (`ElevenLabsChat.vue`). Para que el
   agente le haga caso, necesita además el párrafo de su system prompt (ver
   abajo).
6. Al colgar o cortar se ejecuta `ConversationService.endConversation`. Llegan
   a él dos caminos: el cierre del WebSocket y `PUT /api/conversations/:id/end`,
   que llama el navegador. Primero suma la duración y anota el consumo (solo
   alumnos), y después recoge de ElevenLabs la transcripción y la evaluación.
7. El mensaje de fin sale en el idioma del agente (`END_SESSION_MESSAGES`: es,
   en, fr y de; si el idioma no está, en español).

## Mensajes del WebSocket

| Del navegador al servidor | Del servidor al navegador |
|---|---|
| `init_conversation`, `elevenlabs_conversation_started`, `save_elevenlabs_message`, `translate_turn`, `message`, `ping` | `connected`, `auth_required`, `elevenlabs_ready`, `message_saved`, `quota_update`, `timer_update`, `time_warning`, `time_expired`, `quota_exhausted`, `turn_translation`, `typing`, `error`, `pong` |

## Subtítulos en vivo

- Si el bot tiene `transcriptionLanguage` en ES o EN, el navegador envía
  `translate_turn` por cada turno y el servidor responde con `turn_translation`
  (`openaiService.translateText`). Con NONE no se pide nada.
- Se muestran bajo cada burbuja según llegan. Hay un botón para ocultarlos,
  visible solo si el bot tiene idioma de transcripción; por defecto están
  visibles.

## Feedback

- Se ve en el modal compartido `components/shared/FeedbackModal.vue`, con las
  pestañas Resumen, Evaluación, Feedback y Transcripción.
- Siempre se lee de BD, a través de
  `POST /api/elevenlabs/conversation/:conversationId/ensure-feedback`. Si
  faltan datos, ese endpoint los pide a ElevenLabs, los traduce y los guarda
  (`?force=true` obliga a pedirlos otra vez). Justo después de colgar puede
  que ElevenLabs aún no haya terminado: el modal reintenta cada 5 s durante
  2 min.
- Qué se guarda en `aiAnalysis` de la conversación:
  - el resumen traducido al español en `transcript_summary`, con el original
    en `transcript_summary_original` (F4);
  - la transcripción por turnos, cada uno con su `translation` al idioma del
    bot (G1);
  - los criterios de evaluación con su nombre real, leído de la configuración
    del agente con caché de 10 min (R5).
- La evaluación (criterios y justificaciones) no se traduce sola. El botón
  «Traducir al español» de la pestaña Evaluación la traduce y la guarda
  (`POST …/translate-evaluation`); «Ver original» vuelve al texto original sin
  coste.
- ElevenLabs también puede enviar la evaluación por webhook
  (`POST /api/elevenlabs/webhooks/evaluation`).

## Configuración de cada agente en ElevenLabs

Se hace a mano en el panel de ElevenLabs, agente por agente (checklist F5):

- **Duración máxima** = tiempo del bot + 90 s, para que ElevenLabs no corte
  antes que la aplicación.
- **Párrafo de despedida en el system prompt.** Es imprescindible: sin él, el
  agente recibe el aviso de los 40 s pero lo ignora. El mismo texto en inglés
  vale para todos los agentes, enseñen el idioma que enseñen:

  > You may occasionally receive a background note letting you know the session time is almost up. When you receive it, naturally wrap up the current topic within a sentence or two and say a warm goodbye in the language you are teaching in. Do not mention the time limit, the note itself, or that you were told to end the conversation.

- **Idiomas adicionales** (con el español) y la herramienta de sistema
  **Language detection**, y añadir al system prompt que el agente entiende el
  español pero responde siempre en el idioma que enseña.
- Probarlo hablándole en español.

Estado a 24-oct-2026: el párrafo de despedida solo consta en «maria». Ver
[status.md](../status.md).

## Limitaciones de ElevenLabs conocidas

- La duración máxima del agente corta en seco, y su «mensaje de duración
  máxima» solo sirve en conversaciones de texto, no de voz. El agente no sabe
  cuánto tiempo lleva hablando. Por eso el corte lo lleva la aplicación.
- El reconocimiento de voz queda fijado al idioma del agente durante toda la
  llamada. Si el alumno habla en español a un agente en alemán, la
  transcripción sale «traducida» o distorsionada. Además, corrige lo que oye,
  así que no refleja los errores del alumno. Los idiomas adicionales con
  detección de idioma lo mejoran, sin garantía.
- Una transcripción literal no está implementada. Se podría hacer descargando
  el audio de cada conversación (`GET /v1/convai/conversations/{id}/audio`) y
  volviéndolo a transcribir con Scribe, que costaba 0,22 $ por hora de audio
  en julio de 2026.

## Rutas de la API relacionadas

- `/api/elevenlabs/config` (`GET` y `PUT`) y `/config/test`: la API key de
  ElevenLabs.
- `/api/elevenlabs/agent/:agentId/validate`: comprueba un agente.
- `/api/elevenlabs/agent/:agentId/signed-url` y `/conversation-token`, y las
  mismas por nombre de bot (`/bot/:botName/…`): exigen sesión y cuota.
- `/api/elevenlabs/conversation/:conversationId/ensure-feedback` y
  `/translate-evaluation`: feedback guardado y traducción de la evaluación.
  `/api/elevenlabs/conversation/feedback`: feedback estructurado con OpenAI.
- `/api/elevenlabs/external/…`: vista previa en vivo de una conversación de
  ElevenLabs, sin guardar. La usa la herramienta de depuración del admin
  (`views/admin/SettingsView.vue`, sin enlace en el menú).
- `/api/elevenlabs/conversation/:conversationId` y `/webhooks/evaluation`:
  solo las protege una cabecera secreta opcional. Ver
  [status.md](../status.md#seguridad-urgente).
- `/api/conversations/*`: historial, mensajes, fin (`PUT /:id/end`) y
  borrado.
