Streaming

Streame KI-Antworten, die sich flüssig lesen. Schubweise Chunks vom Modell werden in gleichmäßigem Tempo enthüllt, das zum Aufholen schneller wird, mit sanftem Einblenden neuer Wörter und einem Cursor, während geschrieben wird.

Modelle senden Text in Schüben: ein paar Wörter, eine Pause, dann ein ganzer Satz auf einmal. Jeden Chunk beim Eintreffen auszugeben, lässt eine Antwort stottern. Streaming hält das Empfangene getrennt vom Angezeigten und enthüllt es mit gleichmäßigen 90 Zeichen pro Sekunde.

Wenn sich Chunks stauen, steigt das Tempo, um die Lücke in etwa 0,3 Sekunden zu schließen, und sobald der Stream endet, wird der Rest in etwa 0,12 Sekunden ausgegeben, sodass der Text nie weit hinter dem Modell zurückbleibt. Emojis und andere mehrteilige Zeichen werden mitten in der Enthüllung nie getrennt, und wird der Text ersetzt statt angehängt, etwa nach einem Regenerieren, startet die Enthüllung ab dem, was beide Versionen gemeinsam haben.

Text, der nicht streamt, wird im ersten Frame vollständig gerendert, sodass Verlauf und Server-Renders nie animieren. StreamingText fügt ein sanftes Einblenden der neuesten Wörter und einen Cursor hinzu, während geschrieben wird. useSmoothText liefert nur das Timing, um es vor jeden Renderer zu setzen, auch Markdown.

  1. Die Pro-Registry zu components.json hinzufügen

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. Token hinzufügen

    Erstelle auf deiner Kontoseite einen Token und trage ihn in .env.local als HEXTAUI_PRO_TOKEN ein.

  3. Den Block hinzufügen

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

Mit dem AI SDK

Rendere jeden Text-Part einer useChat-Nachricht. Der eigene Zustand des Parts sagt StreamingText, wann das Modell noch schreibt, sodass fertige Nachrichten und der Verlauf sofort erscheinen.

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

Mit Markdown

Nutze den Hook allein, um den Text vor jedem Renderer zu glätten. Streamdown behandelt unfertiges Markdown beim Streamen; react-markdown funktioniert genauso.

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

Mit jedem Stream

Hänge jeden Chunk an einen String an und übergib ihn. Das funktioniert gleich für fetch, OpenAI- oder Anthropic-Deltas oder 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>
  )
}

Aufbau

Die Teile, die du zusammensetzt, von außen nach innen.

PartBeschreibung
StreamingTextEin Drop-in-Textelement, das taktet, einblendet und den Cursor zeigt. Nutze es für reine Textantworten.
useSmoothTextNur das Timing. Gibt den Text zurück, der gerade angezeigt werden soll, und ob es noch aufholt.

StreamingText

Akzeptiert auch alle div-Props außer children.

PropTypStandard
textAlles, was bisher eingetroffen ist. Hänge daran an, wenn Chunks eintreffen.
string–
streamingOb weiterer Text kommt. Zeigt den Cursor und hält das Aufholfenster offen.
booleanfalse
smoothSchalte das Taktgeben ab, um Text genau so zu rendern, wie er eintrifft.
booleantrue
charsPerSecondGrundlesetempo vor jedem Aufholen.
number90

useSmoothText(text, options)

Gibt { text, animating } zurück. animating bleibt true, bis der angezeigte Text den vollen Text erreicht.

PropTypStandard
textDer bisher empfangene vollständige Text.
string–
options.streamingOb die Quelle noch sendet.
booleanfalse
options.enabledBei false gibt der Hook den Text unverändert zurück.
booleantrue
options.charsPerSecondGrundlesetempo.
number90
  • Der Textcontainer ist eine höfliche Live-Region mit aria-busy, solange gestreamt wird, sodass Screenreader die Antwort ansagen, sobald sie sich setzt, statt jedes Zeichen.
  • Der Cursor ist dekorativ und vor assistiven Technologien verborgen.
  • Bei reduzierter Bewegung erscheinen Wörter ohne Einblenden, und der Cursor hört auf zu pulsieren. Das Timing bleibt, weil es um die Lesegeschwindigkeit geht, nicht um Bewegung.

Gebaut mit

Die kostenlosen HextaUI-Komponenten, aus denen Streaming besteht. Jede lässt sich einzeln installieren.

Code

3 Dateien, hinzugefügt zu components/blocks/streaming.