
Как да предавате отчет на живо без трептене
Когато срещата ви приключи, обобщението на GeekBye вече се попълва на живо, вместо да ви кара да гледате въртящ се индикатор. За да изглежда стриймингът в интерфейса спокоен, а не накъсан, се наложи да решим трептенето два пъти — веднъж за структурираните полета, веднъж за markdown — и втората поправка беше рендер, който вече притежавахме.
Преди това издание приключването на среща в GeekBye означаваше да гледате въртящ се индикатор. Приложението изпращаше цялата ви транскрипция за анализ, изчакваше цялото обобщение — оценка, ключови точки, задачи за действие, всичко — и после го стоварваше на екрана наведнъж. Работеше, но усещането беше за бавнина, защото гледахте в нищото, докато то работеше.
GeekBye v1.6.13–v1.6.15 превърна това в отчет, който се попълва на живо, поле по поле, докато се генерира. Интересната част не е стриймингът — а всичко, което трябваше да направим, за да не изглежда стриймингът накъсан. Спокойният стрийминг и накъсаният стрийминг са една и съща функция с много различно изпълнение.
Трептене, част първа: не позволявайте всяка порция да разтърсва React
Обобщението не е един блок — това са структурирани полета (обща оценка, ключови точки, задачи за действие и т.н.), които бекендът излъчва едно по едно чрез server-sent events. Наивният начин да рендирате това е да обновявате състоянието на React при всяко пристигащо събитие.
Направете го и получавате трептене. Всяко пристигащо поле задейства собствено повторно рендиране; компонентите, които показваха „празно", се монтират, после се демонтират, после се монтират наново като „заредени"; оформлението подскача при всеки пакет. Отчетът се сглобява сам пред очите ви по възможно най-лошия начин — видимо, нервно.
Поправката е дисциплина в две части. Първо, натрупвайте постъпващите полета в ref — променлив контейнер, който не задейства рендирания — и публикувайте в състоянието на React веднъж на порция със свежо копие, така че дървото да се обновява целенасочено, а не при всяко микрособитие. Второ, рендирайте едно стабилно дърво от компоненти през цялото време, независимо дали е в стрийминг или готово, с inline заместители за полетата, които още не са пристигнали:
// 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>
Компонентът, който показва оценката, никога не се демонтира — просто показва …, докато числото пристигне, после числото. Нищо не подскача. А напълно сглобеният обект все така се кешира в локалната база данни накрая, така че стрийминг интерфейсът и трайното съхранение съжителстват, без да се борят.
Трептене, част втора: рендерът, който вече притежавахме
Второто трептене е по-фино и живее в самия markdown. Отчетът рендира форматиран markdown — заглавия, получер текст, списъци, блокове с код. Но markdown, който пристига в средата на стрийминга, е във всеки даден момент недовършен. Списък с една точка засега. Блок с код, който е отворен, но незатворен. Маркер за получер текст, който още няма партньор.
Обикновен markdown рендер преразбира целия низ при всеки токен, а това недовършено състояние се разбира на нещо различно всеки път — така списъкът трепти, блокът с код проблясва отворен и затворен, оформлението подскача, докато токените се завършват. Това е същото нервно сглобяване като преди, един слой по-надолу.
Поправката беше почти смущаващо достъпна: вече използвахме рендер за markdown, съобразен със стрийминга, за живия AI чат. Рендер, изграден да толерира непълни токени — да рендира недовършен markdown стабилно и да го фиксира едва когато токените се завършат — вместо да преразбира от нулата всеки път. Отчетът просто трябваше да използва същия. Бяхме решили точно този проблем за чата месеци по-рано; „поправката" за отчета беше да осъзнаем, че вече имаме инструмента, и да го насочим към втора повърхност. Повторното използване победи изграждането наново.
Бонусът: форматираното копиране е проблем на формата на клипборда
След като отчетът вече изглеждаше добре, хората поискаха да го поставят — в документ, в имейл — и да запазят форматирането и екранните снимки. Инстинктът е да сериализирате отчета в markdown низ и да се надявате, че местоназначението ще го рендира. Обикновено не го прави.
Истинският отговор е, че клипбордът държи няколко представяния наведнъж. Затова записваме две: HTML версия с непокътнато форматиране и екранните снимки, вградени като inline изображения, и резервен вариант в чист текст за местоназначения, които не приемат HTML. Поставете в форматиращ редактор и получавате форматирания отчет със снимки; поставете в поле за чист текст и получавате чист текст. „Форматираното копиране" никога не е било проблем на сериализацията — беше проблем на избора-на-правилния-формат-на-клипборда.
Същото издание даде на отчета и етикети в стил чат Аз / Те с двамата говорители, подравнени в противоположни страни, така че транскрипцията да се чете като разговора, който е била, и премести собствените ви екранни снимки от тяхната страна, за да съвпадат. (Едно издание в този период, v1.6.14, беше чист повторен билд — няма история там, и е честно да го кажем.)
Три неща, на които ни научи стрийминг интерфейсът
- Буферирайте постъпващите полета, публикувайте веднъж на порция. Да оставите всяко server-sent event да задвижва собствено обновяване на състоянието е трептенето. Ref акумулатор плюс единствено публикуване на порция, в дърво, което никога не се демонтира, превръща нервното сглобяване в спокойно попълване.
- Всичко, което се стриймва, се нуждае от рендер, съобразен със стрийминга. Рендер, който преразбира целия низ при всеки токен, ще трепти върху недовършен markdown. Използвайте изграден за непълен вход — и ако вече имате такъв за друга повърхност, използвайте го повторно, преди да строите втори.
- Форматираното копиране е за формати на клипборда, не за сериализация. Записвайте
text/htmlс вградени изображения и резервен вариантtext/plain. Клипбордът беше проектиран да носи и двата; използвайте го.
Това е петата глава от историята за надеждност и излъскване, която става GeekBye v2. За предишната глава вижте бъга с слушалките (v1.6.12); за мястото, където същата идея със server-sent-events по-късно стана цял резервен транспорт, транскрипция на живо, когато защитната стена блокира WebSockets (v2.0.8); а за цялата дъга, анатомията на доставянето на софтуер до съвършенство.