Skip to main content

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 superadmin global si lo tenés; si no, muestra el rol de tenant. Un usuario puede ser superadmin global y supervisor del tenant a la vez (son dimensiones independientes — ver Ajolote y roles).

RutaNavQuiénPara qué
/LlamadasTodosAtender llamadas + copiloto en vivo
/historyHistorialTodosRevisar llamadas pasadas y corregir
/monitorMonitorTodosSupervisar llamadas de voz activas
/whatsappWhatsAppTodosBandeja de hilos de WhatsApp del agente
/analyticsAnalíticasSupervisorCalidad de las sugerencias
/operationsOperaciónSupervisorSalud/rendimiento del sistema
/logsLogsSupervisorAuditoría de acciones de usuario
/anfibiosAnfibiosSuperadminHub de administración (prompt + ajustes)
/anfibios/promptsSuperadminEditar el prompt base del agente
/anfibios/settingsSuperadminAjustes del copiloto en runtime
/help— (ícono ?)TodosGuí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ónEstadoControl
(fijada arriba, verde)La llamada que tomaste vospill Reclamada + Volver
Llamadas entrantes (ámbar)unclaimed — nadie la atendióReclamar
Atendidas (rosa)claimed por otro agentepill 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 con reason: 'agent_disconnected' en el call_released. Un F5 no la libera: la gracia cubre la reconexión.
Por qué importa

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 del rag guardado 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 (knowledgeScore no 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 ( / 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_dni es 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_updated con 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íldoraQué significa
Falta el DNINadie dictó un documento todavía. Donde una llamada pasa casi todo el tiempo
ConsultandoLa consulta está en vuelo
Por DNI (verde)Oxitesa contestó con un paciente
Sin registroSe 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 con preferLive, 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.IDENTIFICATION exacto.
  • 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:

  1. "Marcar revisada y cerrar" en la revisión post-llamada.
  2. Botón explícito "Marcar como revisada" arriba del transcript, en el modal de detalle.
  3. 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 superadmin global, 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.
Solo lectura, por arquitectura

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óndeQué sugerencia
/whatsapp, detalle del hiloCada turno del agente
/ durante la llamadaLa tarjeta de sugerencia activa del copiloto
/ revisión post-llamadaLas burbujas del copiloto en el transcript
/history y el modal de /logsLas 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).

Qué se marca según el origen
  • 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):

  1. 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.
  2. 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).
  3. 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.

Los labels están hardcodeados

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 por IncomingCallsPanel / CallScreen / SuggestionCard / TranscriptStream reales. 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 práctica no puede escribir en la DB

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="…":

PasoApunta aNota
Bienvenido— (position: 'center')
1 · Reclamardata-tour="cola"Avanza solo cuando llega call_meta (tour-sync.tsx), no con un botón
2 · El transcriptdata-tour="transcript"
3 · La sugerenciadata-tour="sugerencia"
4 · Lo más importantedata-tour="sugerencia"Cómo escribir la corrección
5 · Soltardata-tour="soltar"
6 · Al cerrardata-tour="revision"action corta la llamada; actionAfter cierra la revisión y deja otra llamada de práctica en la cola
Listodata-tour="help-icon"position: 'center'
  • tour-sync.tsx hace el puente entre el estado de la app y el tour: vive dentro del TourProvider (el único lugar donde funciona useTour) mientras el estado vive en agent-copilot, arriba del provider. El tour avanza por estado, no adivinando DOM.
  • Solo el paso de la cola es clickeable (disableInteraction por paso). El resto está bloqueado: apretar Soltar desmontaría la pantalla que el tour está señalando, y Incorrecta abre un modal de Radix, que pone pointer-events: none en 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 son h-full y el alto del viewport lo maneja agent-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? en NAV_ITEMS); si no cumplís, no aparece. Los items solo-superadmin usan superadminOnly (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').

Ver Autenticación → Ajolote y roles.