---
title: "StreamText 流式文字"
description: "逐词匀速显影的流式文字"
category: AiReasoning
status: beta
since: 0.6.3
tags: [ai, streaming, text, reveal, caret]
syncStatus: reviewed
verified: true
---

## 安装

:::TuffCodeBlock{lang="bash"}
---
code: |
  pnpm add @talex-touch/tuffex
---
:::

:::TuffCodeBlock{lang="typescript"}
---
code: |
  import { TxStreamText } from '@talex-touch/tuffex/stream-text'
  import '@talex-touch/tuffex/stream-text/style.css'
  import '@talex-touch/tuffex/inline-citation/style.css' // 引用 chip
  import '@talex-touch/tuffex/base.css' // 全局引入一次
---
:::

## 用法

### 流式输出
把目前收到的全部文字传给 `content`，源头输出期间保持 `streaming` 为真。
:::TuffDemoWrapper{demo="StreamTextStreamTextDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  const reply = ref('')
  const busy = ref(true)

  for await (const delta of stream)
    reply.value += delta
  busy.value = false
  </script>

  <template>
    <p>
      <TxStreamText :content="reply" :streaming="busy" />
    </p>
  </template>
---
:::

### 显影预设
`reveal` 决定词的进场方式。内容完整时用 `replay()` 重播，`reserve` 让版面保持不动。
:::TuffDemoWrapper{demo="StreamTextPresetsDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import type { TxStreamTextInstance } from '@talex-touch/tuffex/stream-text'

  const text = ref<TxStreamTextInstance | null>(null)
  </script>

  <template>
    <TxStreamText ref="text" :content="answer" reveal="blur" reserve />
    <TxButton @click="text?.replay()">重播</TxButton>
  </template>
---
:::

### 插槽与状态
`content` 也接受片段数组：带样式与链接的文字、引用和自定义片段。
:::TuffDemoWrapper{demo="StreamTextSlotsDemo" code-lang="vue"}
---
code: |
  <template>
    <TxStreamText :content="runs" @state-change="state = $event" @cite="open">
      <template #caret="{ state }">
        <span class="my-caret" :data-state="state" />
      </template>
      <template #citation="{ source, index }">
        <sup>{{ index }}</sup>
      </template>
      <template #inline="{ name, props }">
        <MyDelta v-if="name === 'delta'" :value="props.value" />
      </template>
    </TxStreamText>
  </template>
---
:::

### 最佳实践

- 传目前为止的完整文字，不要只传增量。
- 源头真正结束前一直保持 `streaming` 为真；已完整的回答（历史、回放）不开，重播用 `replay()`。
- 完整文字在不能挪动的布局（卡片、气泡）里回放时，打开 `reserve`。
- 复制、分享用你持有的完整文字：屏幕显示最多落后源头 `maxLagMs`。
- 组件不是 live region；需要播报回复时，把对话包在 `role="log"` 里。

## API 参考

### 属性

| 名称 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `content` | `string \| StreamInline[]` | — | 目前收到的全部内容，必填。 |
| `streaming` | `boolean` | `false` | 源头仍在输出；可能未写完的末词等后文或静默 200ms 后放出。 |
| `wordMs` | `number` | `24` | 每 `wordMs` 放出一个词。 |
| `maxLagMs` | `number` | `600` | 任何词都在到达后这段时间内显示。 |
| `drainMs` | `number` | `320` | `streaming` 变为 false 后，在这段时间内放完剩余部分。 |
| `pauseMs` | `number` | `400` | 输出中超过这段时间没有新词即报告 `paused`。 |
| `reveal` | `'aurora' \| 'hue' \| 'blur' \| 'languid' \| 'none'` | `'aurora'` | 词的进场方式，见 `StreamRevealPreset`。 |
| `caret` | `boolean` | `true` | 流式进行时显示光标；关闭时立即移除。 |
| `reserve` | `boolean` | `false` | 完整内容回放时用隐形副本占住最终版面；`streaming` 时忽略。 |
| `paced` | `boolean` | `true` | 为 `false` 时词一到就显示（仍有进场）；`TxStreamElement` 统一控制节奏时传 `false`。 |
| `appear` | `boolean` | `false` | 挂载时已有的内容也播放进场；只在挂载时读取。 |
| `tag` | `string` | `'span'` | 根元素。 |
| `locale` | `string` | `'zh'` | `Intl.Segmenter` 分词所用的语言。 |

### 事件

| 名称 | 参数 | 说明 |
|------|------|------|
| `state-change` | `(state: StreamState)` | 流式状态变化时触发。 |
| `done` | — | 每次播放触发一次：末词已显示且源头已结束。 |
| `cite` | `(source: AiSourceItem)` | 引用 chip 被打开时触发；chip 自身不跳转。 |

### 插槽

| 名称 | 作用域 | 说明 |
|------|--------|------|
| `caret` | `{ state }` | 替换 Tuff 光标，只在流式进行时渲染。 |
| `citation` | `{ source, label, index }` | 替换引用 chip；`index` 为标记中的数字。 |
| `inline` | `{ name, props }` | 渲染 `{ type: 'custom' }` 片段。 |

### 暴露方法

| 名称 | 类型 | 说明 |
|------|------|------|
| `state` | `StreamState` | 当前流式状态。 |
| `replay` | `() => void` | 从第一个词起按 `wordMs` 重播全部内容。 |
| `skip` | `() => void` | 立即显示全部内容，不播放进场。 |

### 类型

:::TuffCodeBlock{lang="typescript"}
---
code: |
  type StreamState = 'idle' | 'streaming' | 'paused' | 'draining' | 'done'

  type StreamRevealPreset =
    | 'aurora' // 从 4px 模糊析出，同时扫过蓝紫粉色带（默认）
    | 'hue' // 只扫色带
    | 'blur' // 只有模糊
    | 'languid' // 从 8px 模糊缓慢上浮
    | 'none' // 放出即显示

  type StreamInline =
    | { type: 'text', text: string, marks?: ('strong' | 'em' | 'del' | 'code')[], href?: string }
    | { type: 'citation', source: AiSourceItem, label?: string, index?: number }
    | { type: 'custom', name: string, props?: Record<string, unknown> }
---
:::

### CSS 变量

| 变量 | 默认值 | 说明 |
|------|--------|------|
| `--tx-stream-reveal-1` / `-2` / `-3` | 蓝 / 紫 / 粉，随主题变化 | 色带的三个颜色节点；高对比度主题下均为正文色。 |
| `--tx-stream-caret-start` / `--tx-stream-caret-end` | `#199ffe` / `#810dc6` | 光标渐变，取自 Tuff logo。 |
| `--tx-stream-reveal-duration` | 取决于预设 | 由组件按 `reveal` 写入根节点，无需设置。 |

## 概述

- 词按 `wordMs` 匀速放出；突发时提速，保证每个词在到达后 `maxLagMs` 内显示；源头结束后，剩余部分在 `drainMs` 内放完。
- 状态依次为 `idle` → `streaming` → `paused`（`pauseMs` 内无新词）→ `draining`（源头已结束，积压仍在放出）→ `done`；光标在 `done` 时收起。
- 只有新内容进场：挂载时已有的内容直接显示；前文被改写时，从第一个改动的词起重新放出。
- 用 `Intl.Segmenter` 按词切分，标点随所属的词；不支持时拉丁文按空格、中日韩文字按字切分。
- `href` 只对 `http(s)`、`mailto`、`tel`、相对路径、查询串与 hash 链接生效，`//` 开头的地址保持为文字；链接带 `rel="noopener noreferrer"`。
- 减弱动效时不进场、不控节奏，光标静止；服务端渲染为纯文本；流式进行时根节点带 `aria-busy="true"`。
- 光标与 `reserve` 副本对辅助技术隐藏（副本同时 `inert`）；文字本身不是 live region，需要播报时把对话包在 `role="log"` 里。

## 技术实现

- 时钟 `use-stream-pacer.ts` 与 `style/mixins.scss` 中的 `stream-reveal-*` mixin 由流式家族共用。
- 动效参考 kobra.systems（`hue`）与 Beautiful UI（`blur`）的 streaming text，默认的 `aurora` 合并两者；光标取自 Tuff logo。
- 源码：`packages/tuffex/packages/components/src/stream-text/`。

<TuffDocSourceLink />

## 使用场景

- 逐 token 到达的 AI 回答：对话、侧边面板、通知。
- 以可读的节奏回放存档的回答或脚本演示，版面不动。
- 带出处的简短生成句子，引用落在对应论断处。

## 相关组件

- [StreamMarkdown](./stream-markdown.zh.mdc)：流式渲染完整的 Markdown 文档，使用同一套显影预设与光标。
- [CodeStream](./code-stream.zh.mdc)：流式输出代码。
- [InlineCitation](./inline-citation.zh.mdc)：默认的引用 chip。
- [Sources](./sources.zh.mdc)：在回答末尾列出来源。
