# Chat Composer

## Usage

ChatComposer is the message-entry shell for a chat surface. It coordinates a controlled or uncontrolled draft, provides a default token-capable input and send/stop action, and arranges optional drawer, header, footer, and status content. Custom inputs and send controls can join the same value, submission, disabled, stop, and focus contract through `useChatComposerContext()`.

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

### Anatomy

- **Composer frame**: Outer composition root that groups the body with an optional status message above or below it.
- **Composer body**: Rounded, elevated surface containing the header, input, and footer.
- **Drawer**: Content before the body for attachments or other expandable context, typically ChatComposerDrawer.
- **Header**: Row for start-aligned actions and end-aligned contextual information.
- **Input**: Default ChatComposerInput or a custom editor connected through useChatComposerContext().
- **Footer**: Row for caller-supplied footer and send actions plus the primary action.
- **Send or stop action**: Default ChatSendButton or a custom sendButton; it submits the current draft or requests interruption.
- **Status message**: Error or warning feedback rendered before or after the composer body.

## Examples

Common configurations, variations, and states.

### ChatComposer — Attachments

Chat composer with removable file tokens in a collapsible drawer. Use when users can attach files or context to their message.

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

const files = ref([
  'feature-prd.docx',
  '2026-roadmap.pdf',
  'user-flow.fig',
  'launch-plan.docx',
  'user-feedback.csv',
  'analytics-kpis.csv',
])

function remove(name: string): void {
  files.value = files.value.filter(item => item !== name)
}
</script>

<template>
  <ChatComposer>
    <template #drawer>
      <ChatComposerDrawer :count="files.length">
        <Token
          v-for="name in files"
          :key="name"
          removable
          :label="name"
          @remove="remove(name)"
        />
      </ChatComposerDrawer>
    </template>
  </ChatComposer>
</template>
```

### ChatComposer — Flat

The composer is raised by default (elevation="low"). Set elevation="none" to flatten it; it then draws a border with the same rest / hover / focus treatment as a text input.

```vue
<script setup lang="ts">
import { Button } from '@astryx-vue/core/Button'
import { ChatComposer } from '@astryx-vue/core/Chat'
import { DropdownMenu } from '@astryx-vue/core/DropdownMenu'
import { Icon } from '@astryx-vue/core/Icon'
import { IconButton } from '@astryx-vue/core/IconButton'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'

const models = [
  { label: 'Auto' },
  { label: 'Model A' },
  { label: 'Model B' },
]

const settings = [
  { label: 'Preferences' },
  { label: 'Keyboard shortcuts' },
  { label: 'About' },
]
</script>

<template>
  <VStack :gap="2">
    <Text type="supporting" color="secondary">
      elevation="none" — flat, with a text-input-style border and focus ring
    </Text>
    <ChatComposer elevation="none">
      <template #footerActions>
        <DropdownMenu :items="models" :menu-width="200">
          <template #trigger="triggerProps">
            <Button v-bind="triggerProps" label="Auto" variant="ghost" size="md">
              <template #icon>
                <Icon name="heroicons:sparkles" />
              </template>
            </Button>
          </template>
        </DropdownMenu>
        <DropdownMenu :items="settings" :menu-width="200">
          <template #trigger="triggerProps">
            <Button v-bind="triggerProps" label="Settings" variant="ghost" size="md">
              <template #icon>
                <Icon name="heroicons:cog-6-tooth" />
              </template>
            </Button>
          </template>
        </DropdownMenu>
      </template>
      <template #sendActions>
        <IconButton label="Microphone" variant="ghost" size="md">
          <Icon name="heroicons:microphone" />
        </IconButton>
      </template>
    </ChatComposer>
  </VStack>
</template>
```

### ChatComposer — Footer Actions

Chat composer with dropdown menus for a model selector and settings in the footer, and a mic button in the send actions slot.

```vue
<script setup lang="ts">
import { Button } from '@astryx-vue/core/Button'
import { ChatComposer } from '@astryx-vue/core/Chat'
import { DropdownMenu } from '@astryx-vue/core/DropdownMenu'
import { Icon } from '@astryx-vue/core/Icon'
import { IconButton } from '@astryx-vue/core/IconButton'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'

