# 对话

## 使用方法

`Chat` 这一套组件，就像积木一样，包含搭建完整聊天窗口所需的各个部件：滚动的对话外框、消息列表、区分发送者的消息行、聊天气泡、AI 工具调用折叠栏、思考过程折叠、正在输入动画，以及未读消息提示线。消息正文通过插槽传入。组件本身不带 Markdown 解析器，你可以直接放纯文本，也可以接上你自己喜欢的 Markdown 渲染库。

**什么时候用：**
- 搭建大模型 AI 对话界面、客服客服聊天窗口或即时聊天应用时。

**什么时候不要用：**
- 如果只是为了让用户留个言、发一条评论，用普通的表单评论区组件更轻快。
- 如果只是通知中心的一串流水消息，用时间线列表或通知流即可。

各个积木的组装分工如下：

- `ChatLayout` 是最外层的纵向布局容器。消息记录区在上方自适应伸展，输入框贴在底部，底部还带有一层毛玻璃磨砂效果。默认由布局自己滚动；如果外层已经有滚动容器，可以传入 `scrollRef`，底部停靠会自动调整为跟随固定的模式。
- 当用户浏览到最底部时，新生成的内容会自动把滚动条继续贴在底部。如果用户往上翻看历史记录，自动贴底会立刻暂停，并浮现一个回到最底部的圆形按钮。在没有贴底时来了新消息，按钮文字会变成「有新消息」，未读分割线 `ChatUnreadDivider` 也会标出新旧分界线。
- `ChatComposer` 是底部的输入套件，放进 `ChatLayout` 的 `composer` 插槽里。它负责草稿状态、回车发送、Shift 加回车换行、停止生成按钮等。它还支持附件抽屉以及基于浏览器原生语音识别的麦克风听写功能。落地页和带文档面板的整页会话见[应用模式](/guide/patterns)。

```vue
<script setup lang="ts">
import { ChatComposer, ChatLayout, ChatMessage, ChatMessageBubble, ChatMessageList } from '@astryx-vue/core/Chat'

function onSubmit(value: string) {
  console.log(value)
}
</script>

<template>
  <ChatLayout>
    <ChatMessageList>
      <ChatMessage sender="user">
        <ChatMessageBubble>
          帮我看一下发布说明。
        </ChatMessageBubble>
      </ChatMessage>
      <ChatMessage sender="assistant">
        <ChatMessageBubble name="助手">
          说明在文档里。
        </ChatMessageBubble>
      </ChatMessage>
    </ChatMessageList>
    <template #composer>
      <ChatComposer placeholder="输入消息……" @submit="onSubmit" />
    </template>
  </ChatLayout>
</template>
```

- `ChatMessageList` 是承载消息记录的列表容器，带有 `role="log"`。当 AI 助手的文字还在持续生成时传入 `isStreaming`，读屏软件会等整段输出完毕后再统一朗读，避免不断打扰用户。当用户向上翻到顶部时，`scrollToTopAction` 会触发加载历史消息，加载完成前顶部会显示旋转动画。
- `ChatMessage` 代表单条消息行。`sender` 可以是 `user`（用户）、`assistant`（AI 助手）或 `system`（系统通知）。系统通知会默认水平居中。
- `ChatToolCalls` 用来展示 AI 调用的外部工具或函数。只有一次调用时直接显示一行；多次调用时会收拢进折叠栏，点击可以查看参数与结果明细。
- `ChatReasoning` 是折叠卡片，用来展示 AI 的思考推理过程，默认折叠收起。
- `ChatTypingIndicator` 用来展示「正在输入」的跳动三个小圆点，告诉用户对方正在打字或 AI 正在构思。

下面先看输入套件和消息列表放在一起的样子，再按布局、回到底部、消息、气泡、列表、元信息、系统消息、工具调用往下排。

## 建议

| 建议 | 做法 |
| --- | --- |
| 推荐 | 记录放进消息列表，输入套件放进底部插槽，磨砂和回到底部的按钮才对得齐。 |
| 推荐 | 助手正文还在增长时保持 `isStreaming`，写完再关掉。 |
| 推荐 | 把 `ChatUnreadDivider` 放在读者还没看到的第一条前面。按钮上的「有新消息」负责播报，分隔线负责标位置。 |
| 推荐 | 气泡里放纯文本、引文，或你自己的渲染结果。气泡只提供底色和圆角。 |
| 避免 | 在布局里面再套一个滚动容器。记录的滚动只交给布局自己，或者交给 `scrollRef` 指向的那个元素。 |
| 避免 | 每个流式 token 都自己播报一遍。正在输入靠自己的 `role="status"` 播报一次，新消息文案走布局里的 live region。 |

### 结构

- **消息区**: 可滚动的消息区域。默认插槽（一般是 ChatMessageList）排成纵向 flex，消息少时把内容推到底部。
- **毛玻璃停靠区**: 底部粘住或固定的容器，带一层背景模糊。里面放滚到底按钮和输入区。
- **滚到底按钮**: 用户往上滚，或有新消息时出现。默认是 ChatLayoutScrollButton。把 showScrollButton 设为 false 就隐藏；要换成别的控件，用 scrollButton 插槽。
- **输入区**: 发消息的输入区域，一般是 ChatComposer。停在毛玻璃层里的底部。
- **空状态**: 还没有消息时居中显示的占位。用 EmptyState 可以保持外观一致。
- **头像**: 消息旁边的发送者头像，一般是 size 为 md 的 Avatar。系统消息不显示。
- **名字**: 消息正文上方的发送者名字。用气泡时放在气泡上；自由内容则放在消息外层。
- **内容**: 消息正文：一个或多个 ChatMessageBubble，或图片、工具调用这类自由插槽内容。非气泡内容可以包进幽灵气泡，好跟气泡的文字列对齐。
- **元信息**: 时间、送达状态，以及消息下方的操作。放在最后一个气泡上，或放在消息外层。

## 示例

### ChatLayout — 面板

窄栏里的会话。输入文字后回车，这句话会出现在记录末尾。容器一窄就会用上紧凑密度。

