# useStreamingText

## 使用方法

把一坨一坨到达的文字抹平成稳定的逐字显示，用 requestAnimationFrame 推进。到达的速度和显示的速度互不影响。每次推进会退回到字素边界，所以不会把代理对、旗帜或拼在一起的 emoji 切成两半。节奏取自 `--astryx-duration-fast-min` 这个动效 token，不在主题里时用兜底值。`isStreaming` 变成 false 时直接跳到全文；用户偏好减少动态效果时也一直是全文。

传累积后的整段文字，不要传一块一块的分片，增量显示它自己会处理。`displayedText` 是 `ComputedRef<string>`，直接拿去渲染就行。

```ts
import { useStreamingText } from '@astryx-vue/core'
import { ref } from 'vue'

const targetText = ref('')
const isStreaming = ref(true)
const displayedText = useStreamingText(targetText, isStreaming, { speed: 'natural' })
```

## 建议

| 指引 | 做法 |
| --- | --- |
| Do | `targetText` 传累积后的整段文字，不要传一块一块的分片；增量显示它自己会处理。 |
| Do | 流结束的时候把 `isStreaming` 设成 false，直接跳到最终文字。 |
| Do | 不需要动画的场合用 `speed="instant"`，比如搜索结果，或者用户偏好减少动态效果。 |
| Don't | 用在不会变的静态文字上；它本来就不流式，白搭一层开销。 |

## 示例

常见的用法和不同的状态。

### useStreamingText — 流式回复

用 useStreamingText 把一阵一阵到达的生成文字抹平成稳定显示。

```vue
<script setup lang="ts">
import { useStreamingText } from '@astryx-vue/core'
import { Badge } from '@astryx-vue/core/Badge'
import { Button } from '@astryx-vue/core/Button'
import { Card } from '@astryx-vue/core/Card'
import { Heading } from '@astryx-vue/core/Heading'
import { Section } from '@astryx-vue/core/Section'
import { Stack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { onBeforeUnmount, ref } from 'vue'

const target = 'Astryx hooks keep behavior reusable while components keep visuals consistent.'

const source = ref('')
const isStreaming = ref(false)
const displayedText = useStreamingText(source, isStreaming)

let timer: ReturnType<typeof setInterval> | undefined

function stop(): void {
  if (timer !== undefined) {
    clearInterval(timer)
    timer = undefined
  }
}

function stream(): void {
  stop()
  source.value = ''
  isStreaming.value = true
  let offset = 0
  // 每 200ms 送一小段，模拟生成中的分块到达
  timer = setInterval(() => {
    offset = Math.min(offset + 8, target.length)
    source.value = target.slice(0, offset)
    if (offset >= target.length) {
      isStreaming.value = false
      stop()
    }
  }, 200)
}

onBeforeUnmount(stop)
stream()
</script>

<template>
  <Section variant="muted" :padding="4" :max-width="520">
    <Card :padding="4" width="100%">
      <Stack direction="vertical" :gap="3">
        <Stack direction="horizontal" :gap="2" align="center">
          <Heading :level="4">
            Assistant response
          </Heading>
          <Badge :variant="isStreaming ? 'info' : 'success'">
            {{ isStreaming ? 'Streaming' : 'Complete' }}
          </Badge>
        </Stack>
        <Text class="streaming-text" type="body">
          {{ displayedText }}
        </Text>
        <Stack direction="horizontal" :gap="2">
          <Button label="Stream again" size="sm" variant="secondary" @click="stream" />
        </Stack>
      </Stack>
    </Card>
  </Section>
</template>

<style scoped>
.streaming-text {
  min-height: 3.5rem;
}
</style>
```

## 参数

| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `targetText` | `MaybeRefOrGetter<string>` | — | 要显示出来的完整文字。有新分片到达时，把累积后的文字更新到这里。 |
| `isStreaming` | `MaybeRefOrGetter<boolean>` | `false` | 现在是不是还在流式输出。为 false 时直接返回完整的 `targetText`。 |
| `options` | `MaybeRefOrGetter` | — | 显示节奏的配置，可以不传。 |
| `options.speed` | `'natural' \| 'fast' \| 'instant'` | `'natural'` | 显示节奏的档位。`natural` 每个动效节拍往前推一小段；`fast` 的节拍快一倍，落后的量追得更快；`instant` 不带动画，直接给全文。 |

## 返回值

| 名称 | 类型 | 说明 |
| --- | --- | --- |
| `displayedText` | `ComputedRef<string>` | 该渲染出来的那一段。流式过程中会稳稳地往完整的 `targetText` 长；不在流式时就是 `targetText` 本身。 |
