Steven
Steven5 min citire

Cum transmiți un raport în timp real fără pâlpâire

Când ședința ta se termină, rezumatul GeekBye se completează acum live, în loc să te lase să te uiți la un spinner. Ca UI-ul în flux să pară calm, nu sacadat, a trebuit să rezolvăm pâlpâirea de două ori — o dată pentru câmpurile structurate, o dată pentru markdown — iar a doua reparație a fost un renderer pe care îl aveam deja.

Inginerie
Frontend
Streaming
Lansări GeekBye
Cum transmiți un raport în timp real fără pâlpâire

Înainte de această lansare, terminarea unei ședințe GeekBye însemna să te uiți la un spinner. Aplicația îți trimitea toată transcrierea la analiză, aștepta întregul rezumat — scor, puncte cheie, elemente de acțiune, tot pachetul — și apoi îl arunca pe ecran dintr-odată. Funcțional, dar părea lent, pentru că te uitai la nimic cât timp lucra.

GeekBye v1.6.13–v1.6.15 a transformat asta într-un raport care se completează live, câmp cu câmp, pe măsură ce e generat. Partea interesantă nu e streamingul — e tot ce a trebuit să facem ca streamingul să nu arate sacadat. Streamingul calm și streamingul greoi sunt aceeași funcționalitate cu execuție foarte diferită.

Pâlpâire, partea întâi: nu lăsa fiecare bucată să zdruncine React

Rezumatul nu e un singur bloc — sunt câmpuri structurate (un scor general, puncte cheie, elemente de acțiune și așa mai departe) pe care backendul le emite unul câte unul prin server-sent events. Modul naiv de a randa asta e să actualizezi starea React la fiecare eveniment care sosește.

Fă asta și obții pâlpâire. Fiecare câmp care sosește declanșează propria re-randare; componentele care afișau „gol" se montează, apoi se demontează, apoi se remontează ca „încărcate"; layoutul zvâcnește la fiecare pachet. Raportul se asamblează singur în fața ta în cel mai rău mod posibil — vizibil, nervos.

Reparația e o disciplină în două părți. Mai întâi, acumulează câmpurile primite într-un ref — un container mutabil care nu declanșează randări — și publică în starea React o dată per bucată cu o copie proaspătă, ca arborele să se actualizeze deliberat, nu la fiecare micro-eveniment. În al doilea rând, randează un singur arbore de componente stabil tot timpul, fie că e în flux sau gata, cu placeholdere inline pentru câmpurile care încă n-au sosit:

// 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>

Componenta care afișează scorul nu se demontează niciodată — doar arată până aterizează numărul, apoi numărul. Nimic nu zvâcnește. Iar obiectul complet asamblat tot ajunge în cache-ul bazei de date locale la final, așa că UI-ul în flux și stocarea durabilă coexistă fără să se lupte.

Pâlpâire, partea a doua: rendererul pe care îl aveam deja

A doua pâlpâire e mai subtilă și trăiește în markdown-ul însuși. Raportul randează markdown îmbogățit — titluri, aldine, liste, blocuri de cod. Dar markdown-ul care sosește în mijlocul fluxului e, în orice moment, pe jumătate terminat. O listă cu un singur punct până acum. Un bloc de cod deschis, dar neînchis. Un marker de aldine încă fără pereche.

Un renderer de markdown obișnuit re-parsează întregul șir la fiecare token, iar acea stare pe jumătate terminată parsează la ceva diferit de fiecare dată — așa că lista pâlpâie, blocul de cod se deschide și se închide, layoutul sare pe măsură ce token-urile se completează. E aceeași asamblare nervoasă ca înainte, cu un strat mai jos.

Reparația era aproape stânjenitor de la îndemână: foloseam deja un renderer de markdown conștient de flux pentru chatul AI live. Un renderer construit să tolereze token-uri incomplete — să randeze markdown pe jumătate terminat în mod stabil și să-l așeze abia când token-urile se completează — în loc să re-parseze de la zero de fiecare dată. Raportul trebuia doar să-l folosească pe același. Rezolvasem exact această problemă pentru chat cu luni în urmă; „reparația" pentru raport a fost să recunoaștem că aveam deja unealta și s-o îndreptăm către o a doua suprafață. Reutilizarea a bătut reconstruirea.

Bonusul: copierea îmbogățită e o problemă de format de clipboard

