Skip to main content
Gremorie

PromptInput

A superfície de composição fundamental de qualquer chat de IA. Uma máquina de estados enraizada em form com textarea auto-redimensionável, chips de attachment, action menu, entrada por voz, seletor de modelo e um botão de submit orientado por status.

Visão geral

PromptInput é o maior primitive em @gremorie/rx-ai. É uma máquina de estados enraizada em <form> que compõe um InputGroup do @gremorie/rx-forms (textarea + addons renderizados como uma única superfície coesa) com o conjunto completo de affordances de input de chat: attachments de arquivo com previews em hover card, um action menu, um botão de speech-to-text, um seletor de modelo, botões de ação dentro do input e um botão de submit orientado por status cujo ícone alterna entre seta / spinner / stop / X dependendo do estado do chat.

O estado é gerenciado em três camadas:

  1. Auto-gerenciado (padrão). Tudo vive dentro de PromptInput. Passe onSubmit e pronto.
  2. Elevado por provider via PromptInputProvider. Expõe usePromptInputController() para ler ou mutar o texto e os attachments de qualquer lugar na árvore.
  3. Textarea controlada. O textInput.value / textInput.setInput do provider dirigem PromptInputTextarea automaticamente quando o provider está presente.

O componente também carrega um contrato de teclado imposto: Enter submete, Shift+Enter quebra linha, Backspace numa textarea vazia remove o último attachment, colar com arquivos anexa automaticamente, eventos de composição IME são honrados. O submit é conectado via form.requestSubmit() para que a validação de form HTML ainda se aplique.

Preview

