Steven
Steven7 phút đọc

Cách phát trực tiếp một báo cáo mà không bị nhấp nháy

Khi cuộc họp của bạn kết thúc, bản tóm tắt của GeekBye giờ đây tự điền vào theo thời gian thực thay vì bắt bạn ngồi nhìn một vòng xoay tải. Để giao diện phát trực tiếp trở nên êm ả thay vì giật cục, chúng tôi phải giải quyết nhấp nháy hai lần — một lần cho các trường có cấu trúc, một lần cho markdown — và cách sửa lần thứ hai là một bộ render mà chúng tôi đã sở hữu sẵn.

Kỹ thuật
Frontend
Phát trực tiếp
Bản phát hành GeekBye
Cách phát trực tiếp một báo cáo mà không bị nhấp nháy

Trước bản phát hành này, kết thúc một cuộc họp GeekBye nghĩa là ngồi nhìn một vòng xoay tải. Ứng dụng sẽ gửi toàn bộ bản ghi lời nói của bạn đi phân tích, chờ toàn bộ bản tóm tắt — điểm số, điểm chính, các mục hành động, tất tần tật — rồi đổ hết lên màn hình một lượt. Có chạy được, nhưng cảm giác chậm, bởi bạn chỉ ngồi nhìn khoảng trống trong lúc nó làm việc.

GeekBye v1.6.13–v1.6.15 đã biến điều đó thành một báo cáo chảy vào trực tiếp, theo từng trường, ngay khi nó đang được tạo ra. Phần thú vị không phải là bản thân việc streaming — mà là tất cả những gì chúng tôi phải làm để việc streaming không trông giật cục. Streaming êm ả và streaming lục cục là cùng một tính năng, chỉ khác nhau ở cách thực thi mà thôi.

Nhấp nháy, phần một: đừng để mỗi chunk giằng xé React

Bản tóm tắt không phải một khối duy nhất — nó là các trường có cấu trúc (điểm tổng thể, điểm chính, mục hành động, v.v.) mà backend phát ra từng cái một qua server-sent events. Cách render ngây thơ là cập nhật React state mỗi khi một sự kiện đến.

Làm vậy, bạn sẽ bị nhấp nháy. Mỗi trường đến đều kích hoạt một lần re-render riêng của nó; các component đang hiển thị "rỗng" được mount, rồi unmount, rồi remount thành "đã tải"; bố cục giật lên theo từng gói tin. Báo cáo tự lắp ráp trước mắt bạn theo cách tệ nhất có thể — thấy rõ, và bồn chồn.

Cách sửa là một kỷ luật gồm hai phần. Thứ nhất, tích lũy các trường đến trong một ref — một vật chứa có thể thay đổi mà không kích hoạt render — và công bố sang React state một lần mỗi chunk với một bản sao mới, để cây được cập nhật một cách có chủ đích thay vì theo từng vi-sự-kiện. Thứ hai, render một cây component ổn định duy nhất suốt cả quá trình, dù đang streaming hay đã xong, với các placeholder inline cho những trường chưa đến:

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

Component hiển thị điểm số không bao giờ bị unmount — nó chỉ hiển thị cho đến khi con số đáp xuống, rồi hiển thị con số. Không có gì giật cả. Và đối tượng đã lắp ráp hoàn chỉnh vẫn được cache vào cơ sở dữ liệu cục bộ ở phần cuối, nên giao diện streaming và lưu trữ bền vững cùng tồn tại mà không giành nhau.

Nhấp nháy, phần hai: bộ render chúng tôi đã sở hữu sẵn

Loại nhấp nháy thứ hai tinh vi hơn và nằm trong chính markdown. Báo cáo render markdown phong phú — tiêu đề, chữ đậm, danh sách, khối code. Nhưng markdown đến giữa chừng luồng thì, tại bất kỳ khoảnh khắc nào, đều còn dang dở. Một danh sách mới có một gạch đầu dòng cho tới lúc này. Một code fence đã mở nhưng chưa đóng. Một dấu chữ đậm chưa có bạn đồng hành.

Một bộ render markdown thông thường phân tích lại toàn bộ chuỗi trên mỗi token, và trạng thái dang dở đó lần nào cũng phân tích ra thành thứ khác nhau — nên danh sách nhấp nháy, khối code chớp mở rồi đóng, bố cục nhảy lên khi các token hoàn tất. Vẫn là kiểu lắp ráp bồn chồn như trước, thấp xuống một tầng.

Cách sửa gần như hiển nhiên đến mức ngượng: chúng tôi vốn đã dùng một bộ render markdown nhận biết streaming cho chat AI trực tiếp. Một bộ render được xây để chịu được các token chưa hoàn chỉnh — render markdown còn dang dở một cách ổn định và chỉ để nó an vị khi các token hoàn tất — thay vì phân tích lại từ đầu mỗi lần. Báo cáo chỉ cần dùng đúng cái đó. Chúng tôi đã giải quyết chính bài toán này cho chat từ mấy tháng trước; "cách sửa" cho báo cáo là nhận ra chúng tôi đã có sẵn công cụ và chĩa nó vào một bề mặt thứ hai. Tái sử dụng thắng xây lại.

Phần thưởng: sao chép có định dạng là một bài toán về định dạng clipboard

Một khi báo cáo trông đẹp, người ta muốn dán nó — vào một tài liệu, một email — và giữ nguyên định dạng lẫn ảnh chụp màn hình. Bản năng là tuần tự hóa báo cáo thành một chuỗi markdown rồi hy vọng nơi đến sẽ render nó ra. Thường thì nó không.

