falconstream
v0.2.0
Published
Headless React hooks and primitives for displaying streamed LLM output well.
Maintainers
Readme
FalconStream
Headless React primitives for streamed LLM text: one hook paces the reveal so bursty chunks don't stutter, one keeps a scroll container pinned to the bottom without fighting a user who's scrolled up to reread something, and a markdown renderer that stops re-parsing the part of the message that has already settled.
Install
npm install falconstreamReact 18+ as a peer dependency. No provider, no runtime, no CSS.
API
| Hook | Signature | Returns |
| --- | --- | --- |
| useSmoothStream | (raw: string, isDone: boolean, options?: { charsPerSecond?: number; adaptive?: boolean }) => UseSmoothStreamResult | { text: string; isAnimating: boolean; backlogPercent: number; skip: () => void } |
| useStickToBottom | (options?: { thresholdPx?: number }) => UseStickToBottomResult | { scrollRef: RefObject<HTMLDivElement>; onScroll: () => void; scrollToBottom: (options?: { force?: boolean }) => void } |
| useCopyToClipboard | (options?: { resetAfterMs?: number }) => UseCopyToClipboardResult | { copy: (text: string) => void; status: "idle" \| "copied" \| "error" } |
charsPerSecond defaults to 60, adaptive (speed up under backlog)
defaults to true. thresholdPx (how close to the bottom still counts as
pinned) defaults to 80.
StreamingMarkdown is imported from falconstream/markdown and needs
react-markdown + remark-gfm installed (optional peer dependencies). The
hooks above have no such requirement.
| Prop | Type | Default |
| --- | --- | --- |
| text | string | — |
| isDone | boolean | — |
| showCursor | boolean | false |
| cursorChar | string | "▊" |
| cursorClassName | string | — |
| components | Components | — |
| remarkPlugins | PluggableList | [remarkGfm] |
| rehypePlugins | PluggableList | — |
useSmoothStream
import { useSmoothStream } from "falconstream";
function AssistantMessage({ raw, isDone }: { raw: string; isDone: boolean }) {
const { text, isAnimating } = useSmoothStream(raw, isDone);
return (
<p>
{text}
{isAnimating && <span className="cursor">▊</span>}
</p>
);
}raw only ever grows, append to it however your own fetch loop already
works (ReadableStream, SSE, whatever your SDK hands you). backlogPercent
is how much of raw hasn't been revealed yet, 0-100 use it to show a
"catching up" indicator.
useStickToBottom
import { useStickToBottom } from "falconstream";
function MessageList({ children }: { children: React.ReactNode }) {
const { scrollRef, onScroll, scrollToBottom } = useStickToBottom();
// call scrollToBottom() after new content renders,
// scrollToBottom({ force: true }) when the user sends a message
return (
<div ref={scrollRef} onScroll={onScroll} className="overflow-y-auto">
{children}
</div>
);
}StreamingMarkdown
import { useSmoothStream } from "falconstream";
import { StreamingMarkdown } from "falconstream/markdown";
function AssistantMessage({ raw, isDone }: { raw: string; isDone: boolean }) {
const { text, isAnimating } = useSmoothStream(raw, isDone);
return (
<StreamingMarkdown
text={text}
isDone={isDone}
showCursor={isAnimating}
cursorClassName="cursor"
/>
);
}Settled blocks are memoized, so only the unfinished tail re-parses as text
arrives. The cursor renders inside the markdown, flush against the last
character, instead of on the line below it style it via cursorClassName.
components and remarkPlugins/rehypePlugins are just passed straight
through to react-markdown. Use components to change how an element
renders, or add a plugin like rehype-highlight or remark-math:
import type { Components } from "falconstream/markdown";
const components: Components = {
a: (props) => <a {...props} target="_blank" rel="noreferrer" />,
pre: (props) => <pre className="rounded-lg bg-neutral-900 p-3" {...props} />,
code: (props) => <code className="rounded bg-neutral-100 px-1" {...props} />,
table: (props) => <table className="w-full border-collapse text-left" {...props} />,
th: (props) => <th className="border px-3 py-2 font-semibold" {...props} />,
td: (props) => <td className="border px-3 py-2 align-top" {...props} />,
};
<StreamingMarkdown text={text} isDone={isDone} components={components} />useCopyToClipboard
import { useCopyToClipboard } from "falconstream";
function CopyButton({ text }: { text: string }) {
const { copy, status } = useCopyToClipboard();
return (
<button onClick={() => copy(text)}>
{status === "copied" ? "Copied!" : "Copy"}
</button>
);
}status resets to "idle" after resetAfterMs (default 2000). No toast,
no tooltip — render whatever feedback your app wants from status.
Full source and the demo app live at github.com/dev-avinashh/falcon-stream.
