
Com transmetre un informe en directe sense el parpelleig
Quan s'acaba la teva reunió, el resum de GeekBye ara s'omple en directe en lloc de fer-te mirar un indicador de càrrega. Aconseguir que la interfície de streaming se sentís tranquil·la en lloc de nerviosa va requerir resoldre el parpelleig dues vegades — un cop per als camps estructurats, un cop per al markdown — i la segona solució va ser un renderitzador que ja teníem.
Abans d'aquesta versió, acabar una reunió de GeekBye volia dir mirar un indicador de càrrega. L'aplicació enviava tot el teu transcript per analitzar-lo, esperava el resum complet — puntuació, punts clau, tasques a fer, tot — i llavors ho deixava caure a la pantalla de cop. Funcional, però feia la sensació de ser lent, perquè estaves mirant el no-res mentre treballava.
GeekBye v1.6.13–v1.6.15 va convertir això en un informe que arriba en directe, camp a camp, a mesura que es genera. La part interessant no és el streaming — és tot el que vam haver de fer perquè el streaming no semblés nerviós. El streaming tranquil i el streaming irregular són la mateixa funcionalitat amb una execució molt diferent.
Parpelleig, part u: no deixis que cada fragment sacsegi React
El resum no és una sola massa — són camps estructurats (una puntuació global, punts clau, tasques a fer, etc.) que el backend emet un a un mitjançant server-sent events. La manera ingènua de renderitzar-ho és actualitzar l'estat de React a cada esdeveniment tal com arriba.
Fes això, i tindràs parpelleig. Cada camp que arriba desencadena el seu propi re-render; els components que mostraven "buit" es munten, després es desmunten, i es tornen a muntar com a "carregats"; el disseny es sacseja a cada paquet. L'informe es munta a si mateix davant teu de la pitjor manera possible — visiblement, nerviosament.
La solució és una disciplina de dues parts. Primer, acumula els camps entrants en una ref — un contenidor mutable que no desencadena renders — i publica a l'estat de React un cop per fragment amb una còpia nova, de manera que l'arbre s'actualitzi de manera deliberada en lloc de a cada micro-esdeveniment. Segon, renderitza un sol arbre de components estable tota l'estona, tant si s'està transmetent com si ha acabat, amb marcadors de posició en línia per als camps que encara no han arribat:
// 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 component que mostra la puntuació no es desmunta mai — simplement mostra … fins que arriba el número, i llavors el número. Res es sacseja. I l'objecte totalment muntat encara es desa a la memòria cau de la base de dades local al final, de manera que la interfície de streaming i l'emmagatzematge durador conviuen sense barallar-se.
Parpelleig, part dos: el renderitzador que ja teníem
El segon parpelleig és més subtil i viu en el mateix markdown. L'informe renderitza markdown ric — capçaleres, negreta, llistes, blocs de codi. Però el markdown que arriba a mig stream està, en qualsevol instant donat, a mig acabar. Una llista amb un sol punt fins ara. Un bloc de codi que s'ha obert però no s'ha tancat. Un marcador de negreta encara sense parella.
Un renderitzador de markdown convencional torna a analitzar tota la cadena a cada token, i aquest estat a mig acabar s'analitza com una cosa diferent cada vegada — així que la llista parpelleja, el bloc de codi s'obre i es tanca de cop, el disseny salta a mesura que els tokens es completen. És el mateix muntatge nerviós d'abans, una capa més avall.
La solució era gairebé vergonyosament a l'abast: ja fèiem servir un renderitzador de markdown conscient del streaming per al xat d'IA en directe. Un renderitzador construït per tolerar tokens incomplets — per renderitzar markdown a mig acabar de manera estable i assentar-lo només quan els tokens es completen — en lloc de tornar a analitzar des de zero cada vegada. L'informe només necessitava fer servir el mateix. Havíem resolt aquest problema exacte per al xat mesos abans; la "solució" per a l'informe va ser adonar-nos que ja teníem l'eina i apuntar-la a una segona superfície. Reutilitzar va guanyar a reconstruir.
L'extra: la còpia rica és un problema de format del porta-retalls
Un cop l'informe tenia bon aspecte, la gent volia enganxar-lo — en un document, un correu — i conservar el format i les captures de pantalla. L'instint és serialitzar l'informe a una cadena de markdown i esperar que la destinació el renderitzi. Normalment no ho fa.
La resposta real és que el porta-retalls conté múltiples representacions alhora. Així que n'escrivim dues: una versió HTML amb el format intacte i les captures de pantalla incrustades com a imatges en línia, i una alternativa de text pla per a les destinacions que no accepten HTML. Enganxa en un editor ric i obtens l'informe amb format i imatges; enganxa en una casella de text pla i obtens text net. La "còpia rica" mai va ser un problema de serialització — era un problema de tria-el-format-correcte-del-porta-retalls.
La mateixa versió també va donar a l'informe etiquetes d'estil xat Me / Them amb els dos parlants alineats a costats oposats, de manera que un transcript es llegeix com la conversa que era, i va moure les teves pròpies captures de pantalla al seu propi costat perquè quadressin. (Una versió d'aquesta finestra, la v1.6.14, va ser una reconstrucció pura — cap història allà, i és honest dir-ho.)
Tres coses que la interfície de streaming ens va ensenyar
- Fes buffer dels camps transmesos, publica un cop per fragment. Deixar que cada server-sent event condueixi la seva pròpia actualització d'estat és el parpelleig. Un acumulador ref més una sola publicació per fragment, en un arbre que no es desmunta mai, converteix el muntatge nerviós en un ompliment tranquil.
- Qualsevol cosa que es transmeti necessita un renderitzador conscient del streaming. Un renderitzador que torna a analitzar tota la cadena per cada token parpellejarà amb markdown a mig acabar. Fes servir un construït per a entrada incompleta — i si ja en tens un per a una altra superfície, reutilitza'l abans de construir-ne un segon.
- La còpia rica tracta de formats del porta-retalls, no de serialització. Escriu
text/htmlamb imatges incrustades i una alternativatext/plain. El porta-retalls es va dissenyar per portar tots dos; fes-lo servir.
Aquest és el cinquè capítol de la història de fiabilitat-i-poliment que esdevé GeekBye v2. Per al capítol anterior, mira el bug dels auriculars (v1.6.12); per on la mateixa idea de server-sent events més tard va esdevenir tot un transport alternatiu, transcripció en directe quan el tallafocs bloqueja els WebSockets (v2.0.8); i per a tot l'arc, l'anatomia de llançar programari fins a la perfecció.