UNPKG

@convex-dev/agent

Version:

A agent component for Convex.

68 lines 3.44 kB
import { useEffect, useRef, useState } from "react"; const FPS = 20; const MS_PER_FRAME = 1000 / FPS; const INITIAL_CHARS_PER_SEC = 128; /** * A hook that smoothly displays text as it is streamed. * * @param text The text to display. Pass in the full text each time. * @param charsPerSec The number of characters to display per second. * @returns A tuple of the visible text and the state of the smooth text, * including the current cursor position and whether it's still streaming. * This allows you to decide if it's too far behind and you want to adjust * the charsPerSec or just prefer the full text. */ export function useSmoothText(text, { charsPerSec = INITIAL_CHARS_PER_SEC, startStreaming = false, } = {}) { const [visibleText, setVisibleText] = useState(startStreaming ? "" : text || ""); const smoothState = useRef({ tick: Date.now(), cursor: visibleText.length, lastUpdate: Date.now(), lastUpdateLength: text.length, charsPerMs: charsPerSec / 1000, initial: true, }); const isStreaming = smoothState.current.cursor < text.length; useEffect(() => { if (!isStreaming) { return; } if (smoothState.current.lastUpdateLength !== text.length) { const timeSinceLastUpdate = Date.now() - smoothState.current.lastUpdate; const latestCharsPerMs = (text.length - smoothState.current.lastUpdateLength) / timeSinceLastUpdate; // Is the rate increasing? const rateError = latestCharsPerMs - smoothState.current.charsPerMs; // Is our visible text falling behind what it could show? const charLag = smoothState.current.lastUpdateLength - smoothState.current.cursor; const lagRate = charLag / timeSinceLastUpdate; const charsPerMs = latestCharsPerMs + (smoothState.current.initial ? 0 : Math.max(0, (rateError + lagRate) / 2)); smoothState.current.initial = false; // Smooth out the charsPerSec by weighting it with the previous value. smoothState.current.charsPerMs = Math.min((2 * charsPerMs + smoothState.current.charsPerMs) / 3, smoothState.current.charsPerMs * 2); } smoothState.current.tick = Math.max(smoothState.current.tick, Date.now() - MS_PER_FRAME); smoothState.current.lastUpdate = Date.now(); smoothState.current.lastUpdateLength = text.length; function update() { if (smoothState.current.cursor >= text.length) { return; } const now = Date.now(); const timeSinceLastUpdate = now - smoothState.current.tick; const charsSinceLastUpdate = Math.floor(timeSinceLastUpdate * smoothState.current.charsPerMs); const chars = Math.min(charsSinceLastUpdate, text.length - smoothState.current.cursor); smoothState.current.cursor += chars; smoothState.current.tick += chars / smoothState.current.charsPerMs; setVisibleText(text.slice(0, smoothState.current.cursor)); } update(); const interval = setInterval(update, MS_PER_FRAME); return () => clearInterval(interval); }, [text, isStreaming, charsPerSec]); return [visibleText, { cursor: smoothState.current.cursor, isStreaming }]; } //# sourceMappingURL=useSmoothText.js.map