```vue
<script setup lang="ts">
import { ChatComposer, ChatLayout, ChatMessage, ChatMessageBubble, ChatMessageList, ChatSystemMessage } from '@astryx-vue/core/Chat'
import { ref } from 'vue'

interface Row {
  id: number
  sender: 'user' | 'assistant'
  text: string
}

let nextId = 3
const rows = ref<Row[]>([
  { id: 1, sender: 'user', text: 'Can you review the Button component and fix the focus ring?' },
  { id: 2, sender: 'assistant', text: 'Found the issue — the border radius was hardcoded. Replaced with the theme token.' },
])

function onSubmit(value: string): void {
  rows.value = [...rows.value, { id: nextId, sender: 'user', text: value }]
  nextId += 1
}
</script>

<template>
  <ChatLayout class="chat-preview__panel">
    <ChatMessageList>
      <ChatSystemMessage variant="divider">
        Today
      </ChatSystemMessage>
      <ChatMessage v-for="row in rows" :key="row.id" :sender="row.sender">
        <ChatMessageBubble v-if="row.sender === 'user'">
          {{ row.text }}
        </ChatMessageBubble>
        <div v-else class="chat-preview__lines">
          <p>{{ row.text }}</p>
        </div>
      </ChatMessage>
    </ChatMessageList>
    <template #composer>
      <ChatComposer placeholder="Ask something..." @submit="onSubmit" />
    </template>
  </ChatLayout>
</template>
```

### ChatComposer — 外壳

带附件抽屉、顶栏按钮和语音按钮的输入区。没有字时发送按钮是浅色并且不能点，写上字才变成可点的实心按钮。只有附件、没有字，仍然不能发送。点抽屉把手可以收起附件。点麦克风会在支持语音的浏览器里开始听写。

```vue
<script setup lang="ts">
import { Badge } from '@astryx-vue/core/Badge'
import { ChatComposer, ChatComposerDrawer, ChatDictationButton } from '@astryx-vue/core/Chat'
import { Icon } from '@astryx-vue/core/Icon'
import { IconButton } from '@astryx-vue/core/IconButton'
import { reactive, ref } from 'vue'

const listening = ref(false)
const dictation = reactive({
  isSupported: true,
  bands: [0.12, 0.4, 0.72, 0.36, 0.16],
  volume: 0.05,
  get isListening() {
    return listening.value
  },
  toggle() {
    listening.value = !listening.value
  },
})
const sent = ref('')

function onSubmit(value: string): void {
  sent.value = value
}
</script>

<template>
  <div class="chat-preview-composer">
    <ChatComposer placeholder="Ask about the notes..." @submit="onSubmit">
      <template #drawer>
        <ChatComposerDrawer :count="2" label="Attachments">
          <Badge label="notes.md" variant="neutral" />
          <Badge label="focus.png" variant="neutral" />
        </ChatComposerDrawer>
      </template>
      <template #headerActions>
        <IconButton label="Add attachment" size="sm" variant="ghost">
          <Icon name="carbon:attachment" />
        </IconButton>
      </template>
      <template #headerContext>
        Balanced
      </template>
      <template #footerActions>
        <ChatDictationButton :dictation="dictation" />
      </template>
    </ChatComposer>
    <p v-if="sent" class="chat-preview__caption">
      {{ sent }}
    </p>
  </div>
</template>
```

### ChatComposer — 状态

上面一条正在生成，编辑停用，停止按钮仍可点。中间是紧凑、无阴影，并带警告。下面把错误放在输入区上方。

```vue
<script setup lang="ts">
import { ChatComposer } from '@astryx-vue/core/Chat'
import { ref } from 'vue'

const streaming = ref(true)
</script>

<template>
  <div class="chat-preview-composer">
    <ChatComposer is-disabled is-stop-shown model-value="Still writing the answer." @stop="streaming = false" />
    <ChatComposer
      density="compact"
      elevation="none"
      placeholder="Flat and compact"
      :status="{ type: 'warning', message: 'The connection is slow.' }"
    />
    <ChatComposer
      :status="{ type: 'error', message: streaming ? 'Could not send the last message.' : 'Cleared.' }"
      status-position="top"
    />
  </div>
</template>
```

### ChatComposerInput — 提及

词首输入 @ 可以连续打多个词，比如 Ada Lov，列表会跟着缩到 Ada Lovelace。方向键把高亮移到某一行，回车插入的是这一行，不是列表里的第一条。没有匹配的句子会把列表藏起来，已经打的字还在。词首输入 / 是单词命令，打一个空格列表就关上。选中的人变成标记，命令按原文插入。

```vue
<script setup lang="ts">
import type { ChatComposerTrigger } from '@astryx-vue/core/Chat'
import { createStaticSource } from '@astryx-vue/core'
import { ChatComposer, ChatComposerInput } from '@astryx-vue/core/Chat'
import { ref } from 'vue'

const people = createStaticSource([
  { id: 'ada', label: 'Ada Lovelace' },
  { id: 'maria', label: 'María José García' },
  { id: 'lin', label: 'Lin', auxiliaryData: { group: 'Reviewers' } },
  { id: 'grace', label: 'Grace Hopper', auxiliaryData: { group: 'Reviewers' } },
])

const commands = createStaticSource([
  { id: 'summarize', label: 'summarize' },
  { id: 'translate', label: 'translate' },
])

const triggers: ChatComposerTrigger[] = [
  {
    character: '@',
    hasMultiWordQuery: true,
    menuLabel: 'People',
    searchSource: people,
    onSelect: item => ({ value: `@${item.id}`, label: `@${item.label}`, variant: 'blue' }),
  },
  {
    character: '/',
    menuLabel: 'Commands',
    searchSource: commands,
    onSelect: item => `/${item.label} `,
  },
]

const sent = ref('')

function onSubmit(value: string): void {
  sent.value = value
}
</script>

<template>
  <div class="chat-preview-composer">
    <ChatComposer placeholder="Mention with @, or run /summarize" @submit="onSubmit">
      <template #input>
        <ChatComposerInput :triggers="triggers" />
      </template>
    </ChatComposer>
    <p v-if="sent" class="chat-preview__caption">
      {{ sent }}
    </p>
  </div>
</template>
```

### ChatTokenizedText — 气泡里的标记

同一组标记既能出现在输入框里，也能出现在已经发出的气泡里。

```vue
<script setup lang="ts">
import { ChatMessage, ChatMessageBubble, ChatTokenizedText } from '@astryx-vue/core/Chat'

const tokens = [
  { value: '@ada', label: '@Ada', variant: 'blue' as const },
]
</script>

<template>
  <div class="chat-preview-stack">
    <ChatMessage sender="user">
      <ChatMessageBubble>
        <ChatTokenizedText text="Ask @ada to check the focus ring." :tokens="tokens" />
      </ChatMessageBubble>
    </ChatMessage>
  </div>
</template>
```

### ChatLayoutScrollButton — 文案

回到底部的按钮可以只显示图标，也可以带上新消息、未读回复这类说明。