const models = [
  { label: 'Auto' },
  { label: 'Model A' },
  { label: 'Model B' },
  { label: 'Model C' },
]

const settings = [
  { label: 'Preferences' },
  { label: 'Keyboard shortcuts' },
  { label: 'About' },
]
</script>

<template>
  <VStack :gap="4">
    <VStack :gap="1">
      <Text type="supporting" color="secondary">
        Model selector and settings dropdowns
      </Text>
      <ChatComposer>
        <template #footerActions>
          <DropdownMenu :items="models" :menu-width="200">
            <template #trigger="triggerProps">
              <Button v-bind="triggerProps" label="Auto" variant="ghost" size="md">
                <template #icon>
                  <Icon name="heroicons:sparkles" />
                </template>
              </Button>
            </template>
          </DropdownMenu>
          <DropdownMenu :items="settings" :menu-width="200">
            <template #trigger="triggerProps">
              <Button v-bind="triggerProps" label="Settings" variant="ghost" size="md">
                <template #icon>
                  <Icon name="heroicons:cog-6-tooth" />
                </template>
              </Button>
            </template>
          </DropdownMenu>
        </template>
        <template #sendActions>
          <IconButton label="Microphone" variant="ghost" size="md">
            <Icon name="heroicons:microphone" />
          </IconButton>
        </template>
      </ChatComposer>
    </VStack>
  </VStack>
</template>
```

### ChatComposer — Full Featured

Chat composer with all slots populated: collapsible attachment drawer, header actions, context progress bar, footer dropdown menus, and mic button. Shows the maximum composer configuration.

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

const files = ref([
  'design-spec.pdf',
  'requirements.docx',
  'wireframes.fig',
  'api-spec.yaml',
  'user-research.csv',
])

function remove(name: string): void {
  files.value = files.value.filter(item => item !== name)
}

const models = [
  { label: 'Auto' },
  { label: 'Model A' },
  { label: 'Model B' },
  { label: 'Model C' },
]

const settings = [
  { label: 'Preferences' },
  { label: 'Keyboard shortcuts' },
  { label: 'About' },
]

const isStreaming = ref(false)
let timer: ReturnType<typeof setTimeout> | undefined

function onSubmit(): void {
  isStreaming.value = true
  timer = setTimeout(() => {
    isStreaming.value = false
  }, 3000)
}

function onStop(): void {
  clearTimeout(timer)
  isStreaming.value = false
}
</script>

<template>
  <VStack :gap="4">
    <Text type="supporting" color="secondary">
      All slots populated
    </Text>
    <ChatComposer
      placeholder="Ask me anything..."
      :is-stop-shown="isStreaming"
      @submit="onSubmit"
      @stop="onStop"
    >
      <template #drawer>
        <ChatComposerDrawer :count="files.length">
          <Token
            v-for="name in files"
            :key="name"
            removable
            :label="name"
            @remove="remove(name)"
          />
        </ChatComposerDrawer>
      </template>
      <template #input>
        <ChatComposerInput style="min-height: 44px" />
      </template>
      <template #headerActions>
        <IconButton label="Mention" variant="ghost" size="sm">
          <Icon name="heroicons:at-symbol" />
        </IconButton>
        <IconButton label="Attach file" variant="ghost" size="sm">
          <Icon name="heroicons:paper-clip" />
        </IconButton>
      </template>
      <template #headerContext>
        <ProgressBar label="Context window" :value="50" :width="96" is-label-hidden />
      </template>
      <template #footerActions>
        <DropdownMenu :items="models" :menu-width="200">
          <template #trigger="triggerProps">
            <Button v-bind="triggerProps" label="Auto" variant="ghost" size="md">
              <template #icon>
                <Icon name="heroicons:sparkles" />
              </template>
            </Button>
          </template>
        </DropdownMenu>
        <DropdownMenu :items="settings" :menu-width="200">
          <template #trigger="triggerProps">
            <Button v-bind="triggerProps" label="Settings" variant="ghost" size="md">
              <template #icon>
                <Icon name="heroicons:cog-6-tooth" />
              </template>
            </Button>
          </template>
        </DropdownMenu>
      </template>
      <template #sendActions>
        <IconButton label="Microphone" variant="ghost" size="md">
          <Icon name="heroicons:microphone" />
        </IconButton>
      </template>
    </ChatComposer>
  </VStack>
</template>
```

