UNPKG

react-tree-stream

Version:

Stream React trees recursively, LLM-style progressive rendering.

213 lines (208 loc) 8.11 kB
import * as react_jsx_runtime from 'react/jsx-runtime'; import React from 'react'; /** * TreeStream * * A client-only React component that renders its children incrementally over time. * It walks the provided React node tree into a linear execution plan of units: * - text units (streamed by word or character) * - instant units (regular React elements rendered immediately) * - nested stream units (child TreeStream elements, coordinated by onComplete) * * Contract (inputs/outputs): * - Props: * - as: optional polymorphic element type; use 'fragment' for no wrapper * - children: any renderable React nodes; fragments/arrays are flattened * - speed: number of tokens per tick (tokens are words or characters) * - interval: ms between ticks * - streamBy: 'word' | 'character' determines tokenization of text units * - autoStart: start streaming automatically when inputs/signature change * - onComplete: called after the final unit completes (including nested) * - DOM: adds data attributes for observability: * - data-tree-stream, data-streaming, data-complete * - SSR: client-only ('use client'); streaming occurs in the browser * * Notes: * - Nested TreeStream children have autoStart forced to true, and their * onComplete is composed so the parent resumes after the child completes. * - A stable "plan signature" is used to reset the stream only when structure * or text content changes; this limits unnecessary restarts. */ /** * Props for TreeStream. * - speed: tokens per tick (>= 1). Tokens are words or characters per streamBy. * - interval: delay between ticks in ms. * - streamBy: tokenization strategy for text nodes. * - autoStart: if false, the component initializes idle until inputs change again or programmatically started in a future version. */ /** * Core properties for the TreeStream component */ type OwnProps = { /** * Number of tokens to display per tick (must be >= 1). * Tokens are either words or characters based on the `streamBy` prop. * @default 5 */ speed?: number; /** * Delay between ticks in milliseconds. * Controls the animation speed of the streaming effect. * @default 50 */ interval?: number; /** * Tokenization strategy for text nodes. * - 'word': Text is split and streamed word by word * - 'character': Text is split and streamed character by character * @default 'word' */ streamBy?: 'word' | 'character'; /** * Whether to automatically start streaming when the component mounts * or when inputs/signature change. If false, the component initializes * in an idle state. * @default true */ autoStart?: boolean; /** * Callback invoked after all content (including nested TreeStream components) * has finished streaming. */ onComplete?: () => void; }; type AsProp<E extends React.ElementType> = { as?: E; }; type PropsToOmit<E extends React.ElementType> = keyof (AsProp<E> & OwnProps); type PolymorphicProps<E extends React.ElementType> = AsProp<E> & OwnProps & Omit<React.ComponentPropsWithoutRef<E>, PropsToOmit<E>>; type FragmentPropsGuard<E extends React.ElementType> = E extends typeof React.Fragment ? { className?: never; style?: never; } : {}; /** * Props for the TreeStream component with polymorphic support. * * @template E - The element type for the wrapper component * @example * ```tsx * // Default div wrapper * <TreeStream>Content</TreeStream> * * // Custom element wrapper * <TreeStream as="section">Content</TreeStream> * * // No wrapper (fragment) * <TreeStream as={React.Fragment}>Content</TreeStream> * ``` */ type TreeStreamProps<E extends React.ElementType = 'div'> = PolymorphicProps<E> & FragmentPropsGuard<E>; /** * TreeStream - A React component that renders content with a streaming animation effect. * * Renders children incrementally over time, creating a typewriter-like effect. * Supports text streaming (by word or character), instant rendering of React elements, * and nested TreeStream components with coordinated completion callbacks. * * @template E - The element type for the wrapper component (defaults to 'div') * @param props - The component props * @param props.as - Optional polymorphic element type. Use React.Fragment for no wrapper * @param props.children - React nodes to stream. Fragments and arrays are flattened * @param props.speed - Number of tokens to display per tick (default: 5) * @param props.interval - Milliseconds between ticks (default: 50) * @param props.streamBy - Tokenization strategy: 'word' or 'character' (default: 'word') * @param props.autoStart - Start streaming automatically on mount/change (default: true) * @param props.onComplete - Callback when streaming completes (including nested streams) * * @returns A React element that streams its content progressively * * @example * ```tsx * // Basic usage * <TreeStream speed={10} interval={30}> * Hello world! This text will stream in. * </TreeStream> * * // Character-by-character streaming * <TreeStream streamBy="character" speed={1}> * Typing effect... * </TreeStream> * * // With completion callback * <TreeStream onComplete={() => console.log('Done!')}> * Content here * </TreeStream> * * // Nested streaming components * <TreeStream> * First part * <TreeStream>Nested content streams after parent</TreeStream> * Final part * </TreeStream> * ``` */ declare function TreeStream<E extends React.ElementType = 'div'>({ as, children, speed, interval, streamBy, autoStart, onComplete, ...rest }: TreeStreamProps<E>): react_jsx_runtime.JSX.Element; declare namespace TreeStream { var displayName: string; } /** * Execution units produced from children to drive the streaming executor. * - text_stream: a text node that will be tokenized and streamed * - instant_render: any non-stream Tree element rendered immediately * - nested_stream: a nested TreeStream element coordinated by onComplete */ type ExecutionUnit = { type: 'text_stream'; content: string; } | { type: 'instant_render'; content: React.ReactElement; } | { type: 'nested_stream'; component: React.ReactElement; }; /** * buildPlan * * Convert an arbitrary React node tree into a flat execution plan. * * Inputs: * - node: any React renderable input (string/number/elements/arrays/fragments) * * Outputs: * - Array of ExecutionUnit preserving in-order appearance from the tree * * Rules: * - Strings/numbers become text stream units (empty strings are skipped) * - Fragments/arrays are flattened recursively * - Elements marked as TreeStream become nested stream units * - All other elements are instant render units * * Notes/edge cases: * - null/undefined/boolean nodes are ignored * - For strings we keep original content (including whitespace), but empty * strings after trim() are ignored to avoid no-op streaming units */ declare function buildPlan(node: React.ReactNode): ExecutionUnit[]; /** * planSignature * * Create a stable string signature for a plan that captures structural shape * and text content for text units. This is used to decide when to reset/run. * * Implementation detail: * - text_stream includes its content to re-run when text changes * - instant_render and nested_stream capture only their type (not identity) */ declare function planSignature(plan: ExecutionUnit[]): string; /** Marker symbol placed on the component function to detect wrappers. */ declare const STREAMING_MARKER: unique symbol; /** * isTreeStreamElement * * Detect whether a React element is a TreeStream component, even if wrapped * by React.memo or forwardRef. We check the function itself, .type for memo, * and .render for forwardRef. As a fallback we also check displayName. */ declare function isTreeStreamElement(el: React.ReactElement): boolean; export { type ExecutionUnit, STREAMING_MARKER, TreeStream, type TreeStreamProps, buildPlan, TreeStream as default, isTreeStreamElement, planSignature };