Câu trả lời thực sự là clipboard giữ nhiều biểu diễn cùng một lúc. Nên chúng tôi ghi hai bản: một bản HTML với định dạng còn nguyên và ảnh chụp màn hình được nhúng thành ảnh inline, và một bản văn bản thuần dự phòng cho những nơi đến không nhận HTML. Dán vào một trình soạn thảo có định dạng và bạn nhận được báo cáo có định dạng kèm hình ảnh; dán vào một ô văn bản thuần và bạn nhận được văn bản sạch sẽ. "Sao chép có định dạng" chưa bao giờ là một bài toán tuần tự hóa — nó là một bài toán chọn đúng định dạng clipboard.

Cùng bản phát hành đó cũng cho báo cáo các nhãn kiểu chat Me / Them với hai người nói được căn về hai phía đối diện, để một bản ghi lời nói đọc lên như đúng cuộc trò chuyện mà nó vốn là, và dời ảnh chụp màn hình của chính bạn sang phía của nó cho khớp. (Một bản phát hành trong quãng này, v1.6.14, là một bản rebuild thuần túy — không có câu chuyện nào ở đó, và nói thẳng ra như vậy là trung thực.)

Ba điều mà giao diện streaming đã dạy chúng tôi

  1. Đệm các trường được stream, công bố một lần mỗi chunk. Để mỗi server-sent event tự lái một lần cập nhật state riêng chính là nhấp nháy. Một bộ tích lũy ref cộng với một lần công bố duy nhất mỗi chunk, vào một cây không bao giờ unmount, biến sự lắp ráp bồn chồn thành sự điền vào êm ả.
  2. Bất cứ thứ gì stream đều cần một bộ render nhận biết streaming. Một bộ render phân tích lại toàn bộ chuỗi trên mỗi token sẽ nhấp nháy với markdown còn dang dở. Hãy dùng một bộ được xây cho đầu vào chưa hoàn chỉnh — và nếu bạn đã có một cái cho một bề mặt khác, hãy tái sử dụng nó trước khi xây cái thứ hai.
  3. Sao chép có định dạng là về định dạng clipboard, không phải tuần tự hóa. Hãy ghi text/html có nhúng ảnh một bản dự phòng text/plain. Clipboard được thiết kế để mang cả hai; hãy dùng nó.

Đây là chương thứ năm của câu chuyện về độ tin cậy và sự trau chuốt sẽ trở thành GeekBye v2. Chương trước, xem lỗi tai nghe (v1.6.12); còn nơi chính ý tưởng server-sent-events về sau trở thành cả một kênh truyền dự phòng, xem phiên âm trực tiếp khi tường lửa chặn WebSocket (v2.0.8); và cho toàn bộ vòng cung, xem giải phẫu việc xuất xưởng phần mềm đến độ hoàn hảo.

Bài Viết Liên Quan

Một cú rớt kết nối không nên làm sập cả ứng dụng — nhưng của chúng tôi thì có
Steven
Steven8 phút đọc

Một cú rớt kết nối không nên làm sập cả ứng dụng — nhưng của chúng tôi thì có

Khi backend của chúng tôi rớt mạng giữa cuộc họp, nó không chỉ tạm dừng việc chép lời — nó làm sập toàn bộ ứng dụng. Nguyên nhân là một sự kiện đơn lẻ không được xử lý, và bản vá chỉ mười dòng. Đây là cụm bản phát hành đã khiến GeekBye giữ được đăng nhập và giữ được kết nối xuyên qua những thứ từng giết chết nó.

Kỹ thuật
Độ tin cậy
Electron
Giải phẫu quá trình đưa phần mềm đến độ hoàn hảo: Cách đánh giá mã bắt được lỗi mà kiểm thử bỏ sót
Steven
Steven9 phút đọc

Giải phẫu quá trình đưa phần mềm đến độ hoàn hảo: Cách đánh giá mã bắt được lỗi mà kiểm thử bỏ sót

Xuyên suốt loạt bài GeekBye v2, cùng một điều cứ lặp đi lặp lại: một bản sửa lỗi vượt qua mọi bài kiểm thử trên máy của lập trình viên, rồi khâu đánh giá mã chứng minh rằng nó sẽ thất bại với gần như tất cả mọi người. Đây là quy trình đằng sau chín bản phát hành — cửa kiểm soát đánh giá, những phát hiện bắt lỗi ngay từ đầu, và kỷ luật kiểm thử trước khi phát hành đã biến "nó chạy được trên máy tôi" thành "nó chạy được".

Kỹ thuật
Quy trình
Đánh giá mã
Cơ chế kiểm tra an toàn khiến ứng dụng của chúng tôi không thể tắt nổi
Steven
Steven8 phút đọc

Cơ chế kiểm tra an toàn khiến ứng dụng của chúng tôi không thể tắt nổi

Auto-update là tính năng khó ship nhất mà chúng tôi từng làm — sáu bản phát hành trong bốn ngày chỉ để nó ngừng làm hỏng ứng dụng. Con bug tệ nhất lại chính là thứ chúng tôi tự tạo ra khi cố gắng cẩn thận: một bước kiểm tra "an toàn" dài 500 mili-giây biến một bản cập nhật thất bại thành một tiến trình mà bạn thực sự không thể tắt được.

Kỹ thuật
Electron
Độ tin cậy