# 对话消息列表

## 使用方法

`ChatMessageList` 是装消息的容器，只负责排版：它把疏密传给里面的消息，也支持滚到顶部加载更早的消息。它自带 `role="log"` 和 `aria-live="polite"`，读屏能把它当消息记录来读。消息不多、填不满时，底下有一块弹性占位把消息顶到底部；设成 `align="top"` 就改成从顶上开始排。

消息放进默认插槽，一般是 `ChatMessage` 和 `ChatSystemMessage`。`density` 会传给没有自己指定疏密的那些消息。`gap` 只改行与行之间的距离，不动内边距——大模型事件流那种行距不均匀的场景用得上。

`scrollToTopAction` 配一个顶部哨兵：哨兵一进入滚动容器就调用它，返回的 Promise 没结束前顶部一直显示加载中。它要跟一个真正会滚动的祖先配合；放在 `ChatLayout` 里时，滚动交给布局。

`isStreaming` 会把整个日志标成 `aria-busy`，读屏就会等助手写完再播报一次，而不是每来一个 token 就重念半句话。

`align` 只管消息不到一屏时停在哪里。默认 `bottom` 保留那块弹性占位，消息就贴在输入框上方；`top` 不留占位，适合文档式的会话记录。一旦消息撑满容器，两种模式占位都收成 0，滚动和布局的自动贴底行为完全一样。

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

<template>
  <ChatMessageList :is-streaming="true">
    <ChatMessage sender="user">
      <ChatMessageBubble>How should I structure a monorepo?</ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="assistant">
      <ChatMessageBubble variant="ghost">
        Use workspaces with a shared packages directory.
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

## 示例

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

### 疏密

紧凑、平衡、宽松三种疏密并排看。侧栏和面板用紧凑，整页对话用平衡，长文阅读用宽松。行距想跟疏密分开调，就用 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>
```

### 完整示例

一段完整的对话，里面有系统消息、多条气泡、加粗文字、代码块和元信息。日期分隔、透明气泡、分组消息、富内容都用上了。

```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>
```

## 属性

| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `align` | `'top' \| 'bottom'` | `'bottom'` | 消息不到一屏时，贴顶还是贴底。`'bottom'` 用一个空白撑满剩下的高度，消息就贴在输入框上方；`'top'` 不放这块空白，消息从顶上开始排。已经超出高度的列表滚动行为两种都一样。 |
| `density` | `ChatDensity` | `'balanced'` | 疏密程度：`'compact'`、`'balanced'` 或 `'spacious'`。会传给里面的消息。 |
| `gap` | `SpacingStep` | 跟疏密 | 顶层行与行之间的距离，按间距刻度取值。只改行距，不动内边距。 |
| `scrollToTopAction` | `() => Promise<void>` | — | 顶部哨兵进入滚动容器时调用，用来加载更早的消息。返回的 Promise 没结束前，顶部一直显示加载中。 |
| `isStreaming` | `boolean` | `false` | 助手还在写的时候把它设上，日志会标成 `aria-busy`。读屏就会等写完再播报一次，而不是一直念半句话。 |
| `label` | `string` | 本地化的「消息列表」 | 这块消息记录给读屏用的名字。 |

## 插槽

- `default`：消息行，一般放 `ChatMessage` 和 `ChatSystemMessage`。
- `emptyState`：没有传默认插槽时显示的内容。
- `empty`：`emptyState` 的别名。

## 暴露

- `element`：根元素 `<div role="log">`。
