// Shared thinking / streaming primitives. // — inline "coach is reading…" indicator with a drifting nib. // — stacked skeleton lines for streaming content. // — wraps a text node; swaps a skeleton out for text with a soft ink-bleed. // // All components are plain React; no state outside what they own. function ThinkingInk({ label = 'thinking…', mono = false, style }) { return ( {label} ); } function SkelLines({ lines = 3, widths = null, gap = 8 }) { // widths is an optional array of % strings; defaults to a tapered look const defaults = ['92%', '86%', '70%', '48%', '58%']; return (
{Array.from({ length: lines }).map((_, i) => (
))}
); } // A helper: given a partial string that may grow token-by-token, render each // "new" chunk with stream-reveal so incoming text softly bleeds in. // Tracks the previous length on a ref to emit only the delta class. function StreamingText({ text, className, style }) { const ref = React.useRef({ prev: '' }); // force a remount-ish reveal each time text meaningfully grows by wrapping // the new tail in a . const prev = ref.current.prev; let head = prev; let tail = ''; if (text && text.startsWith(prev) && text.length > prev.length) { tail = text.slice(prev.length); } else { // text changed non-monotonically (e.g. a partial JSON reparse replaced the string); // render it whole and treat it as a fresh reveal. head = ''; tail = text || ''; } ref.current.prev = text || ''; return ( {head} {tail && {tail}} ); } Object.assign(window, { ThinkingInk, SkelLines, StreamingText }); console.log('[cg] thinking primitives loaded');