```vue
<script setup lang="ts">
import { ChatLayoutScrollButton } from '@astryx-vue/core/Chat'
</script>

<template>
  <div class="chat-preview-stack chat-preview-stack--center">
    <p class="chat-preview__caption">
      {{ 'Labels expand the button to give context' }}
    </p>
    <div class="chat-preview-row chat-preview-row--center">
      <ChatLayoutScrollButton is-visible />
      <ChatLayoutScrollButton is-visible label="New messages" />
      <ChatLayoutScrollButton is-visible label="3 unread replies" />
    </div>
  </div>
</template>
```

### ChatMessage — 头像与名字

带头像和发送者名字。正文以气泡开头时，名字放在气泡上；没有气泡时，名字放在消息上。

```vue
<script setup lang="ts">
import { Avatar } from '@astryx-vue/core/Avatar'
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata } from '@astryx-vue/core/Chat'

const agent = 'Agent'
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="assistant">
      <template #avatar>
        <Avatar :name="agent" size="md" />
      </template>
      <ChatMessageBubble :name="agent">
        {{ 'I reviewed the pull request. The changes look solid — clean code and good test coverage.' }}
        <template #metadata>
          <ChatMessageMetadata timestamp="10:15 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="user">
      <ChatMessageBubble>
        {{ 'Thanks! Merging it now.' }}
        <template #metadata>
          <ChatMessageMetadata status="read" timestamp="10:16 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="assistant">
      <template #avatar>
        <Avatar :name="agent" size="md" />
      </template>
      <ChatMessageBubble :name="agent">
        {{ 'I can run the deployment pipeline once it lands. Just let me know.' }}
        <template #metadata>
          <ChatMessageMetadata timestamp="10:17 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessage — 透明气泡

透明气泡没有可见边界，仍保留内边距以便对齐。适合助手的长回复。

```vue
<script setup lang="ts">
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata } from '@astryx-vue/core/Chat'
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="assistant">
      <ChatMessageBubble variant="ghost">
        {{ 'Here is an analysis of your production metrics from last week. Traffic peaked at 12,400 requests per second on Wednesday, with a p99 latency of 45ms. Error rate stayed below 0.1% across all endpoints.' }}
        <template #metadata>
          <ChatMessageMetadata footer="Claude Opus 4.6" timestamp="9:45 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="user">
      <ChatMessageBubble>
        {{ 'That looks great. Can you compare it to the week before?' }}
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="assistant">
      <ChatMessageBubble variant="ghost">
        {{ 'Compared to the previous week, traffic is up 8% and latency improved by 3ms. The deployment on Tuesday seems to have helped.' }}
        <template #metadata>
          <ChatMessageMetadata footer="Claude Opus 4.6" timestamp="9:46 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessage — 多气泡

用 group 收紧同一侧的圆角，把同一人连续发出的几条气泡连在一起。

```vue
<script setup lang="ts">
import { Avatar } from '@astryx-vue/core/Avatar'
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata } from '@astryx-vue/core/Chat'

const agent = 'Agent'
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="user">
      <ChatMessageBubble group="first">
        {{ 'I have a couple of questions about the new API.' }}
      </ChatMessageBubble>
      <ChatMessageBubble group="middle">
        {{ 'First, how should we handle pagination?' }}
      </ChatMessageBubble>
      <ChatMessageBubble group="last">
        {{ 'And second, what\'s the rate limit?' }}
        <template #metadata>
          <ChatMessageMetadata status="delivered" timestamp="11:00 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="assistant">
      <template #avatar>
        <Avatar :name="agent" size="md" />
      </template>
      <ChatMessageBubble group="first" :name="agent">
        {{ 'Great questions! For pagination, use cursor-based with a limit parameter. The response includes a nextCursor field.' }}
      </ChatMessageBubble>
      <ChatMessageBubble group="last">
        {{ 'Rate limit is 100 requests per minute per API key. You\'ll get a 429 response with a Retry-After header if you exceed it.' }}
        <template #metadata>
          <ChatMessageMetadata timestamp="11:01 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessageBubble — 自定义内容

气泡里可以放对齐到文字列的自定义内容。透明气泡拉满消息列，时间贴在气泡的元信息上。

```vue
<script setup lang="ts">
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata } from '@astryx-vue/core/Chat'
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="user">
      <ChatMessageBubble>
        {{ 'Can you pull up the Q3 report?' }}
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="assistant">
      <ChatMessageBubble variant="ghost" width="100%" name="Navi">
        {{ 'Sure, here is the artifact from last quarter.' }}
        <div class="chat-preview__card">
          <p class="chat-preview__card-title">
            {{ 'Q3 Performance Report' }}
          </p>
          <p class="chat-preview__card-meta">
            {{ '12 pages · updated 2 days ago' }}
          </p>
        </div>
        <template #metadata>
          <ChatMessageMetadata footer="Claude Opus 4.6" timestamp="9:45 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessageBubble — 密度

紧凑、均衡、宽松并排。密度决定气泡内边距、圆角，以及成组气泡之间的间距。

```vue
<script setup lang="ts">
import type { ChatDensity } from '@astryx-vue/core/Chat'
import { ChatMessage, ChatMessageBubble, ChatMessageList } from '@astryx-vue/core/Chat'
import { Divider } from '@astryx-vue/core/Divider'

const densities: { id: ChatDensity, label: string }[] = [
  { id: 'compact', label: 'Compact' },
  { id: 'balanced', label: 'Balanced' },
  { id: 'spacious', label: 'Spacious' },
]
</script>

<template>
  <div class="chat-preview-densities">
    <template v-for="(density, index) in densities" :key="density.id">
      <Divider v-if="index > 0" />
      <div>
        <p class="chat-preview__caption">
          {{ density.label }}
        </p>
        <ChatMessageList :density="density.id">
          <ChatMessage sender="assistant">
            <ChatMessageBubble>
              {{ 'The build completed in 4.2 seconds.' }}
            </ChatMessageBubble>
          </ChatMessage>
          <ChatMessage sender="user">
            <ChatMessageBubble>
              {{ 'Ship it to staging.' }}
            </ChatMessageBubble>
          </ChatMessage>
        </ChatMessageList>
      </div>
    </template>
  </div>
</template>
```

### ChatMessageBubble — 分组

一条消息里用 first、middle、last 收紧发送者那一侧的圆角。

