# Composer

`ChatComposer` is where the user writes to your agent. It has a text field that grows with the text, photos from the system photo picker, and a microphone that dictates into the field. While the agent answers, its send button turns into a stop button. Put it under the [conversation](https://getstream.io/chat/docs/sdk/android/ai-components/conversation-view/):

```kotlin
Column(modifier = Modifier.fillMaxSize()) {
    AIConversation(
        messages = messages,
        modifier = Modifier.weight(1f),
    )
    ChatComposer(
        onSendClick = { data: MessageData -> send(data) },
        onStopClick = { stop() },
        isGenerating = isAnswering,
    )
}
```

The composer pads itself above the keyboard and the navigation bar. To let the conversation scroll under it, [float it over the list](https://getstream.io/chat/docs/sdk/android/ai-components/conversation-view/#float-the-composer-over-the-list).

## Permissions

The attachment button opens the system photo picker, which needs no permission. The user picks up to three images at a time.

Dictation records with the microphone, so declare the permission in your app's `AndroidManifest.xml`:

```xml
<uses-permission android:name="android.permission.RECORD_AUDIO" />
```

The composer asks for it the first time the user taps the microphone. If they decline, it shows a message with a button that opens your app's settings. Apps that use Stream Video already declare the permission.

## Send a message

`onSendClick` receives a `MessageData`, with the `text` and the `attachments` the user picked, as `content://` URIs:

```kotlin
ChatComposer(
    onSendClick = { data: MessageData ->
        viewModel.send(text = data.text, attachments = data.attachments.toList())
    },
    onStopClick = viewModel::stop,
    isGenerating = isAnswering,
)
```

The composer clears its text and attachments once a message is sent. Stream Chat uploads an attachment from a `File`, so copy each picked file first, as shown in [Show a message while it's sent](https://getstream.io/chat/docs/sdk/android/ai-components/conversation-view/#show-a-message-while-its-sent). With [Stream Agents](https://getstream.io/agents/docs/quickstart/?lang=android), ask the session instead of sending to the channel, as the [Quickstart](https://getstream.io/chat/docs/sdk/android/ai-components/) does.

## Control it from your code

By default, the composer keeps the message being written itself. To fill in text from elsewhere, such as a [suggestion](https://getstream.io/chat/docs/sdk/android/ai-components/conversation-view/#suggestions) or a restored draft, or to keep what was written when sending fails, own the message with the overload that takes `onMessageDataChange`:

```kotlin
var message by rememberSaveable(stateSaver = MessageData.Saver) { mutableStateOf(MessageData()) }
val focus = remember { FocusRequester() }

ChatComposer(
    messageData = message,
    onMessageDataChange = { message = it },
    onSendClick = { sent -> if (!viewModel.trySend(sent)) message = sent },
    onStopClick = viewModel::stop,
    isGenerating = isAnswering,
    focusRequester = focus,
)
```

The composer reports an empty message through `onMessageDataChange` just before it calls `onSendClick`, so setting the message back there keeps it. `MessageData.Saver` keeps the message across configuration changes and process death. Call `focus.requestFocus()` to put the cursor in the text field, for example after filling it in.

## Stop the reply

While `isGenerating` is `true`, the composer shows a stop button in place of the send button, and calls `onStopClick` when the user taps it. It also disables its text field and attachment button, so set `isGenerating` for the whole reply, and make sure it always resets: while it's stuck at `true`, the user can't type.

With Stream Agents, stop the reply with `session.interrupt()`. With a backend of your own and a Stream Chat channel, send the channel an `ai_indicator.stop` event with the id of the reply:

```kotlin
fun stop(cid: String, replyId: String) {
    ChatClient.instance().channel(cid)
        .sendEvent(
            eventType = EventType.AI_TYPING_INDICATOR_STOP,
            extraData = mapOf("message_id" to replyId),
        )
        .enqueue()
}
```

[What your backend sends](https://getstream.io/chat/docs/sdk/android/ai-components/conversation-state/#stop-the-reply) covers how the backend stops the reply.

## Dictation

`SpeechToTextButton` turns speech into text with Android's `SpeechRecognizer`, in the device's language. The composer shows one inside its text field while the agent isn't answering, which dictates into it after the text already there. To use the button in an input of your own, remember its state with the callbacks that receive the transcript:

```kotlin
var text by remember { mutableStateOf("") }

val state = rememberSpeechToTextButtonState(
    // Called with what's recognized so far, while the user speaks.
    onPartialResult = { partial -> text = partial },
    // Called with the whole transcript once recording stops.
    onFinalResult = { final -> text = final },
)

SpeechToTextButton(
    state = state,
    onPermissionDenied = { showMicrophoneRationale() },
)
```

When tapped, it asks for microphone access and records. Recording stops after 3 seconds of silence, or when the user taps the button again. `state.isRecording()` tells whether it's recording. The device needs a speech recognition service, such as Google's, which most phones have.

To let the user talk to your agent instead of dictating, see the [Voice agent](https://getstream.io/chat/docs/sdk/android/ai-components/use-cases/voice-agent/) use case.

## Customize the composer

The composer draws its parts through `ChatAiComponentFactory`, which you provide with `LocalChatAiComponentFactory`. Each slot has a default, so you override only the ones you want to change:

| Slot                                 | Shows                                                       | Default                              |
| ------------------------------------ | ----------------------------------------------------------- | ------------------------------------ |
| `ComposerLeadingContent`             | Left of the text field                                      | A button that opens the photo picker |
| `ComposerInputContent`               | The text field, with the dictation and send or stop buttons | The built-in field                   |
| `ComposerInputTrailingContent`       | Inside the text field, while the agent isn't answering      | `SpeechToTextButton`                 |
| `ComposerTrailingContent`            | Right of the text field                                     | Nothing                              |
| `SpeechToTextButtonIdleContent`      | The dictation button while it isn't recording               | A microphone button                  |
| `SpeechToTextButtonRecordingContent` | The dictation button while it records, with the audio level | Bars that follow the voice           |

For example, this replaces the attachment button with a paperclip and leaves dictation out, on top of the factory already provided:

```kotlin
CompoundChatAiComponentFactory(
    factory = { current ->
        object : ChatAiComponentFactory by current {
            @Composable
            override fun RowScope.ComposerLeadingContent(params: ComposerLeadingContentParams) {
                IconButton(onClick = params.onAttachmentsClick, enabled = !params.isGenerating) {
                    Icon(Icons.Default.AttachFile, contentDescription = "Attach photos")
                }
            }

            @Composable
            override fun RowScope.ComposerInputTrailingContent(params: ComposerInputTrailingContentParams) {
                // Nothing: no dictation.
            }
        }
    },
) {
    ChatComposer(
        onSendClick = { data -> send(data) },
        onStopClick = { stop() },
        isGenerating = isAnswering,
    )
}
```

Override `ComposerTrailingContent` to add a mode toggle, a slash-command trigger, or another control next to the field. The params carry what each slot needs, such as `onAttachmentsClick`, which opens the photo picker, and the `speechToTextState` of the field's dictation.

## Appearance

The composer takes its colors and text styles from the current `MaterialTheme`. Its texts, such as the "Ask Assistant" placeholder and the descriptions of its buttons, are string resources whose names start with `stream_ai_compose_composer_` and `stream_ai_compose_speech_to_text_`, which you can override in your app's `strings.xml`.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/android/ai-components/composer/](https://getstream.io/chat/docs/sdk/android/ai-components/composer/).