# 对话输入框

## 使用方法

草稿需要「敲一个字符弹出候选列表」、行内标签、上下键翻历史、语音转写插入、或者接收文件时，把 `ChatComposerInput` 放进 `ChatComposer` 的 `input` 插槽。

```ts
import { ChatComposerInput } from '@astryx-vue/core/Chat'
```

### 结构

- **输入根节点**: 主题化的容器，负责转发根节点引用和样式入口。
- **可编辑区**: 带名字的 contenteditable 文本框或组合框，文字、选区、键盘、粘贴、拖放都由它管。
- **占位文字**: 只有序列化后的草稿为空时才会出现的提示。
- **候选列表**: 配置的触发字符生效时弹出的建议列表。
- **行内标签**: 不可编辑的徽标或自定义内容，带一个稳定的序列化值。

## 建议

| 指引 | 实践 |
| --- | --- |
| 推荐 | 要用代码往里面塞文字或标签，用组件暴露出来的那组方法；塞进去的内容和手动输入走同一条 `change` 流程。 |
| 推荐 | 粘贴或拖进来的文件通过 `files` 事件接住，文件校验和上传进度留在业务层做。 |
| 避免 | 普通的提及和命令也用自定义标签渲染。用结构化的徽标形式就好，标签才可控、也才能跟着主题走。 |

## 示例

常见的用法、变化和状态。

### ChatComposerInput — 受控

受控的输入框，下面实时显示当前的值。需要在输入区外面读草稿、或者改写草稿时，用受控模式。

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

const value = ref('')

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

<template>
  <VStack :gap="3">
    <ChatComposer v-model="value" @submit="onSubmit">
      <template #input>
        <ChatComposerInput v-model="value" placeholder="Type a message..." />
      </template>
    </ChatComposer>
    <Text type="supporting" color="secondary">
      Value: {{ JSON.stringify(value) }}
    </Text>
  </VStack>
</template>
```

### ChatComposerInput — 禁用

禁用状态的输入区。输入框要看得见但不能操作时用它，例如正在流式输出，或者某个前置条件还没满足。

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

<template>
  <ChatComposer is-disabled>
    <template #input>
      <ChatComposerInput is-disabled placeholder="Input is disabled" />
    </template>
  </ChatComposer>
</template>
```

### ChatComposerInput — 提及

敲 @ 会弹出人员候选列表。选中的人变成行内标签。

```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 { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { TypeaheadItem } from '@astryx-vue/core/Typeahead'
import { h, ref } from 'vue'

interface UserMeta {
  role: string
}

const users = [
  { id: 'cindy', label: 'Cindy Zhang', auxiliaryData: { role: 'Design Systems' } },
  { id: 'alex', label: 'Alex Johnson', auxiliaryData: { role: 'Frontend' } },
  { id: 'sam', label: 'Sam Rivera', auxiliaryData: { role: 'Backend' } },
  { id: 'jordan', label: 'Jordan Lee', auxiliaryData: { role: 'Product' } },
]

const userSource = createStaticSource(users)

const value = ref('')

const mentionTrigger: ChatComposerTrigger = {
  character: '@',
  searchSource: userSource,
  renderItem: item => h(TypeaheadItem, {
    item,
    description: (item.auxiliaryData as UserMeta | undefined)?.role,
  }),
  onSelect: item => ({
    value: `@${item.id}`,
    label: item.label,
    variant: 'blue',
  }),
}
</script>

<template>
  <VStack :gap="3">
    <ChatComposer @submit="value = ''">
      <template #input>
        <ChatComposerInput
          v-model="value"
          :triggers="[mentionTrigger]"
          placeholder="Type @ to mention someone..."
        />
      </template>
    </ChatComposer>
    <Text type="supporting" color="secondary">
      Value: {{ JSON.stringify(value) }}
    </Text>
  </VStack>
</template>
```

### ChatComposerInput — 多种触发

同时支持 @ 提及和 / 命令。两种标签用不同颜色，用户一眼能分清。

```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 { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { ref } from 'vue'

const users = [
  { id: 'cindy', label: 'Cindy Zhang' },
  { id: 'alex', label: 'Alex Johnson' },
  { id: 'sam', label: 'Sam Rivera' },
  { id: 'jordan', label: 'Jordan Lee' },
]

const commands = [
  { id: 'summarize', label: 'summarize' },
  { id: 'translate', label: 'translate' },
  { id: 'search', label: 'search' },
  { id: 'code', label: 'code' },
]

const userSource = createStaticSource(users)
const commandSource = createStaticSource(commands)

const value = ref('')

const mentionTrigger: ChatComposerTrigger = {
  character: '@',
  searchSource: userSource,
  onSelect: item => ({
    value: `@${item.id}`,
    label: item.label,
    variant: 'blue',
  }),
}

const commandTrigger: ChatComposerTrigger = {
  character: '/',
  searchSource: commandSource,
  onSelect: item => ({
    value: `/${item.label}`,
    label: `/${item.label}`,
    variant: 'yellow',
  }),
}
</script>

<template>
  <VStack :gap="3">
    <Text type="supporting" color="secondary">
      Type @ for mentions (blue) or / for commands (yellow)
    </Text>
    <ChatComposer @submit="value = ''">
      <template #input>
        <ChatComposerInput
          v-model="value"
          :triggers="[mentionTrigger, commandTrigger]"
          placeholder="Type @ or / ..."
        />
      </template>
    </ChatComposer>
    <Text type="supporting" color="secondary">
      Value: {{ JSON.stringify(value) }}
    </Text>
  </VStack>
</template>
```

