Steven
Steven5 мин четене

Как да предавате отчет на живо без трептене

Когато срещата ви приключи, обобщението на GeekBye вече се попълва на живо, вместо да ви кара да гледате въртящ се индикатор. За да изглежда стриймингът в интерфейса спокоен, а не накъсан, се наложи да решим трептенето два пъти — веднъж за структурираните полета, веднъж за markdown — и втората поправка беше рендер, който вече притежавахме.

Инженеринг
Фронтенд
Стрийминг
Издания на GeekBye
Как да предавате отчет на живо без трептене

Преди това издание приключването на среща в 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, беше чист повторен билд — няма история там, и е честно да го кажем.)

Три неща, на които ни научи стрийминг интерфейсът

  1. Буферирайте постъпващите полета, публикувайте веднъж на порция. Да оставите всяко server-sent event да задвижва собствено обновяване на състоянието е трептенето. Ref акумулатор плюс единствено публикуване на порция, в дърво, което никога не се демонтира, превръща нервното сглобяване в спокойно попълване.
  2. Всичко, което се стриймва, се нуждае от рендер, съобразен със стрийминга. Рендер, който преразбира целия низ при всеки токен, ще трепти върху недовършен markdown. Използвайте изграден за непълен вход — и ако вече имате такъв за друга повърхност, използвайте го повторно, преди да строите втори.
  3. Форматираното копиране е за формати на клипборда, не за сериализация. Записвайте text/html с вградени изображения и резервен вариант text/plain. Клипбордът беше проектиран да носи и двата; използвайте го.

Това е петата глава от историята за надеждност и излъскване, която става GeekBye v2. За предишната глава вижте бъга с слушалките (v1.6.12); за мястото, където същата идея със server-sent-events по-късно стана цял резервен транспорт, транскрипция на живо, когато защитната стена блокира WebSockets (v2.0.8); а за цялата дъга, анатомията на доставянето на софтуер до съвършенство.

Свързани статии

Анатомията на изпращането на софтуер до съвършенство: как прегледът на кода улови това, което тестовете не можаха
Steven
Steven6 мин четене

Анатомията на изпращането на софтуер до съвършенство: как прегледът на кода улови това, което тестовете не можаха

В цялата поредица за GeekBye v2 се случва едно и също нещо: дадена поправка минава всеки тест на машината на разработчика, а после прегледът на кода доказва, че тя би се провалила за почти всеки друг. Това е работният процес зад девет издания — портата на прегледа, ранните улавяния при поправките и дисциплината „тествай преди да изпратиш“, която превръща „при мен работи“ в „работи“.

Инженеринг
Процес
Преглед на кода
Вашето Mac приложение получава достъп до микрофона — после го забравя при всяко стартиране
Steven
Steven5 мин четене

Вашето Mac приложение получава достъп до микрофона — после го забравя при всяко стартиране

GeekBye поиска разрешение за микрофона, вие го дадохте и всичко работеше. При следващото стартиране: изчезнало. А приложението изобщо не се появи в Системни настройки → Микрофон. Виновникът беше функция за сигурност на macOS, която тихомълком пускаше приложението от път, който изчезва — ето диагнозата и корекцията с един-единствен подкана.

macOS
Разрешения
Инженеринг
Изтрихме 5000 реда аудио код — и транскриптите започнаха да се появяват по два пъти
Steven
Steven6 мин четене

Изтрихме 5000 реда аудио код — и транскриптите започнаха да се появяват по два пъти

GeekBye v1.6 изкорени два транскрибера на Swift, работещи на самото устройство, и ги замени с един унифициран пайплайн — нетно изтриване на над 5000 реда, направено докато хора бяха насред срещи в приложението. После транскриптите започнаха да се появяват по два пъти, защото грешката се премести в единствения слой, който още смяташе, че двигателите са два.

Инженеринг
Транскрипция
Архитектура