```vue
<script setup lang="ts">
import { Avatar } from '@astryx-vue/core/Avatar'
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata } from '@astryx-vue/core/Chat'

const agent = 'Agent'
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="assistant">
      <template #avatar>
        <Avatar :name="agent" size="md" />
      </template>
      <ChatMessageBubble group="first" :name="agent">
        {{ 'I reviewed the three files you shared.' }}
      </ChatMessageBubble>
      <ChatMessageBubble group="middle">
        {{ 'The data model looks solid, but the API handler has a race condition on concurrent writes.' }}
      </ChatMessageBubble>
      <ChatMessageBubble group="last">
        {{ 'I can draft a fix if you want.' }}
        <template #metadata>
          <ChatMessageMetadata timestamp="10:45 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="user">
      <ChatMessageBubble group="first">
        {{ 'Yes please!' }}
      </ChatMessageBubble>
      <ChatMessageBubble group="last">
        {{ 'Also add a test for the concurrent case.' }}
        <template #metadata>
          <ChatMessageMetadata status="delivered" timestamp="10:46 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessageBubble — 元信息

名字和元信息都对齐气泡的内边距。名字放在第一条气泡上，元信息放在最后一条上。

```vue
<script setup lang="ts">
import { Avatar } from '@astryx-vue/core/Avatar'
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata } from '@astryx-vue/core/Chat'

const agent = 'Agent'
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="assistant">
      <template #avatar>
        <Avatar :name="agent" size="md" />
      </template>
      <ChatMessageBubble :name="agent">
        {{ 'Your deployment finished successfully. All 14 checks passed.' }}
        <template #metadata>
          <ChatMessageMetadata footer="Claude Opus 4.6" timestamp="9:15 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="user">
      <ChatMessageBubble>
        {{ 'Great, can you send me the production URL?' }}
        <template #metadata>
          <ChatMessageMetadata status="read" timestamp="9:16 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessageBubble — 变体

实心和透明两种气泡，用户和助手都会用到。实心是普通消息，透明用来对齐、但不画边界。

```vue
<script setup lang="ts">
import { ChatMessage, ChatMessageBubble, ChatMessageList } from '@astryx-vue/core/Chat'
</script>

<template>
  <ChatMessageList>
    <p class="chat-preview__caption">
      {{ 'Filled — sender-colored background (default)' }}
    </p>
    <ChatMessage sender="user">
      <ChatMessageBubble>
        {{ 'Can you summarize the latest deployment logs?' }}
      </ChatMessageBubble>
    </ChatMessage>
    <p class="chat-preview__caption">
      {{ 'Ghost — transparent background, keeps alignment padding' }}
    </p>
    <ChatMessage sender="assistant">
      <ChatMessageBubble variant="ghost">
        {{ 'The last deploy completed at 2:41 PM with zero errors across all three regions.' }}
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessageList — 密度

三种密度并排。侧栏用紧凑，整页对话多用均衡，长文用宽松。行距要和密度分开时，用 gap。

```vue
<script setup lang="ts">
import type { AvatarSize } from '@astryx-vue/core/Avatar'
import type { ChatDensity } from '@astryx-vue/core/Chat'
import { Avatar } from '@astryx-vue/core/Avatar'
import { ChatMessage, ChatMessageBubble, ChatMessageList } from '@astryx-vue/core/Chat'
import { Divider } from '@astryx-vue/core/Divider'

const agent = 'Agent'

const densities: { id: ChatDensity, label: string, avatar: AvatarSize }[] = [
  { id: 'compact', label: 'Compact', avatar: 'sm' },
  { id: 'balanced', label: 'Balanced', avatar: 'md' },
  { id: 'spacious', label: 'Spacious', avatar: 'md' },
]
</script>

<template>
  <div class="chat-preview-densities">
    <template v-for="(density, index) in densities" :key="density.id">
      <Divider v-if="index > 0" />
      <div>
        <p class="chat-preview__caption">
          {{ density.label }}
        </p>
        <ChatMessageList :density="density.id">
          <ChatMessage sender="user">
            <ChatMessageBubble>
              {{ 'How does density work?' }}
            </ChatMessageBubble>
          </ChatMessage>
          <ChatMessage sender="assistant">
            <template #avatar>
              <Avatar :name="agent" :size="density.avatar" />
            </template>
            <ChatMessageBubble>
              {{ 'Density provides default spacing at every level — message gap, bubble padding, and gap between child elements. Use gap to tune row spacing independently.' }}
            </ChatMessageBubble>
          </ChatMessage>
        </ChatMessageList>
      </div>
    </template>
  </div>
</template>
```

### ChatMessageList — 完整会话

一次会话里放上日期分隔、文件名、代码、透明气泡和元信息。

```vue
<script setup lang="ts">
import { Avatar } from '@astryx-vue/core/Avatar'
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata, ChatSystemMessage } from '@astryx-vue/core/Chat'
import { CodeBlock } from '@astryx-vue/core/Code'

const agent = 'Agent'
const bubbleWidth = 'min(100%, 21.25rem)'

const snippet = `const [state, dispatch] = useReducer(
  (state, action) => ({
    ...state,
    [action.field]: action.value,
  }),
  { name: '', email: '' }
);`
</script>

<template>
  <ChatMessageList>
    <ChatSystemMessage variant="divider">
      Today
    </ChatSystemMessage>
    <ChatMessage sender="user">
      <div class="chat-preview__files">
        <span class="chat-preview__file">useReducer.ts</span>
        <span class="chat-preview__file">formState.ts</span>
      </div>
      <ChatMessageBubble>
        Can you review these files?
        <template #metadata>
          <ChatMessageMetadata status="read" timestamp="2:30 PM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="assistant">
      <template #avatar>
        <Avatar :name="agent" size="md" />
      </template>
      <ChatMessageBubble group="first" :width="bubbleWidth">
        Sure! Here's the key pattern from <strong>useReducer.ts</strong>:
      </ChatMessageBubble>
      <ChatMessageBubble group="last" :width="bubbleWidth">
        The reducer is <strong>pure and easy to test</strong> — pass in state and action, assert on the output.
      </ChatMessageBubble>
      <ChatMessageBubble variant="ghost" :width="bubbleWidth">
        <CodeBlock :code="snippet" language="tsx" width="100%" />
        <template #metadata>
          <ChatMessageMetadata timestamp="2:30 PM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatSystemMessage>
      Agent shared a code snippet
    </ChatSystemMessage>
    <ChatMessage sender="user">
      <ChatMessageBubble>
        That's clean, thanks!
        <template #metadata>
          <ChatMessageMetadata status="delivered" timestamp="2:31 PM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessageMetadata — 底部操作