'use client';import {  PromptInput,  PromptInputActionAddAttachments,  PromptInputActionMenu,  PromptInputActionMenuContent,  PromptInputActionMenuItem,  PromptInputActionMenuTrigger,  PromptInputAttachment,  PromptInputAttachments,  PromptInputBody,  PromptInputButton,  PromptInputFooter,  PromptInputSelect,  PromptInputSelectContent,  PromptInputSelectItem,  PromptInputSelectTrigger,  PromptInputSelectValue,  PromptInputSubmit,  PromptInputTextarea,  PromptInputTools,} from '@gremorie/rx-ai';import { GlobeIcon, MicIcon } from 'lucide-react';const MODELS: { id: string; label: string; disabled?: boolean }[] = [  { id: 'claude-opus-4-7', label: 'Claude Opus 4.7' },  { id: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6' },  { id: 'claude-haiku-4-5', label: 'Claude Haiku 4.5' },  { id: 'gpt-5', label: 'GPT-5', disabled: true },];export function PromptInputPreview() {  return (    <PromptInput      className="mx-auto max-w-xl"      globalDrop      multiple      onSubmit={() => {        // demo: wire onSubmit to the AI SDK to make this real      }}    >      <PromptInputBody>        <PromptInputAttachments>          {(attachment) => <PromptInputAttachment data={attachment} />}        </PromptInputAttachments>        <PromptInputTextarea placeholder="Ask anything..." />      </PromptInputBody>      <PromptInputFooter>        <PromptInputTools>          <PromptInputActionMenu>            <PromptInputActionMenuTrigger aria-label="More actions" />            <PromptInputActionMenuContent>              <PromptInputActionAddAttachments />              <PromptInputActionMenuItem>                <MicIcon className="mr-2 size-4" /> Record voice              </PromptInputActionMenuItem>            </PromptInputActionMenuContent>          </PromptInputActionMenu>          <PromptInputButton aria-label="Search the web">            <GlobeIcon className="size-4" />            <span>Search</span>          </PromptInputButton>          <PromptInputSelect defaultValue="claude-sonnet-4-6">            <PromptInputSelectTrigger>              <PromptInputSelectValue placeholder="Select model" />            </PromptInputSelectTrigger>            <PromptInputSelectContent>              {MODELS.map((model) => (                <PromptInputSelectItem                  disabled={model.disabled}                  key={model.id}                  value={model.id}                >                  {model.label}                </PromptInputSelectItem>              ))}            </PromptInputSelectContent>          </PromptInputSelect>        </PromptInputTools>        <PromptInputSubmit status="ready" />      </PromptInputFooter>    </PromptInput>  );}

Anatomia

PromptInput
├─ PromptInputHeader
│  ├─ PromptInputMentions          picker de add-context com @
│  └─ Context                      medidor de uso de tokens
├─ PromptInputBody
│  ├─ PromptInputAttachments       opcional
│  └─ PromptInputTextarea
└─ PromptInputFooter
   ├─ PromptInputTools             esquerda · mode + model
   │  ├─ PromptInputSelect         mode
   │  └─ PromptInputSelect         model
   └─ PromptInputTools             direita
      ├─ PromptInputButton         busca na web
      ├─ PromptInputAttachButton   anexar
      ├─ PromptInputSpeechButton   voz
      └─ PromptInputSubmit         enviar

Instalação

bash npx gremorie@latest add rx-prompt-input

bash pnpm dlx gremorie@latest add rx-prompt-input

bash yarn dlx gremorie@latest add rx-prompt-input

bash bunx --bun gremorie@latest add rx-prompt-input

Traz rx-forms (família InputGroup + Button + Select), rx-overlays (DropdownMenu + HoverCard + Tooltip), rx-display (Command + Dialog) e rx-core (o helper cn) como dependências de registry.

Uso

Mínimo

"use client";

import { useState } from "react";
import {
  type ChatStatus,
  PromptInput,
  PromptInputBody,
  PromptInputFooter,
  PromptInputSubmit,
  PromptInputTextarea,
} from "@gremorie/rx-ai";

export function ChatBox() {
  const [status, setStatus] = useState<ChatStatus>("ready");

return (

<PromptInput onSubmit={(message) => send(message.text, message.files)}>
<PromptInputBody>
<PromptInputTextarea placeholder="Ask anything" />
</PromptInputBody>
<PromptInputFooter>
<span />
<PromptInputSubmit status={status} />
</PromptInputFooter>
</PromptInput>
);
}
import {
  PromptInput,
  PromptInputSubmit,
  PromptInputTextarea,
  PromptInputToolbar,
} from "@gremorie/ng-ai";

@Component({
  selector: "app-chat-box",
  standalone: true,
  imports: [PromptInput, PromptInputTextarea, PromptInputToolbar, PromptInputSubmit],
  template: `
    <prompt-input (submitted)="send($event)">
      <prompt-input-textarea placeholder="Ask anything" />
      <prompt-input-toolbar>
        <prompt-input-submit [status]="status()" />
      </prompt-input-toolbar>
    </prompt-input>
  `,
})
export class ChatBoxComponent {
  status = signal<ChatStatus>("ready");
}

Completo

'use client';

import { useRef, useState } from 'react';
import {
  type ChatStatus,
  PromptInput,
  PromptInputActionAddAttachments,
  PromptInputActionMenu,
  PromptInputActionMenuContent,
  PromptInputActionMenuTrigger,
  PromptInputAttachment,
  PromptInputAttachments,
  PromptInputBody,
  PromptInputButton,
  PromptInputFooter,
  PromptInputSelect,
  PromptInputSelectContent,
  PromptInputSelectItem,
  PromptInputSelectTrigger,
  PromptInputSelectValue,
  PromptInputSpeechButton,
  PromptInputSubmit,
  PromptInputTextarea,
  PromptInputTools,
} from '@gremorie/rx-ai';
import { GlobeIcon } from 'lucide-react';

const models = [
  { id: 'gpt-4o', label: 'GPT-4o' },
  { id: 'claude-4.5', label: 'Claude 4.5' },
];

export function ChatBox() {
  const textareaRef = useRef<HTMLTextAreaElement>(null);
  const [status, setStatus] = useState<ChatStatus>('ready');
  const [model, setModel] = useState('gpt-4o');

  return (
    <PromptInput
      accept="image/*"
      multiple
      maxFileSize={5_000_000}
      onSubmit={(message) => send(message.text, message.files)}
    >
      <PromptInputBody>
        <PromptInputAttachments>
          {(file) => <PromptInputAttachment data={file} />}
        </PromptInputAttachments>
        <PromptInputTextarea ref={textareaRef} placeholder="Ask anything" />
      </PromptInputBody>
      <PromptInputFooter>
        <PromptInputTools>
          <PromptInputActionMenu>
            <PromptInputActionMenuTrigger aria-label="Add attachment" />
            <PromptInputActionMenuContent>
              <PromptInputActionAddAttachments />
            </PromptInputActionMenuContent>
          </PromptInputActionMenu>
          <PromptInputSpeechButton textareaRef={textareaRef} />
          <PromptInputButton aria-label="Search the web">
            <GlobeIcon />
            Web
          </PromptInputButton>
          <PromptInputSelect value={model} onValueChange={setModel}>
            <PromptInputSelectTrigger>
              <PromptInputSelectValue placeholder="Model" />
            </PromptInputSelectTrigger>
            <PromptInputSelectContent>
              {models.map((m) => (
                <PromptInputSelectItem key={m.id} value={m.id}>
                  {m.label}
                </PromptInputSelectItem>
              ))}
            </PromptInputSelectContent>
          </PromptInputSelect>
        </PromptInputTools>
        <PromptInputSubmit status={status} />
      </PromptInputFooter>
    </PromptInput>
  );
}

Receitas

PromptInput é customizado por composição, não por configuração. Não existe uma prop showAttachments ou showVoice, por design (o mesmo contrato do Vercel AI Elements): cada parte é um componente que você inclui na árvore ou deixa de fora. Para remover a seção de context, o botão de voz ou o fluxo de attachments, você deleta a parte do seu código em vez de alternar uma flag. As receitas abaixo são pontos de partida completos e copiáveis, ordenados do menor composer possível até a superfície B2B completa.

Mínimo

A menor composição válida: um body com a textarea e um footer com o botão de submit. O <span /> vazio mantém o layout justify-between do footer empurrando o submit para a direita. Todo o resto é opt-in.

'use client';import {  PromptInput,  PromptInputBody,  PromptInputFooter,  PromptInputSubmit,  PromptInputTextarea,} from '@gremorie/rx-ai';export function PromptInputMinimalPreview() {  return (    <PromptInput      className="mx-auto max-w-xl"      onSubmit={() => {        // demo: wire onSubmit to the AI SDK to make this real      }}    >      <PromptInputBody>        <PromptInputTextarea placeholder="Ask anything..." />      </PromptInputBody>      <PromptInputFooter>        <span />        <PromptInputSubmit status="ready" />      </PromptInputFooter>    </PromptInput>  );}

Com attachments

Adiciona o affordance de anexar e a linha de chips. PromptInputAttachButton abre o file picker, e PromptInputAttachments renderiza um chip por arquivo anexado (colar e arrastar-e-soltar convergem para a mesma lista). A linha standalone Attachments semeia o preview com arquivos de exemplo para que você veja o tratamento do chip por tipo de mídia.

q3-revenue-chart.png
q3-revenue-chart.png
q3-report.pdf
earnings-call.mp3
'use client';import {  type AttachmentFile,  Attachments,  PromptInput,  PromptInputAttachButton,  PromptInputAttachment,  PromptInputAttachments,  PromptInputBody,  PromptInputFooter,  PromptInputSubmit,  PromptInputTextarea,  PromptInputTools,} from '@gremorie/rx-ai';// Pre-seeded demo chips so the surface is not empty. Files the user// actually attaches flow through PromptInputAttachments below.const SAMPLE_FILES: AttachmentFile[] = [  {    id: 'chart',    type: 'file',    url: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=400&h=400&fit=crop',    mediaType: 'image/jpeg',    filename: 'q3-revenue-chart.png',  },  {    id: 'pdf',    type: 'file',    url: '',    mediaType: 'application/pdf',    filename: 'q3-report.pdf',  },  {    id: 'call',    type: 'file',    url: '',    mediaType: 'audio/mpeg',    filename: 'earnings-call.mp3',  },];export function PromptInputAttachmentsPreview() {  return (    <PromptInput      className="mx-auto max-w-xl"      globalDrop      multiple      onSubmit={() => {        // demo: wire onSubmit to the AI SDK to make this real      }}    >      <PromptInputBody>        <Attachments className="w-full px-3" variant="inline">          {SAMPLE_FILES.map((file) => (            <PromptInputAttachment data={file} key={file.id} />          ))}        </Attachments>        <PromptInputAttachments>          {(attachment) => <PromptInputAttachment data={attachment} />}        </PromptInputAttachments>        <PromptInputTextarea placeholder="Ask about these files..." />      </PromptInputBody>      <PromptInputFooter>        <PromptInputTools>          <PromptInputAttachButton tooltip="Attach files" />        </PromptInputTools>        <PromptInputSubmit status="ready" />      </PromptInputFooter>    </PromptInput>  );}

B2B completo

A composição business padrão da Gremorie, a mesma superfície que o bloco Assistant distribui: um command palette de add-context com @ mais um medidor de uso de tokens no header, selects de Mode e Model com ícones de provider no footer, e um grupo de ícones de busca na web / anexar / voz ao lado do botão de submit. Cada ação carrega um tooltip.

'use client';import {  Context,  ContextCacheUsage,  ContextContent,  ContextContentBody,  ContextContentFooter,  ContextContentHeader,  ContextInputUsage,  ContextOutputUsage,  ContextReasoningUsage,  ContextTrigger,  PromptInput,  PromptInputAttachButton,  PromptInputBody,  PromptInputButton,  PromptInputFooter,  PromptInputHeader,  PromptInputMentions,  type PromptInputMentionsItem,  PromptInputSelect,  PromptInputSelectContent,  PromptInputSelectItem,  PromptInputSelectTrigger,  PromptInputSelectValue,  PromptInputSpeechButton,  PromptInputSubmit,  PromptInputTextarea,  PromptInputTools,} from '@gremorie/rx-ai';import { ClaudeIcon, GeminiIcon, OpenAiIcon } from '@gremorie/rx-icons';import { Tooltip, TooltipContent, TooltipTrigger } from '@gremorie/rx-overlays';import {  ArrowUpIcon,  BookOpenIcon,  ChartColumnBigIcon,  DatabaseIcon,  FileSpreadsheetIcon,  GitBranchIcon,  GlobeIcon,  LayersIcon,  ListChecksIcon,  MessageCircleQuestionIcon,  NotebookTextIcon,  TelescopeIcon,} from 'lucide-react';import type { LanguageModelUsage } from 'ai';const MODES = [  {    id: 'ask',    label: 'Ask',    icon: <MessageCircleQuestionIcon className="size-4" />,  },  {    id: 'analyze',    label: 'Analyze',    icon: <ChartColumnBigIcon className="size-4" />,  },  {    id: 'research',    label: 'Research',    icon: <TelescopeIcon className="size-4" />,  },  { id: 'plan', label: 'Plan', icon: <ListChecksIcon className="size-4" /> },];const MODELS = [  {    id: 'claude-opus-4-8',    label: 'Claude Opus 4.8',    icon: <ClaudeIcon className="size-4" />,  },  {    id: 'claude-sonnet-4-6',    label: 'Claude Sonnet 4.6',    icon: <ClaudeIcon className="size-4" />,  },  { id: 'gpt-5', label: 'GPT-5', icon: <OpenAiIcon className="size-4" /> },  {    id: 'gemini-2-5-pro',    label: 'Gemini 2.5 Pro',    icon: <GeminiIcon className="size-4" />,  },];// Example context catalogue for the "@ Add context" command palette.const CONTEXT_ITEMS: PromptInputMentionsItem[] = [  {    id: 'q3-revenue',    label: 'Q3 revenue.csv',    group: 'Recent',    icon: <FileSpreadsheetIcon />,  },  {    id: 'brand',    label: 'Brand guidelines',    group: 'Recent',    icon: <BookOpenIcon />,  },  {    id: 'onboarding-prd',    label: 'Onboarding PRD',    group: 'Recent',    icon: <NotebookTextIcon />,  },  {    id: 'notion-roadmap',    label: 'Notion: Roadmap',    group: 'Workspace',    icon: <DatabaseIcon />,  },  {    id: 'linear-sprint',    label: 'Linear: Sprint 24',    group: 'Workspace',    icon: <LayersIcon />,  },  {    id: 'github-app',    label: 'GitHub: gremorie/app',    group: 'Workspace',    icon: <GitBranchIcon />,  },];// Per-category token usage powering the context hovercard breakdown + cost.const USAGE: LanguageModelUsage = {  inputTokens: 52_600,  outputTokens: 6_400,  reasoningTokens: 2_200,  cachedInputTokens: 14_000,  totalTokens: 61_200,  inputTokenDetails: {    noCacheTokens: 38_600,    cacheReadTokens: 14_000,    cacheWriteTokens: 0,  },  outputTokenDetails: {    textTokens: 6_400,    reasoningTokens: 2_200,  },};// Outlined trigger to match the Button "outline" variant. The default// PromptInputSelectTrigger is borderless/ghost, so re-add the border and keep// the fill transparent so the control sits flush on the card surface.const OUTLINE_TRIGGER =  'border border-input border-solid bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground aria-expanded:bg-accent';export function PromptInputB2BPreview() {  return (    <PromptInput      className="mx-auto max-w-2xl"      globalDrop      multiple      onSubmit={() => {        // demo: wire onSubmit to the AI SDK to make this real      }}    >      <PromptInputHeader>        <PromptInputMentions items={CONTEXT_ITEMS} />        <Context          maxTokens={200_000}          modelId="anthropic:claude-3-5-sonnet"          usage={USAGE}          usedTokens={62_600}        >          <ContextTrigger className="ml-auto" />          <ContextContent>            <ContextContentHeader />            <ContextContentBody>              <div className="space-y-1">                <ContextInputUsage />                <ContextOutputUsage />                <ContextReasoningUsage />                <ContextCacheUsage />              </div>            </ContextContentBody>            <ContextContentFooter />          </ContextContent>        </Context>      </PromptInputHeader>      <PromptInputBody>        <PromptInputTextarea placeholder="What would you like to know?" />      </PromptInputBody>      <PromptInputFooter>        <PromptInputTools className="gap-2">          <PromptInputSelect defaultValue="research">            <Tooltip>              <TooltipTrigger asChild>                <PromptInputSelectTrigger                  aria-label="Select mode"                  className={OUTLINE_TRIGGER}                  size="sm"                >                  <PromptInputSelectValue placeholder="Mode" />                </PromptInputSelectTrigger>              </TooltipTrigger>              <TooltipContent>Response mode</TooltipContent>            </Tooltip>            <PromptInputSelectContent>              {MODES.map((mode) => (                <PromptInputSelectItem key={mode.id} value={mode.id}>                  {mode.icon}                  {mode.label}                </PromptInputSelectItem>              ))}            </PromptInputSelectContent>          </PromptInputSelect>          <PromptInputSelect defaultValue="claude-sonnet-4-6">            <Tooltip>              <TooltipTrigger asChild>                <PromptInputSelectTrigger                  aria-label="Select model"                  className={OUTLINE_TRIGGER}                  size="sm"                >                  <PromptInputSelectValue placeholder="Model" />                </PromptInputSelectTrigger>              </TooltipTrigger>              <TooltipContent>Model</TooltipContent>            </Tooltip>            <PromptInputSelectContent>              {MODELS.map((model) => (                <PromptInputSelectItem key={model.id} value={model.id}>                  {model.icon}                  {model.label}                </PromptInputSelectItem>              ))}            </PromptInputSelectContent>          </PromptInputSelect>        </PromptInputTools>        <PromptInputTools className="gap-2">          <PromptInputTools>            <PromptInputButton              aria-label="Search the web"              tooltip="Search the web"            >              <GlobeIcon className="size-4" />            </PromptInputButton>            <PromptInputAttachButton tooltip="Attach files" />            <PromptInputSpeechButton tooltip="Voice input" />          </PromptInputTools>          <PromptInputSubmit status="ready" tooltip="Send">            <ArrowUpIcon className="size-4" />          </PromptInputSubmit>        </PromptInputTools>      </PromptInputFooter>    </PromptInput>  );}

API

Tipos

TypeShapeDescription
ChatStatus"ready" | "submitted" | "streaming" | "error"Os estados do ciclo de vida aos quais PromptInputSubmit reage.
FileUIPart{ type: "file"; url: string; mediaType: string; filename?: string }O shape de file part do Vercel AI SDK. Exportado aqui por conveniência.
AttachmentFileFileUIPart & { id: string }Representação interna que adiciona um ID estável. Usada pelo context de attachments.
PromptInputMessage{ text: string; files: FileUIPart[] }O que onSubmit recebe.

<PromptInput>

O form raiz. Gerencia attachments, drag-drop de arquivos e o ciclo de vida do submit. Use um pai PromptInputProvider para elevar o estado para fora.

PropTypeDefaultDescription
onSubmit(message: PromptInputMessage, event: FormEvent) => void | Promise<void>-Obrigatório. Dispara no submit do form (tecla Enter, clique em Submit ou requestSubmit).
acceptstring-Padrão MIME separado por vírgulas ("image/*,application/pdf"). Valida arquivos colados e soltos.
multipleboolean-Permite mais de um arquivo por seleção.
globalDropbooleanfalseQuando true, aceita drops em qualquer lugar do documento. O escopo padrão é o elemento form.
syncHiddenInputbooleanfalseLimpa o input de arquivo oculto quando a lista de attachments vai a 0. Útil para re-selecionar o mesmo nome de arquivo.
maxFilesnumber-Limite no total de attachments. Arquivos extras disparam onError({ code: "max_files" }).
maxFileSizenumber-Limite de bytes por arquivo. Arquivos acima do limite disparam onError({ code: "max_file_size" }).
onError(err: { code: "max_files" | "max_file_size" | "accept"; message: string }) => void-Dispara quando a validação rejeita arquivos.
classNamestring-Classes extras no InputGroup subjacente.
childrenReactNode-Tipicamente PromptInputBody e seus descendentes.

<PromptInputProvider>

Provider opcional que eleva o estado de texto e attachment acima de PromptInput. Envolva sua árvore uma vez perto da raiz, e então qualquer descendente pode usar usePromptInputController() ou useProviderAttachments().

PropTypeDefaultDescription
initialInputstring""Valor semente para a textarea controlada.
childrenReactNode-A árvore que deve observar o estado elevado.

<PromptInputBody> / <PromptInputHeader> / <PromptInputFooter> / <PromptInputTools>

Primitives de layout.

  • PromptInputBody envolve os children em display: contents para que fluam diretamente para dentro do InputGroup.
  • PromptInputHeader é um InputGroupAddon fixado em align="block-end" com order-first flex-wrap gap-1. Use para toolbars no estilo header (acima da textarea).
  • PromptInputFooter é um InputGroupAddon fixado em align="block-end" com justify-between gap-1. Casa padrão para tools + submit.
  • PromptInputTools é uma linha flex items-center gap-1 feita para viver dentro do footer.

Todos estendem as props do elemento DOM subjacente.

<PromptInputTextarea>

O composer. Auto-redimensiona entre min-h-16 e max-h-48. name="message" padrão para que serialize como o campo de texto do form.

PropTypeDefaultDescription
placeholderstring"What would you like to know?"Texto de placeholder.
value / onChangeprops controladas-Quando fornecidas, a textarea é totalmente controlada. Caso contrário, o text input do provider a dirige (se houver).

Contrato de teclado embutido:

KeyBehavior
EnterrequestSubmit() (ignorado se o Submit está desabilitado)
Shift+EnterQuebra de linha
Backspace na textarea vaziaRemove o attachment mais recente
Composição IMEEnter é ignorado durante a composição
Colar com arquivosAnexa arquivos automaticamente

<PromptInputAttachments> / <PromptInputAttachment>

ComponentPurpose
PromptInputAttachmentsContainer que renderiza um child por attachment via function child. Não renderiza nada quando a lista está vazia.
PromptInputAttachmentUm único chip com um preview em hover card (thumbnail de imagem ou ícone de clipe) e um botão de remoção.

PromptInputAttachments:

PropTypeDefaultDescription
children(attachment: AttachmentFile) => ReactNode-Obrigatório. Função de render.
classNamestring-Classes extras no wrapper.

PromptInputAttachment:

PropTypeDefaultDescription
dataAttachmentFile-Obrigatório. O attachment a renderizar.
classNamestring-Classes extras no chip.

<PromptInputButton> / <PromptInputSubmit>

PromptInputButton envolve InputGroupButton. Escolhe size="icon-sm" para ícones de child único e size="sm" para ícone+label, a menos que você defina size explicitamente. variant="ghost" padrão.

PromptInputSubmit é um InputGroupButton orientado por status cujo ícone depende de status:

statusIconBehavior
"ready"CornerDownLeftIconSubmete ao clicar. Desabilitado quando a textarea está vazia.
"submitted"Loader2Icon (girando)Desabilitado.
"streaming"SquareIconClique para parar a geração (o host conecta o handler).
"error"XIconClique para tentar de novo (o host conecta o handler).
undefinedCornerDownLeftIconIgual a "ready".

Carrega aria-label="Submit".

Família action menu

PromptInputActionMenu, PromptInputActionMenuTrigger, PromptInputActionMenuContent e PromptInputActionMenuItem são wrappers finos em torno de DropdownMenu, DropdownMenuTrigger, DropdownMenuContent e DropdownMenuItem. O trigger tem como padrão um PromptInputButton com um PlusIcon.

PromptInputActionAddAttachments é um DropdownMenuItem pré-conectado que chama attachments.openFileDialog() ao selecionar. Label padrão: "Add photos or files". Aceita uma prop label para sobrescrever.

<PromptInputSpeechButton>

Toggle da Web Speech API.

PropTypeDefaultDescription
textareaRefRefObject<HTMLTextAreaElement | null>-Quando fornecido, o texto transcrito é anexado ao valor da textarea.
onTranscriptionChange(text: string) => void-Dispara sempre que um transcript final é anexado.
langstring"en-US"Tag de idioma BCP-47 passada para SpeechRecognition.

Carrega aria-label="Toggle voice input". Desabilitado quando nem SpeechRecognition nem webkitSpeechRecognition está disponível. Anima uma superfície accent pulsante enquanto escuta.

Família <PromptInputSelect>

Wrappers em torno de Select do @gremorie/rx-forms (PromptInputSelect, PromptInputSelectTrigger, PromptInputSelectContent, PromptInputSelectItem, PromptInputSelectValue). O trigger é re-estilizado para parecer inline dentro da linha de input (sem borda, fundo transparente, foreground muted).

Família <PromptInputHoverCard>

PromptInputHoverCard, PromptInputHoverCardTrigger, PromptInputHoverCardContent. HoverCard do @gremorie/rx-overlays com openDelay e closeDelay padronizados em 0 para que os previews pareçam instantâneos. O content tem como padrão align="start". Usado internamente por PromptInputAttachment.

Família <PromptInputCommand>

PromptInputCommand, PromptInputCommandInput, PromptInputCommandList, PromptInputCommandEmpty, PromptInputCommandGroup, PromptInputCommandItem, PromptInputCommandSeparator. Wrappers finos em torno dos primitives do Command palette.

PromptInputCommandDialog é o palette flutuante Cmd+K - o mesmo Command palette dentro de um Dialog para uso como launcher global.

Família <PromptInputTabs*>

PromptInputTabsList, PromptInputTab, PromptInputTabLabel, PromptInputTabBody, PromptInputTabItem. Painéis com abas leves e sem estado para uso dentro de menus e dropdowns. São containers estilizados - traga seu próprio estado de seleção.

Hooks

HookPurpose
usePromptInputController()Lê ou muta o valor do text input e os attachments. Lança erro se não houver pai PromptInputProvider.
useProviderAttachments()Lê o estado de attachment elevado ao provider. Lança erro se não houver pai PromptInputProvider.
usePromptInputAttachments()Lê o estado de attachment do PromptInput mais próximo. Usado dentro de descendentes como PromptInputTextarea e PromptInputAttachment.

Composição

PromptInput (raiz <form>, máquina de estados + drag-drop + wrapper InputGroup)
  PromptInputBody (display: contents, envolve apenas os attachments + textarea)
    PromptInputAttachments (lista de chips com render-prop, oculta quando vazia)
      PromptInputAttachment (chip + preview HoverCard + botão de remoção)
    PromptInputTextarea (auto-resize, Enter submete, Shift+Enter quebra linha)
  PromptInputHeader (opcional, irmão do Body, renderiza acima da textarea)
  PromptInputFooter (casa padrão para tools + submit, irmão do Body)
    PromptInputTools (esquerda, botões de ação)
      PromptInputActionMenu (trigger de dropdown + content + items)
        PromptInputActionAddAttachments (item pré-conectado)
      PromptInputSpeechButton (toggle Web Speech)
      PromptInputButton (ações customizadas - busca na web, etc.)
      PromptInputSelect (seletor de modelo)
    PromptInputSubmit (direita, ícone orientado por status)

[!important] PromptInputHeader e PromptInputFooter devem ser irmãos de PromptInputBody, não aninhados dentro dele. PromptInputBody é display: contents, e o InputGroup subjacente só muda para seu layout de coluna empilhada quando um addon data-align="block-end" é seu filho direto. Um footer aninhado dentro do Body de contents não é um filho direto, então o group permanece uma única linha e a textarea colapsa em uma barra fina.

Como as partes se encaixam

  1. Wrapping do InputGroup: A raiz React envolve o conteúdo do <form> em um InputGroup do @gremorie/rx-forms. Isso te dá uma única superfície arredondada com estados de focus, invalid e disabled compartilhados entre a textarea e cada addon.
  2. Addons alinhados por bloco: PromptInputHeader e PromptInputFooter são ambos InputGroupAddon align="block-end". Eles renderizam abaixo da textarea visualmente, mas participam do focus ring do group.
  3. Dimensionamento: Cada botão de toolbar escolhe xs, sm, icon-xs ou icon-sm para combinar com a altura do group automaticamente. PromptInputButton escolhe icon-sm quando tem um único child e sm quando tem ícone+label, a menos que você sobrescreva.
  4. Context de estado de attachment: PromptInput (ou PromptInputProvider quando presente) provê o context de attachments que PromptInputAttachments, PromptInputAttachment e PromptInputActionAddAttachments todos leem. Adicionar via action menu, colar, drag-drop ou attachments.add(files) tudo converge para a mesma lista.

Variações

Estado elevado via Provider

Use PromptInputProvider para compartilhar o valor da textarea e os attachments com irmãos (por exemplo, um painel de sugestões que quer escrever no input).

import { PromptInputProvider, usePromptInputController } from '@gremorie/rx-ai';

function SuggestionRow() {
  const { textInput } = usePromptInputController();
  return (
    <Button onClick={() => textInput.setInput('Summarize this thread')}>
      Insert suggestion
    </Button>
  );
}

export function ComposerExample() {
  return (
    <PromptInputProvider initialInput="">
      <SuggestionRow />
      <PromptInput onSubmit={handle}>
        <PromptInputBody>
          <PromptInputTextarea />
        </PromptInputBody>
        <PromptInputFooter>
          <span />
          <PromptInputSubmit />
        </PromptInputFooter>
      </PromptInput>
    </PromptInputProvider>
  );
}

Attachments de imagem restritos

Rejeite tudo exceto imagens e limite em 5 MB.

<PromptInput
  accept="image/*"
  multiple
  maxFiles={4}
  maxFileSize={5_000_000}
  onError={(err) => toast.error(err.message)}
  onSubmit={handle}
>
  {/* ... */}
</PromptInput>

Zona de drop global

Aceite arquivos soltos em qualquer lugar da página.

<PromptInput globalDrop accept="image/*" onSubmit={handle}>
  {/* ... */}
</PromptInput>

Com command palette Cmd+K

Combine o composer inline com um launcher global usando PromptInputCommandDialog.

const [open, setOpen] = useState(false);

useEffect(() => {
  const handler = (e: KeyboardEvent) => {
    if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
      e.preventDefault();
      setOpen((v) => !v);
    }
  };
  document.addEventListener('keydown', handler);
  return () => document.removeEventListener('keydown', handler);
}, []);

<PromptInputCommandDialog open={open} onOpenChange={setOpen}>
  <PromptInputCommandInput placeholder="Type a command..." />
  <PromptInputCommandList>
    <PromptInputCommandEmpty>No results.</PromptInputCommandEmpty>
    <PromptInputCommandGroup heading="Actions">
      <PromptInputCommandItem>New chat</PromptInputCommandItem>
      <PromptInputCommandItem>Clear history</PromptInputCommandItem>
    </PromptInputCommandGroup>
  </PromptInputCommandList>
</PromptInputCommandDialog>;

Status de streaming

Conecte status ao seu estado de rede para alternar o Submit entre seta, spinner e stop.

const [status, setStatus] = useState<ChatStatus>('ready');

async function send(message: PromptInputMessage) {
  setStatus('submitted');
  try {
    await streamResponse(message, {
      onStreamStart: () => setStatus('streaming'),
      onComplete: () => setStatus('ready'),
    });
  } catch {
    setStatus('error');
  }
}

<PromptInputSubmit
  status={status}
  onClick={status === 'streaming' ? stop : undefined}
/>;

Acessibilidade

  • Semântica de <form> real: PromptInput renderiza um <form>, então Enter submete via form.requestSubmit(). A validação de form HTML ainda roda - atributos nativos required / pattern funcionam em qualquer campo dentro.
  • aria-label do Submit: PromptInputSubmit sempre carrega aria-label="Submit". Se sua localização difere, sobrescreva via sua própria composição de Button.
  • Contrato de teclado (documentado acima e imposto no código): Enter submete, Shift+Enter insere uma quebra de linha, Backspace na textarea vazia remove o attachment mais recente. A flag de composição IME (onCompositionStart / onCompositionEnd) suprime o Enter enquanto se compõe input CJK.
  • Degradação do botão de speech: Quando a Web Speech API está ausente, PromptInputSpeechButton é desabilitado em vez de oculto. aria-label="Toggle voice input" permanece no botão desabilitado para que leitores de tela possam descrever o que está indisponível.
  • Remoção de attachment: O botão de remoção de cada chip carrega aria-label="Remove attachment" mais um span sr-only "Remove".
  • Tools que precisam de tooltips: PromptInputButtons apenas com ícone exigem um aria-label. Não há tooltip embutido, então envolva cada um em Tooltip do @gremorie/rx-overlays quando quiser uma dica em hover.
  • Anúncio de status: Quando status muda, considere anunciar o novo estado via uma região aria-live separada no app host - a troca de ícone do Submit sozinha é silenciosa para leitores de tela.
  • Focus visível: A superfície do InputGroup dirige o focus ring. Não remova os outlines.
  • Movimento reduzido: A animação de pulso do botão de speech e o spinner do submit respeitam prefers-reduced-motion via as variantes motion-reduce: padrão do Tailwind (aplique manualmente a adições customizadas).

Relacionados

  • Message - o turno do assistant / usuário renderizado acima deste composer
  • Conversation - a superfície rolável que hospeda tanto as mensagens quanto este composer
  • Model Selector - seletor de modelo standalone alternativo se você não quiser inline
  • Suggestion - prompts prontos que os usuários podem clicar para semear a textarea
  • Open in Chat - passe a query atual para um chat externo

On this page