# Prompt Input

Prompt Input [#prompt-input]

The **PromptInput** is the chat composer. It manages its own textarea state by default — auto-resize, Enter to submit, Shift+Enter for a newline — and swaps its submit control for a stop control while streaming. It exposes `onSubmit(text, attachments?)` and optional controlled `value` / `onValueChange`. A `PromptInputProvider` hoists composer state for external control, `PromptInputMic` toggles local Whisper dictation, and `PromptInputAddButton` anchors a slash-command / + picker.

Preview [#preview]

```tsx
'use client';

/**
 * @file prompt-input-demo.tsx
 * @description Docs preview for `PromptInput`. Type and submit (Enter), insert a
 * newline (Shift+Enter), watch auto-resize, and toggle a simulated streaming
 * state to see the submit→stop swap. No model download.
 */
import * as React from 'react';
import {
  PromptInput,
  PromptInputAddButton,
  PromptInputMic,
  PromptInputSubmit,
  PromptInputTextarea,
  PromptInputTools,
} from '@/components/prompt-input';

export default function PromptInputDemo() {
  const [log, setLog] = React.useState<string[]>([]);
  const [streaming, setStreaming] = React.useState(false);
  const [recording, setRecording] = React.useState(false);

  return (
    <div className="flex w-full max-w-xl flex-col gap-3">
      <PromptInput
        streaming={streaming}
        onStop={() => setStreaming(false)}
        onSubmit={(text) => {
          setLog((l) => [...l, text]);
          // Simulate a brief stream so the stop control is visible.
          setStreaming(true);
          window.setTimeout(() => setStreaming(false), 1500);
        }}
      >
        <PromptInputTextarea placeholder="Ask anything… (Enter to send, Shift+Enter for a newline)" />
        <PromptInputTools>
          <div className="flex items-center gap-1">
            <PromptInputAddButton />
            <PromptInputMic
              recording={recording}
              onToggle={setRecording}
            />
          </div>
          <PromptInputSubmit />
        </PromptInputTools>
      </PromptInput>

      {log.length > 0 && (
        <ul className="space-y-1 text-sm text-muted-foreground">
          {log.map((m, i) => (
            <li key={i}>• {m}</li>
          ))}
        </ul>
      )}
    </div>
  );
}
```

Installation [#installation]

```bash
npx shadcn@latest add @localmode/ui/conversation/prompt-input
```

Data source & dependencies [#data-source--dependencies]

**Data source:** emits `onSubmit(text, attachments)` / `onStop` — wire to any backend. Recommended producer: `useChat` from `@localmode/react` (on-device). See [Use with the Vercel AI SDK](/docs/use-with-ai-sdk).

* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)

Files installed [#files-installed]

* `prompt-input.tsx` — `PromptInput`, `PromptInputTextarea`, `PromptInputSubmit`, `PromptInputTools`, `PromptInputProvider`, `PromptInputMic`, `PromptInputAddButton`
* `lib/utils.ts` — the `cn()` helper (if not already present)

Props [#props]

**PromptInput**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `onSubmit` | `function` | — | **Required.** Fired with the trimmed text (and attachments) on submit. |
| `value` | `string` | — | Controlled value (optional). |
| `onValueChange` | `function` | — | Reports edits in controlled mode. |
| `streaming` | `boolean` | `false` | When true, the submit control becomes a stop control. |
| `onStop` | `function` | — | Fired when the user activates the stop control. |
| `attachments` | `array` | — | Attachments to include in the next submit (from `PromptInputAttachments`). |
| `disabled` | `boolean` | — | Disable the whole composer. |

Examples [#examples]

Wire to `useChat` [#wire-to-usechat]

```tsx
import { useChat } from '@localmode/react';
import {
  PromptInput,
  PromptInputSubmit,
  PromptInputTextarea,
  PromptInputTools,
} from '@/components/prompt-input';

export function Composer({ model }) {
  const { send, cancel, isStreaming } = useChat({ model });
  return (
    <PromptInput streaming={isStreaming} onStop={cancel} onSubmit={(text) => send(text)}>
      <PromptInputTextarea placeholder="Ask anything…" />
      <PromptInputTools>
        <span />
        <PromptInputSubmit />
      </PromptInputTools>
    </PromptInput>
  );
}
```

Customization [#customization]

A `CharLimitIndicator` from the input-controls family slots into `PromptInputTools`; until it is installed, drop in a simple `text.length` counter. Adjust `maxHeight` on the textarea, or wire `PromptInputMic.onToggle` to a `useVoiceRecorder` + `transcribe` flow to fill the input from local speech.

These primitives are presentational and hook-driven: they render props and emit callbacks, holding only local view state. The orchestration state (e.g. `useChat`) lives in your app. Every surface uses shadcn/ui CSS-variable utilities, so it inherits your theme — restyle the copied file freely.