Mikkel Manniche
Dansk English Deutsch

KI-Agenten

Streaming response

Zeigt eine Antwort, während sie geschrieben wird. Neue Wörter blenden ein, und ein Cursor markiert das Ende.

Vorschau

Verwendung

streaming-response-demo.tsx
import { useEffect, useState } from 'react'
import { StreamingResponse } from '@/components/streaming-response'

const REPLY = 'Your order ships tomorrow morning. It should reach you on Thursday.'

export default function StreamingResponseDemo() {
  const [shown, setShown] = useState('')

  // Stand-in for a streamed model reply: pass the text so far and whether more is coming.
  useEffect(() => {
    const words = REPLY.split(/(\s+)/)
    let i = 0
    const id = setInterval(() => {
      i += 2
      setShown(words.slice(0, i).join(''))
      if (i >= words.length) clearInterval(id)
    }, 70)
    return () => clearInterval(id)
  }, [])

  return <StreamingResponse text={shown} streaming={shown.length < REPLY.length} />
}

Quellcode

streaming-response.tsx
import { cn } from '@/lib/utils'

export type StreamingResponseProps = {
  /** The text so far. Append to it as tokens arrive. */
  text: string
  /** True while more text is coming. Shows the caret. */
  streaming?: boolean
  className?: string
}

/**
 * Shows a reply as it is written. New words fade in; the caret blinks at the end while streaming.
 * Paragraphs are split on blank lines. Under reduced motion the words appear without fading.
 */
export function StreamingResponse({ text, streaming = false, className }: StreamingResponseProps) {
  const paragraphs = text.split(/\n{2,}/)

  return (
    <div className={cn('max-w-[65ch] text-base leading-7 text-pretty', className)} aria-busy={streaming}>
      {paragraphs.map((p, i) => {
        const last = i === paragraphs.length - 1
        return (
          <p key={i} className="mb-4 last:mb-0">
            {p.split(/(\s+)/).map((word, j) =>
              /^\s+$/.test(word) ? (
                word
              ) : (
                // A stable key per position: a word only animates the first time it renders.
                <span key={j} className="animate-[stream-in_240ms_cubic-bezier(0.23,1,0.32,1)_both] motion-reduce:animate-none">
                  {word}
                </span>
              ),
            )}
            {last && streaming && (
              <span
                className="ml-0.5 inline-block h-[1.1em] w-[2px] translate-y-[0.2em] animate-[stream-caret_1s_steps(1)_infinite] bg-foreground motion-reduce:animate-none"
                aria-hidden
              />
            )}
          </p>
        )
      })}
      <style href="manniche-streaming" precedence="default">{`
        @keyframes stream-in { from { opacity: 0 } }
        @keyframes stream-caret { 50% { opacity: 0 } }
      `}</style>
    </div>
  )
}

Installieren

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/streaming-response.json

Mit eingetragener Registry (components.json)

npx shadcn@latest add @manniche/streaming-response

Abhängigkeiten

Dateien

  • streaming-response.tsx