ストリーミング

滑らかに読めるAIの応答をストリーミングします。モデルからまとまって届くチャンクは、追いつくために加速する一定のペースで表示され、新しい単語はやわらかくフェードし、書き込み中はキャレットが表示されます。

モデルは、数語、間、そして一文まるごと、というようにテキストをまとまって送ります。届いたチャンクをそのまま表示すると、返信がぎくしゃくします。Streamingは、受け取った内容と表示する内容を分けて保持し、毎秒90文字の一定のペースで表示します。

チャンクが溜まると、約0.3秒で差を埋めるようにペースが上がり、ストリームが終わると残りは約0.12秒で一気に表示されるため、テキストがモデルから大きく遅れることはありません。絵文字などの複数のパーツからなる文字は、表示の途中で分割されません。また、再生成の後のようにテキストが追記ではなく置き換えられた場合は、2つのバージョンが共有する部分から表示をやり直します。

ストリーミングされていないテキストは最初のフレームで全文が描画されるため、履歴やサーバーレンダリングがアニメーションすることはありません。StreamingTextは、最新の単語にやわらかなフェードを、書き込み中にキャレットを加えます。useSmoothTextは、ペース調整だけを提供するので、Markdownを含む任意のレンダラーの前に置けます。

  1. Proレジストリをcomponents.jsonに追加する

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. トークンを追加する

    アカウントページでトークンを作成し、.env.local に HEXTAUI_PRO_TOKEN として設定してください。

  3. ブロックを追加する

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

AI SDKと使う

useChatメッセージの各テキストパーツを描画します。パーツ自身のstateにより、StreamingTextはモデルがまだ書き込み中かどうかを判断するため、完了したメッセージや履歴は即座に表示されます。

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

Markdownと使う

フックを単独で使うと、どのレンダラーの前でもテキストを滑らかにできます。Streamdownはストリーミング中の未完了のMarkdownを処理し、react-markdownも同様に動作します。

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

任意のストリームと使う

各チャンクを文字列に追記して渡します。fetch、OpenAI、Anthropicの差分、サーバー送信イベントのいずれでも同じように動作します。

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

構造

外側から内側へ組み合わせるパーツ。

パーツ説明
StreamingTextペースを調整し、フェードさせ、キャレットを表示する、そのまま使えるテキスト要素。プレーンテキストの返信に使います。
useSmoothTextペース調整だけの機能です。今すぐ表示するテキストと、まだ追いついている途中かどうかを返します。

StreamingText

childrenを除くすべてのdivプロップも受け付けます。

プロパティ型デフォルト
textここまでに受け取ったすべて。チャンクが届くたびに追記します。
string–
streamingさらにテキストが届くかどうか。キャレットを表示し、追いつくための猶予を開いたままにします。
booleanfalse
smoothペース調整をオフにすると、受け取ったとおりにテキストを描画します。
booleantrue
charsPerSecond追いつく処理の前の、基本の読み上げペース。
number90

useSmoothText(text, options)

{ text, animating } を返します。animatingは、表示中のテキストが全文に追いつくまでtrueのままです。

プロパティ型デフォルト
textここまでに受け取った全文。
string–
options.streamingソースがまだ送信中かどうか。
booleanfalse
options.enabledfalseの場合、フックはテキストをそのまま返します。
booleantrue
options.charsPerSecond基本の読み上げペース。
number90
  • テキストのコンテナは、ストリーミング中はaria-busyを持つpoliteなライブリージョンになるため、スクリーンリーダーは1文字ずつではなく、返信が落ち着いた時点で読み上げます。
  • キャレットは装飾であり、支援技術からは隠されます。
  • モーション軽減時は、単語はフェードなしで表示され、キャレットの点滅は止まります。ペース調整は、モーションではなく読む速度の問題なので、そのまま維持されます。

使用技術

Streaming を構成する無料のHextaUIコンポーネントです。それぞれ単独でインストールできます。

コード

3 個のファイルを components/blocks/streaming に追加しました。