Odată ce raportul arăta bine, oamenii au vrut să-l lipească — într-un document, un email — și să păstreze formatarea și capturile de ecran. Instinctul e să serializezi raportul într-un șir de markdown și să speri că destinația îl randează. De obicei nu o face.

Răspunsul real e că clipboard-ul ține mai multe reprezentări deodată. Așa că scriem două: o versiune HTML cu formatarea intactă și capturile de ecran încorporate ca imagini inline, și o variantă text simplu de rezervă pentru destinațiile care nu acceptă HTML. Lipești într-un editor îmbogățit și obții raportul formatat cu imagini; lipești într-o casetă de text simplu și obții text curat. „Copierea îmbogățită" nu a fost niciodată o problemă de serializare — a fost o problemă de alegere-a-formatului-de-clipboard-potrivit.

Aceeași lansare a dat raportului și etichete de tip chat Eu / Ei cu cei doi vorbitori aliniați pe laturi opuse, ca o transcriere să se citească precum conversația care a fost, și a mutat propriile tale capturi de ecran pe partea lor ca să se potrivească. (O lansare din această fereastră, v1.6.14, a fost o pură reconstrucție — nicio poveste acolo, și e cinstit s-o spunem.)

Trei lucruri pe care ni le-a învățat UI-ul în flux

  1. Buferizează câmpurile în flux, publică o dată per bucată. Să lași fiecare server-sent event să conducă propria actualizare de stare e pâlpâirea. Un acumulator ref plus o singură publicare per bucată, într-un arbore care nu se demontează niciodată, transformă asamblarea nervoasă într-o completare calmă.
  2. Orice curge în flux are nevoie de un renderer conștient de flux. Un renderer care re-parsează întregul șir per token va pâlpâi pe markdown pe jumătate terminat. Folosește unul construit pentru intrare incompletă — și dacă ai deja unul pentru altă suprafață, reutilizează-l înainte să construiești un al doilea.
  3. Copierea îmbogățită ține de formatele de clipboard, nu de serializare. Scrie text/html cu imagini încorporate și o variantă text/plain de rezervă. Clipboard-ul a fost proiectat să le poarte pe amândouă; folosește-l.

Acesta e al cincilea capitol al poveștii de fiabilitate și finisaj care devine GeekBye v2. Pentru capitolul anterior, vezi bug-ul căștilor (v1.6.12); pentru locul unde aceeași idee de server-sent-events a devenit mai târziu un întreg transport de rezervă, transcriere live când firewallul blochează WebSockets (v2.0.8); iar pentru întregul arc, anatomia livrării software-ului la perfecțiune.

Articole Similare

O conexiune pierdută n-ar trebui să-ți prăbușească toată aplicația — dar pe a noastră o făcea
Steven
Steven7 min citire

O conexiune pierdută n-ar trebui să-ți prăbușească toată aplicația — dar pe a noastră o făcea

Când backend-ul nostru a picat în plină ședință, nu doar că a oprit transcrierea — a prăbușit întreaga aplicație. Cauza era un singur eveniment netratat, iar reparația a fost de zece linii. Acesta e grupul de lansări care a făcut GeekBye să rămână autentificat și conectat prin lucrurile care înainte îl omorau.

Inginerie
Fiabilitate
Electron
Anatomia livrării unui software la perfecțiune: cum a prins code review-ul ceea ce testele nu au putut
Steven
Steven7 min citire

Anatomia livrării unui software la perfecțiune: cum a prins code review-ul ceea ce testele nu au putut

De-a lungul seriei GeekBye v2, același lucru se tot întâmplă: o remediere trece toate testele pe mașina dezvoltatorului, iar apoi code review-ul dovedește că ar fi eșuat pentru aproape toată lumea. Acesta este fluxul de lucru din spatele a nouă lansări — poarta de review, prinderea problemelor înainte de fix și disciplina testării-înainte-de-livrare care transformă „merge la mine” în „merge”.

Inginerie
Proces
Code Review
Aplicația ta de Mac primește acces la microfon — apoi îl uită la fiecare pornire
Steven
Steven6 min citire

Aplicația ta de Mac primește acces la microfon — apoi îl uită la fiecare pornire

GeekBye a cerut permisiune pentru microfon, ai acordat-o și a funcționat. La următoarea pornire: dispărută. Iar aplicația nu a apărut deloc în Setări de sistem → Microfon. Vinovatul era o funcție de securitate macOS care rula pe tăcute aplicația dintr-o cale care dispare — iată diagnosticul și fix-ul dintr-un singur prompt.

macOS
Permisiuni
Inginerie