PromptInput

free

PromptInput combines autosizing message entry, attachments, command selection, and explicit leading and trailing toolbars.

Preview
Dark
import { useState } from 'react';
import PromptInput from '@/components/composites/PromptInput';
import Button from '@/components/ui/Button';
import { PiSlidersHorizontal } from 'react-icons/pi'

export default function UsageDemo() {
  const [submitted, setSubmitted] = useState('');

  return (
    <div className="space-y-2 w-full">
      <PromptInput
        placeholder="Write a message"
        onSubmit={(value) => setSubmitted(value)}
      >
        <PromptInput.Toolbar>
          <PromptInput.ToolbarStart>
            <PromptInput.AttachButton />
          </PromptInput.ToolbarStart>
          <PromptInput.ToolbarEnd>
            <Button
              type="button"
              variant="ghost"
              shape="circle"
              size="sm"
              icon={<PiSlidersHorizontal />}
              aria-label="Message options"
            />
            <PromptInput.Submit />
          </PromptInput.ToolbarEnd>
        </PromptInput.Toolbar>
      </PromptInput>
      {submitted && (
        <p className="text-sm text-muted-foreground">
          Submitted: {submitted}
        </p>
      )}
    </div>
  );
}

Installation

Add this component with the NateUI CLI.

npx nateui@latest add PromptInput

Examples

Compact

Compact mode starts as a one-row composer and expands into the stacked layout after a line wraps or an attachment is present.

Preview
Dark
import PromptInput from '@/components/composites/PromptInput';

export default function CompactDemo() {
  return (
    <div className="w-full">
      <PromptInput
        layout="compact"
        placeholder="Add a follow-up"
        onSubmit={() => undefined}
      >
        <PromptInput.Toolbar>
          <PromptInput.ToolbarStart>
            <PromptInput.AttachButton />
          </PromptInput.ToolbarStart>
          <PromptInput.ToolbarEnd>
            <PromptInput.Submit />
          </PromptInput.ToolbarEnd>
        </PromptInput.Toolbar>
      </PromptInput>
    </div>
  );
}

Attachments

The built-in attach action reads selected browser files as data URLs, shows removable previews above the text area, and returns them as the second submit argument.

Preview
Dark
brief.png
brief.png
image/png
import { useState } from 'react';
import PromptInput from '@/components/composites/PromptInput';

const initialAttachments = [
  {
    id: 'brief',
    name: 'brief.png',
    mediaType: 'image/png',
    url: '/img/thumbs/misc/img-1.png',
  },
];

export default function AttachmentsDemo() {
  const [attachments, setAttachments] = useState(initialAttachments);

  return (
    <div className="w-full">
      <PromptInput
        attachments={attachments}
        onAttachmentsChange={setAttachments}
        placeholder="Add context"
        onSubmit={() => setAttachments([])}
      >
        <PromptInput.Toolbar>
          <PromptInput.ToolbarStart>
            <PromptInput.AttachButton />
          </PromptInput.ToolbarStart>
          <PromptInput.ToolbarEnd>
            <PromptInput.Submit />
          </PromptInput.ToolbarEnd>
        </PromptInput.Toolbar>
      </PromptInput>
    </div>
  );
}

Slash Commands

Pass plain command records. Typing / filters the list; arrow keys move the active row, Enter inserts the command token, and the parent receives the selected record.

Preview
Dark
import { useState } from 'react';
import PromptInput from '@/components/composites/PromptInput';
import { PiFileText, PiLightning, PiMagnifyingGlass } from 'react-icons/pi'

const commands = [
  {
    id: 'summarize',
    label: 'Summarize',
    description: 'Turn the current note into a short summary',
    icon: <PiFileText />,
    keywords: ['brief'],
  },
  {
    id: 'search',
    label: 'Search',
    description: 'Find related material',
    icon: <PiMagnifyingGlass />,
  },
  {
    id: 'run',
    label: 'Run check',
    description: 'Start the selected workflow',
    icon: <PiLightning />,
  },
];

export default function SlashCommandsDemo() {
  const [selected, setSelected] = useState('');

  return (
    <div className="space-y-2 w-full">
      <PromptInput
        commands={commands}
        placeholder="Type / to choose a command"
        onCommandSelect={(command) => setSelected(command.label)}
        onSubmit={() => undefined}
      />
      {selected && (
        <p className="text-sm text-muted-foreground">
          Selected: {selected}
        </p>
      )}
    </div>
  );
}

Busy And Stop

Preview
Dark
Thinking...
import { useState } from 'react';
import Shimmer from '@/components/ui/Shimmer'
import PromptInput from '@/components/composites/PromptInput';
import Button from '@/components/ui/Button';

export default function BusyAndStopDemo() {
  const [busy, setBusy] = useState(true);

  return (
    <div className="space-y-4 w-full">
      {busy && <Shimmer active>Thinking...</Shimmer>}
      <PromptInput
        status={busy ? 'busy' : 'idle'}
        onSubmit={() => setBusy(true)}
        onStop={() => setBusy(false)}
        placeholder="Queue another message"
      />
    </div>
  );
}

API

PromptInput

PropDescriptionTypeDefault
onSubmitReceives the trimmed message and current attachments.(value: string, attachments: AttachmentData[]) => void
layoutUses a stacked composer or a single-row compact start state.'stacked' | 'compact''stacked'
attachmentsControlled attachment records rendered above the field.AttachmentData[]
onAttachmentsChangeReceives selected and removed attachment records.(attachments: AttachmentData[]) => void
attachmentAcceptNative file-input accept filter.string
commandsRecords filtered when the cursor is in a / token.PromptInputCommand[][]
onCommandSelectCalled after a command token is inserted.(command: PromptInputCommand) => void
onStopCalled by PromptInput.Submit while status="busy".() => void
statusSwitches the submit control between send and stop.PromptInputStatus'idle'
valueCurrent controlled message value. Pair with onChange.string
onChangeReceives each message-value change.(value: string) => void
placeholderText shown when the message field is empty.string'Message…'
refExposes clear() and focus() methods.Ref<PromptInputRef>
...formPropsStandard <form> attributes forwarded to the root.ComponentProps<'form'>

PromptInput.Toolbar

Use one ToolbarStart and one ToolbarEnd inside each Toolbar. The end group contains PromptInput.Submit explicitly.

PartDescription
ToolbarStartLeading actions, such as AttachButton or a caller-owned picker.
ToolbarEndTrailing controls and Submit.
AttachButtonOpens the native multi-file picker owned by the root composer.
SubmitSends the current text/attachments, or stops while busy.

Types

type PromptInputLayout = 'stacked' | 'compact';
type PromptInputStatus = 'idle' | 'busy';
 
type PromptInputCommand = {
  id: string;
  label: string;
  description?: ReactNode;
  icon?: ReactNode;
  keywords?: string[];
  value?: string;
};