助手回复下面的操作：复制、重试、有用、没用，以及模型名。

```vue
<script setup lang="ts">
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata } from '@astryx-vue/core/Chat'
import { IconButton } from '@astryx-vue/core/IconButton'
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="user">
      <ChatMessageBubble>
        {{ 'Summarize the Q1 revenue report.' }}
        <template #metadata>
          <ChatMessageMetadata status="read" timestamp="9:41 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="assistant">
      <ChatMessageBubble variant="ghost">
        {{ 'Q1 revenue reached $2.4B, up 18% year-over-year. Enterprise subscriptions drove 62% of the growth, while ad revenue held steady at $890M.' }}
        <template #metadata>
          <ChatMessageMetadata timestamp="9:42 AM">
            <template #footer>
              <span class="chat-preview__actions">
                <IconButton label="Copy" variant="ghost" size="sm">
                  <span class="i-carbon-copy" />
                </IconButton>
                <IconButton label="Retry" variant="ghost" size="sm">
                  <span class="i-carbon-renew" />
                </IconButton>
                <IconButton label="Thumbs up" variant="ghost" size="sm">
                  <span class="i-carbon-thumbs-up" />
                </IconButton>
                <IconButton label="Thumbs down" variant="ghost" size="sm">
                  <span class="i-carbon-thumbs-down" />
                </IconButton>
                <span>GPT-4o</span>
              </span>
            </template>
          </ChatMessageMetadata>
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessageMetadata — 状态

五种送达状态：发送中、已发送、已送达、已读、失败。每条都带时间。

```vue
<script setup lang="ts">
import type { ChatMessageStatus } from '@astryx-vue/core/Chat'
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata } from '@astryx-vue/core/Chat'

const rows: { status: ChatMessageStatus, text: string }[] = [
  { status: 'sending', text: 'Deploying the update now…' },
  { status: 'sent', text: 'Config pushed to staging.' },
  { status: 'delivered', text: 'Verified on the staging cluster.' },
  { status: 'read', text: 'Looks good — promoting to prod.' },
  { status: 'error', text: 'Rollback triggered, checking logs.' },
]
</script>

<template>
  <ChatMessageList>
    <ChatMessage v-for="row in rows" :key="row.status" sender="user">
      <ChatMessageBubble>
        {{ row.text }}
        <template #metadata>
          <ChatMessageMetadata :status="row.status" timestamp="10:15 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessageMetadata — 时间

只有时间的元信息。可以是钟点，也可以是相对时间。

```vue
<script setup lang="ts">
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata } from '@astryx-vue/core/Chat'
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="user">
      <ChatMessageBubble>
        {{ 'Thanks — any blockers I should know about?' }}
        <template #metadata>
          <ChatMessageMetadata timestamp="2:30 PM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="assistant">
      <ChatMessageBubble variant="ghost">
        {{ 'Relative timestamps work too — helpful for older messages where the exact time matters less than recency.' }}
        <template #metadata>
          <ChatMessageMetadata timestamp="5 months ago" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatSystemMessage — 图标

系统消息前面放一个图标，用来区分加入、加密、生成和核验。

```vue
<script setup lang="ts">
import { ChatMessageList, ChatSystemMessage } from '@astryx-vue/core/Chat'
</script>

<template>
  <ChatMessageList>
    <p class="chat-preview__caption">
      {{ 'Icons reinforce the message type' }}
    </p>
    <ChatSystemMessage>
      <template #icon>
        <span class="i-carbon-user-follow" style="width: 14px; height: 14px" />
      </template>
      {{ 'Jordan was added to the conversation' }}
    </ChatSystemMessage>
    <ChatSystemMessage>
      <template #icon>
        <span class="i-carbon-locked" style="width: 14px; height: 14px" />
      </template>
      {{ 'Messages are end-to-end encrypted' }}
    </ChatSystemMessage>
    <ChatSystemMessage>
      <template #icon>
        <span class="i-carbon-renew" style="width: 14px; height: 14px" />
      </template>
      {{ 'Agent is generating a response…' }}
    </ChatSystemMessage>
    <ChatSystemMessage>
      <template #icon>
        <span class="i-carbon-checkmark" style="width: 14px; height: 14px" />
      </template>
      {{ 'Conversation verified by admin' }}
    </ChatSystemMessage>
  </ChatMessageList>
</template>
```

### ChatSystemMessage — 状态更新

会话里的状态流水：谁加入、话题改了、谁离开、日期，以及已解决。

```vue
<script setup lang="ts">
import { ChatMessageList, ChatSystemMessage } from '@astryx-vue/core/Chat'
</script>

<template>
  <ChatMessageList>
    <ChatSystemMessage variant="divider">
      {{ 'March 14, 2026' }}
    </ChatSystemMessage>
    <ChatSystemMessage>
      <template #icon>
        <span class="i-heroicons-user-plus" style="width: 20px; height: 20px" />
      </template>
      {{ 'Sarah Chen joined the conversation' }}
    </ChatSystemMessage>
    <ChatSystemMessage>
      {{ 'Topic changed to "Q2 Launch Planning"' }}
    </ChatSystemMessage>
    <ChatSystemMessage variant="divider">
      {{ 'March 15, 2026' }}
    </ChatSystemMessage>
    <ChatSystemMessage>
      <template #icon>
        <span class="i-heroicons-user-minus" style="width: 20px; height: 20px" />
      </template>
      {{ 'Alex Rivera left the conversation' }}
    </ChatSystemMessage>
    <ChatSystemMessage variant="divider">
      {{ 'Today' }}
    </ChatSystemMessage>
    <ChatSystemMessage>
      <template #icon>
        <span class="i-heroicons-check-circle" style="width: 20px; height: 20px" />
      </template>
      {{ 'Conversation marked as resolved' }}
    </ChatSystemMessage>
  </ChatMessageList>
</template>
```

### ChatSystemMessage — 变体

默认和分隔线并排。默认用来写一行状态，分隔线用来标日期或分段。

```vue
<script setup lang="ts">
import { ChatMessageList, ChatSystemMessage } from '@astryx-vue/core/Chat'
</script>