### ChatComposer — Simple

Minimal chat composer with a placeholder and submit handler. The simplest way to drop a message input into a page.

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

<template>
  <ChatComposer />
</template>
```

### ChatComposer — Streaming

Chat composer with streaming state and a stop button. Use when the assistant is generating a response and the user can cancel.

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

const isStreaming = ref(false)
const value = ref('Click the send button to start streaming.')

function onSubmit(): void {
  value.value = ''
  isStreaming.value = true
  setTimeout(() => {
    isStreaming.value = false
  }, 5000)
}

function onStop(): void {
  isStreaming.value = false
}
</script>

<template>
  <VStack :gap="4">
    <VStack :gap="1">
      <Text type="supporting" color="secondary">
        {{ isStreaming ? 'Streaming — click stop to cancel' : 'Send a message to start streaming' }}
      </Text>
      <ChatComposer
        v-model="value"
        placeholder="Send a message to start streaming..."
        :is-stop-shown="isStreaming"
        @submit="onSubmit"
        @stop="onStop"
      />
    </VStack>
  </VStack>
</template>
```

### ChatComposer — Validation

Chat composer with error and warning status messages. Status can appear above or below the composer to surface validation or system feedback.

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

<template>
  <VStack :gap="4">
    <VStack :gap="1">
      <Text type="supporting" color="secondary">
        Error message (with top position)
      </Text>
      <ChatComposer
        status-position="top"
        :status="{ type: 'error', message: 'Failed to send message. Please try again.' }"
      />
    </VStack>
    <VStack :gap="1">
      <Text type="supporting" color="secondary">
        Warning message (with bottom position)
      </Text>
      <ChatComposer :status="{ type: 'warning', message: 'Context window is 90% full.' }" />
    </VStack>
  </VStack>
</template>
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `v-model` | `string` | — | The draft. Bound with `v-model`, the composer renders exactly that string; left unbound, it keeps the draft itself. |
| `placeholder` | `string` | Localized “Type a message…” | Text shown while the draft is empty. |
| `isDisabled` | `boolean` | `false` | Stops editing and sending. A visible stop button stays reachable, so the reader can still cancel a reply. |
| `density` | `ChatComposerDensity` | `'balanced'` | Spacing inside the body. `compact`, `balanced`, or `spacious`. |
| `elevation` | `'none' \| 'low'` | `'low'` | `low` is raised and deepens on hover and focus. `none` is flat and draws a border. Keyboard focus in the editor adds the shared focus ring either way; pointer focus does not. |
| `isStopShown` | `boolean` | `false` | Swaps the send action for stop. Use it while a reply is still streaming in. |
| `status` | `ChatComposerStatus` | — | Feedback object, `{ type: 'error' \| 'warning', message }`. The error type is announced as an alert, the warning type as a status. |
| `statusPosition` | `'top' \| 'bottom'` | `'bottom'` | Which side of the body the status sits on. |

## Events

| Event | Payload | Description |
| --- | --- | --- |
| `submit` | `string` | A trimmed, non-empty draft was submitted. The draft is then cleared. |
| `stop` | — | The stop action was activated. |
| `change` | `string` | The draft changed, including the clear after a successful submit. |
| `update:modelValue` | `string` | The draft the composer is asking for. This is what `v-model` binds. |

## Slots

| Slot | Description |
| --- | --- |
| `drawer` | Collapsible content above the body. Usually `ChatComposerDrawer`. |
| `headerActions` | Start side of the header. Small icon buttons line up with the row. |
| `headerContext` | End side of the header. Context such as a usage meter. |
| `input` | Replaces the default token input. Read the draft from `useChatComposerContext`. |
| `footerActions` | Start side of the footer. Medium buttons match the send button. |
| `sendActions` | Sits just before the send button, for example a dictation or microphone button. |
| `sendButton` | Replaces the default send and stop button. |

## Exposed

| Name | Type | Description |
| --- | --- | --- |
| `element` | `HTMLElement \| null` | The root frame, for measuring or scrolling. |
