wui
组件

AI 流式文本 AI Stream

可用于普通回答与思考过程的通用流式文本动效。

基础示例

Loading…
pnpm dlx wui@latest add @wui/ai-stream
components/ui/ai-stream.tsx
"use client"

import * as React from "react"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"

import { cn } from "@/lib/utils"

export interface AiStreamProps
  extends Omit<React.ComponentProps<"div">, "children"> {
  /** The complete text received so far. */
  children: string
  /** Whether more text is expected. @default false */
  isStreaming?: boolean
  /** Number of trailing characters covered by the live feather. @default 18 */
  featherLength?: number
}

/** Renders cumulative streamed text with a feathered right edge. */
function AiStream({
  className,
  children,
  isStreaming = false,
  featherLength = 18,
  ...props
}: AiStreamProps) {
  const tailLength = isStreaming
    ? Math.min(Math.max(featherLength, 0), children.length)
    : 0
  const stableText = tailLength > 0 ? children.slice(0, -tailLength) : children
  const liveEdge = tailLength > 0 ? children.slice(-tailLength) : ""

  return (
    <div
      data-slot="ai-stream"
      aria-live={isStreaming ? "polite" : undefined}
      aria-busy={isStreaming}
      className={cn("whitespace-pre-wrap", className)}
      {...props}
    >
      {stableText}
      {liveEdge ? (
        <span
          data-slot="ai-stream-edge"
          className="box-decoration-clone"
          style={{
            backgroundImage:
              "linear-gradient(90deg, currentColor 0%, currentColor 42%, transparent 100%)",
            backgroundClip: "text",
            WebkitBackgroundClip: "text",
            WebkitTextFillColor: "transparent",
          }}
        >
          {liveEdge}
        </span>
      ) : null}
    </div>
  )
}

export interface AiStreamDeltasProps
  extends Omit<React.ComponentProps<"div">, "children"> {
  /** Ordered delta segments. Append entries without rewriting previous ones. */
  deltas: readonly string[]
  /** Whether more deltas are expected. @default false */
  isStreaming?: boolean
}

/** Renders each incoming delta as a short, independent fade-in segment. */
function AiStreamDeltas({
  className,
  deltas,
  isStreaming = false,
  ...props
}: AiStreamDeltasProps) {
  const reduceMotion = useReducedMotion()

  return (
    <div
      data-slot="ai-stream-deltas"
      aria-live={isStreaming ? "polite" : undefined}
      aria-busy={isStreaming}
      className={cn("whitespace-pre-wrap", className)}
      {...props}
    >
      <AnimatePresence initial={false}>
        {deltas.map((delta, index) => (
          <motion.span
            key={index}
            data-slot="ai-stream-delta"
            initial={
              reduceMotion
                ? false
                : { opacity: 0, y: 3, filter: "blur(3px)" }
            }
            animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
            transition={
              reduceMotion
                ? { duration: 0 }
                : { duration: 0.4, ease: [0.22, 1, 0.36, 1] }
            }
          >
            {delta}
          </motion.span>
        ))}
      </AnimatePresence>
    </div>
  )
}

export { AiStream, AiStreamDeltas }

组件作用

AiStream 是与消息类型无关的流式文本组件。普通助手回答可以直接使用;需要展示模型返回的推理摘要时,也可以把同一个组件放进 AiReasoning

传入当前累计文本与 isStreaming 状态即可。生成期间只有最新文字的最右侧边缘羽化,已稳定的正文不会重复执行动画;生成结束后立即恢复完整文字。

组件属性

PropTypeDefaultDescription
children *stringThe complete text received so far.
isStreamingbooleanfalseWhether more text is expected.
featherLengthnumber18Number of trailing characters covered by the live feather.

Delta 分段模式

保留原始 delta 数组时可以使用 AiStreamDeltas。每个新片段只淡入一次,适合粒度较大的服务端分段;连续的小字符增量优先使用 AiStream