# PromptBar

**The AI chat composer for Compose Multiplatform.** Multi-line auto-growing textarea, slash-command
autocomplete, `@`-mention dropdown, attachment chips with image / file previews, a unified
Send → Sending → Stop button state machine, voice button, prompt template chips, model selector,
live token counter, keyboard shortcuts — all on every CMP target, all driven from a single
headless `PromptBarState`.

[![Maven Central](https://img.shields.io/maven-central/v/io.github.nadeemiqbal/prompt-bar)](https://central.sonatype.com/artifact/io.github.nadeemiqbal/prompt-bar)
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
[![Build](https://github.com/NadeemIqbal/prompt-bar/actions/workflows/build.yml/badge.svg)](https://github.com/NadeemIqbal/prompt-bar/actions/workflows/build.yml)

<p align="center">
  <img src="docs/hero.gif" width="320" alt="PromptBar driving a fake LLM chat with slash commands, attachments, send/stop, and llm-typewriter streaming markdown responses on iOS" />
</p>

> **Pairs naturally with [`llm-typewriter`](https://github.com/NadeemIqbal/llm-typewriter)** — see the sample app for the full "AI chat starter kit" wiring (Send/Stop button auto-syncs with the typewriter's streaming state; the demo above shows it in action on iOS).

## Why

The Compose Multiplatform ecosystem has **no AI-focused chat composer** today. Stream Chat ships
an Android-only `MessageComposer` locked inside a SaaS SDK. The de-facto React stack (Vercel AI
Elements `PromptInput`, OpenAI playground) leaves slash commands and `@` mentions as open
issues. PromptBar is the **first CMP composer that ships those features as a cohesive unit**:

| Capability | PromptBar | Vercel AI Elements (React) | Stream `MessageComposer` |
|---|---|---|---|
| CMP target coverage | Android · iOS · Desktop · Wasm | Web only | Android only |
| Slash-command autocomplete | ✅ built-in | open issue #179 | ❌ |
| `@`-mention dropdown (async provider) | ✅ built-in | open issue #179 | ❌ |
| Attachment chips + previews | ✅ image / file / custom | ✅ | partial (max 3) |
| Send → Sending → Stop state machine | ✅ single button | ✅ | partial |
| Prompt template chips | ✅ | partial | ❌ |
| Voice button (BYO transcription) | ✅ | ✅ | partial |
| Live token counter | ✅ pluggable tokenizer | ❌ | ❌ |
| Keyboard shortcuts (Enter/Shift+Enter) | ✅ | ✅ | ✅ |
| Smart paste-tokenizer | ✅ comma/semicolon/newline | ❌ | ❌ |
| Headless state container | ✅ `PromptBarState` | ✅ | partial |

## Install

```kotlin
dependencies {
    implementation("io.github.nadeemiqbal:prompt-bar:0.1.0")
}
```

## Usage

```kotlin
@Composable
fun ChatScreen(vm: ChatViewModel) {
    val state = rememberPromptBarState()
    PromptBar(
        state = state,
        onSend = { vm.send(state.outgoing) },
        onStop = { vm.cancelStream() },
        onVoiceTap = { vm.startVoiceInput() },

        // Optional: slash commands
        slashCommands = listOf(
            SlashCommand("clear", "Clear conversation") { vm.clear() },
            SlashCommand("imagine", "Generate an image", hotkey = "Cmd+I"),
        ),

        // Optional: @-mention provider (sync or async)
        mentionProvider = MentionProvider.fromList(vm.workspaceMentions),

        // Optional: quick-start templates above the input
        templates = listOf(
            PromptTemplate("Summarize", "Please summarize this conversation."),
            PromptTemplate("Explain code", "Explain what this code does:\n```\n\n```"),
        ),

        // Optional: inline model picker on the bottom bar
        modelSelector = { ModelDropdown(state) },
    )
}
```

### Sync with `llm-typewriter`

Pair with [`llm-typewriter`](https://github.com/NadeemIqbal/llm-typewriter) to wire the
Send/Stop button to your streaming response composable:

```kotlin
val typewriterState = rememberStreamingTypewriterState()
val promptState = rememberPromptBarState()

// Send → Stop transitions sync automatically with typewriter streaming state.
LaunchedEffect(typewriterState.isStreaming) {
    if (typewriterState.isStreaming) promptState.markStreaming() else promptState.markReady()
}
```

## Headless state

`PromptBarState` is fully usable without composition. Mutate it from a coroutine, observe its
phases, drive integration tests directly.

```kotlin
val state = PromptBarState()
state.fieldValue = TextFieldValue("Hello")
state.addAttachment(PromptAttachment("a1", "report.pdf", AttachmentPreview.File("pdf")))
state.markStreaming()   // Send button becomes Stop
state.markReady()       // back to derived state
state.applyTemplate(PromptTemplate("Summarize", "Please summarize."))
state.pasteTokensAsAttachments(clipboardText) { tok ->
    PromptAttachment(id = tok, displayName = tok, preview = AttachmentPreview.File("eml"))
}
val outgoing: PromptBarOutgoing = state.outgoing  // text + attachments + model
```

## Slash commands

Type `/` at the start of a line or after whitespace → the autocomplete panel appears with
matching commands. Pick one → the command's `onSelect` lambda fires and the `/text` is replaced
with the full command name.

```kotlin
SlashCommand(
    name = "code",
    description = "Switch to coding mode",
    hotkey = "Cmd+K",                                  // shown as a chip in the suggestion
    onSelect = { vm.toggleCodingMode() },
)
```

## `@` mentions

Type `@` after whitespace → the `MentionProvider.suggest(query)` runs and the dropdown shows
results. Selecting one inserts `@display` into the textfield.

```kotlin
// Static list — case-insensitive substring match.
mentionProvider = MentionProvider.fromList(workspace.files.map {
    Mention(id = it.path, display = it.name, subtitle = it.path, category = "File")
})

// Async — call your DB/API.
mentionProvider = MentionProvider { query -> repository.suggestMentions(query) }
```

## Send button state machine

| State | Visual | Click |
|---|---|---|
| `Disabled` | Send icon, dimmed | Ignored |
| `Ready` | Send icon, primary | Fires `onSend` |
| `Sending` | Spinner | Ignored |
| `Streaming` | Stop icon | Fires `onStop` |

The state is derived from the composer's content by default. Override with `markSending()` /
`markStreaming()` / `markReady()` when you know better than the derivation.

## Platforms

| Target | Status |
|---|---|
| Android (minSdk 24) | ✅ |
| iOS (x64, arm64, simulatorArm64) | ✅ |
| Desktop (JVM 11) | ✅ |
| Web (wasmJs) | ✅ |

## Sample app

```bash
./gradlew :sample:desktopApp:run                 # Desktop
./gradlew :sample:androidApp:assembleDebug       # Android
./gradlew :sample:webApp:wasmJsBrowserDevelopmentRun   # Web
```

The sample is a fake chat app that exercises every feature — slash commands, mentions,
attachments via the voice button, templates, model selector cycling, Send/Stop transitions.

## License

Apache 2.0 — see [LICENSE](LICENSE).
