Streaming

Transmita respostas de IA que se leem com fluidez. Pedaços em rajadas vindos do modelo são revelados num ritmo constante que acelera para alcançar, com um fade suave nas palavras novas e um cursor enquanto escreve.

Os modelos enviam texto em rajadas: algumas palavras, uma pausa e depois uma frase inteira de uma vez. Imprimir cada pedaço assim que chega faz a resposta engasgar. O Streaming mantém o que você recebeu separado do que você mostra e o revela num ritmo constante de 90 caracteres por segundo.

Quando os pedaços se acumulam, o ritmo sobe para fechar a diferença em cerca de 0,3 segundo, e quando o stream termina o restante é despejado em cerca de 0,12 segundo, então o texto nunca fica muito atrás do modelo. Emojis e outros caracteres de várias partes nunca são divididos no meio da revelação, e se o texto é substituído em vez de acrescentado, como após uma regeneração, a revelação recomeça do que as duas versões têm em comum.

O texto que não está em streaming é renderizado por inteiro no primeiro quadro, então o histórico e as renderizações no servidor nunca são animados. O StreamingText adiciona um fade suave nas palavras mais novas e um cursor enquanto escreve. O useSmoothText oferece apenas o ritmo, para colocar na frente de qualquer renderizador, inclusive o Markdown.

  1. Adicione o registro Pro ao components.json

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

    Crie um token na sua página de conta e coloque-o em .env.local como HEXTAUI_PRO_TOKEN.

  3. Adicione o bloco

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

Com o AI SDK

Renderize cada parte de texto de uma mensagem do useChat. O state da própria parte informa ao StreamingText quando o modelo ainda está escrevendo, então mensagens concluídas e o histórico aparecem instantaneamente.

"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>
  )
}

Com Markdown

Use o hook isoladamente para suavizar o texto antes de qualquer renderizador. O Streamdown trata o Markdown inacabado enquanto chega; o react-markdown funciona do mesmo jeito.

"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>
  )
}

Com qualquer stream

Acrescente cada pedaço a uma string e passe-a. Funciona igual para fetch, deltas da OpenAI ou da Anthropic, ou 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>
  )
}

Anatomia

As partes que você compõe, de fora para dentro.

ParteDescrição
StreamingTextUm elemento de texto pronto para uso que dá ritmo, faz fade e mostra o cursor. Use para respostas em texto simples.
useSmoothTextApenas o ritmo. Retorna o texto a mostrar agora e se ainda está alcançando.

StreamingText

Também aceita todas as props de div, exceto children.

PropTipoPadrão
textTudo o que foi recebido até agora. Acrescente a isso conforme os pedaços chegam.
string–
streamingSe mais texto está a caminho. Mostra o cursor e mantém a janela de recuperação aberta.
booleanfalse
smoothDesative o ritmo para renderizar o texto exatamente como recebido.
booleantrue
charsPerSecondRitmo de leitura base antes de qualquer recuperação.
number90

useSmoothText(text, options)

Retorna { text, animating }. animating permanece true até o texto exibido alcançar o texto completo.

PropTipoPadrão
textO texto completo recebido até agora.
string–
options.streamingSe a origem ainda está enviando.
booleanfalse
options.enabledQuando false, o hook retorna o texto sem alterações.
booleantrue
options.charsPerSecondRitmo de leitura base.
number90
  • O contêiner de texto é uma região live polite com aria-busy enquanto chega em streaming, para que os leitores de tela anunciem a resposta quando ela se estabiliza, e não cada caractere.
  • O cursor é decorativo e fica oculto para tecnologias assistivas.
  • Com movimento reduzido, as palavras aparecem sem o fade e o cursor para de pulsar. O ritmo permanece, porque diz respeito à velocidade de leitura, não ao movimento.

Construído com

Os componentes gratuitos do HextaUI de que Streaming é feito. Cada um é instalado separadamente.

Código

3 arquivos, adicionados a components/blocks/streaming.