214 lines
6.4 KiB
JavaScript
214 lines
6.4 KiB
JavaScript
import { later } from "@ember/runloop";
|
|
import PostUpdater from "./updaters/post-updater";
|
|
|
|
const PROGRESS_INTERVAL = 40;
|
|
const GIVE_UP_INTERVAL = 60000;
|
|
export const MIN_LETTERS_PER_INTERVAL = 6;
|
|
const MAX_FLUSH_TIME = 800;
|
|
|
|
let progressTimer = null;
|
|
|
|
/**
|
|
* Finds the last non-empty child element or text node of a given DOM element.
|
|
* Iterates backward through the element's child nodes and skips over empty text nodes.
|
|
*
|
|
* @param {HTMLElement} element - The DOM element to inspect.
|
|
* @returns {Node} - The last non-empty child node or null if none found.
|
|
*/
|
|
function lastNonEmptyChild(element) {
|
|
let lastChild = element.lastChild;
|
|
while (
|
|
lastChild &&
|
|
lastChild.nodeType === Node.TEXT_NODE &&
|
|
!/\S/.test(lastChild.textContent)
|
|
) {
|
|
lastChild = lastChild.previousSibling;
|
|
}
|
|
return lastChild;
|
|
}
|
|
|
|
/**
|
|
* Adds a progress dot (a span element with a "progress-dot" class) at the end of the
|
|
* last non-empty block within a given DOM element. This is used to visually indicate
|
|
* progress while content is being streamed.
|
|
*
|
|
* @param {HTMLElement} element - The DOM element to which the progress dot will be added.
|
|
*/
|
|
export function addProgressDot(element) {
|
|
let lastBlock = element;
|
|
|
|
while (true) {
|
|
let lastChild = lastNonEmptyChild(lastBlock);
|
|
if (!lastChild) {
|
|
break;
|
|
}
|
|
|
|
if (lastChild.nodeType === Node.ELEMENT_NODE) {
|
|
lastBlock = lastChild;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
const dotElement = document.createElement("span");
|
|
dotElement.classList.add("progress-dot");
|
|
lastBlock.appendChild(dotElement);
|
|
}
|
|
|
|
/**
|
|
* Applies progress to a streaming operation, updating the raw and cooked text,
|
|
* handling progress dots, and stopping streaming when complete.
|
|
*
|
|
* @param {Object} status - The current streaming status object.
|
|
* @param {Object} updater - An instance of a stream updater (e.g., PostUpdater or SummaryUpdater).
|
|
* @returns {Promise<boolean>} - Resolves to true if streaming is complete, otherwise false.
|
|
*/
|
|
export async function applyProgress(status, updater) {
|
|
status.startTime = status.startTime || Date.now();
|
|
|
|
if (Date.now() - status.startTime > GIVE_UP_INTERVAL) {
|
|
updater.streaming = false;
|
|
return true;
|
|
}
|
|
|
|
if (!updater.element) {
|
|
// wait till later
|
|
return false;
|
|
}
|
|
|
|
const oldRaw = updater.raw;
|
|
if (status.raw === oldRaw && !status.done) {
|
|
const hasProgressDot = updater.element.querySelector(".progress-dot");
|
|
if (hasProgressDot) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (status.raw !== undefined) {
|
|
let newRaw = status.raw;
|
|
|
|
if (!status.done) {
|
|
// rush update if we have a </details> tag (function call)
|
|
if (oldRaw.length === 0 && newRaw.indexOf("</details>") !== -1) {
|
|
newRaw = status.raw;
|
|
} else {
|
|
const diff = newRaw.length - oldRaw.length;
|
|
|
|
// progress interval is 40ms
|
|
// by default we add 6 letters per interval
|
|
// but ... we want to be done in MAX_FLUSH_TIME
|
|
let letters = Math.floor(diff / (MAX_FLUSH_TIME / PROGRESS_INTERVAL));
|
|
if (letters < MIN_LETTERS_PER_INTERVAL) {
|
|
letters = MIN_LETTERS_PER_INTERVAL;
|
|
}
|
|
|
|
newRaw = status.raw.substring(0, oldRaw.length + letters);
|
|
}
|
|
}
|
|
|
|
await updater.setRaw(newRaw, status.done);
|
|
}
|
|
|
|
if (status.done) {
|
|
if (status.cooked) {
|
|
await updater.setCooked(status.cooked);
|
|
}
|
|
updater.streaming = false;
|
|
}
|
|
|
|
return status.done;
|
|
}
|
|
|
|
/**
|
|
* Handles progress updates for a post stream by applying the streaming status of
|
|
* each post and updating its content accordingly. This function ensures that progress
|
|
* is tracked and handled for multiple posts simultaneously.
|
|
*
|
|
* @param {Object} postStream - The post stream object containing the posts to be updated.
|
|
* @returns {Promise<boolean>} - Resolves to true if polling should continue, otherwise false.
|
|
*/
|
|
async function handleProgress(postStream) {
|
|
const status = postStream.aiStreamingStatus;
|
|
|
|
let keepPolling = false;
|
|
|
|
const promises = Object.keys(status).map(async (postId) => {
|
|
let postStatus = status[postId];
|
|
|
|
const postUpdater = new PostUpdater(postStream, postStatus.post_id);
|
|
const done = await applyProgress(postStatus, postUpdater);
|
|
|
|
if (done) {
|
|
delete status[postId];
|
|
} else {
|
|
keepPolling = true;
|
|
}
|
|
});
|
|
|
|
await Promise.all(promises);
|
|
return keepPolling;
|
|
}
|
|
|
|
/**
|
|
* Ensures that progress for a post stream is being updated. It starts a progress timer
|
|
* if one is not already active, and continues polling for progress updates at regular intervals.
|
|
*
|
|
* @param {Object} postStream - The post stream object containing the posts to be updated.
|
|
*/
|
|
function ensureProgress(postStream) {
|
|
if (!progressTimer) {
|
|
progressTimer = later(async () => {
|
|
const keepPolling = await handleProgress(postStream);
|
|
|
|
progressTimer = null;
|
|
|
|
if (keepPolling) {
|
|
ensureProgress(postStream);
|
|
}
|
|
}, PROGRESS_INTERVAL);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Streams the raw text for a post by tracking its status and applying progress updates.
|
|
* If streaming is already in progress, this function ensures it continues to update the content.
|
|
*
|
|
* @param {Object} postStream - The post stream object containing the post to be updated.
|
|
* @param {Object} data - The data object containing raw and cooked content of the post.
|
|
*/
|
|
export function streamPostText(postStream, data) {
|
|
if (data.noop) {
|
|
return;
|
|
}
|
|
|
|
let status = (postStream.aiStreamingStatus =
|
|
postStream.aiStreamingStatus || {});
|
|
status[data.post_id] = data;
|
|
ensureProgress(postStream);
|
|
}
|
|
|
|
/**
|
|
* A generalized function to handle streaming of content using any specified updater class.
|
|
* It applies progress updates to the content (raw and cooked) based on the given data.
|
|
* Use this function to stream content for Glimmer components.
|
|
*
|
|
* @param {Function} updaterClass - The updater class to be used for streaming (e.g., PostUpdater, SummaryUpdater).
|
|
* @param {Object} data - The data object containing the content to be streamed.
|
|
* @param {Object} context - Additional context required for the updater (typically the context of the Ember component).
|
|
*/
|
|
export default function streamUpdaterText(updaterClass, data, context) {
|
|
const updaterInstance = new updaterClass(data, context);
|
|
|
|
if (!progressTimer) {
|
|
progressTimer = later(async () => {
|
|
await applyProgress(data, updaterInstance);
|
|
|
|
progressTimer = null;
|
|
|
|
if (!data.done) {
|
|
await applyProgress(data, updaterInstance);
|
|
}
|
|
}, PROGRESS_INTERVAL);
|
|
}
|
|
}
|