Streaming

Transmite respuestas de IA que se leen con fluidez. Los fragmentos a ráfagas del modelo se revelan a un ritmo constante que se acelera para alcanzarlos, con un suave fundido en las palabras nuevas y un cursor mientras escribe.

Los modelos envían el texto a ráfagas: unas pocas palabras, una pausa y luego una frase entera de golpe. Imprimir cada fragmento según llega hace que la respuesta tartamudee. Streaming mantiene lo que has recibido separado de lo que muestras, y lo revela a un ritmo constante de 90 caracteres por segundo.

Cuando los fragmentos se acumulan, el ritmo sube para cerrar la brecha en unos 0,3 segundos, y cuando termina el flujo el resto se vuelca en unos 0,12 segundos, así que el texto nunca se queda muy atrás del modelo. Los emoji y otros caracteres de varias partes nunca se dividen a mitad de la revelación, y si el texto se reemplaza en lugar de añadirse, como tras una regeneración, la revelación se reinicia desde lo que comparten las dos versiones.

El texto que no está en streaming se renderiza completo en el primer fotograma, así que el historial y los renderizados del servidor nunca se animan. StreamingText añade un suave fundido en las palabras más nuevas y un cursor mientras escribe. useSmoothText te da solo el ritmo, para ponerlo delante de cualquier renderizador, incluido Markdown.

  1. Añade el registro Pro a components.json

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. Añade tu token

    Crea un token en tu página de cuenta y colócalo en .env.local como HEXTAUI_PRO_TOKEN.

  3. Añade el bloque

    pnpm dlx shadcn@latest add @hextaui-pro/streaming

Con AI SDK

Renderiza cada parte de texto de un mensaje de useChat. El propio estado de la parte le dice a StreamingText cuándo el modelo sigue escribiendo, así que los mensajes terminados y el historial aparecen al instante.

"use client"

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

import { StreamingText } from "@/components/blocks/streaming/streaming-text"

export function Chat() {
  const { messages, sendMessage, status } = useChat()
  const [input, setInput] = React.useState("")

  return (
    <div className="flex flex-col gap-4">
      {messages.map((message) => (
        <div key={message.id} className="flex flex-col gap-2">
          {message.parts.map((part, index) =>
            part.type === "text" ? (
              <StreamingText
                key={index}
                text={part.text}
                streaming={part.state === "streaming"}
              />
            ) : null
          )}
        </div>
      ))}
      <form
        onSubmit={(event) => {
          event.preventDefault()
          sendMessage({ text: input })
          setInput("")
        }}
      >
        <input
          value={input}
          disabled={status !== "ready"}
          onChange={(event) => setInput(event.target.value)}
        />
      </form>
    </div>
  )
}

Con Markdown

Usa el hook por sí solo para suavizar el texto antes de cualquier renderizador. Streamdown gestiona el Markdown sin terminar mientras llega; react-markdown funciona igual.

"use client"

import { Streamdown } from "streamdown"

import { useSmoothText } from "@/components/blocks/streaming/use-smooth-text"

export function MarkdownAnswer({
  text,
  streaming,
}: {
  text: string
  streaming: boolean
}) {
  const smooth = useSmoothText(text, { streaming })

  return (
    <Streamdown mode="streaming" isAnimating={streaming || smooth.animating}>
      {smooth.text}
    </Streamdown>
  )
}

Con cualquier flujo

Añade cada fragmento a una cadena y pásala. Funciona igual con fetch, deltas de OpenAI o Anthropic, o server-sent events.

"use client"

import * as React from "react"

import { StreamingText } from "@/components/blocks/streaming/streaming-text"

export function Answer({ prompt }: { prompt: string }) {
  const [text, setText] = React.useState("")
  const [streaming, setStreaming] = React.useState(false)

  const ask = async () => {
    setText("")
    setStreaming(true)
    const response = await fetch("/api/chat", {
      method: "POST",
      body: JSON.stringify({ prompt }),
    })
    const reader = response
      .body!.pipeThrough(new TextDecoderStream())
      .getReader()
    for (;;) {
      const { done, value } = await reader.read()
      if (done) break
      setText((current) => current + value)
    }
    setStreaming(false)
  }

  return (
    <div className="flex flex-col gap-3">
      <button onClick={ask}>Ask</button>
      <StreamingText text={text} streaming={streaming} />
    </div>
  )
}

Anatomía

Las partes que compones, de fuera hacia dentro.

ParteDescripción
StreamingTextUn elemento de texto listo para usar que regula el ritmo, hace aparecer el texto con un fundido y muestra el cursor. Úsalo para respuestas de texto plano.
useSmoothTextEl ritmo por sí solo. Devuelve el texto que mostrar ahora mismo y si aún está alcanzándolo.

StreamingText

También acepta todas las props de div excepto children.

PropTipoPredeterminado
textTodo lo recibido hasta ahora. Añade a ello a medida que llegan los fragmentos.
string–
streamingSi va a llegar más texto. Muestra el cursor y mantiene abierta la ventana de recuperación.
booleanfalse
smoothDesactiva el ritmo para renderizar el texto exactamente como se recibe.
booleantrue
charsPerSecondRitmo de lectura base antes de cualquier recuperación.
number90

useSmoothText(text, options)

Devuelve { text, animating }. animating se mantiene true hasta que el texto mostrado alcanza el texto completo.

PropTipoPredeterminado
textEl texto completo recibido hasta ahora.
string–
options.streamingSi la fuente sigue enviando.
booleanfalse
options.enabledCuando es false, el hook devuelve el texto sin cambios.
booleantrue
options.charsPerSecondRitmo de lectura base.
number90
  • El contenedor de texto es una región live polite con aria-busy mientras llega, para que los lectores de pantalla anuncien la respuesta una vez asentada en lugar de cada carácter.
  • El cursor es decorativo y está oculto para las tecnologías de asistencia.
  • Con movimiento reducido, las palabras aparecen sin fundido y el cursor deja de pulsar. El ritmo se mantiene, porque tiene que ver con la velocidad de lectura, no con el movimiento.

Construido con

Los componentes gratuitos de HextaUI con los que está hecho Streaming. Cada uno se instala por separado.

Código

3 archivos, añadidos a components/blocks/streaming.