
Cómo transmitir un informe en vivo sin el parpadeo
Cuando termina tu reunión, el resumen de GeekBye ahora se rellena en vivo en lugar de hacerte mirar un spinner. Lograr que la interfaz en streaming se sintiera calmada en vez de nerviosa exigió resolver el parpadeo dos veces — una para los campos estructurados, otra para el markdown — y el segundo arreglo fue un renderizador que ya teníamos.
Antes de este lanzamiento, terminar una reunión de GeekBye significaba mirar un spinner. La app enviaba toda tu transcripción para su análisis, esperaba el resumen completo — puntuación, puntos clave, tareas pendientes, todo — y luego lo soltaba en pantalla de golpe. Funcional, pero se sentía lento, porque te quedabas mirando la nada mientras trabajaba.
GeekBye v1.6.13–v1.6.15 convirtió eso en un informe que se transmite en vivo, campo a campo, a medida que se genera. La parte interesante no es el streaming — es todo lo que tuvimos que hacer para evitar que el streaming se viera nervioso. El streaming calmado y el streaming brusco son la misma función con ejecuciones muy distintas.
Parpadeo, parte uno: no dejes que cada fragmento sacuda a React
El resumen no es un solo bloque — son campos estructurados (una puntuación general, puntos clave, tareas pendientes, etc.) que el backend emite uno a uno sobre server-sent events. La forma ingenua de renderizarlo es actualizar el estado de React en cada evento a medida que llega.
Haz eso, y obtienes parpadeo. Cada campo que llega dispara su propio re-renderizado; componentes que mostraban "vacío" se montan, luego se desmontan, luego se vuelven a montar como "cargado"; el diseño se sacude con cada paquete. El informe se ensambla solo frente a ti de la peor manera posible — visible, nerviosamente.
El arreglo es una disciplina de dos partes. Primero, acumula los campos entrantes en un ref — un contenedor mutable que no dispara renderizados — y publica al estado de React una vez por fragmento con una copia nueva, para que el árbol se actualice de forma deliberada y no en cada micro-evento. Segundo, renderiza un único árbol de componentes estable todo el tiempo, ya sea en streaming o terminado, con marcadores en línea para los campos que aún no han llegado:
// accumulate in a ref, publish once per chunk
partialRef.current = { ...partialRef.current, [field]: value }
setPartial({ ...partialRef.current })
// one tree, never swapped; missing fields show a placeholder in place
const report = isStreaming ? partial : saved
<Score>{report.overallScore ?? '…'}</Score>
El componente que muestra la puntuación nunca se desmonta — solo muestra … hasta que aterriza el número, y luego el número. Nada se sacude. Y el objeto ya completamente ensamblado sigue guardándose en caché en la base de datos local al final, así que la interfaz en streaming y el almacenamiento duradero conviven sin pelearse.
Parpadeo, parte dos: el renderizador que ya teníamos
El segundo parpadeo es más sutil y vive en el propio markdown. El informe renderiza markdown enriquecido — encabezados, negritas, listas, bloques de código. Pero el markdown que llega a mitad del stream está, en cualquier instante dado, a medio terminar. Una lista con un solo punto por ahora. Un bloque de código que se ha abierto pero no cerrado. Un marcador de negrita sin pareja todavía.
Un renderizador de markdown convencional reanaliza toda la cadena en cada token, y ese estado a medio terminar se analiza como algo distinto cada vez — así que la lista parpadea, el bloque de código destella abriéndose y cerrándose, el diseño salta a medida que los tokens se completan. Es el mismo ensamblaje nervioso de antes, una capa más abajo.
El arreglo estaba casi vergonzosamente a mano: ya estábamos usando un renderizador de markdown consciente del streaming para el chat de IA en vivo. Un renderizador construido para tolerar tokens incompletos — para renderizar markdown a medio terminar de forma estable y asentarlo solo cuando los tokens se completan — en lugar de reanalizar desde cero cada vez. El informe solo necesitaba usar el mismo. Habíamos resuelto exactamente este problema para el chat meses antes; el "arreglo" para el informe fue reconocer que ya teníamos la herramienta y apuntarla a una segunda superficie. Reutilizar le ganó a reconstruir.
El extra: la copia enriquecida es un problema de formato de portapapeles
Una vez que el informe se veía bien, la gente quería pegarlo — en un documento, un correo — y conservar el formato y las capturas de pantalla. El instinto es serializar el informe a una cadena de markdown y esperar que el destino la renderice. Normalmente no lo hace.
La verdadera respuesta es que el portapapeles guarda varias representaciones a la vez. Así que escribimos dos: una versión HTML con el formato intacto y las capturas incrustadas como imágenes en línea, y una alternativa en texto plano para destinos que no aceptan HTML. Pega en un editor enriquecido y obtienes el informe con formato y con imágenes; pega en una caja de texto plano y obtienes texto limpio. La "copia enriquecida" nunca fue un problema de serialización — fue un problema de elegir-el-formato-de-portapapeles-correcto.
El mismo lanzamiento también dio al informe etiquetas al estilo del chat Yo / Ellos con los dos hablantes alineados a lados opuestos, para que una transcripción se lea como la conversación que fue, y movió tus propias capturas de pantalla a su propio lado para que coincidieran. (Un lanzamiento dentro de esta ventana, v1.6.14, fue una recompilación pura — no hay historia ahí, y es honesto decirlo.)
Tres cosas que nos enseñó la interfaz en streaming
- Almacena en búfer los campos en streaming, publica una vez por fragmento. Dejar que cada server-sent event impulse su propia actualización de estado es el parpadeo. Un acumulador con ref más una única publicación por fragmento, en un árbol que nunca se desmonta, convierte el ensamblaje nervioso en un rellenado calmado.
- Cualquier cosa que se transmita necesita un renderizador consciente del streaming. Un renderizador que reanaliza toda la cadena por token parpadeará con markdown a medio terminar. Usa uno construido para entradas incompletas — y si ya tienes uno para otra superficie, reutilízalo antes de construir un segundo.
- La copia enriquecida trata de formatos de portapapeles, no de serialización. Escribe
text/htmlcon imágenes incrustadas y una alternativatext/plain. El portapapeles fue diseñado para llevar ambos; úsalo.
Este es el quinto capítulo de la historia de fiabilidad y pulido que se convierte en GeekBye v2. Para el capítulo anterior, mira el bug de los auriculares (v1.6.12); para el punto donde la misma idea de los server-sent events se convirtió después en todo un transporte de reserva, transcripción en vivo cuando el firewall bloquea WebSockets (v2.0.8); y para todo el arco, la anatomía de publicar software hasta la perfección.