

<Preview name="PromptInputDefaultExample" />

## Overview [#overview]

`PromptInput` is a visual form shell for composing chat prompts. It captures prompt text and optional file parts on submit, while the consumer owns sending, cancellation, upload, persistence, and chat state.

`PromptInput` owns one transient draft for its children. The text editor, attachments, submit button, speech button, and prompt-specific actions coordinate through internal composer state, so consumers do not need to install a prompt-level provider. `PromptInputSubmit` derives its enabled state from that draft. Omit `status` or pass `null` when idle; pass `status="processing"` or `status="streaming"` to show a disabled spinner while the runtime is busy.

## Usage [#usage]

```tsx
import {
  Attachment,
  AttachmentInfo,
  AttachmentPreview,
  AttachmentRemove,
  Attachments,
  Conversation,
  ConversationContent,
  ConversationScrollButton,
  Message,
  MessageContent,
  MessageResponse,
  PromptInput,
  PromptInputActionAddAttachments,
  PromptInputActionMenu,
  PromptInputActionMenuContent,
  PromptInputActionMenuTrigger,
  PromptInputFooter,
  PromptInputSpeechButton,
  PromptInputSubmit,
  PromptInputTextEditor,
  PromptInputTools,
  Shimmer,
  type UIMessage,
  usePromptInputAttachments,
} from "@tilt-legal/cubitt-components/chat-elements";
```

## Examples [#examples]

### Default [#default]

Omit `status` or pass `null` when idle. Pass `status="processing"` or `status="streaming"` to show a disabled spinner in the submit button.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="PromptInputDefaultExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    const isTextPart = (part: unknown): part is { text: string; type: "text" } =>
      typeof part === "object" &&
      part !== null &&
      "type" in part &&
      part.type === "text" &&
      "text" in part &&
      typeof part.text === "string";

    const getMessageText = (message: UIMessage) =>
      message.parts.filter(isTextPart).map((part) => part.text).join("");

    function DefaultPromptInput() {
      const [status, setStatus] = useState<
        "processing" | "streaming" | null
      >(null);
      const [messages, setMessages] = useState<UIMessage[]>([]);

      return (
        <>
          {messages.length > 0 || status !== null ? (
            <Conversation className="h-64 rounded-xl border border-border-3 bg-background">
              <ConversationContent className="gap-4 p-3">
                {messages.map((message) => (
                  <Message from={message.role} key={message.id}>
                    <MessageContent>
                      {message.role === "assistant" ? (
                        <MessageResponse>{getMessageText(message)}</MessageResponse>
                      ) : (
                        getMessageText(message)
                      )}
                    </MessageContent>
                  </Message>
                ))}
                {status !== null ? (
                  <Message from="assistant">
                    <MessageContent>
                      <Shimmer>Thinking through the matter context...</Shimmer>
                    </MessageContent>
                  </Message>
                ) : null}
              </ConversationContent>
              <ConversationScrollButton />
            </Conversation>
          ) : null}

          <PromptInput
            onSubmit={({ text, files }) => {
              sendMessage({ text, files });
              setMessages((current) => [
                ...current,
                {
                  id: crypto.randomUUID(),
                  parts: [{ text, type: "text" }],
                  role: "user",
                },
              ]);
              setStatus("processing");
              window.setTimeout(() => setStatus("streaming"), 350);
              window.setTimeout(() => setStatus(null), 1800);
            }}
          >
            <PromptInputTextEditor placeholder="Ask about the matter..." />
            <PromptInputFooter className="justify-end">
              <PromptInputSubmit status={status} />
            </PromptInputFooter>
          </PromptInput>
        </>
      );
    }

    <DefaultPromptInput />;
    ```
  </Tab>
</Tabs>

### Speech Input [#speech-input]

`PromptInputSpeechButton` writes final and interim speech into `PromptInputTextEditor`. Use standalone [Speech Input](/components/chat-elements/speech-input) when dictation is needed outside a prompt composer.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="PromptInputSpeechExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    function SpeechPromptInput() {
      return (
        <PromptInput onSubmit={handleSubmit}>
          <PromptInputTextEditor placeholder="Dictate or type a prompt..." />
          <PromptInputFooter className="justify-end">
            <PromptInputSpeechButton
              aria-label="Dictate prompt"
              onAudioRecorded={async (blob) => transcribe(blob)}
            />
            <PromptInputSubmit />
          </PromptInputFooter>
        </PromptInput>
      );
    }

    <SpeechPromptInput />;
    ```
  </Tab>
</Tabs>

### Attachments [#attachments]

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="PromptInputAttachmentsExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    function PromptInputAttachmentsDisplay() {
      const attachments = usePromptInputAttachments();

      if (!attachments.files.length) {
        return null;
      }

      return (
        <Attachments variant="inline">
          {attachments.files.map((attachment) => (
            <Attachment
              data={attachment}
              key={attachment.id}
              onRemove={() => attachments.remove(attachment.id)}
            >
              <AttachmentPreview />
              <AttachmentInfo />
              <AttachmentRemove />
            </Attachment>
          ))}
        </Attachments>
      );
    }

    function AttachmentsPromptInput() {
      return (
        <PromptInput
          accept="image/*,.pdf,.doc,.docx,.xls,.xlsx"
          multiple
          onSubmit={handleSubmit}
        >
          <PromptInputAttachmentsDisplay />
          <PromptInputTextEditor placeholder="Attach evidence and ask a question..." />
          <PromptInputFooter>
            <PromptInputTools>
              <PromptInputActionMenu>
                <PromptInputActionMenuTrigger aria-label="Add attachment" />
                <PromptInputActionMenuContent>
                  <PromptInputActionAddAttachments />
                </PromptInputActionMenuContent>
              </PromptInputActionMenu>
            </PromptInputTools>
            <PromptInputSubmit />
          </PromptInputFooter>
        </PromptInput>
      );
    }

    <AttachmentsPromptInput />;
    ```
  </Tab>
</Tabs>
