AI Prompt Input
AI Prompt Input é um componente de formulário para React e Tailwind CSS, da biblioteca HextaUI, com licença MIT. Copia e cola no teu projeto.
HextaUI
MIT
forms
O que é
AI Prompt Input é um componente de formulário para React e Tailwind CSS, da biblioteca HextaUI, com licença MIT. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Não precisa de pacotes além do que o projeto já tem
Licença
Este componente vem de HextaUI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.
AI Prompt Input
"use client";
import {
Check,
Globe,
Lightbulb,
Mic,
MoreHorizontal,
Paperclip,
Plus,
Send,
X,
} from "lucide-react";
import Image from "next/image";
import { useCallback, useState } from "react";
import { Button } from "@/registry/new-york/ui/button";
import { Card, CardContent } from "@/registry/new-york/ui/card";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/registry/new-york/ui/dropdown-menu";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/registry/new-york/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/new-york/ui/select";
import { Textarea } from "@/registry/new-york/ui/textarea";
import { Toggle } from "@/registry/new-york/ui/toggle";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/registry/new-york/ui/tooltip";
const mockUsers = [
{
id: "shadcn",
name: "shadcn",
avatar: "https://github.com/shadcn.png",
},
{
id: "preetsuthar17",
name: "preetsuthar17",
avatar: "https://github.com/preetsuthar17.png",
},
{
id: "evilrabbit",
name: "evilrabbit",
avatar: "https://github.com/evilrabbit.png",
},
];
const mockPages = [
{ id: "analytics-report", name: "Analytics Report", icon: "📊" },
{ id: "technical-specs", name: "Technical Specs", icon: "📑" },
{ id: "user-guide", name: "User Guide", icon: "💡" },
{ id: "team-directory", name: "Team Directory", icon: "👥" },
{ id: "attachments", name: "Attachments", icon: "📎" },
{ id: "new-feature", name: "New Feature Plan", icon: "✨" },
];
const models = [
{ value: "claude-sonnet-4-preview", label: "Claude Sonnet 4 (Preview)" },
{ value: "claude-sonnet-3.7", label: "Claude Sonnet 3.7" },
{ value: "claude-sonnet-3.5", label: "Claude Sonnet 3.5" },
{ value: "gemini-2.5-pro-preview", label: "Gemini 2.5 pro (Preview)" },
{ value: "gpt-3.5-turbo", label: "GPT-3.5 Turbo" },
{ value: "gpt-4", label: "GPT-4" },
{ value: "gpt-4o", label: "GPT-4o" },
{ value: "gpt-o4-mini", label: "o4-mini (Preview)" },
];
const agentModes = [
{ value: "auto", label: "Auto" },
{ value: "agent", label: "Agent" },
{ value: "plan", label: "Plan" },
];
const hideScrollbar = {
msOverflowStyle: "none",
scrollbarWidth: "none" as const,
} as React.CSSProperties;
interface ContextBadgeProps {
label: string;
icon?: string;
avatar?: string;
onRemove: () => void;
}
function ContextBadge({ label, icon, avatar, onRemove }: ContextBadgeProps) {
return (
<span
className="inline-flex items-center gap-1 rounded-xl border bg-secondary p-0.5 font-medium text-muted-foreground text-xs"
style={{ whiteSpace: "nowrap", flex: "0 0 auto" }}
>
{icon && (
<span className="relative inline-block size-4 overflow-hidden rounded-full bg-white p-0.5 align-middle">
<span
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
height: "100%",
fontSize: "12px",
lineHeight: 1,
objectFit: "cover",
borderRadius: "9999px",
}}
>
{icon}
</span>
</span>
)}
{avatar && (
<span className="relative inline-block h-4 w-4">
<Image
alt={label}
className="rounded-full"
fill
priority
sizes="16px"
src={avatar}
style={{ objectFit: "cover" }}
/>
</span>
)}
{label}
<button
aria-label={`Remove ${label}`}
className="rounded-full p-0.5 hover:bg-muted-foreground/10 focus:outline-none focus:ring-1 focus:ring-muted-foreground"
onClick={onRemove}
tabIndex={0}
type="button"
>
<X className="h-3 w-3 text-muted-foreground/60" />
</button>
</span>
);
}
interface ContextPopoverProps {
open: boolean;
onOpenChange: (open: boolean) => void;
selectedUsers: string[];
selectedPages: string[];
onUserToggle: (userId: string) => void;
onPageToggle: (pageId: string) => void;
}
function ContextPopover({
open,
onOpenChange,
selectedUsers,
selectedPages,
onUserToggle,
onPageToggle,
}: ContextPopoverProps) {
return (
<Popover onOpenChange={onOpenChange} open={open}>
<PopoverTrigger asChild>
<button
aria-label="Add context"
className="inline-flex items-center gap-1 rounded-xl border bg-secondary px-2 py-0.5 font-medium text-muted-foreground text-xs transition"
style={{
whiteSpace: "nowrap",
flex: "0 0 auto",
height: "auto",
appearance: "none",
background: "none",
}}
type="button"
>
<Plus className="h-3 w-3 text-muted-foreground" />
<span>Add context</span>
</button>
</PopoverTrigger>
<PopoverContent align="start" className="z-30 w-max p-2">
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<div className="px-1 font-semibold text-muted-foreground text-xs uppercase tracking-wide">
Pages
</div>
<div className="flex flex-col gap-1">
{mockPages.map((page) => {
const isSelected = selectedPages.includes(page.id);
return (
<button
aria-pressed={isSelected}
className={`inline-flex items-center justify-start gap-1 rounded-xl border px-2 py-0.5 font-medium text-xs transition ${
isSelected
? "bg-secondary text-muted-foreground"
: "border-transparent bg-background text-foreground"
}`}
key={page.id}
onClick={() => onPageToggle(page.id)}
style={{ fontWeight: 400, whiteSpace: "nowrap" }}
type="button"
>
{page.icon} {page.name}
</button>
);
})}
</div>
</div>
<div className="flex flex-col gap-1">
<div className="px-1 font-semibold text-muted-foreground text-xs uppercase tracking-wide">
Users
</div>
<div className="flex flex-col gap-1">
{mockUsers.map((user) => {
const isSelected = selectedUsers.includes(user.id);
return (
<button
aria-pressed={isSelected}
className={`inline-flex items-center justify-start gap-1 rounded-xl border p-0.5 font-medium text-xs transition ${
isSelected
? "bg-secondary text-muted-foreground"
: "border-transparent bg-background text-foreground hover:bg-accent hover:text-accent-foreground"
}`}
key={user.id}
onClick={() => onUserToggle(user.id)}
style={{ fontWeight: 400, whiteSpace: "nowrap" }}
type="button"
>
<Image
alt={user.name}
className="h-4 w-4 rounded-full"
height={16}
src={user.avatar}
style={{ display: "inline-block" }}
width={16}
/>
{user.name}
</button>
);
})}
</div>
</div>
</div>
</PopoverContent>
</Popover>
);
}
interface ContextSelectorProps {
selectedUsers: string[];
selectedPages: string[];
onUserToggle: (userId: string) => void;
onPageToggle: (pageId: string) => void;
onRemoveUser: (userId: string) => void;
onRemovePage: (pageId: string) => void;
}
function ContextSelector({
selectedUsers,
selectedPages,
onUserToggle,
onPageToggle,
onRemoveUser,
onRemovePage,
}: ContextSelectorProps) {
const [popoverOpen, setPopoverOpen] = useState(false);
const hasSelectedContext =
selectedPages.length > 0 || selectedUsers.length > 0;
return (
<div className="flex w-full flex-col gap-2">
<div className="flex w-full items-center gap-1">
<ContextPopover
onOpenChange={setPopoverOpen}
onPageToggle={onPageToggle}
onUserToggle={onUserToggle}
open={popoverOpen}
selectedPages={selectedPages}
selectedUsers={selectedUsers}
/>
<div
aria-label="Selected context"
className="scroll-hide hidden flex-1 gap-1 overflow-x-auto sm:flex"
style={{
...hideScrollbar,
WebkitOverflowScrolling: "touch",
minWidth: 0,
}}
tabIndex={0}
>
<style>
{`.scroll-hide::-webkit-scrollbar {
display: none !important;
height: 0 !important;
width: 0 !important;
background: transparent !important;
}`}
</style>
{selectedPages.map((id) => {
const page = mockPages.find((p) => p.id === id);
if (!page) return null;
return (
<ContextBadge
icon={page.icon}
key={`page-${id}`}
label={page.name}
onRemove={() => onRemovePage(id)}
/>
);
})}
{selectedUsers.map((id) => {
const user = mockUsers.find((u) => u.id === id);
if (!user) return null;
return (
<ContextBadge
avatar={user.avatar}
key={`user-${id}`}
label={user.name}
onRemove={() => onRemoveUser(id)}
/>
);
})}
</div>
</div>
{hasSelectedContext && (
<div
aria-label="Selected context (mobile)"
className="scroll-hide flex w-[18rem] gap-1 overflow-x-auto sm:hidden sm:w-full"
style={{
...hideScrollbar,
WebkitOverflowScrolling: "touch",
minWidth: 0,
}}
tabIndex={0}
>
{selectedPages.map((id) => {
const page = mockPages.find((p) => p.id === id);
if (!page) return null;
return (
<ContextBadge
icon={page.icon}
key={`page-${id}`}
label={page.name}
onRemove={() => onRemovePage(id)}
/>
);
})}
{selectedUsers.map((id) => {
const user = mockUsers.find((u) => u.id === id);
if (!user) return null;
return (
<ContextBadge
avatar={user.avatar}
key={`user-${id}`}
label={user.name}
onRemove={() => onRemoveUser(id)}
/>
);
})}
</div>
)}
</div>
);
}
interface ModelSelectProps {
value: string;
onValueChange: (value: string) => void;
}
function ModelSelect({ value, onValueChange }: ModelSelectProps) {
return (
<Select onValueChange={onValueChange} value={value}>
<SelectTrigger className="font-medium" size="default">
<SelectValue placeholder="Select Model" />
</SelectTrigger>
<SelectContent className="text-sm">
{models.map((model) => (
<SelectItem key={model.value} value={model.value}>
{model.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
interface AgentModeSelectProps {
value: string;
onValueChange: (value: string) => void;
}
function AgentModeSelect({ value, onValueChange }: AgentModeSelectProps) {
return (
<Select onValueChange={onValueChange} value={value}>
<SelectTrigger aria-label="Agent mode" className="font-medium">
<SelectValue placeholder="Agent" />
</SelectTrigger>
<SelectContent className="text-sm">
{agentModes.map((mode) => (
<SelectItem key={mode.value} value={mode.value}>
{mode.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
interface LeftActionButtonsProps {
webSearchEnabled: boolean;
thinkingEnabled: boolean;
onWebSearchToggle: (enabled: boolean) => void;
onThinkingToggle: (enabled: boolean) => void;
}
function LeftActionButtons({
webSearchEnabled,
thinkingEnabled,
onWebSearchToggle,
onThinkingToggle,
}: LeftActionButtonsProps) {
return (
<>
<div className="hidden items-center justify-center gap-1 sm:flex">
<Tooltip>
<TooltipTrigger asChild>
<Button
aria-label="Attach file"
size="icon"
type="button"
variant="ghost"
>
<Paperclip />
</Button>
</TooltipTrigger>
<TooltipContent side="top">Attach file</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Toggle
aria-label="Enable Thinking"
onPressedChange={onThinkingToggle}
pressed={thinkingEnabled}
size="default"
variant={thinkingEnabled ? "outline" : "default"}
>
<Lightbulb />
</Toggle>
</TooltipTrigger>
<TooltipContent side="top">
{thinkingEnabled ? "Disable Thinking" : "Enable Thinking"}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Toggle
aria-label="Enable web search"
className="relative flex w-9 min-w-9 items-center justify-center overflow-hidden p-0"
onPressedChange={onWebSearchToggle}
pressed={webSearchEnabled}
size="default"
variant={webSearchEnabled ? "outline" : "default"}
>
<span className="flex h-full w-full items-center justify-center opacity-100 transition-opacity duration-200">
<Globe />
</span>
<span
className={`absolute left-8 whitespace-nowrap font-medium text-sm transition-all duration-200 ${
webSearchEnabled
? "visible left-9 ml-2 opacity-100"
: "invisible left-4 opacity-0"
}`}
style={{
width: webSearchEnabled ? "auto" : 0,
pointerEvents: webSearchEnabled ? "auto" : "none",
}}
>
Search
</span>
</Toggle>
</TooltipTrigger>
<TooltipContent side="top">
{webSearchEnabled ? "Disable web search" : "Enable web search"}
</TooltipContent>
</Tooltip>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label="More options"
className="sm:hidden"
size="icon"
type="button"
variant="ghost"
>
<MoreHorizontal />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => {}}>
<Paperclip className="size-4" />
<span>Attach file</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onThinkingToggle(!thinkingEnabled)}>
<Lightbulb className="size-4" />
<span>Thinking</span>
{thinkingEnabled && <Check className="ml-auto size-4" />}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onWebSearchToggle(!webSearchEnabled)}
>
<Globe className="size-4" />
<span>Web search</span>
{webSearchEnabled && <Check className="ml-auto size-4" />}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
);
}
interface RightActionButtonsProps {
input: string;
}
function RightActionButtons({ input }: RightActionButtonsProps) {
const hasInput = input.trim().length > 0;
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
aria-label={hasInput ? "Send message" : "Start voice input"}
size="icon"
type={hasInput ? "submit" : "button"}
>
{hasInput ? <Send /> : <Mic />}
</Button>
</TooltipTrigger>
<TooltipContent side="top">
{hasInput ? "Send message" : "Start voice input"}
</TooltipContent>
</Tooltip>
);
}
export default function AIPromptInput() {
const [input, setInput] = useState("");
const [model, setModel] = useState("gpt-3.5-turbo");
const [agentMode, setAgentMode] = useState("auto");
const [contextOptions, setContextOptions] = useState<string[]>([]);
const [webSearchEnabled, setWebSearchEnabled] = useState(false);
const [thinkingEnabled, setThinkingEnabled] = useState(false);
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
const [selectedPages, setSelectedPages] = useState<string[]>([]);
const handleUserToggle = (userId: string) => {
setSelectedUsers((prev) =>
prev.includes(userId)
? prev.filter((id) => id !== userId)
: [...prev, userId]
);
if (!contextOptions.includes("users")) {
setContextOptions((prev) => [...prev, "users"]);
}
};
const handlePageToggle = (pageId: string) => {
setSelectedPages((prev) =>
prev.includes(pageId)
? prev.filter((id) => id !== pageId)
: [...prev, pageId]
);
if (!contextOptions.includes("pages")) {
setContextOptions((prev) => [...prev, "pages"]);
}
};
const handleRemoveUser = (userId: string) => {
setSelectedUsers((prev) => prev.filter((id) => id !== userId));
};
const handleRemovePage = (pageId: string) => {
setSelectedPages((prev) => prev.filter((id) => id !== pageId));
};
const handleSubmit = useCallback(
(e?: React.FormEvent) => {
e?.preventDefault();
if (!input.trim()) return;
console.log("Submitted:", {
input,
model,
agentMode,
contextOptions,
webSearchEnabled,
thinkingEnabled,
selectedUsers,
selectedPages,
});
setInput("");
},
[
input,
model,
agentMode,
contextOptions,
webSearchEnabled,
thinkingEnabled,
selectedUsers,
selectedPages,
]
);
return (
<TooltipProvider>
<div className="flex w-full flex-col items-center gap-2">
<Card className="w-full p-0 shadow-xs">
<CardContent className="p-0">
<form
aria-label="AI chat input"
className="flex flex-col gap-3"
onSubmit={handleSubmit}
>
<div className="flex w-full items-start justify-between p-4 pb-0">
<ContextSelector
onPageToggle={handlePageToggle}
onRemovePage={handleRemovePage}
onRemoveUser={handleRemoveUser}
onUserToggle={handleUserToggle}
selectedPages={selectedPages}
selectedUsers={selectedUsers}
/>
</div>
<Textarea
className="resize-none border-none p-4 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
handleSubmit();
}
}}
placeholder="Type your message…"
rows={4}
value={input}
/>
<div className="flex w-full items-center justify-between gap-2 p-4">
<div className="flex items-center justify-center gap-1">
<LeftActionButtons
onThinkingToggle={setThinkingEnabled}
onWebSearchToggle={setWebSearchEnabled}
thinkingEnabled={thinkingEnabled}
webSearchEnabled={webSearchEnabled}
/>
<div className="hidden sm:block">
<AgentModeSelect
onValueChange={setAgentMode}
value={agentMode}
/>
</div>
</div>
<RightActionButtons input={input} />
</div>
</form>
</CardContent>
</Card>
<div className="flex w-full items-center justify-between gap-2">
<ModelSelect onValueChange={setModel} value={model} />
<div className="sm:hidden">
<AgentModeSelect onValueChange={setAgentMode} value={agentMode} />
</div>
</div>
</div>
</TooltipProvider>
);
}