
Πώς να μεταδίδεις μια αναφορά σε ζωντανή ροή χωρίς τρεμόπαιγμα
Όταν τελειώνει η συνάντησή σου, η σύνοψη του GeekBye συμπληρώνεται πλέον ζωντανά, αντί να σε αφήνει να κοιτάς έναν δείκτη φόρτωσης. Για να μοιάζει το UI ροής ήρεμο αντί για σπασμωδικό, χρειάστηκε να λύσουμε το τρεμόπαιγμα δύο φορές — μία για τα δομημένα πεδία, μία για το markdown — και η δεύτερη διόρθωση ήταν ένας renderer που ήδη είχαμε.
Πριν από αυτή την έκδοση, το τέλος μιας συνάντησης στο GeekBye σήμαινε να κοιτάς έναν δείκτη φόρτωσης. Η εφαρμογή έστελνε ολόκληρη την απομαγνητοφώνησή σου για ανάλυση, περίμενε ολόκληρη τη σύνοψη — βαθμολογία, βασικά σημεία, ενέργειες προς εκτέλεση, τα πάντα — και μετά τα έριχνε στην οθόνη όλα μαζί. Λειτουργικό, αλλά έμοιαζε αργό, γιατί κοιτούσες το τίποτα όσο δούλευε.
Το GeekBye v1.6.13–v1.6.15 το μετέτρεψε σε μια αναφορά που εισρέει ζωντανά, πεδίο προς πεδίο, καθώς παράγεται. Το ενδιαφέρον μέρος δεν είναι η ροή — είναι όλα όσα χρειάστηκε να κάνουμε ώστε η ροή να μη φαίνεται σπασμωδική. Η ήρεμη ροή και η τραχιά ροή είναι η ίδια λειτουργία με πολύ διαφορετική εκτέλεση.
Τρεμόπαιγμα, μέρος πρώτο: μην αφήνεις κάθε κομμάτι να ταρακουνά το React
Η σύνοψη δεν είναι ένα ενιαίο μπλοκ — είναι δομημένα πεδία (μια συνολική βαθμολογία, βασικά σημεία, ενέργειες προς εκτέλεση και ούτω καθεξής) που το backend εκπέμπει ένα προς ένα μέσω server-sent events. Ο αφελής τρόπος να τα αποδώσεις είναι να ενημερώνεις την κατάσταση του React σε κάθε συμβάν που φτάνει.
Κάνε το αυτό και παίρνεις τρεμόπαιγμα. Κάθε πεδίο που φτάνει πυροδοτεί τη δική του επανασχεδίαση· τα στοιχεία που έδειχναν «κενό» τοποθετούνται, μετά αποσυναρμολογούνται, μετά επανατοποθετούνται ως «φορτωμένα»· η διάταξη τινάζεται σε κάθε πακέτο. Η αναφορά συναρμολογείται μόνη της μπροστά σου με τον χειρότερο δυνατό τρόπο — ορατά, νευρικά.
Η διόρθωση είναι μια πειθαρχία δύο μερών. Πρώτον, συσσώρευσε τα εισερχόμενα πεδία σε ένα ref — έναν μεταβλητό υποδοχέα που δεν πυροδοτεί σχεδιάσεις — και δημοσίευσε στην κατάσταση του React μία φορά ανά κομμάτι με ένα φρέσκο αντίγραφο, ώστε το δέντρο να ενημερώνεται σκόπιμα και όχι σε κάθε μικρο-συμβάν. Δεύτερον, απόδωσε ένα σταθερό δέντρο στοιχείων όλη την ώρα, είτε βρίσκεται σε ροή είτε έχει ολοκληρωθεί, με inline placeholders για τα πεδία που δεν έχουν φτάσει ακόμη:
// 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>
Το στοιχείο που δείχνει τη βαθμολογία δεν αποσυναρμολογείται ποτέ — απλώς δείχνει … μέχρι να προσγειωθεί ο αριθμός, μετά τον αριθμό. Τίποτα δεν τινάζεται. Και το πλήρως συναρμολογημένο αντικείμενο εξακολουθεί να αποθηκεύεται στην τοπική βάση δεδομένων στο τέλος, ώστε το UI ροής και η μόνιμη αποθήκευση να συνυπάρχουν χωρίς να μάχονται.
Τρεμόπαιγμα, μέρος δεύτερο: ο renderer που ήδη είχαμε
Το δεύτερο τρεμόπαιγμα είναι πιο λεπτό και ζει μέσα στο ίδιο το markdown. Η αναφορά αποδίδει εμπλουτισμένο markdown — επικεφαλίδες, έντονη γραφή, λίστες, μπλοκ κώδικα. Αλλά το markdown που φτάνει στη μέση της ροής είναι, σε κάθε δεδομένη στιγμή, μισοτελειωμένο. Μια λίστα με μία κουκκίδα μέχρι στιγμής. Ένα μπλοκ κώδικα που άνοιξε αλλά δεν έκλεισε. Ένας δείκτης έντονης γραφής χωρίς σύντροφο ακόμα.
Ένας συμβατικός renderer markdown επαναναλύει ολόκληρη τη συμβολοσειρά σε κάθε token, και αυτή η μισοτελειωμένη κατάσταση αναλύεται σε κάτι διαφορετικό κάθε φορά — έτσι η λίστα τρεμοπαίζει, το μπλοκ κώδικα ανοιγοκλείνει, η διάταξη πηδά καθώς τα tokens ολοκληρώνονται. Είναι η ίδια νευρική συναρμολόγηση όπως πριν, ένα επίπεδο πιο κάτω.
Η διόρθωση ήταν σχεδόν ενοχλητικά διαθέσιμη: χρησιμοποιούσαμε ήδη έναν renderer markdown που αντιλαμβάνεται τη ροή για τη ζωντανή συνομιλία AI. Έναν renderer φτιαγμένο να ανέχεται ημιτελή tokens — να αποδίδει μισοτελειωμένο markdown σταθερά και να το καθηλώνει μόνο όταν τα tokens ολοκληρώνονται — αντί να επαναναλύει από την αρχή κάθε φορά. Η αναφορά απλώς χρειαζόταν να χρησιμοποιήσει τον ίδιο. Είχαμε λύσει ακριβώς αυτό το πρόβλημα για τη συνομιλία μήνες νωρίτερα· η «διόρθωση» για την αναφορά ήταν να αναγνωρίσουμε ότι είχαμε ήδη το εργαλείο και να το στρέψουμε σε μια δεύτερη επιφάνεια. Η επαναχρησιμοποίηση νίκησε την ανακατασκευή.
Το μπόνους: η εμπλουτισμένη αντιγραφή είναι πρόβλημα μορφής προχείρου
Μόλις η αναφορά φαινόταν καλή, ο κόσμος ήθελε να την επικολλήσει — σε ένα έγγραφο, ένα email — και να κρατήσει τη μορφοποίηση και τα στιγμιότυπα οθόνης. Το ένστικτο είναι να σειριοποιήσεις την αναφορά σε μια συμβολοσειρά markdown και να ελπίζεις ότι ο προορισμός θα την αποδώσει. Συνήθως δεν το κάνει.
Η πραγματική απάντηση είναι ότι το πρόχειρο κρατά πολλαπλές αναπαραστάσεις ταυτόχρονα. Έτσι γράφουμε δύο: μια έκδοση HTML με τη μορφοποίηση άθικτη και τα στιγμιότυπα οθόνης ενσωματωμένα ως inline εικόνες, και μια εναλλακτική απλού κειμένου για προορισμούς που δεν δέχονται HTML. Επικόλλησε σε έναν εμπλουτισμένο επεξεργαστή και παίρνεις τη μορφοποιημένη αναφορά με εικόνες· επικόλλησε σε ένα πλαίσιο απλού κειμένου και παίρνεις καθαρό κείμενο. Η «εμπλουτισμένη αντιγραφή» δεν ήταν ποτέ πρόβλημα σειριοποίησης — ήταν πρόβλημα επιλογής-της-σωστής-μορφής-προχείρου.
Η ίδια έκδοση έδωσε επίσης στην αναφορά ετικέτες τύπου συνομιλίας Εγώ / Αυτοί με τους δύο ομιλητές ευθυγραμμισμένους σε αντίθετες πλευρές, ώστε μια απομαγνητοφώνηση να διαβάζεται σαν τη συνομιλία που ήταν, και μετακίνησε τα δικά σου στιγμιότυπα οθόνης στη δική τους πλευρά για να ταιριάζουν. (Μία έκδοση σε αυτό το διάστημα, η v1.6.14, ήταν καθαρή ανακατασκευή — καμία ιστορία εκεί, και είναι τίμιο να το πούμε.)
Τρία πράγματα που μας δίδαξε το UI ροής
- Αποθήκευσε προσωρινά τα εισερχόμενα πεδία, δημοσίευσε μία φορά ανά κομμάτι. Το να αφήνεις κάθε server-sent event να οδηγεί τη δική του ενημέρωση κατάστασης είναι το τρεμόπαιγμα. Ένας συσσωρευτής ref συν μία μοναδική δημοσίευση ανά κομμάτι, σε ένα δέντρο που δεν αποσυναρμολογείται ποτέ, μετατρέπει τη νευρική συναρμολόγηση σε ήρεμη συμπλήρωση.
- Οτιδήποτε ρέει χρειάζεται έναν renderer που αντιλαμβάνεται τη ροή. Ένας renderer που επαναναλύει ολόκληρη τη συμβολοσειρά ανά token θα τρεμοπαίζει σε μισοτελειωμένο markdown. Χρησιμοποίησε έναν φτιαγμένο για ημιτελή είσοδο — και αν έχεις ήδη έναν για μια άλλη επιφάνεια, επαναχρησιμοποίησέ τον προτού φτιάξεις δεύτερο.
- Η εμπλουτισμένη αντιγραφή αφορά μορφές προχείρου, όχι σειριοποίηση. Γράψε
text/htmlμε ενσωματωμένες εικόνες και μια εναλλακτικήtext/plain. Το πρόχειρο σχεδιάστηκε να κουβαλά και τα δύο· χρησιμοποίησέ το.
Αυτό είναι το πέμπτο κεφάλαιο της ιστορίας αξιοπιστίας και φινιρίσματος που γίνεται GeekBye v2. Για το προηγούμενο κεφάλαιο, δες το bug με τα ακουστικά (v1.6.12)· για το πού η ίδια ιδέα των server-sent-events έγινε αργότερα ένα ολόκληρο εναλλακτικό μέσο μεταφοράς, ζωντανή απομαγνητοφώνηση όταν το firewall μπλοκάρει τα WebSockets (v2.0.8)· και για ολόκληρο το τόξο, η ανατομία της παράδοσης λογισμικού στην τελειότητα.