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:
- Auto-gerenciado (padrão). Tudo vive dentro de
PromptInput. PasseonSubmite pronto. - Elevado por provider via
PromptInputProvider. ExpõeusePromptInputController()para ler ou mutar o texto e os attachments de qualquer lugar na árvore. - Textarea controlada. O
textInput.value/textInput.setInputdo provider dirigemPromptInputTextareaautomaticamente 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 enviarInstalaçã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.
'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
| Type | Shape | Description |
|---|---|---|
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. |
AttachmentFile | FileUIPart & { 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.
| Prop | Type | Default | Description |
|---|---|---|---|
onSubmit | (message: PromptInputMessage, event: FormEvent) => void | Promise<void> | - | Obrigatório. Dispara no submit do form (tecla Enter, clique em Submit ou requestSubmit). |
accept | string | - | Padrão MIME separado por vírgulas ("image/*,application/pdf"). Valida arquivos colados e soltos. |
multiple | boolean | - | Permite mais de um arquivo por seleção. |
globalDrop | boolean | false | Quando true, aceita drops em qualquer lugar do documento. O escopo padrão é o elemento form. |
syncHiddenInput | boolean | false | Limpa o input de arquivo oculto quando a lista de attachments vai a 0. Útil para re-selecionar o mesmo nome de arquivo. |
maxFiles | number | - | Limite no total de attachments. Arquivos extras disparam onError({ code: "max_files" }). |
maxFileSize | number | - | 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. |
className | string | - | Classes extras no InputGroup subjacente. |
children | ReactNode | - | 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().
| Prop | Type | Default | Description |
|---|---|---|---|
initialInput | string | "" | Valor semente para a textarea controlada. |
children | ReactNode | - | A árvore que deve observar o estado elevado. |
<PromptInputBody> / <PromptInputHeader> / <PromptInputFooter> / <PromptInputTools>
Primitives de layout.
PromptInputBodyenvolve os children emdisplay: contentspara que fluam diretamente para dentro doInputGroup.PromptInputHeaderé umInputGroupAddonfixado emalign="block-end"comorder-first flex-wrap gap-1. Use para toolbars no estilo header (acima da textarea).PromptInputFooteré umInputGroupAddonfixado emalign="block-end"comjustify-between gap-1. Casa padrão para tools + submit.PromptInputToolsé uma linhaflex items-center gap-1feita 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.
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder | string | "What would you like to know?" | Texto de placeholder. |
value / onChange | props controladas | - | Quando fornecidas, a textarea é totalmente controlada. Caso contrário, o text input do provider a dirige (se houver). |
Contrato de teclado embutido:
| Key | Behavior |
|---|---|
Enter | requestSubmit() (ignorado se o Submit está desabilitado) |
Shift+Enter | Quebra de linha |
Backspace na textarea vazia | Remove o attachment mais recente |
| Composição IME | Enter é ignorado durante a composição |
| Colar com arquivos | Anexa arquivos automaticamente |
<PromptInputAttachments> / <PromptInputAttachment>
| Component | Purpose |
|---|---|
PromptInputAttachments | Container que renderiza um child por attachment via function child. Não renderiza nada quando a lista está vazia. |
PromptInputAttachment | Um único chip com um preview em hover card (thumbnail de imagem ou ícone de clipe) e um botão de remoção. |
PromptInputAttachments:
| Prop | Type | Default | Description |
|---|---|---|---|
children | (attachment: AttachmentFile) => ReactNode | - | Obrigatório. Função de render. |
className | string | - | Classes extras no wrapper. |
PromptInputAttachment:
| Prop | Type | Default | Description |
|---|---|---|---|
data | AttachmentFile | - | Obrigatório. O attachment a renderizar. |
className | string | - | 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:
status | Icon | Behavior |
|---|---|---|
"ready" | CornerDownLeftIcon | Submete ao clicar. Desabilitado quando a textarea está vazia. |
"submitted" | Loader2Icon (girando) | Desabilitado. |
"streaming" | SquareIcon | Clique para parar a geração (o host conecta o handler). |
"error" | XIcon | Clique para tentar de novo (o host conecta o handler). |
undefined | CornerDownLeftIcon | Igual 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.
| Prop | Type | Default | Description |
|---|---|---|---|
textareaRef | RefObject<HTMLTextAreaElement | null> | - | Quando fornecido, o texto transcrito é anexado ao valor da textarea. |
onTranscriptionChange | (text: string) => void | - | Dispara sempre que um transcript final é anexado. |
lang | string | "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
| Hook | Purpose |
|---|---|
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]
PromptInputHeaderePromptInputFooterdevem ser irmãos dePromptInputBody, não aninhados dentro dele.PromptInputBodyédisplay: contents, e oInputGroupsubjacente só muda para seu layout de coluna empilhada quando um addondata-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
- Wrapping do InputGroup: A raiz React envolve o conteúdo do
<form>em umInputGroupdo@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. - Addons alinhados por bloco:
PromptInputHeaderePromptInputFootersão ambosInputGroupAddon align="block-end". Eles renderizam abaixo da textarea visualmente, mas participam do focus ring do group. - Dimensionamento: Cada botão de toolbar escolhe
xs,sm,icon-xsouicon-smpara combinar com a altura do group automaticamente.PromptInputButtonescolheicon-smquando tem um único child esmquando tem ícone+label, a menos que você sobrescreva. - Context de estado de attachment:
PromptInput(ouPromptInputProviderquando presente) provê o context de attachments quePromptInputAttachments,PromptInputAttachmentePromptInputActionAddAttachmentstodos leem. Adicionar via action menu, colar, drag-drop ouattachments.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:PromptInputrenderiza um<form>, então Enter submete viaform.requestSubmit(). A validação de form HTML ainda roda - atributos nativosrequired/patternfuncionam em qualquer campo dentro. aria-labeldo Submit:PromptInputSubmitsempre carregaaria-label="Submit". Se sua localização difere, sobrescreva via sua própria composição deButton.- 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 spansr-only"Remove". - Tools que precisam de tooltips:
PromptInputButtons apenas com ícone exigem umaria-label. Não há tooltip embutido, então envolva cada um emTooltipdo@gremorie/rx-overlaysquando quiser uma dica em hover. - Anúncio de status: Quando
statusmuda, considere anunciar o novo estado via uma regiãoaria-liveseparada no app host - a troca de ícone do Submit sozinha é silenciosa para leitores de tela. - Focus visível: A superfície do
InputGroupdirige 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-motionvia as variantesmotion-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