
Sådan streamer du en live-rapport uden flimmer
Når dit møde slutter, fylder GeekBye's opsummering nu ud i realtid i stedet for at få dig til at stirre på en spinner. At få streaming-UI til at føles roligt frem for hakkende krævede, at vi løste flimmeret to gange — én gang for strukturerede felter, én gang for markdown — og den anden løsning var en renderer, vi allerede ejede.
Før denne release betød det at afslutte et GeekBye-møde, at man kiggede på en spinner. Appen ville sende hele dit transkript afsted til analyse, vente på hele opsummeringen — score, nøglepunkter, handlingspunkter, det hele — og så smide det på skærmen på én gang. Funktionelt, men det føltes langsomt, fordi du stirrede på ingenting, mens den arbejdede.
GeekBye v1.6.13–v1.6.15 lavede det om til en rapport, der streamer ind i realtid, felt for felt, efterhånden som den genereres. Den interessante del er ikke streamingen — det er alt det, vi var nødt til at gøre for at holde streamingen fra at se hakkende ud. Rolig streaming og hakkende streaming er den samme funktion med meget forskellig udførelse.
Flimmer, del et: lad ikke hver chunk overbelaste React
Opsummeringen er ikke én klump — det er strukturerede felter (en samlet score, nøglepunkter, handlingspunkter og så videre), som backenden udsender ét ad gangen over server-sent events. Den naive måde at rendere det på er at opdatere React-state ved hver event, efterhånden som den ankommer.
Gør det, og du får flimmer. Hvert felt, der ankommer, udløser sin egen genrendering; komponenter, der viste "tomt", monteres, unmountes derefter og genmonteres så som "indlæst"; layoutet ryster ved hver pakke. Rapporten samler sig selv foran dig på den værst tænkelige måde — synligt, nervøst.
Løsningen er en todelt disciplin. Først, akkumulér de indkommende felter i en ref — en muterbar holder, der ikke udløser renderinger — og publicér til React-state én gang per chunk med en frisk kopi, så træet opdaterer bevidst frem for ved hver mikro-event. For det andet, render ét stabilt komponenttræ hele tiden, uanset om der streames eller er færdigt, med inline-pladsholdere for felter, der endnu ikke er ankommet:
// 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>
Komponenten, der viser scoren, unmountes aldrig — den viser bare …, indtil tallet lander, og derefter tallet. Intet ryster. Og det fuldt samlede objekt bliver stadig cachet til den lokale database til sidst, så streaming-UI og holdbar lagring sameksisterer uden at slås.
Flimmer, del to: rendereren vi allerede ejede
Det andet flimmer er mere subtilt og lever i selve markdownen. Rapporten renderer rig markdown — overskrifter, fed skrift, lister, kodeblokke. Men markdown, der ankommer midt i strømmen, er i ethvert givet øjeblik halvfærdig. En liste med indtil videre kun én bullet. En code fence, der er blevet åbnet, men ikke lukket. En fed-skrift-markør uden partner endnu.
En konventionel markdown-renderer genparser hele strengen ved hver token, og den halvfærdige tilstand parser til noget forskelligt hver gang — så listen flimrer, kodeblokken blinker åben og lukket, layoutet hopper, efterhånden som tokens fuldføres. Det er den samme nervøse samling som før, ét lag længere nede.
Løsningen var næsten pinligt tilgængelig: vi brugte allerede en streaming-bevidst markdown-renderer til live-AI-chatten. En renderer bygget til at tolerere ufuldstændige tokens — til at rendere halvfærdig markdown stabilt og først få den til at falde på plads, efterhånden som tokens fuldføres — i stedet for at genparse fra bunden hver gang. Rapporten skulle bare bruge den samme. Vi havde løst præcis dette problem for chatten måneder tidligere; "løsningen" for rapporten var at erkende, at vi allerede havde værktøjet og pege det mod en anden overflade. Genbrug slog genopbygning.
Bonussen: rich copy er et udklipsholder-format-problem
Da rapporten så godt ud, ville folk indsætte den — i et dokument, en e-mail — og bevare formateringen og skærmbillederne. Instinktet er at serialisere rapporten til en markdown-streng og håbe, at destinationen renderer den. Det gør den som regel ikke.
Det rigtige svar er, at udklipsholderen rummer flere repræsentationer på én gang. Så vi skriver to: en HTML-version med formateringen intakt og skærmbillederne indlejret som inline-billeder, og en fallback i ren tekst til destinationer, der ikke tager imod HTML. Indsæt i en rig editor, og du får den formaterede rapport med billeder; indsæt i en ren tekstboks, og du får ren tekst. "Rich copy" var aldrig et serialiseringsproblem — det var et vælg-det-rigtige-udklipsholder-format-problem.
Den samme release gav også rapporten chat-agtige Me / Them-labels med de to talere justeret mod modsatte sider, så et transkript læses som den samtale, det var, og flyttede dine egne skærmbilleder over på deres egen side for at matche. (Én release i dette vindue, v1.6.14, var en ren genopbygning — ingen historie der, og det er ærligt at sige det.)
Tre ting streaming-UI lærte os
- Buffer streamede felter, publicér én gang per chunk. At lade hver server-sent event drive sin egen state-opdatering er flimmeret. En ref-akkumulator plus én enkelt publicering per chunk, ind i et træ, der aldrig unmountes, forvandler nervøs samling til rolig udfyldning.
- Alt, der streamer, har brug for en streaming-bevidst renderer. En renderer, der genparser hele strengen per token, vil flimre på halvfærdig markdown. Brug en bygget til ufuldstændigt input — og hvis du allerede har en til en anden overflade, så genbrug den, før du bygger en nummer to.
- Rich copy handler om udklipsholder-formater, ikke serialisering. Skriv
text/htmlmed indlejrede billeder og entext/plain-fallback. Udklipsholderen blev designet til at bære begge dele; brug det.
Dette er femte kapitel i historien om pålidelighed og finish, der bliver til GeekBye v2. For det forrige kapitel, se earbuds-buggen (v1.6.12); for hvor den samme server-sent-events-idé senere blev en hel fallback-transport, live-transkribering når firewallen blokerer WebSockets (v2.0.8); og for hele buen, anatomien af at skeppe software til perfektion.