11import { later } from "@ember/runloop" ;
2- import loadMorphlex from "discourse/lib/load-morphlex" ;
3- import { cook } from "discourse/lib/text" ;
2+ import PostUpdater from "./updaters/post-updater" ;
43
54const PROGRESS_INTERVAL = 40 ;
65const GIVE_UP_INTERVAL = 60000 ;
@@ -9,6 +8,13 @@ const MAX_FLUSH_TIME = 800;
98
109let progressTimer = null ;
1110
11+ /**
12+ * Finds the last non-empty child element or text node of a given DOM element.
13+ * Iterates backward through the element's child nodes and skips over empty text nodes.
14+ *
15+ * @param {HTMLElement } element - The DOM element to inspect.
16+ * @returns {Node } - The last non-empty child node or null if none found.
17+ */
1218function lastNonEmptyChild ( element ) {
1319 let lastChild = element . lastChild ;
1420 while (
@@ -21,6 +27,13 @@ function lastNonEmptyChild(element) {
2127 return lastChild ;
2228}
2329
30+ /**
31+ * Adds a progress dot (a span element with a "progress-dot" class) at the end of the
32+ * last non-empty block within a given DOM element. This is used to visually indicate
33+ * progress while content is being streamed.
34+ *
35+ * @param {HTMLElement } element - The DOM element to which the progress dot will be added.
36+ */
2437export function addProgressDot ( element ) {
2538 let lastBlock = element ;
2639
@@ -42,143 +55,14 @@ export function addProgressDot(element) {
4255 lastBlock . appendChild ( dotElement ) ;
4356}
4457
45- // this is the interface we need to implement
46- // for a streaming updater
47- class StreamUpdater {
48- set streaming ( value ) {
49- throw "not implemented" ;
50- }
51-
52- async setCooked ( ) {
53- throw "not implemented" ;
54- }
55-
56- async setRaw ( ) {
57- throw "not implemented" ;
58- }
59-
60- get element ( ) {
61- throw "not implemented" ;
62- }
63-
64- get raw ( ) {
65- throw "not implemented" ;
66- }
67- }
68-
69- class PostUpdater extends StreamUpdater {
70- morphingOptions = {
71- beforeAttributeUpdated : ( element , attributeName ) => {
72- return ! ( element . tagName === "DETAILS" && attributeName === "open" ) ;
73- } ,
74- } ;
75-
76- constructor ( postStream , postId ) {
77- super ( ) ;
78- this . postStream = postStream ;
79- this . postId = postId ;
80- this . post = postStream . findLoadedPost ( postId ) ;
81-
82- if ( this . post ) {
83- this . postElement = document . querySelector (
84- `#post_${ this . post . post_number } `
85- ) ;
86- }
87- }
88-
89- get element ( ) {
90- return this . postElement ;
91- }
92-
93- set streaming ( value ) {
94- if ( this . postElement ) {
95- if ( value ) {
96- this . postElement . classList . add ( "streaming" ) ;
97- } else {
98- this . postElement . classList . remove ( "streaming" ) ;
99- }
100- }
101- }
102-
103- async setRaw ( value , done ) {
104- this . post . set ( "raw" , value ) ;
105- const cooked = await cook ( value ) ;
106-
107- // resets animation
108- this . element . classList . remove ( "streaming" ) ;
109- void this . element . offsetWidth ;
110- this . element . classList . add ( "streaming" ) ;
111-
112- const cookedElement = document . createElement ( "div" ) ;
113- cookedElement . innerHTML = cooked ;
114-
115- if ( ! done ) {
116- addProgressDot ( cookedElement ) ;
117- }
118-
119- await this . setCooked ( cookedElement . innerHTML ) ;
120- }
121-
122- async setCooked ( value ) {
123- this . post . set ( "cooked" , value ) ;
124-
125- ( await loadMorphlex ( ) ) . morphInner (
126- this . postElement . querySelector ( ".cooked" ) ,
127- `<div>${ value } </div>` ,
128- this . morphingOptions
129- ) ;
130- }
131-
132- get raw ( ) {
133- return this . post . get ( "raw" ) || "" ;
134- }
135- }
136-
137- export class SummaryUpdater extends StreamUpdater {
138- constructor ( topicSummary , componentContext ) {
139- super ( ) ;
140- this . topicSummary = topicSummary ;
141- this . componentContext = componentContext ;
142-
143- if ( this . topicSummary ) {
144- this . summaryBox = document . querySelector ( "article.ai-summary-box" ) ;
145- }
146- }
147-
148- get element ( ) {
149- return this . summaryBox ;
150- }
151-
152- set streaming ( value ) {
153- if ( this . element ) {
154- if ( value ) {
155- this . componentContext . isStreaming = true ;
156- } else {
157- this . componentContext . isStreaming = false ;
158- }
159- }
160- }
161-
162- async setRaw ( value , done ) {
163- this . componentContext . oldRaw = value ;
164- const cooked = await cook ( value ) ;
165-
166- await this . setCooked ( cooked ) ;
167-
168- if ( done ) {
169- this . componentContext . finalSummary = cooked ;
170- }
171- }
172-
173- async setCooked ( value ) {
174- this . componentContext . text = value ;
175- }
176-
177- get raw ( ) {
178- return this . componentContext . oldRaw || "" ;
179- }
180- }
181-
58+ /**
59+ * Applies progress to a streaming operation, updating the raw and cooked text,
60+ * handling progress dots, and stopping streaming when complete.
61+ *
62+ * @param {Object } status - The current streaming status object.
63+ * @param {Object } updater - An instance of a stream updater (e.g., PostUpdater or SummaryUpdater).
64+ * @returns {Promise<boolean> } - Resolves to true if streaming is complete, otherwise false.
65+ */
18266export async function applyProgress ( status , updater ) {
18367 status . startTime = status . startTime || Date . now ( ) ;
18468
@@ -235,6 +119,14 @@ export async function applyProgress(status, updater) {
235119 return status . done ;
236120}
237121
122+ /**
123+ * Handles progress updates for a post stream by applying the streaming status of
124+ * each post and updating its content accordingly. This function ensures that progress
125+ * is tracked and handled for multiple posts simultaneously.
126+ *
127+ * @param {Object } postStream - The post stream object containing the posts to be updated.
128+ * @returns {Promise<boolean> } - Resolves to true if polling should continue, otherwise false.
129+ */
238130async function handleProgress ( postStream ) {
239131 const status = postStream . aiStreamingStatus ;
240132
@@ -257,22 +149,12 @@ async function handleProgress(postStream) {
257149 return keepPolling ;
258150}
259151
260- export function streamSummaryText ( topicSummary , context ) {
261- const summaryUpdater = new SummaryUpdater ( topicSummary , context ) ;
262-
263- if ( ! progressTimer ) {
264- progressTimer = later ( async ( ) => {
265- await applyProgress ( topicSummary , summaryUpdater ) ;
266-
267- progressTimer = null ;
268-
269- if ( ! topicSummary . done ) {
270- await applyProgress ( topicSummary , summaryUpdater ) ;
271- }
272- } , PROGRESS_INTERVAL ) ;
273- }
274- }
275-
152+ /**
153+ * Ensures that progress for a post stream is being updated. It starts a progress timer
154+ * if one is not already active, and continues polling for progress updates at regular intervals.
155+ *
156+ * @param {Object } postStream - The post stream object containing the posts to be updated.
157+ */
276158function ensureProgress ( postStream ) {
277159 if ( ! progressTimer ) {
278160 progressTimer = later ( async ( ) => {
@@ -287,7 +169,14 @@ function ensureProgress(postStream) {
287169 }
288170}
289171
290- export default function streamText ( postStream , data ) {
172+ /**
173+ * Streams the raw text for a post by tracking its status and applying progress updates.
174+ * If streaming is already in progress, this function ensures it continues to update the content.
175+ *
176+ * @param {Object } postStream - The post stream object containing the post to be updated.
177+ * @param {Object } data - The data object containing raw and cooked content of the post.
178+ */
179+ export function streamPostText ( postStream , data ) {
291180 if ( data . noop ) {
292181 return ;
293182 }
@@ -297,3 +186,28 @@ export default function streamText(postStream, data) {
297186 status [ data . post_id ] = data ;
298187 ensureProgress ( postStream ) ;
299188}
189+
190+ /**
191+ * A generalized function to handle streaming of content using any specified updater class.
192+ * It applies progress updates to the content (raw and cooked) based on the given data.
193+ * Use this function to stream content for Glimmer components.
194+ *
195+ * @param {Function } updaterClass - The updater class to be used for streaming (e.g., PostUpdater, SummaryUpdater).
196+ * @param {Object } data - The data object containing the content to be streamed.
197+ * @param {Object } context - Additional context required for the updater (typically the context of the Ember component).
198+ */
199+ export default function streamUpdaterText ( updaterClass , data , context ) {
200+ const updaterInstance = new updaterClass ( data , context ) ;
201+
202+ if ( ! progressTimer ) {
203+ progressTimer = later ( async ( ) => {
204+ await applyProgress ( data , updaterInstance ) ;
205+
206+ progressTimer = null ;
207+
208+ if ( ! data . done ) {
209+ await applyProgress ( data , updaterInstance ) ;
210+ }
211+ } , PROGRESS_INTERVAL ) ;
212+ }
213+ }
0 commit comments