Components · Chat

Stream Text

A streamed reply written out whole words at a time, at the rate the stream arrives, so bursts and pauses read as one steady cadence.

Installation

The command adds the @aiellie registry to components.json on its first run. With it there, shadcn installs it too: npx shadcn@latest add @aiellie/stream-text.

Usage

Pass the reply so far as text, growing as it arrives, and streaming while more is on its way. Words go out whole, at the rate the stream has been arriving, so a burst doesn't land all at once and a pause slows the writing instead of stopping it.

import { StreamText } from "@/components/aiellie/stream-text"

export function Reply({
  text,
  streaming,
}: {
  text: string
  streaming: boolean
}) {
  return (
    <p className="text-sm leading-6">
      <StreamText text={text} streaming={streaming} />
    </p>
  )
}

Composition

In a thread, it goes inside a MessagePart. It draws its own caret, so the message leaves its caret out. With the AI SDK, stream the latest reply and show the rest as they are.

"use client"

import { useChat } from "@ai-sdk/react"

import {
  Message,
  MessageContent,
  MessagePart,
} from "@/components/aiellie/message"
import { StreamText } from "@/components/aiellie/stream-text"

export function Messages() {
  const { messages, status } = useChat()

  return messages.map((message, index) => {
    const latest = index === messages.length - 1
    const streaming = latest && status === "streaming"

    return (
      <Message
        key={message.id}
        align={message.role === "user" ? "end" : "start"}
        variant={message.role === "user" ? "secondary" : "ghost"}
        streaming={streaming}
      >
        <MessageContent>
          {message.parts.map((part, key) =>
            part.type === "text" ? (
              <MessagePart key={key}>
                {message.role === "assistant" ? (
                  <StreamText text={part.text} streaming={streaming} />
                ) : (
                  part.text
                )}
              </MessagePart>
            ) : null
          )}
        </MessageContent>
      </Message>
    )
  })
}

API reference

StreamText

Renders a span. Any other prop, like className, goes to it.

PropTypeDefault
text (required)The reply so far. Each value should carry on from the one before.string—
streamingWhether more is still on its way. While it is, the last word waits until it's whole and the newest words hold the live ink.booleanfalse
paceWords a second, until the stream's own rate is known, and for revealInitial.number16
revealInitialWrite out the text it mounts with too, instead of showing it at once.booleanfalse
inlineCodeSet runs inside backticks as code.booleantrue
freshWordsHow many of the newest words hold the live ink while more are coming.number2
onRevealedCalled once the reply is finished and every word of it is on screen.() => void—
  • The newest words and the caret take the --live color from the aiellie theme, and fall back to the text color without it. The caret's blink comes from tw-animate-css.
  • With reduced motion, or in a tab that's hidden, each word shows as it arrives instead of being paced.