<template>
  <div class="chat-preview-columns" style="grid-template-columns: repeat(2, minmax(0, 1fr));">
    <div>
      <p class="chat-preview__caption">
        {{ 'Default' }}
      </p>
      <ChatMessageList class="chat-preview-pane">
        <ChatSystemMessage>
          {{ 'Alex joined the conversation' }}
        </ChatSystemMessage>
        <ChatSystemMessage>
          {{ 'Conversation marked as resolved' }}
        </ChatSystemMessage>
      </ChatMessageList>
    </div>
    <div>
      <p class="chat-preview__caption">
        {{ 'Divider' }}
      </p>
      <ChatMessageList class="chat-preview-pane">
        <ChatSystemMessage variant="divider">
          {{ 'March 15, 2026' }}
        </ChatSystemMessage>
        <ChatSystemMessage variant="divider">
          {{ 'Today' }}
        </ChatSystemMessage>
      </ChatMessageList>
    </div>
  </div>
</template>
```

### ChatToolCalls — 可展开

工具调用可以展开结果，比如差异和命令输出。点一行就能打开。

```vue
<script setup lang="ts">
import type { ChatToolCallItem } from '@astryx-vue/core/Chat'
import { ChatMessage, ChatMessageList, ChatToolCalls } from '@astryx-vue/core/Chat'
import { computed } from 'vue'

const calls = computed<ChatToolCallItem[]>(() => [
  {
    name: 'edit',
    status: 'complete',
    node: 'cli:remote-server',
    target: 'src/utils/formatDate.ts',
    additions: 6,
    deletions: 3,
    duration: '85ms',
    detail: 'diff',
  },
  {
    name: 'bash',
    status: 'complete',
    node: 'cli:remote-server',
    target: 'yarn test',
    duration: '1.8s',
    detail: 'ok',
  },
  {
    name: 'web_search',
    status: 'complete',
    target: 'Intl.DateTimeFormat locale options',
    duration: '1.2s',
    detail: 'ok',
  },
])

const diff = `function formatDate(value) {
-  return value.toLocaleDateString();
+  return new Intl.DateTimeFormat(locale).format(value);
}`
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="assistant">
      <ChatToolCalls :calls="calls" default-is-expanded>
        <template #detail="{ call }">
          <pre class="chat-preview__code">{{ call.name === 'edit' ? diff : 'passed' }}</pre>
        </template>
      </ChatToolCalls>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatToolCalls — 简单

上面是单条调用，下面是可折叠的一组，并带上增删行数。

```vue
<script setup lang="ts">
import type { ChatToolCallItem } from '@astryx-vue/core/Chat'
import { ChatMessage, ChatMessageList, ChatToolCalls } from '@astryx-vue/core/Chat'
import { computed } from 'vue'

const single = computed<ChatToolCallItem[]>(() => [
  { name: 'bash', status: 'complete', target: 'git status', duration: '1.2s' },
])

const group = computed<ChatToolCallItem[]>(() => [
  { name: 'read', status: 'complete', target: 'src/components/DataGrid.tsx', duration: '30ms' },
  { name: 'edit', status: 'complete', target: 'src/components/DataGrid.tsx', additions: 24, deletions: 8, duration: '85ms' },
  { name: 'edit', status: 'complete', target: 'src/components/DataGrid.test.tsx', additions: 45, duration: '60ms' },
])
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="assistant">
      <ChatToolCalls :calls="single" />
      <ChatToolCalls :calls="group" default-is-expanded />
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatToolCalls — 各种状态

等待、运行、完成、失败四种状态放在同一组里。

```vue
<script setup lang="ts">
import type { ChatToolCallItem } from '@astryx-vue/core/Chat'
import { ChatMessage, ChatMessageList, ChatToolCalls } from '@astryx-vue/core/Chat'
import { computed } from 'vue'

const calls = computed<ChatToolCallItem[]>(() => [
  { name: 'bash', status: 'pending', target: 'yarn build' },
  { name: 'bash', status: 'running', target: 'yarn test' },
  { name: 'edit', status: 'complete', target: 'src/App.tsx', additions: 8, deletions: 2, duration: '120ms' },
  {
    name: 'bash',
    status: 'error',
    target: 'yarn lint',
    errorMessage: '3 lint errors found',
  },
])
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="assistant">
      <ChatToolCalls :calls="calls" default-is-expanded />
    </ChatMessage>
  </ChatMessageList>
