Prompt InputPreview
Composable prompt form with text editor, actions, attachments, and submit states.
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
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
Default
Omit status or pass null when idle. Pass status="processing" or status="streaming" to show a disabled spinner in the submit button.
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 />;Speech Input
PromptInputSpeechButton writes final and interim speech into PromptInputTextEditor. Use standalone Speech Input when dictation is needed outside a prompt composer.
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 />;Attachments
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 />;