### ChatComposerInput — 斜杠命令

敲 / 选命令的输入框。支持固定指令的 AI 助手或机器可以用它。

```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 { TypeaheadItem } from '@astryx-vue/core/Typeahead'
import { h } from 'vue'

interface CommandMeta {
  description: string
}

const commands = [
  { id: 'summarize', label: 'summarize', auxiliaryData: { description: 'Summarize the conversation' } },
  { id: 'translate', label: 'translate', auxiliaryData: { description: 'Translate text to another language' } },
  { id: 'search', label: 'search', auxiliaryData: { description: 'Search the web or documents' } },
  { id: 'code', label: 'code', auxiliaryData: { description: 'Generate or explain code' } },
  { id: 'help', label: 'help', auxiliaryData: { description: 'Show available commands' } },
]

const commandSource = createStaticSource(commands)

const commandTrigger: ChatComposerTrigger = {
  character: '/',
  searchSource: commandSource,
  renderItem: item => h(TypeaheadItem, {
    item,
    description: (item.auxiliaryData as CommandMeta | undefined)?.description,
  }),
  onSelect: item => ({
    value: `/${item.label}`,
    label: `/${item.label}`,
    variant: 'yellow',
  }),
}
</script>

<template>
  <ChatComposer>
    <template #input>
      <ChatComposerInput
        :triggers="[commandTrigger]"
        placeholder="Type / for commands..."
      />
    </template>
  </ChatComposer>
</template>
```

## 属性

| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `v-model` | `string` | — | 草稿内容。放在 `ChatComposer` 里时，不传就用外层那份草稿。 |
| `placeholder` | `string` | 先用外层的，再用本地化的“Type a message…” | 草稿为空时才显示的提示文字。 |
| `maxRows` | `number` | `8` | 超过几行以后输入框开始自己滚动。 |
| `triggers` | `ChatComposerTrigger[]` | — | 敲哪些字符会弹出候选列表。每个触发项带上字符、搜索来源、以及选中后插什么。 |
| `debounceMs` | `number` | `150` | 异步搜索的等待时间。同步数据源不等待。 |
| `hasHistory` | `boolean` | `true` | 光标在草稿首尾时按方向键，翻出之前提交过的内容。 |
| `label` | `string` | 本地化的“Message input” | 编辑区给读屏用的名字。 |
| `isDisabled` | `boolean` | 跟随外层输入区 | 不能编辑。不传就跟着外层输入区的禁用状态。 |
| `pasteAsToken` | `{ onPaste } \| false` | 内置的长度阈值 | 粘贴的长文本会变成一个标签。传 `useChatPasteAsToken` 的返回值可以改阈值，传 `false` 就原样插入文字。 |
| `pasteAction` | `(event, text) => boolean \| void` | — | 纯文本粘贴时最先拿到机会。返回 `true` 表示你处理了，输入框不再自己插入。 |

## 事件

| 事件 | 参数 | 说明 |
| --- | --- | --- |
| `update:modelValue` | `string` | 序列化后的草稿，标签的值也算在里面。`v-model` 绑的就是它。 |
| `change` | `string` | 草稿变了，不管是手打、粘贴，还是代码塞进去的。 |
| `submit` | `string` | 没按 Shift 的回车。Shift+回车是换行。 |
| `files` | `File[]` | 粘贴或拖进来的文件。拖文件进来不会让页面跳走。 |
| `keydown` | `KeyboardEvent` | 候选列表没有接这个按键时的按下事件。调 `preventDefault()` 可以跳过回车发送和历史翻阅。 |

## 暴露的方法

| 名称 | 类型 | 说明 |
| --- | --- | --- |
| `insertToken` | `(token) => string \| undefined` | 在光标处塞一个标签，返回它的 id。 |
| `expandToken` | `(id: string) => void` | 把一个标签还原成它序列化出来的文字。 |
| `insertText` | `(text: string) => void` | 在光标处插入纯文本。 |
| `focus` | `() => void` | 聚焦到编辑区，用户原来在这个框里的光标位置不动。 |
| `getValue` | `() => string` | 序列化后的草稿，标签的值也算在里面。 |
