Guía de pantallas
El dashboard (aura-front) tiene una barra de navegación superior con las pantallas. Los
items marcados (supervisor) solo aparecen para usuarios con rol supervisor (o
superadmin global); un agente común no los ve. Los marcados (superadmin) solo los ve
un superadmin global (no el supervisor del tenant). Arriba a la derecha están el ícono de
Ayuda (?), el rol actual y Salir.
El rol que se muestra a la derecha prioriza
superadminglobal si lo tenés; si no, muestra el rol de tenant. Un usuario puede sersuperadminglobal ysupervisordel tenant a la vez (son dimensiones independientes — ver Ajolote y roles).
| Ruta | Nav | Quién | Para qué |
|---|---|---|---|
/ | Llamadas | Todos | Atender llamadas + copiloto en vivo |
/history | Historial | Todos | Revisar llamadas pasadas y corregir |
/monitor | Monitor | Todos | Supervisar llamadas de voz activas |
/whatsapp | Todos | Bandeja de hilos de WhatsApp del agente | |
/analytics | Analíticas | Supervisor | Calidad de las sugerencias |
/operations | Operación | Supervisor | Salud/rendimiento del sistema |
/logs | Logs | Supervisor | Auditoría de acciones de usuario |
/anfibios | Anfibios | Superadmin | Hub de administración (prompt + ajustes) |
/anfibios/prompts | — | Superadmin | Editar el prompt base del agente |
/anfibios/settings | — | Superadmin | Ajustes del copiloto en runtime |
/help | — (ícono ?) | Todos | Guía de onboarding del agente |
Llamadas (/)
La pantalla principal. Tiene dos estados:
Sin llamada activa — muestra la cola de llamadas entrantes sin atender (de Retell y audio-fork). El agente toma una (claim) para empezar a asistirla. La cola tiene tres secciones:
| Sección | Estado | Control |
|---|---|---|
| (fijada arriba, verde) | La llamada que tomaste vos | pill Reclamada + Volver |
| Llamadas entrantes (ámbar) | unclaimed — nadie la atendió | Reclamar |
| Atendidas (rosa) | claimed por otro agente | pill Atendida, sin acción |
Gana el primero que aprieta: POST /audio-fork/claim responde 409 si la llamada ya está
tomada (o no existe). El claim es lo que abre la transcripción — mientras la llamada está
unclaimed el audio va a un buffer circular y no se gasta STT ni se generan sugerencias; al
tomarla se hace backfill de lo buffereado (ver
Visión general).
Soltar (footer de la llamada, en rojo) devuelve la llamada a unclaimed
(POST /audio-fork/release) para que la tome otro agente: aborta el LLM en vuelo, cierra las
sesiones de Deepgram (el audio vuelve al buffer, así que una llamada sin reclamar no cuesta
STT ni antes ni después del claim), re-anuncia la llamada a todos y registra call_released.
Solo puede soltarla quien la tomó. Con Llamadas
(flecha arriba a la izquierda) o con el ícono de Ayuda se sale de la pantalla de llamada
sin soltarla.
El claim no sobrevive a la sesión del agente. Hay dos caminos, y los dos terminan en el
mismo unclaim():
- Salir suelta la llamada antes del
signOut(el release necesita la cookie de sesión). - Si el agente se va sin avisar —cerró la pestaña, se le colgó el browser, se quedó sin red— el
barrido de sesiones lo detecta: cuando el agente que reclamó no tiene ningún dashboard
conectado por más de
session.orphanClaimMs(30 s por defecto, ver Ajustes runtime), la llamada vuelve a la cola conreason: 'agent_disconnected'en elcall_released. Un F5 no la libera: la gracia cubre la reconexión.
Un claim huérfano no es solo cosmético: Deepgram sigue abierto y se siguen generando
sugerencias para nadie (STT + LLM que se pagan), ningún otro agente puede tomar la llamada
(claimCall devuelve 409) y al terminar se persiste con el agentId del que se fue,
contaminando sus métricas en Analíticas.
En llamada — el copiloto en vivo:
- Header: datos del paciente (nombre, id) y duración.
- Tarjeta de sugerencia: la respuesta que propone Aura, que se va escribiendo en vivo
a medida que el LLM la genera (streaming), en vez de aparecer de golpe al terminar. El agente
la valida con Usar / Incorrecta (abre diálogo de corrección) / Divergente
(descartar). Cada acción registra feedback (ver flywheel). Al marcar, la
tarjeta queda 1,5 s mostrando el veredicto ("Marcada como correcta/incorrecta/divergente")
antes de desaparecer: descartarla en el mismo tick dejaba al agente sin ninguna confirmación,
y Divergente además no emite toast.
Cuando la respuesta salió de un manual, debajo del texto aparece el chip de Fuentes con el
modelo de ese manual y la sección. Está en las tres superficies donde se lee una
sugerencia: la tarjeta activa, las burbujas del transcript (en vivo y en la revisión) y el
detalle de
/history. En las llamadas viejas también, porque se deriva delragguardado al leer la fila y no de algo que hubiera que haber grabado en su momento. No es decorativo: el panel de equipos puede decir "7F-5" mientras la sugerencia contesta sobre el M50 que el paciente nombró — es la unión del filtro funcionando, pero sin el chip las dos cosas se leen como una contradicción. Sale sólo cuando un manual se usó de verdad (knowledgeScoreno nulo): en un miss no hay fuente que nombrar. - Transcript en vivo de la conversación.
- Panel de contexto: datos del paciente y entidades detectadas.
- Panel "Equipos en Oxitesa": lo que el sistema de Oxitesa tiene registrado para este paciente. Ver abajo.
- Métricas del turno: sentimiento, ritmo (WPM) y silencios largos.
Revisión post-llamada — al terminar, la pantalla queda en modo revisión sobre el transcript crudo, y aparece un diálogo preguntando si querés mejorarlo con IA (Sí / No).
La corrección ya no es automática. Antes corría al cortar todas las llamadas: gastaba una
pasada de LLM en llamadas que nadie iba a leer, y dejaba al operador esperando detrás de un spinner
para mostrarle un transcript que ya existía. Ahora el "Sí" es
POST /audio-fork/sessions/:id/polish, que trabaja sobre la fila guardada — así que también se
puede pedir después, desde el detalle de la llamada, incluso días más tarde.
El diálogo no se cierra clickeando afuera ni con Escape a propósito: las dos respuestas están a un click, y uno que se desvanece sin respuesta deja al operador sin saber si lo que está leyendo es la versión corregida o no. Es lo único que acá no puede quedar ambiguo.
Si la corrección falla o se pasa de tiempo, la oferta sigue en pie y el transcript en pantalla es el
real. transcript.polishEnabled en /anfibios/settings decide si la pregunta aparece.
Revisión post-llamada — al terminar, la pantalla queda en modo revisión. Se marcan las
sugerencias del copiloto (burbujas violeta "Copiloto" en el transcript): ✓ Usada o
✗ Incorrecta (con la corrección). No se marcan los turnos del agente humano — la
persona no es lo que el flywheel corrige, sino la respuesta del bot. Alimenta el aprendizaje.
Acá todavía no hay ↱ Divergente: solo está en la tarjeta en vivo y en el modal de
detalle de /history y /logs.
Al cerrar con "Marcar revisada y cerrar" la llamada queda registrada como revisada
(POST /audio-fork/sessions/:id/review), así que el estado sobrevive a un refresh. Lo mismo
aplica al banner de revisión del monitor. Si la sesión todavía no terminó de persistirse el
POST puede fallar: se avisa con un toast y se puede marcar después desde el historial.
Historial (/history)
Lista de llamadas pasadas (call_sessions) en una grilla responsive (1/2/3 columnas según
el ancho). Al hacer click en una tarjeta se abre el mismo modal de detalle que en /logs
(transcript + sugerencias + feedback). El transcript se trae lazy: la lista solo pide
resúmenes, y el detalle (GET /audio-fork/sessions/:id) se busca recién al abrir el modal.
Permite:
- Filtrar por "incorrectas" (llamadas con feedback negativo).
- Filtrar por estado de revisión con un chip que cicla
Todas → Sin revisar → Revisadas. Es independiente del filtro de incorrectas, así que se combinan (ej.: incorrectas todavía sin revisar). - Corregir una sugerencia marcada (textarea + guardar) → la corrección se guarda como lección en la base vectorial y mejora futuras sugerencias.
- Marcar cada sugerencia del bot con las mismas tres opciones de la tarjeta en vivo:
✓ Usada, ✗ Incorrecta o ↱ Divergente (ámbar, sin veredicto). El detalle trae
las tres desde
GET /audio-fork/sessions/:id(correctFeedback,incorrectFeedback,discardedFeedback), así que una divergente marcada en vivo se distingue de una sugerencia que nadie revisó. - Los supervisores además pueden editar o eliminar una marca existente, incluido convertirla en divergente — lo que desaprende su lección (ver flywheel).
Cada tarjeta muestra una píldora "sin revisar" (ámbar) o "revisada" (verde, con el email del revisor en el tooltip).
Datos del paciente (editable)
Se edita en los dos lugares donde se revisa una llamada:
- Revisión post-llamada (la pantalla del operador al cortar): el panel derecho "Datos del paciente" suma un botón Corregir que abre un diálogo con los cuatro campos. Aparece recién cuando la llamada terminó y la fila ya se escribió — durante la llamada el panel es read-only, porque todavía no hay fila que editar y además el extractor podría sobrescribir lo tipeado.
- Detalle de llamada de
/logs: arriba del transcript, en una barra con los valores y el mismo botón (Completar si no se capturó nada).
Muestra paciente, DNI, WhatsApp y motivo. Cualquier usuario autenticado puede editarlo: el operador que atendió es quien sabe qué se dijo, y la transcripción de dígitos falla seguido.
- Vaciar un campo lo borra. "Se entendió mal y no hay forma de saber qué era" es una respuesta válida; un DNI incorrecto es peor que ninguno.
- Corregir el DNI reagrupa la llamada con el paciente correcto, porque
patient_dnies la asociación (ver Base de datos). Los valores corregidos también se propagan al roster de pacientes. - El DNI y el WhatsApp se validan en el front y en el back (solo dígitos, sin puntos ni
+54), justamente para no crear un paciente duplicado con un formato distinto. - Cada guardado queda auditado como
patient_data_updatedcon qué campos se tocaron.
En el detalle de una llamada de Retell la sección no aparece: esos datos salen del guion del copiloto, no del lookup por DNI.
Equipos en Oxitesa
Debajo de "Datos del paciente", en el panel derecho de la llamada. Es la otra mitad del contexto y una afirmación distinta: "Datos del paciente" es lo que se dijo en esta llamada, esto es lo que otra empresa tiene en su sistema. Por eso son dos paneles y no uno.
La consulta se dispara sola cuando el paciente dicta el DNI, que es el único identificador que hay: el lookup por el teléfono de la línea se sacó, porque un número de teléfono no es una afirmación de identidad. La píldora del encabezado dice hasta dónde llegó la identificación:
| Píldora | Qué significa |
|---|---|
| Falta el DNI | Nadie dictó un documento todavía. Donde una llamada pasa casi todo el tiempo |
| Consultando | La consulta está en vuelo |
| Por DNI (verde) | Oxitesa contestó con un paciente |
| Sin registro | Se consultó y Oxitesa contestó que no conoce ese documento — conviene reconfirmarlo |
| Sin conexión (ámbar) | No se pudo consultar. No dice nada sobre el paciente |
Cuando el documento lo cargó el operador a mano la píldora agrega · manual (por ejemplo "Por DNI · manual"), que es una afirmación distinta: ese documento no salió de la transcripción.
Cada aparato muestra su descripción, marca, categoría y sku. La etiqueta de la derecha es el modelo canónico con el que el copiloto buscó — o "sin manual" cuando no tenemos manual de esa máquina, que es información y no un error: significa que las sugerencias de esa llamada no se acotaron a ese equipo.
Casos que el panel dice en voz alta en vez de mostrar una lista vacía:
- Falta el DNI: dice que está esperando el documento. Es el estado normal del primer tramo de toda llamada, y es distinto de "no está" a propósito.
- Sin registro: dice qué documento se buscó, que es lo que hay que reconfirmar.
- Sin conexión: la consulta falló y el panel lo dice con todas las letras — "No pudimos consultar Oxitesa. No sabemos si este paciente está registrado — no le digas que no figura" — más qué se puede hacer según la causa (clave que no coincide, ruta no publicada, timeout, red). Es el único estado en ámbar de los cinco: los otros cuatro son momentos normales de una llamada, este es una falla.
- Oxitesa no respondió: los modelos salen del cache de Aura, con su antigüedad, y sin el
detalle de cada aparato — el cache nunca lo guarda. Ojo con la distinción: el aviso ámbar sale
sólo cuando el cache se usó porque Oxitesa falló (
stale). Un cache todavía fresco es el camino rápido funcionando y se muestra en gris, sin alarma. En el copiloto además casi no pasa: esa consulta corre conpreferLive, porque nadie está esperando en silencio y el cache no tiene los aparatos, que son justamente lo que el panel muestra. - Sin equipos activos: encontramos al paciente y no tiene aparatos entregados.
- Datos de prueba: el backend está con el roster mock, no con Oxitesa.
Buscar por DNI a mano
Abajo del panel hay un campo de DNI siempre visible, no uno que aparezca recién cuando la búsqueda automática falla. El motivo: la consulta automática usa el DNI que se saca de la transcripción, y el reconocimiento de voz confunde dígitos lo suficiente como para que el operador tenga que poder preguntar por el documento que le leyó al paciente en cualquier momento — antes de que la automática conteste, o después de que haya contestado sobre otra persona.
- Sólo dígitos, 7 u 8, sin puntos: Oxitesa matchea
PATIENTS.IDENTIFICATIONexacto. - Repite un documento que la automática ya consultó, a propósito: reintentar después de que Oxitesa no contestó es exactamente para eso.
- Lo que se escribe queda fijo. Pasa a ser el DNI de la llamada (el que agrupa al paciente en
call_sessions) y las pasadas de extracción dejan de tocarlo: el operador llegó ahí justamente porque la transcripción venía mal, así que dejar que la próxima pasada la vuelva a pisar sería deshacer la corrección. - Sólo está durante la llamada en curso y sólo para quien la tiene tomada
(
POST /audio-fork/patient-lookup, 404 si no es suya). En la llamada de práctica no aparece, y en la pantalla de revisión el DNI se corrige desde "Datos del paciente".
La lógica de identidad está en RAG · pipeline.
Estado de revisión
Es global por llamada, no por usuario: quien la revisa primero la marca para todos, y ese revisor no se sobrescribe. Se marca de tres formas:
- "Marcar revisada y cerrar" en la revisión post-llamada.
- Botón explícito "Marcar como revisada" arriba del transcript, en el modal de detalle.
- Automáticamente al leerla: scrollear el transcript hasta el final cuenta como vista (si el transcript es corto y no scrollea, se marca al abrirlo).
Reabrir una llamada revisada (volverla a pendiente) es solo para supervisor o
superadmin — el botón "Reabrir" no aparece para los demás. Ambas acciones quedan auditadas
en el event log como call_reviewed (payload.reviewed = true/false).
Endpoints: GET /audio-fork/sessions (acepta filter=incorrect y reviewed=true|false) ·
GET /audio-fork/sessions/:id · POST /audio-fork/sessions/:id/review (cualquier usuario
autenticado, idempotente) · DELETE /audio-fork/sessions/:id/review (rol supervisor) ·
PATCH /audio-fork/sessions/:id/patient-data (cualquier usuario autenticado).
Monitor (/monitor)
Supervisión de llamadas de voz activas en tiempo real, sin importar por qué camino entraron: Retell o el endpoint Custom LLM de ElevenLabs. Muestra las llamadas en curso con su origen (el nav trae un contador de llamadas vivas). Al abrir una, se ve su transcript en vivo en pantalla completa (solo lectura) y la última respuesta del bot, que se puede marcar como correcta o incorrecta.
Fuente: GET /retell/active y GET /conversations/active (catch-up) + eventos WebSocket
(transcript_snapshot, retell_suggestion, conversation_snapshot).
WhatsApp (/whatsapp)
Bandeja de los hilos de WhatsApp que atiende el agente, ordenada por último mensaje. Es una pantalla propia y no una pestaña del monitor a propósito: un hilo de WhatsApp es asincrónico —alguien puede contestar tres horas después— así que no tiene duración de llamada y lo que importa es la antigüedad del último mensaje.
La lista muestra, por hilo: paciente (hoy sin identificar, ver el ticket de identidad), preview del último mensaje, antigüedad, cantidad de mensajes y el equipo detectado en la conversación. El nav trae su propio contador de hilos abiertos.
Al abrir un hilo se ve el transcript como chat, read-only, con:
- el score de RAG por turno del agente (solo
superadminglobal, ver abajo) — sin operador humano en el loop, es el único rastro de por qué el agente contestó lo que contestó. Tres estados: respondió con el manual (verde), consultó y quedó bajo el umbral (ámbar) o nunca consultó (Sin RAG). Si el turno no registró nada, no se muestra badge: es distinto de saber que no consultó; - marcación por turno (correcta / incorrecta + corrección), que alimenta el flywheel;
- el equipo mencionado en el hilo y el que figura en ficha, separados: el caso interesante es justamente cuando no coinciden.
No es una limitación de la pantalla: la cuenta de WhatsApp Business es de ElevenLabs, así que desde acá no se puede escribir en el hilo ni mutear al bot — el bot somos nosotros. Ver Custom LLM (ElevenLabs).
El badge de RAG
El mismo badge (components/copilot/shared/rag-badge.tsx) aparece en las cuatro superficies
donde el bot contesta o sugiere:
| Dónde | Qué sugerencia |
|---|---|
/whatsapp, detalle del hilo | Cada turno del agente |
/ durante la llamada | La tarjeta de sugerencia activa del copiloto |
/ revisión post-llamada | Las burbujas del copiloto en el transcript |
/history y el modal de /logs | Las sugerencias persistidas de la llamada |
Lo ve solo el superadmin global: un 0.62 crudo no significa nada para un operador en medio
de una llamada, y se lee como un veredicto sobre la respuesta cuando en realidad habla de su
procedencia. Como todo gate del front, solo oculta: el score viaja igual en el payload del
socket, así que no es un control de acceso — si hiciera falta eso, hay que filtrarlo en el
backend según el rol.
En las llamadas anteriores a que el score se persistiera, el badge simplemente no aparece. "Sin dato" y "no consultó" son cosas distintas y la UI no las mezcla.
Un hilo desaparece de la bandeja cuando el barrido de inactividad lo cierra
(conversation_ended), que es una inferencia nuestra: nadie nos avisa cuándo termina una
conversación. Si el paciente vuelve a escribir, el hilo reaparece. El hilo que ya está abierto
en pantalla no se cierra solo: queda visible con un aviso.
Fuente: GET /conversations/active (catch-up, sobrevive un redeploy) + eventos WebSocket
conversation_snapshot / conversation_ended.
El score de RAG viaja por turno dentro del transcript, no a nivel de conversación: un score
de conversación solo podría describir la última respuesta. En el snapshot en vivo lo trae solo
el turno que acaba de terminar (los anteriores son historia que manda ElevenLabs, y sus scores
están en la DB, no a mano), y el dashboard conserva los que ya vio. El catch-up los lee de
messages, así que un refresh recupera el score de cada respuesta.
Analíticas (/analytics) — supervisor
Calidad de las sugerencias según el juicio humano: % de aciertos, sugerencias usadas / incorrectas / descartadas, % de aceptación. Soporta modo Ver y Comparar (período A vs B). Depende de que haya feedback cargado.
Detalle en Observabilidad → Métricas y paneles.
Operación (/operations) — supervisor
Salud/rendimiento del sistema, medido automáticamente (no depende de feedback): cobertura (por turno y por llamada), tasa "sin contexto (RAG)", tasa "se calló" —con el corte entre lo que silenció el gate y lo que el modelo decidió—, tasa "derivó", y latencia p50/p95 por capa (embedding / búsqueda / LLM), con gráfico de tendencia.
Detalle en Observabilidad → Métricas y paneles.
Logs (/logs) — supervisor
Auditoría de acciones de usuario: quién marcó feedback, tomó o liberó llamadas. Tabla con fecha, usuario (email), acción, llamada (teléfono) y detalle; filtros por tipo, rango y usuario, con paginación.
En la columna Detalle, las filas con llamada asociada muestran un icono de ojo que abre el modal de detalle de la llamada: el transcript completo con sus marcaciones, enfocado en esa llamada. Desde ahí el supervisor supervisa y corrige el trabajo de los agentes:
- Marcar turnos aún sin revisar (correcta / incorrecta + corrección).
- Editar una marcación existente: cambiar el veredicto (correcta ↔ incorrecta) y/o el texto de la corrección (icono de lápiz sobre el turno).
- Eliminar una marcación.
Estas ediciones sincronizan el aprendizaje del bot: editar re-entrena la lección y eliminar la borra de la base vectorial, para que una corrección equivocada de un agente deje de influir en las sugerencias futuras (ver flywheel).
- Retell: el "agente" ES el bot, así que se marcan sus turnos (correcta / incorrecta).
- Central (audio_fork): el agente es un humano — marcar sus turnos no tiene sentido y está deshabilitado. Lo que se marca son las sugerencias del bot (burbujas violeta: ✓ usada / ✗ incorrecta). Además, en estas llamadas la sugerencia del bot se muestra prominente (a la derecha y en tamaño grande) y el turno del humano en tamaño reducido, para priorizar visualmente la respuesta del LLM.
Endpoints: GET /audit/logs (tabla) · GET /audio-fork/sessions/:id (detalle) ·
PATCH /feedback/:id y DELETE /feedback/:id (editar/eliminar marcación, solo supervisor).
Detalle en Observabilidad → Métricas y paneles.
Anfibios (/anfibios) — superadmin
Hub de administración, solo para superadmin global. Es una landing con tarjetas hacia las
herramientas de configuración:
- Prompt del agente →
/anfibios/prompts - Ajustes del copiloto →
/anfibios/settings
La ruta está ofuscada a propósito (no
/admin) como leve disuasivo ante un ataque externo. La seguridad real es el guard de rol, no el nombre.
Prompt (/anfibios/prompts)
Edición en caliente del prompt base del agente (identidad, tono, reglas), sin redeploy. Un editor de texto con la versión vigente, un historial de versiones (con quién la creó), un botón Restaurar por versión para hacer rollback, y Comparar para ver el diff línea a línea entre dos versiones cualesquiera (estilo GitHub). Cada guardado crea una versión nueva y queda auditado.
Un banner permanente advierte los riesgos: el cambio aplica a todas las llamadas nuevas de inmediato, y guardar durante una llamada activa cambia su prompt a mitad de camino. Detalle en Arquitectura → Prompts del agente.
Ajustes del copiloto (/anfibios/settings)
Editor de las perillas tuneables en runtime (transcripción, sugerencias, sesiones), agrupadas, con descripción, rango y valor por defecto de cada una. Los cambios se aplican en vivo, sin redeploy (a las próximas llamadas/turnos). Cada perilla tiene "Guardar" y "Restablecer al default", y muestra quién/cuándo la cambió. Detalle en Arquitectura → Ajustes runtime.
Ambas pantallas son solo superadmin global; el supervisor del tenant no las ve.
Ayuda (/help)
Guía de onboarding del agente humano dentro del producto — el único material de uso que ve
el agente sin salir de la app. Se llega solo por el ícono ? Ayuda del header (a la
izquierda del rol) — a propósito no tiene item en la barra de navegación, para no competir
con las pantallas operativas. Sin guard de rol. Desde acá se lanza también el
modo práctica.
Contenido (help-panel.tsx, en español rioplatense, a ancho completo con las listas en grilla
desde md para arriba):
- Cómo reclamar una llamada — las tres secciones de la cola, que sin claim no hay sugerencias, que gana el primero que aprieta, y Soltar.
- Cómo interactuar con el copiloto — las tres acciones (Usar / Divergente / Incorrecta) y, sobre todo, cómo escribir la corrección: la instrucción de cómo debe razonar el bot, no la frase textual para el paciente (con ejemplo malo vs. bueno).
- Revisión post-llamada — marcar en el transcript y Marcar revisada y cerrar.
La página es solo texto: la parte práctica no vive acá, sino en el modo práctica sobre las pantallas reales (abajo). Así hay una sola simulación que mantener.
La app no tiene i18n: la guía cita los textos de los botones literalmente. Si se renombra un
botón (Reclamar, Soltar, Usar, Divergente, Incorrecta, Marcar revisada y cerrar) hay
que actualizar help-panel.tsx y practice/tour.tsx a mano. Lo único que queda sincronizado
solo es el texto de la corrección, importado de transcript/constants.ts
(CORRECTION_PLACEHOLDER / CORRECTION_TIP).
Modo práctica y tour guiado
El tour no corre sobre una página de demo: corre sobre las pantallas reales, con una llamada
inventada. Código en src/components/copilot/practice/.
Cómo se fabrica la llamada
PracticeSocket (src/api/socket/practice-socket.ts) envuelve el socket vivo en vez de
reemplazarlo: reenvía los eventos reales y agrega un emitPractice() que inyecta eventos
fabricados por los mismos listeners. Consecuencias:
- La llamada de práctica pasa por el reducer real (
use-agent-socket.ts) y porIncomingCallsPanel/CallScreen/SuggestionCard/TranscriptStreamreales. Las transiciones (claim →call_meta→ turnos → sugerencia →call_ended→ revisión) son las de producción. - El socket real sigue conectado: un agente practicando ve y puede tomar una llamada real.
- Los eventos fabricados nunca salen del browser.
usePractice() es el guion: emite incoming_call al arrancar y, al reclamar, call_meta +
patient_data y después los turnos y la sugerencia con delays, para que el transcript se
escriba en vivo. Al cortar emite patient_data de nuevo con persisted: true, que es lo que
habilita el botón Corregir — igual que en una llamada real. Los fixtures
(practice/fixtures.ts) usan el caso canónico del concentrador portátil, el mismo del ejemplo de
corrección.
Dos datos están mal a propósito, porque el tour enseña a arreglarlos: la sugerencia
(PRACTICE_SUGGESTION, que ofrece el recambio sin averiguar cómo se usa el equipo) y el DNI
(PRACTICE_PATIENT_DATA, con dos dígitos permutados respecto de lo que la paciente dicta en el
transcript). El WhatsApp viene vacío, así que también hay algo que completar.
La práctica no manda context_update: los slots de afiliado y equipo son del lookup de
Retell y una llamada de central nunca los muestra, así que mostrarlos en el onboarding enseñaba
una pantalla que no existe.
La guarda es el call_id, no un flag de modo: la llamada de práctica lleva prefijo demo- e
isPracticeCall() se chequea en handleClaim, handleAction, handleRelease, handleSignOut y
handleCloseReview antes de pegarle a la API. La corrección de datos del paciente lo chequea
adentro de PatientDataFields, pegado al fetch, en vez de recibirlo como prop: así ningún
caller futuro se lo puede olvidar. Así el feedback de práctica no entra al flywheel,
ni a accuracy, ni al event-log — y las acciones de una llamada real siguen funcionando con
el modo práctica activo. Un flag global se puede desincronizar; el call_id no.
El tour
Ocho pasos (practice/tour.tsx), selectores por atributo data-tour="…":
| Paso | Apunta a | Nota |
|---|---|---|
| Bienvenido | — (position: 'center') | |
| 1 · Reclamar | data-tour="cola" | Avanza solo cuando llega call_meta (tour-sync.tsx), no con un botón |
| 2 · El transcript | data-tour="transcript" | |
| 3 · La sugerencia | data-tour="sugerencia" | |
| 4 · Lo más importante | data-tour="sugerencia" | Cómo escribir la corrección |
| 5 · Soltar | data-tour="soltar" | |
| 6 · Al cerrar | data-tour="revision" | action corta la llamada; actionAfter cierra la revisión y deja otra llamada de práctica en la cola |
| Listo | data-tour="help-icon" | position: 'center' |
tour-sync.tsxhace el puente entre el estado de la app y el tour: vive dentro delTourProvider(el único lugar donde funcionauseTour) mientras el estado vive enagent-copilot, arriba del provider. El tour avanza por estado, no adivinando DOM.- Solo el paso de la cola es clickeable (
disableInteractionpor paso). El resto está bloqueado: apretarSoltardesmontaría la pantalla que el tour está señalando, yIncorrectaabre un modal de Radix, que ponepointer-events: noneen el<body>y deja muertos los botones del propio tour. Al cerrar el último paso todo vuelve a estar vivo: ahí se practica. - Los pasos que apuntan a targets pegados al borde superior usan
position: 'center': anclados ahí, el popover se sale de la ventana y lo primero que se corta es el badge del contador.
Entrada y persistencia
- Arranca solo en el primer ingreso del agente (
agent-copilot.tsx). Nunca interrumpe una llamada real. - Se repite desde Hacer el tour, en
/help. - Mientras la llamada inventada está en juego hay un cartel amarillo
Modo práctica · Salir(practice-banner.tsx) para que nadie la confunda con una real. Por eso las pantallas sonh-fully el alto del viewport lo manejaagent-copilot: el cartel se lleva una franja de arriba sin desbordar nada. - El "ya lo vio" va a localStorage por usuario (
aura.tour.v1.<agentId>,src/lib/onboarding.ts); no hay store de preferencias en el backend, así que reaparece una vez en una máquina o navegador nuevo. Se marca al arrancar, no al terminar: descartarlo no lo trae de vuelta en cada recarga.
El ícono se oculta durante la revisión post-llamada: esa vista no se puede abandonar (hay que cerrarla explícitamente), así que el botón no tendría efecto.
Cómo se controla el acceso
- Nav: cada item puede exigir rol (
roles?enNAV_ITEMS); si no cumplís, no aparece. Los items solo-superadmin usansuperadminOnly(chequea el rol global, no el de tenant). - Ruta: las pantallas de supervisor están envueltas en
<RequireRole>; las de superadmin en<RequireSuperadmin>(si entrás por URL directa sin el rol, ves un fallback). - Backend: los endpoints de supervisor están gateados con
@RequireAppRole; los de superadmin con@RequireGlobalRole('superadmin').