</template>
```

## ChatLayout

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `density` | `'compact' \| 'balanced' \| 'spacious'` | `'balanced'` | 间距、气泡宽度和底部磨砂。单条消息可以自己覆盖。 |
| `scrollRef` | `HTMLElement \| null` | - | 代替布局自己滚动的元素。不传时由布局滚动。自己滚动时底部停靠是 sticky，外部滚动时改成 fixed。 |
| `showScrollButton` | `boolean` | `true` | 是否渲染内置的回到底部按钮。用 `scrollButton` 插槽替换，或者根本不需要这个按钮时设为 `false`。 |

| 名称 | 说明 |
| --- | --- |
| `default` | 会话记录，通常放一个 `ChatMessageList`。 |
| `composer` | 贴在底部，位于磨砂层之上。 |
| `empty` | 没有默认插槽时显示。 |
| `scrollButton` | 替换内置按钮。参数：`isVisible`、`label`（只在有未读新消息时有值）、`scrollToBottom`。 |

模板引用暴露 <code>element</code> 和 <code>scrollToBottom()</code>。<code>scrollToBottom</code> 会清掉「有新消息」，再用弹簧滚到底部。

## ChatLayoutScrollButton

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `isVisible` | `boolean` | - | 胶囊是否可见。隐藏时仍挂在树上，方便淡出，并带 `inert`。 |
| `label` | `string` | - | 把胶囊拉宽。不传时只有向下的图标。无障碍名称回退到「滚动到底部」。 |

按下按钮时发出 <code>click</code>。

## ChatMessageList

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `align` | `'top' \| 'bottom'` | `'bottom'` | 消息不到一屏时贴顶还是贴底。`'bottom'` 在消息前面留一块弹性空白；`'top'` 不留，消息从顶上排。 |
| `density` | `'compact' \| 'balanced' \| 'spacious'` | `'balanced'` | 传给没有自己设置密度的消息。 |
| `gap` | `SpacingStep` | - | 顶层行之间的间距，走间距刻度。只改间距，不改内边距。 |
| `scrollToTopAction` | `() => Promise<void>` | - | 顶部哨兵进入滚动容器时加载更早的消息。Promise 结束前显示加载指示。上一次还没结束时，再次进入不会重复触发。 |
| `isStreaming` | `boolean` | `false` | 助手消息还在增长时设置 `aria-busy`。 |
| `label` | `string` | 本地化的「消息列表」 | 日志的无障碍名称。 |

<code>default</code> 插槽是各行。<code>emptyState</code> 在没有 <code>default</code> 时显示，<code>empty</code> 是它的别名。根节点是 <code>role="log"</code>，<code>aria-live="polite"</code>。

## ChatMessage

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `sender` | `'user' \| 'assistant' \| 'system'` | - | 这条消息是谁发的。`system` 居中，头像、名字和元信息都忽略。 |
| `name` | `string` | - | 纯文本名字。正文以气泡开头时，更适合把名字放在气泡上。 |
| `density` | `'compact' \| 'balanced' \| 'spacious'` | 先看列表，再回退到 `'balanced'` | 只作用于这一行的密度。 |

插槽：<code>default</code>（正文）、<code>avatar</code>、<code>name</code>（替换属性）、<code>metadata</code>。article 的名称优先用可见名字，否则是「来自{发送者}的消息」。

## ChatMessageBubble

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `variant` | `'filled' \| 'ghost'` | `'filled'` | `filled` 有发送者底色。`ghost` 去掉背景和上下内边距，混排内容仍然对齐。 |
| `name` | `string` | - | 与气泡内边距对齐的名字。 |
| `group` | `'first' \| 'middle' \| 'last'` | - | 多条气泡叠在一起时收紧一角。单独一条不用传。 |

发送者和密度来自外层 <code>ChatMessage</code>。插槽：<code>default</code>、<code>name</code>、<code>metadata</code>。属性落到气泡元素上。

## ChatMessageMetadata

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `timestamp` | `string` | - | 时间等短文本。`timestamp` 插槽会替换它。 |
| `footer` | `string` | - | 次要文字，例如模型名。`footer` 插槽会替换它。 |
| `status` | `'sending' \| 'sent' \| 'delivered' \| 'read' \| 'error'` | - | 投递状态。时间、补充文字和状态都没有时，这一行不渲染。 |

状态图标的无障碍名称会本地化，例如「消息已读」。用户消息上，各项从行尾开始排。

## ChatSystemMessage

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `variant` | `'default' \| 'divider'` | `'default'` | `default` 是居中的一行。`divider` 两侧各有一条线。根节点是 `role="status"`。 |

插槽：<code>default</code>，以及 <code>icon</code>（<code>divider</code> 时忽略）。

## ChatToolCalls

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `calls` | `ChatToolCallItem[]` | - | 一轮回复里的工具调用。空数组不渲染。每项可以带 `name`、`status`、`target`、`duration`、`node`、`additions`、`deletions`、`stats`、`errorMessage`、`key`、`resultDetail`、`data`。`detail` 是 `resultDetail` 的别名。 |
| `label` | `string` | 本地化的「{count} 个工具调用」 | 分组展开时的标题。 |
| `v-model:isExpanded` | `boolean` | - | 多条调用这一组的受控展开状态。 |
| `defaultIsExpanded` | `boolean` | `false` | `isExpanded` 未绑定时的初始状态。 |

发出 <code>expandedChange</code>，参数是下一个布尔值。<code>detail</code> 插槽收到 <code>{ call }</code>，用来替换 <code>call.resultDetail</code>。分组收起时只显示最新一条；面板在打开前是 <code>inert</code>。等待中和进行中用加载指示。错误会放进一段视觉隐藏的文字里读出来。

## ChatReasoning

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `label` | `string` | 本地化的「思考中」 | 标题。 |
| `duration` | `string` | - | 流式结束后跟在标题后面，例如 `12s`。 |
| `isStreaming` | `boolean` | `false` | 标题闪动，并藏起预览和耗时。 |
| `v-model:isExpanded` | `boolean` | - | 受控展开状态。 |
| `defaultIsExpanded` | `boolean` | `false` | `isExpanded` 未绑定时的初始状态。 |

默认插槽是完整推理。只有一个文本节点时，它也是收起状态的预览。发出 <code>expandedChange</code>。收起时面板是 <code>inert</code>。

## ChatTypingIndicator

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `names` | `string[]` | - | 正在输入的人。空字符串会被丢掉。不传名字时只有三个点。一个人、两个人、三个及以上各有一句文案。 |

根节点是 <code>role="status"</code>，并带 <code>aria-live="polite"</code>。文案就写在这个区域里，不再经 <code>useAnnounce</code> 播第二遍。

## ChatUnreadDivider

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `label` | `string` | 本地化的「新消息」 | 线末端的短标签。无障碍名称是「{label}以下是未读消息」。 |

根节点是 <code>role="separator"</code>。它没有 <code>.astryx-chat-message</code>，新消息追踪不会把它当成一条消息。

## ChatComposer

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `modelValue` | `string` | - | 受控草稿，用 `v-model` 绑定。不传时由外壳自己保存。 |
| `placeholder` | `string` | 当前语言的「输入消息……」 | 草稿为空时的占位文字。 |
| `isDisabled` | `boolean` | `false` | 停用编辑和发送。正在生成时如果显示停止，停止按钮仍然可以点。 |
| `density` | `'compact' \| 'balanced' \| 'spacious'` | `'balanced'` | 外壳内边距。宽松和均衡用同一套间距。 |
| `elevation` | `'low' \| 'none'` | `'low'` | 低阴影在悬停和焦点时加深。none 是平的，靠边框区分。只有键盘把焦点放进编辑器时才画出焦点环。 |
| `isStopShown` | `boolean` | `false` | 用停止代替发送。 |
| `status` | `{ type: 'error' \| 'warning', message?: string }` | - | 错误用 alert，警告用 status。 |
| `statusPosition` | `'top' \| 'bottom'` | `'bottom'` | 状态条贴在外壳上方或下方，并伸进圆角后面。 |

| 名称 | 参数 | 说明 |
| --- | --- | --- |
| `submit` | `value: string` | 草稿去掉首尾空白后仍有内容时发出，然后清空草稿。 |
| `stop` | - | 停止被点下。 |
| `change` | `value: string` | 草稿变化，包括发送后的清空。 |
| `update:modelValue` | `value: string` | 和 change 一起发出，给 `v-model` 用。 |

插槽有 drawer、headerActions、headerContext、input、footerActions、sendActions、sendButton。模板引用暴露 element。

## ChatComposerInput

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `modelValue` | `string` | - | 单独受控时使用。放在外壳里且不传时，跟外壳的草稿走。 |
| `placeholder` | `string` | 外壳或当前语言的占位 | 空编辑器上的提示。 |
| `maxRows` | `number` | `8` | 超过这么多行后开始内部滚动。一行按 22px 算。 |
| `triggers` | `ChatComposerTrigger[]` | - | 在词首输入触发字符时打开建议列表。触发器上的 `hasMultiWordQuery` 让查询穿过空格和标点，直到换行、超过 64 个字符，或同一个触发字符再次出现。不设时，空格或制表符结束查询。多词查询没有匹配、也没写 `emptySearchText` 时，列表先藏起来。有触发器时编辑器是 combobox。 |
| `debounceMs` | `number` | `150` | 异步搜索的等待。同步结果不延迟，第一次异步搜索立刻开始。 |
| `hasHistory` | `boolean` | `true` | 光标在开头时上方向键取回上一条已发送草稿，在末尾时下方向键往回走。 |
| `label` | `string` | 当前语言的「消息输入框」 | 编辑器的无障碍名称。 |
| `isDisabled` | `boolean` | 外壳的停用状态 | 停用编辑。 |
| `pasteAsToken` | `useChatPasteAsToken 的返回值 \| false` | 超过 200 字变成标记 | 传 false 时长文本按原文插入。 |
| `pasteAction` | `(event, text) => boolean \| void` | - | 纯文本粘贴的第一站。返回 true 时输入框不再自己插入。 |

事件有 change、update:modelValue、submit、files、keydown。files 来自粘贴或拖入的文件，拖入不会让浏览器打开文件。keydown 在建议列表处理完之后发出，preventDefault 会跳过回车发送和历史。模板引用上有 insertToken、expandToken、insertText、focus、getValue。

## ChatComposerDrawer

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `count` | `number` | - | 传入后才能折叠，并显示在默认摘要里。 |
| `label` | `string` | 当前语言的「项目」 | 摘要和展开、折叠的无障碍名称都用它。 |
| `isCollapsed` | `boolean` | - | 受控折叠，用 `v-model:is-collapsed`。 |
| `defaultIsCollapsed` | `boolean` | `false` | 未绑定时的初始状态。 |

collapsedChange 在折叠状态变化时发出。默认插槽是展开后的内容。collapsedSummary 替换计数和名称，按钮自己仍带无障碍名称。

## ChatSendButton

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `isStopShown` | `boolean` | 外壳的停止状态 | 停止用次要按钮，发送用主要按钮。停止不受停用影响。 |
| `isDisabled` | `boolean` | 草稿为空或外壳停用 | 只停用发送。 |
| `size` | `'sm' \| 'md'` | `'md'` | 按钮尺寸。外壳会把圆角收成和外边框同心的圆形。 |

send、stop、click 三个事件。sendIcon 和 stopIcon 替换上箭头和方块。放在外壳里且没有自己监听时，点击会提交当前草稿或发出外壳的停止。

## ChatDictationButton

需要传入 useChatDictation 或 useSpeechRecognition 的返回值。size 默认 md。isHiddenWhenUnsupported 默认 true，不支持时不渲染；设为 false 时按钮留着并停用。label 覆盖开始或停止听写的无障碍名称。听的时候按钮上是五根音量条，音量过大时颜色从强调色混向错误色。

## ChatTokenizedText

默认插槽放消息文字；要直接传字符串时用 text 属性，效果一样。tokens 里每个非空 value 按字面替换成徽章或自定义内容。空 value 会被忽略。同一组对象可以同时给输入框的 onSelect 和这里用。

## ChatComposerTokenElement

包住一枚标记，让它在可编辑区域里不可编辑，并带上序列化用的值。一般由输入框自己插入，需要在气泡外单独画一枚时也可以直接用。

## 组合式函数

<code>useChatStreamScroll({ scrollRef })</code> 跟着 <code>scrollRef</code> 的底部走。内容第一次能滚时直接跳到底；之后的增长用弹簧，除非用户要求减少动效。向上滚会立刻松开。停在距底部 10px 以内会重新贴住。<code>isScrolledUp</code> 决定按钮出不出现。

<code>useChatNewMessages({ isLocked, onResize })</code> 看着记录高度。未贴底时，最后一个 <code>.astryx-chat-message</code> 换了，<code>hasNewMessages</code> 变成 true，直到 <code>dismiss()</code>。<code>ChatLayout</code> 会播报这次变化。

<code>useChatStreamScroll</code>、<code>useChatNewMessages</code> 和 <code>useAnnounce</code> 从 <code>@astryx-vue/core</code> 导出。减少动效通过 <code>useMediaQuery</code> 读取。

输入套件另外导出 useChatComposerContext、useChatComposerTokens、useChatPasteAsToken、useChatDictation 和 useSpeechRecognition。它们从 Chat 子路径引入，包根也会再导出一份。语音识别只用浏览器接口，不绑定语音服务。长文本粘贴的默认阈值是 200 个字符。

## 主题

`packages/themes/neutral/chat.css` 把组件变量映射到语义 token。换主题时覆盖这些变量即可。

- `--astryx-chat-bubble-background` / `--astryx-chat-foreground`：填充气泡的底色和文字，对应 `--astryx-color-neutral` 和 `--astryx-color-text-primary`。
- `--astryx-chat-foreground-secondary`、`--astryx-chat-foreground-muted`：元信息、系统行、工具调用的次要文字。
- `--astryx-chat-error`、`--astryx-chat-success`、`--astryx-chat-accent`：失败状态、删除行数、进行中的工具。
- `--astryx-chat-unread-color`：未读分隔线的线和标签，Neutral 映射到 `--astryx-color-border-red`，两条视口用同一个颜色。
- `--astryx-chat-focus`：折叠按钮的键盘焦点描边。
- `--astryx-chat-scroll-background` / `--astryx-chat-scroll-shadow`：回到底部的胶囊。
- `--astryx-chat-radius`：单独气泡的圆角。紧凑密度用 `--astryx-chat-radius-compact`。成组气泡的内侧用 `--astryx-chat-radius-group`。
- `--astryx-duration-fast-max`、`--astryx-duration-medium-max`、`--astryx-duration-slow-max`、`--astryx-ease-standard`：胶囊、折叠和输入中圆点的动效。

## 原生属性

`ChatLayout`、`ChatMessageList`、`ChatMessage`、`ChatSystemMessage`、`ChatToolCalls`、`ChatReasoning`、`ChatTypingIndicator`、`ChatUnreadDivider`、`ChatComposer`、`ChatComposerDrawer`、`ChatComposerInput`、`ChatTokenizedText` 把属性转到根节点。`ChatMessageBubble` 转到气泡，不转到名字或元信息。`ChatMessageMetadata` 转到元信息那一行。`ChatSendButton` 和 `ChatDictationButton` 转到按钮外层。
