Chatbot

Prompt Input

Allows a user to send a message with file attachments to a large language model. It includes a textarea, file upload capabilities, a submit button, and a dropdown for selecting the model.

The PromptInput component allows a user to send a message with file attachments to a large language model. It includes a textarea, file upload capabilities, a submit button, and a dropdown for selecting the model.

Header Controls via PromptInputProvider

"use client";import {  PromptInput,  PromptInputActionAddAttachments,  PromptInputActionMenu,  PromptInputActionMenuContent,  PromptInputActionMenuTrigger,  PromptInputAttachment,  PromptInputAttachments,  PromptInputBody,  PromptInputButton,  PromptInputFooter,  type PromptInputMessage,  PromptInputModelSelect,  PromptInputModelSelectContent,  PromptInputModelSelectItem,  PromptInputModelSelectTrigger,  PromptInputModelSelectValue,  PromptInputProvider,  PromptInputSpeechButton,  PromptInputSubmit,  PromptInputTextarea,  PromptInputTools,  usePromptInputController,} from "@/components/ai-elements/elements/prompt-input";import { Button } from "@/components/ui/button";import { GlobeIcon } from "lucide-react";import { useRef, useState } from "react";const models = [  { id: "gpt-4", name: "GPT-4" },  { id: "gpt-3.5-turbo", name: "GPT-3.5 Turbo" },  { id: "claude-2", name: "Claude 2" },  { id: "claude-instant", name: "Claude Instant" },  { id: "palm-2", name: "PaLM 2" },  { id: "llama-2-70b", name: "Llama 2 70B" },  { id: "llama-2-13b", name: "Llama 2 13B" },  { id: "cohere-command", name: "Command" },  { id: "mistral-7b", name: "Mistral 7B" },];const SUBMITTING_TIMEOUT = 200;const STREAMING_TIMEOUT = 2000;function HeaderControls() {  const controller = usePromptInputController();  return (    <header className="mb-10">      <h4 className="mb-4 text-sm">        Header Controls via{" "}        <code className="rounded-md bg-gray-100 p-1 font-bold">          PromptInputProvider        </code>      </h4>      <div className="mb-10 flex gap-2 rounded-md bg-gray-100 p-2">        <Button          onClick={() => {            controller.textInput.clear();          }}          type="button"        >          clear input        </Button>        <Button          onClick={() => {            controller.textInput.setInput("Inserted via PromptInputProvider");          }}          type="button"        >          set input        </Button>        <Button          onClick={() => {            controller.attachments.clear();          }}          type="button"        >          clear attachments        </Button>      </div>    </header>  );}const Example = () => {  const [model, setModel] = useState<string>(models[0].id);  const [status, setStatus] = useState<    "submitted" | "streaming" | "ready" | "error"  >("ready");  const textareaRef = useRef<HTMLTextAreaElement>(null);  const handleSubmit = (message: PromptInputMessage) => {    const hasText = Boolean(message.text);    const hasAttachments = Boolean(message.files?.length);    if (!(hasText || hasAttachments)) {      return;    }    setStatus("submitted");    // eslint-disable-next-line no-console    console.log("Submitting message:", message);    setTimeout(() => {      setStatus("streaming");    }, SUBMITTING_TIMEOUT);    setTimeout(() => {      setStatus("ready");    }, STREAMING_TIMEOUT);  };  return (    <PromptInputProvider>      <HeaderControls />      <PromptInput globalDrop multiple onSubmit={handleSubmit}>        <PromptInputBody>          <PromptInputAttachments>            {(attachment) => <PromptInputAttachment data={attachment} />}          </PromptInputAttachments>          <PromptInputTextarea ref={textareaRef} />        </PromptInputBody>        <PromptInputFooter>          <PromptInputTools>            <PromptInputActionMenu>              <PromptInputActionMenuTrigger />              <PromptInputActionMenuContent>                <PromptInputActionAddAttachments />              </PromptInputActionMenuContent>            </PromptInputActionMenu>            <PromptInputSpeechButton textareaRef={textareaRef} />            <PromptInputButton>              <GlobeIcon size={16} />              <span>Search</span>            </PromptInputButton>            <PromptInputModelSelect onValueChange={setModel} value={model}>              <PromptInputModelSelectTrigger>                <PromptInputModelSelectValue />              </PromptInputModelSelectTrigger>              <PromptInputModelSelectContent>                {models.map((modelOption) => (                  <PromptInputModelSelectItem                    key={modelOption.id}                    value={modelOption.id}                  >                    {modelOption.name}                  </PromptInputModelSelectItem>                ))}              </PromptInputModelSelectContent>            </PromptInputModelSelect>          </PromptInputTools>          <PromptInputSubmit status={status} />        </PromptInputFooter>      </PromptInput>    </PromptInputProvider>  );};export default Example;

Installation

npx ai-elements@latest add prompt-input
npx shadcn@latest add @ai-elements/prompt-input
"use client";import { Button } from "@repo/shadcn-ui/components/ui/button";import {  Command,  CommandEmpty,  CommandGroup,  CommandInput,  CommandItem,  CommandList,  CommandSeparator,} from "@repo/shadcn-ui/components/ui/command";import {  DropdownMenu,  DropdownMenuContent,  DropdownMenuItem,  DropdownMenuTrigger,} from "@repo/shadcn-ui/components/ui/dropdown-menu";import {  HoverCard,  HoverCardContent,  HoverCardTrigger,} from "@repo/shadcn-ui/components/ui/hover-card";import {  InputGroup,  InputGroupAddon,  InputGroupButton,  InputGroupTextarea,} from "@repo/shadcn-ui/components/ui/input-group";import {  Select,  SelectContent,  SelectItem,  SelectTrigger,  SelectValue,} from "@repo/shadcn-ui/components/ui/select";import {  Tooltip,  TooltipContent,  TooltipTrigger,} from "@repo/shadcn-ui/components/ui/tooltip";import { cn } from "@repo/shadcn-ui/lib/utils";import type { ChatStatus, FileUIPart } from "ai";import {  ImageIcon,  Loader2Icon,  MicIcon,  PaperclipIcon,  PlusIcon,  SendIcon,  SquareIcon,  XIcon,} from "lucide-react";import { nanoid } from "nanoid";import {  type ChangeEvent,  type ChangeEventHandler,  Children,  type ClipboardEventHandler,  type ComponentProps,  createContext,  type FormEvent,  type FormEventHandler,  Fragment,  type HTMLAttributes,  type KeyboardEventHandler,  type PropsWithChildren,  type ReactNode,  type RefObject,  useCallback,  useContext,  useEffect,  useLayoutEffect,  useMemo,  useRef,  useState,} from "react";// ============================================================================// Provider Context & Types// ============================================================================export type AttachmentsContext = {  files: (FileUIPart & { id: string })[];  add: (files: File[] | FileList) => void;  remove: (id: string) => void;  clear: () => void;  openFileDialog: () => void;  fileInputRef: RefObject<HTMLInputElement | null>;};export type TextInputContext = {  value: string;  setInput: (v: string) => void;  clear: () => void;};export type PromptInputControllerProps = {  textInput: TextInputContext;  attachments: AttachmentsContext;  /** INTERNAL: Allows PromptInput to register its file textInput + "open" callback */  __registerFileInput: (    ref: RefObject<HTMLInputElement | null>,    open: () => void  ) => void;};const PromptInputController = createContext<PromptInputControllerProps | null>(  null);const ProviderAttachmentsContext = createContext<AttachmentsContext | null>(  null);export const usePromptInputController = () => {  const ctx = useContext(PromptInputController);  if (!ctx) {    throw new Error(      "Wrap your component inside <PromptInputProvider> to use usePromptInputController()."    );  }  return ctx;};// Optional variants (do NOT throw). Useful for dual-mode components.const useOptionalPromptInputController = () =>  useContext(PromptInputController);export const useProviderAttachments = () => {  const ctx = useContext(ProviderAttachmentsContext);  if (!ctx) {    throw new Error(      "Wrap your component inside <PromptInputProvider> to use useProviderAttachments()."    );  }  return ctx;};const useOptionalProviderAttachments = () =>  useContext(ProviderAttachmentsContext);export type PromptInputProviderProps = PropsWithChildren<{  initialInput?: string;}>;/** * Optional global provider that lifts PromptInput state outside of PromptInput. * If you don't use it, PromptInput stays fully self-managed. */export function PromptInputProvider({  initialInput: initialTextInput = "",  children,}: PromptInputProviderProps) {  // ----- textInput state  const [textInput, setTextInput] = useState(initialTextInput);  const clearInput = useCallback(() => setTextInput(""), []);  // ----- attachments state (global when wrapped)  const [attachements, setAttachements] = useState<    (FileUIPart & { id: string })[]  >([]);  const fileInputRef = useRef<HTMLInputElement | null>(null);  const openRef = useRef<() => void>(() => {});  const add = useCallback((files: File[] | FileList) => {    const incoming = Array.from(files);    if (incoming.length === 0) return;    setAttachements((prev) =>      prev.concat(        incoming.map((file) => ({          id: nanoid(),          type: "file" as const,          url: URL.createObjectURL(file),          mediaType: file.type,          filename: file.name,        }))      )    );  }, []);  const remove = useCallback((id: string) => {    setAttachements((prev) => {      const found = prev.find((f) => f.id === id);      if (found?.url) URL.revokeObjectURL(found.url);      return prev.filter((f) => f.id !== id);    });  }, []);  const clear = useCallback(() => {    setAttachements((prev) => {      for (const f of prev) if (f.url) URL.revokeObjectURL(f.url);      return [];    });  }, []);  const openFileDialog = useCallback(() => {    openRef.current?.();  }, []);  const attachments = useMemo<AttachmentsContext>(    () => ({      files: attachements,      add,      remove,      clear,      openFileDialog,      fileInputRef,    }),    [attachements, add, remove, clear, openFileDialog]  );  const __registerFileInput = useCallback(    (ref: RefObject<HTMLInputElement | null>, open: () => void) => {      fileInputRef.current = ref.current;      openRef.current = open;    },    []  );  const controller = useMemo<PromptInputControllerProps>(    () => ({      textInput: {        value: textInput,        setInput: setTextInput,        clear: clearInput,      },      attachments,      __registerFileInput,    }),    [textInput, clearInput, attachments, __registerFileInput]  );  return (    <PromptInputController.Provider value={controller}>      <ProviderAttachmentsContext.Provider value={attachments}>        {children}      </ProviderAttachmentsContext.Provider>    </PromptInputController.Provider>  );}// ============================================================================// Component Context & Hooks// ============================================================================const LocalAttachmentsContext = createContext<AttachmentsContext | null>(null);export const usePromptInputAttachments = () => {  // Dual-mode: prefer provider if present, otherwise use local  const provider = useOptionalProviderAttachments();  const local = useContext(LocalAttachmentsContext);  const context = provider ?? local;  if (!context) {    throw new Error(      "usePromptInputAttachments must be used within a PromptInput or PromptInputProvider"    );  }  return context;};export type PromptInputAttachmentProps = HTMLAttributes<HTMLDivElement> & {  data: FileUIPart & { id: string };  className?: string;};export function PromptInputAttachment({  data,  className,  ...props}: PromptInputAttachmentProps) {  const attachments = usePromptInputAttachments();  const mediaType =    data.mediaType?.startsWith("image/") && data.url ? "image" : "file";  return (    <div      className={cn(        "group relative h-14 w-14 rounded-md border",        className,        mediaType === "image" ? "h-14 w-14" : "h-8 w-auto max-w-full"      )}      key={data.id}      {...props}    >      {mediaType === "image" ? (        <img          alt={data.filename || "attachment"}          className="size-full rounded-md object-cover"          height={56}          src={data.url}          width={56}        />      ) : (        <div className="flex size-full max-w-full cursor-pointer items-center justify-start gap-2 overflow-hidden px-2 text-muted-foreground">          <PaperclipIcon className="size-4 shrink-0" />          <Tooltip delayDuration={400}>            <TooltipTrigger className="min-w-0 flex-1">              <h4 className="w-full truncate text-left font-medium text-sm">                {data.filename || "Unknown file"}              </h4>            </TooltipTrigger>            <TooltipContent>              <div className="text-muted-foreground text-xs">                <h4 className="max-w-[240px] overflow-hidden whitespace-normal break-words text-left font-semibold text-sm">                  {data.filename || "Unknown file"}                </h4>                {data.mediaType && <div>{data.mediaType}</div>}              </div>            </TooltipContent>          </Tooltip>        </div>      )}      <Button        aria-label="Remove attachment"        className="-right-1.5 -top-1.5 absolute h-6 w-6 rounded-full opacity-0 group-hover:opacity-100"        onClick={() => attachments.remove(data.id)}        size="icon"        type="button"        variant="outline"      >        <XIcon className="h-3 w-3" />      </Button>    </div>  );}export type PromptInputAttachmentsProps = Omit<  HTMLAttributes<HTMLDivElement>,  "children"> & {  children: (attachment: FileUIPart & { id: string }) => ReactNode;};export function PromptInputAttachments({  className,  children,  ...props}: PromptInputAttachmentsProps) {  const attachments = usePromptInputAttachments();  const [height, setHeight] = useState(0);  const contentRef = useRef<HTMLDivElement>(null);  useLayoutEffect(() => {    const el = contentRef.current;    if (!el) {      return;    }    const ro = new ResizeObserver(() => {      setHeight(el.getBoundingClientRect().height);    });    ro.observe(el);    setHeight(el.getBoundingClientRect().height);    return () => ro.disconnect();  }, []);  // biome-ignore lint/correctness/useExhaustiveDependencies: Force height measurement when attachments change  useLayoutEffect(() => {    const el = contentRef.current;    if (!el) {      return;    }    setHeight(el.getBoundingClientRect().height);  }, [attachments.files.length]);  if (attachments.files.length === 0) {    return null;  }  return (    <InputGroupAddon      align="block-start"      aria-live="polite"      className={cn(        "overflow-hidden transition-[height] duration-200 ease-out",        className      )}      style={{ height: attachments.files.length ? height : 0 }}      {...props}    >      <div className="space-y-2 py-1" ref={contentRef}>        <div className="flex flex-wrap gap-2">          {attachments.files            .filter((f) => !(f.mediaType?.startsWith("image/") && f.url))            .map((file) => (              <Fragment key={file.id}>{children(file)}</Fragment>            ))}        </div>        <div className="flex flex-wrap gap-2">          {attachments.files            .filter((f) => f.mediaType?.startsWith("image/") && f.url)            .map((file) => (              <Fragment key={file.id}>{children(file)}</Fragment>            ))}        </div>      </div>    </InputGroupAddon>  );}export type PromptInputActionAddAttachmentsProps = ComponentProps<  typeof DropdownMenuItem> & {  label?: string;};export const PromptInputActionAddAttachments = ({  label = "Add photos or files",  ...props}: PromptInputActionAddAttachmentsProps) => {  const attachments = usePromptInputAttachments();  return (    <DropdownMenuItem      {...props}      onSelect={(e) => {        e.preventDefault();        attachments.openFileDialog();      }}    >      <ImageIcon className="mr-2 size-4" /> {label}    </DropdownMenuItem>  );};export type PromptInputMessage = {  text?: string;  files?: FileUIPart[];};export type PromptInputProps = Omit<  HTMLAttributes<HTMLFormElement>,  "onSubmit" | "onError"> & {  accept?: string; // e.g., "image/*" or leave undefined for any  multiple?: boolean;  // When true, accepts drops anywhere on document. Default false (opt-in).  globalDrop?: boolean;  // Render a hidden input with given name and keep it in sync for native form posts. Default false.  syncHiddenInput?: boolean;  // Minimal constraints  maxFiles?: number;  maxFileSize?: number; // bytes  onError?: (err: {    code: "max_files" | "max_file_size" | "accept";    message: string;  }) => void;  onSubmit: (    message: PromptInputMessage,    event: FormEvent<HTMLFormElement>  ) => void | Promise<void>;};export const PromptInput = ({  className,  accept,  multiple,  globalDrop,  syncHiddenInput,  maxFiles,  maxFileSize,  onError,  onSubmit,  children,  ...props}: PromptInputProps) => {  // Try to use a provider controller if present  const controller = useOptionalPromptInputController();  const usingProvider = !!controller;  // Refs  const inputRef = useRef<HTMLInputElement | null>(null);  const anchorRef = useRef<HTMLSpanElement>(null);  const formRef = useRef<HTMLFormElement | null>(null);  // Find nearest form to scope drag & drop  useEffect(() => {    const root = anchorRef.current?.closest("form");    if (root instanceof HTMLFormElement) {      formRef.current = root;    }  }, []);  // ----- Local attachments (only used when no provider)  const [items, setItems] = useState<(FileUIPart & { id: string })[]>([]);  const files = usingProvider ? controller.attachments.files : items;  const openFileDialogLocal = useCallback(() => {    inputRef.current?.click();  }, []);  const matchesAccept = useCallback(    (f: File) => {      if (!accept || accept.trim() === "") {        return true;      }      if (accept.includes("image/*")) {        return f.type.startsWith("image/");      }      // NOTE: keep simple; expand as needed      return true;    },    [accept]  );  const addLocal = useCallback(    (fileList: File[] | FileList) => {      const incoming = Array.from(fileList);      const accepted = incoming.filter((f) => matchesAccept(f));      if (incoming.length && accepted.length === 0) {        onError?.({          code: "accept",          message: "No files match the accepted types.",        });        return;      }      const withinSize = (f: File) =>        maxFileSize ? f.size <= maxFileSize : true;      const sized = accepted.filter(withinSize);      if (accepted.length > 0 && sized.length === 0) {        onError?.({          code: "max_file_size",          message: "All files exceed the maximum size.",        });        return;      }      setItems((prev) => {        const capacity =          typeof maxFiles === "number"            ? Math.max(0, maxFiles - prev.length)            : undefined;        const capped =          typeof capacity === "number" ? sized.slice(0, capacity) : sized;        if (typeof capacity === "number" && sized.length > capacity) {          onError?.({            code: "max_files",            message: "Too many files. Some were not added.",          });        }        const next: (FileUIPart & { id: string })[] = [];        for (const file of capped) {          next.push({            id: nanoid(),            type: "file",            url: URL.createObjectURL(file),            mediaType: file.type,            filename: file.name,          });        }        return prev.concat(next);      });    },    [matchesAccept, maxFiles, maxFileSize, onError]  );  const add = usingProvider    ? (files: File[] | FileList) => controller.attachments.add(files)    : addLocal;  const remove = usingProvider    ? (id: string) => controller.attachments.remove(id)    : (id: string) =>        setItems((prev) => {          const found = prev.find((file) => file.id === id);          if (found?.url) {            URL.revokeObjectURL(found.url);          }          return prev.filter((file) => file.id !== id);        });  const clear = usingProvider    ? () => controller.attachments.clear()    : () =>        setItems((prev) => {          for (const file of prev) {            if (file.url) {              URL.revokeObjectURL(file.url);            }          }          return [];        });  const openFileDialog = usingProvider    ? () => controller.attachments.openFileDialog()    : openFileDialogLocal;  // Let provider know about our hidden file input so external menus can call openFileDialog()  useEffect(() => {    if (!usingProvider) return;    controller.__registerFileInput(inputRef, () => inputRef.current?.click());  }, [usingProvider, controller]);  // Note: File input cannot be programmatically set for security reasons  // The syncHiddenInput prop is no longer functional  useEffect(() => {    if (syncHiddenInput && inputRef.current && files.length === 0) {      inputRef.current.value = "";    }  }, [files, syncHiddenInput]);  // Attach drop handlers on nearest form and document (opt-in)  useEffect(() => {    const form = formRef.current;    if (!form) return;    const onDragOver = (e: DragEvent) => {      if (e.dataTransfer?.types?.includes("Files")) {        e.preventDefault();      }    };    const onDrop = (e: DragEvent) => {      if (e.dataTransfer?.types?.includes("Files")) {        e.preventDefault();      }      if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {        add(e.dataTransfer.files);      }    };    form.addEventListener("dragover", onDragOver);    form.addEventListener("drop", onDrop);    return () => {      form.removeEventListener("dragover", onDragOver);      form.removeEventListener("drop", onDrop);    };  }, [add]);  useEffect(() => {    if (!globalDrop) return;    const onDragOver = (e: DragEvent) => {      if (e.dataTransfer?.types?.includes("Files")) {        e.preventDefault();      }    };    const onDrop = (e: DragEvent) => {      if (e.dataTransfer?.types?.includes("Files")) {        e.preventDefault();      }      if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {        add(e.dataTransfer.files);      }    };    document.addEventListener("dragover", onDragOver);    document.addEventListener("drop", onDrop);    return () => {      document.removeEventListener("dragover", onDragOver);      document.removeEventListener("drop", onDrop);    };  }, [add, globalDrop]);  useEffect(    () => () => {      if (!usingProvider) {        for (const f of files) {          if (f.url) URL.revokeObjectURL(f.url);        }      }    },    [usingProvider, files]  );  const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {    if (event.currentTarget.files) {      add(event.currentTarget.files);    }  };  const convertBlobUrlToDataUrl = async (url: string): Promise<string> => {    const response = await fetch(url);    const blob = await response.blob();    return new Promise((resolve, reject) => {      const reader = new FileReader();      reader.onloadend = () => resolve(reader.result as string);      reader.onerror = reject;      reader.readAsDataURL(blob);    });  };  const ctx = useMemo<AttachmentsContext>(    () => ({      files: files.map((item) => ({ ...item, id: item.id })),      add,      remove,      clear,      openFileDialog,      fileInputRef: inputRef,    }),    [files, add, remove, clear, openFileDialog]  );  const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {    event.preventDefault();    const form = event.currentTarget;    const text = usingProvider      ? controller.textInput.value      : (() => {          const formData = new FormData(form);          return (formData.get("message") as string) || "";        })();    // Reset form immediately after capturing text to avoid race condition    // where user input during async blob conversion would be lost    if (!usingProvider) {      form.reset();    }    // Convert blob URLs to data URLs asynchronously    Promise.all(      files.map(async ({ id, ...item }) => {        if (item.url && item.url.startsWith("blob:")) {          return {            ...item,            url: await convertBlobUrlToDataUrl(item.url),          };        }        return item;      })    ).then((convertedFiles: FileUIPart[]) => {      try {        const result = onSubmit({ text, files: convertedFiles }, event);        // Handle both sync and async onSubmit        if (result instanceof Promise) {          result            .then(() => {              clear();              if (usingProvider) {                controller.textInput.clear();              }            })            .catch(() => {              // Don't clear on error - user may want to retry            });        } else {          // Sync function completed without throwing, clear attachments          clear();          if (usingProvider) {            controller.textInput.clear();          }        }      } catch (error) {        // Don't clear on error - user may want to retry      }    });  };  // Render with or without local provider  const inner = (    <>      <span aria-hidden="true" className="hidden" ref={anchorRef} />      <input        accept={accept}        aria-label="Upload files"        className="hidden"        multiple={multiple}        onChange={handleChange}        ref={inputRef}        title="Upload files"        type="file"      />      <form        className={cn("w-full", className)}        onSubmit={handleSubmit}        {...props}      >        <InputGroup>{children}</InputGroup>      </form>    </>  );  return usingProvider ? (    inner  ) : (    <LocalAttachmentsContext.Provider value={ctx}>      {inner}    </LocalAttachmentsContext.Provider>  );};export type PromptInputBodyProps = HTMLAttributes<HTMLDivElement>;export const PromptInputBody = ({  className,  ...props}: PromptInputBodyProps) => (  <div className={cn("contents", className)} {...props} />);export type PromptInputTextareaProps = ComponentProps<  typeof InputGroupTextarea>;export const PromptInputTextarea = ({  onChange,  className,  placeholder = "What would you like to know?",  ...props}: PromptInputTextareaProps) => {  const controller = useOptionalPromptInputController();  const attachments = usePromptInputAttachments();  const [isComposing, setIsComposing] = useState(false);  const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {    if (e.key === "Enter") {      if (isComposing || e.nativeEvent.isComposing) {        return;      }      if (e.shiftKey) {        return;      }      e.preventDefault();      e.currentTarget.form?.requestSubmit();    }    // Remove last attachment when Backspace is pressed and textarea is empty    if (      e.key === "Backspace" &&      e.currentTarget.value === "" &&      attachments.files.length > 0    ) {      e.preventDefault();      const lastAttachment = attachments.files.at(-1);      if (lastAttachment) {        attachments.remove(lastAttachment.id);      }    }  };  const handlePaste: ClipboardEventHandler<HTMLTextAreaElement> = (event) => {    const items = event.clipboardData?.items;    if (!items) {      return;    }    const files: File[] = [];    for (const item of items) {      if (item.kind === "file") {        const file = item.getAsFile();        if (file) {          files.push(file);        }      }    }    if (files.length > 0) {      event.preventDefault();      attachments.add(files);    }  };  const controlledProps = controller    ? {        value: controller.textInput.value,        onChange: (e: ChangeEvent<HTMLTextAreaElement>) => {          controller.textInput.setInput(e.currentTarget.value);          onChange?.(e);        },      }    : {        onChange,      };  return (    <InputGroupTextarea      className={cn("field-sizing-content max-h-48 min-h-16", className)}      name="message"      onCompositionEnd={() => setIsComposing(false)}      onCompositionStart={() => setIsComposing(true)}      onKeyDown={handleKeyDown}      onPaste={handlePaste}      placeholder={placeholder}      {...props}      {...controlledProps}    />  );};export type PromptInputHeaderProps = Omit<  ComponentProps<typeof InputGroupAddon>,  "align">;export const PromptInputHeader = ({  className,  ...props}: PromptInputHeaderProps) => (  <InputGroupAddon    align="block-end"    className={cn("order-first gap-1", className)}    {...props}  />);export type PromptInputFooterProps = Omit<  ComponentProps<typeof InputGroupAddon>,  "align">;export const PromptInputFooter = ({  className,  ...props}: PromptInputFooterProps) => (  <InputGroupAddon    align="block-end"    className={cn("justify-between gap-1", className)}    {...props}  />);export type PromptInputToolsProps = HTMLAttributes<HTMLDivElement>;export const PromptInputTools = ({  className,  ...props}: PromptInputToolsProps) => (  <div className={cn("flex items-center gap-1", className)} {...props} />);export type PromptInputButtonProps = ComponentProps<typeof InputGroupButton>;export const PromptInputButton = ({  variant = "ghost",  className,  size,  ...props}: PromptInputButtonProps) => {  const newSize =    size ?? (Children.count(props.children) > 1 ? "sm" : "icon-sm");  return (    <InputGroupButton      className={cn(className)}      size={newSize}      type="button"      variant={variant}      {...props}    />  );};export type PromptInputActionMenuProps = ComponentProps<typeof DropdownMenu>;export const PromptInputActionMenu = (props: PromptInputActionMenuProps) => (  <DropdownMenu {...props} />);export type PromptInputActionMenuTriggerProps = PromptInputButtonProps;export const PromptInputActionMenuTrigger = ({  className,  children,  ...props}: PromptInputActionMenuTriggerProps) => (  <DropdownMenuTrigger asChild>    <PromptInputButton className={className} {...props}>      {children ?? <PlusIcon className="size-4" />}    </PromptInputButton>  </DropdownMenuTrigger>);export type PromptInputActionMenuContentProps = ComponentProps<  typeof DropdownMenuContent>;export const PromptInputActionMenuContent = ({  className,  ...props}: PromptInputActionMenuContentProps) => (  <DropdownMenuContent align="start" className={cn(className)} {...props} />);export type PromptInputActionMenuItemProps = ComponentProps<  typeof DropdownMenuItem>;export const PromptInputActionMenuItem = ({  className,  ...props}: PromptInputActionMenuItemProps) => (  <DropdownMenuItem className={cn(className)} {...props} />);// Note: Actions that perform side-effects (like opening a file dialog)// are provided in opt-in modules (e.g., prompt-input-attachments).export type PromptInputSubmitProps = ComponentProps<typeof InputGroupButton> & {  status?: ChatStatus;};export const PromptInputSubmit = ({  className,  variant = "default",  size = "icon-sm",  status,  children,  ...props}: PromptInputSubmitProps) => {  let Icon = <SendIcon className="size-4" />;  if (status === "submitted") {    Icon = <Loader2Icon className="size-4 animate-spin" />;  } else if (status === "streaming") {    Icon = <SquareIcon className="size-4" />;  } else if (status === "error") {    Icon = <XIcon className="size-4" />;  }  return (    <InputGroupButton      aria-label="Submit"      className={cn(className)}      size={size}      type="submit"      variant={variant}      {...props}    >      {children ?? Icon}    </InputGroupButton>  );};interface SpeechRecognition extends EventTarget {  continuous: boolean;  interimResults: boolean;  lang: string;  start(): void;  stop(): void;  onstart: ((this: SpeechRecognition, ev: Event) => any) | null;  onend: ((this: SpeechRecognition, ev: Event) => any) | null;  onresult:    | ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => any)    | null;  onerror:    | ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => any)    | null;}interface SpeechRecognitionEvent extends Event {  results: SpeechRecognitionResultList;}type SpeechRecognitionResultList = {  readonly length: number;  item(index: number): SpeechRecognitionResult;  [index: number]: SpeechRecognitionResult;};type SpeechRecognitionResult = {  readonly length: number;  item(index: number): SpeechRecognitionAlternative;  [index: number]: SpeechRecognitionAlternative;  isFinal: boolean;};type SpeechRecognitionAlternative = {  transcript: string;  confidence: number;};interface SpeechRecognitionErrorEvent extends Event {  error: string;}declare global {  interface Window {    SpeechRecognition: {      new (): SpeechRecognition;    };    webkitSpeechRecognition: {      new (): SpeechRecognition;    };  }}export type PromptInputSpeechButtonProps = ComponentProps<  typeof PromptInputButton> & {  textareaRef?: RefObject<HTMLTextAreaElement | null>;  onTranscriptionChange?: (text: string) => void;};export const PromptInputSpeechButton = ({  className,  textareaRef,  onTranscriptionChange,  ...props}: PromptInputSpeechButtonProps) => {  const [isListening, setIsListening] = useState(false);  const [recognition, setRecognition] = useState<SpeechRecognition | null>(    null  );  const recognitionRef = useRef<SpeechRecognition | null>(null);  useEffect(() => {    if (      typeof window !== "undefined" &&      ("SpeechRecognition" in window || "webkitSpeechRecognition" in window)    ) {      const SpeechRecognition =        window.SpeechRecognition || window.webkitSpeechRecognition;      const speechRecognition = new SpeechRecognition();      speechRecognition.continuous = true;      speechRecognition.interimResults = true;      speechRecognition.lang = "en-US";      speechRecognition.onstart = () => {        setIsListening(true);      };      speechRecognition.onend = () => {        setIsListening(false);      };      speechRecognition.onresult = (event) => {        let finalTranscript = "";        const results = Array.from(event.results);        for (const result of results) {          if (result.isFinal) {            finalTranscript += result[0].transcript;          }        }        if (finalTranscript && textareaRef?.current) {          const textarea = textareaRef.current;          const currentValue = textarea.value;          const newValue =            currentValue + (currentValue ? " " : "") + finalTranscript;          textarea.value = newValue;          textarea.dispatchEvent(new Event("input", { bubbles: true }));          onTranscriptionChange?.(newValue);        }      };      speechRecognition.onerror = (event) => {        console.error("Speech recognition error:", event.error);        setIsListening(false);      };      recognitionRef.current = speechRecognition;      setRecognition(speechRecognition);    }    return () => {      if (recognitionRef.current) {        recognitionRef.current.stop();      }    };  }, [textareaRef, onTranscriptionChange]);  const toggleListening = useCallback(() => {    if (!recognition) {      return;    }    if (isListening) {      recognition.stop();    } else {      recognition.start();    }  }, [recognition, isListening]);  return (    <PromptInputButton      className={cn(        "relative transition-all duration-200",        isListening && "animate-pulse bg-accent text-accent-foreground",        className      )}      disabled={!recognition}      onClick={toggleListening}      {...props}    >      <MicIcon className="size-4" />    </PromptInputButton>  );};export type PromptInputModelSelectProps = ComponentProps<typeof Select>;export const PromptInputModelSelect = (props: PromptInputModelSelectProps) => (  <Select {...props} />);export type PromptInputModelSelectTriggerProps = ComponentProps<  typeof SelectTrigger>;export const PromptInputModelSelectTrigger = ({  className,  ...props}: PromptInputModelSelectTriggerProps) => (  <SelectTrigger    className={cn(      "border-none bg-transparent font-medium text-muted-foreground shadow-none transition-colors",      'hover:bg-accent hover:text-foreground [&[aria-expanded="true"]]:bg-accent [&[aria-expanded="true"]]:text-foreground',      className    )}    {...props}  />);export type PromptInputModelSelectContentProps = ComponentProps<  typeof SelectContent>;export const PromptInputModelSelectContent = ({  className,  ...props}: PromptInputModelSelectContentProps) => (  <SelectContent className={cn(className)} {...props} />);export type PromptInputModelSelectItemProps = ComponentProps<typeof SelectItem>;export const PromptInputModelSelectItem = ({  className,  ...props}: PromptInputModelSelectItemProps) => (  <SelectItem className={cn(className)} {...props} />);export type PromptInputModelSelectValueProps = ComponentProps<  typeof SelectValue>;export const PromptInputModelSelectValue = ({  className,  ...props}: PromptInputModelSelectValueProps) => (  <SelectValue className={cn(className)} {...props} />);export type PromptInputHoverCardProps = ComponentProps<typeof HoverCard>;export const PromptInputHoverCard = ({  openDelay = 0,  closeDelay = 0,  ...props}: PromptInputHoverCardProps) => (  <HoverCard closeDelay={closeDelay} openDelay={openDelay} {...props} />);export type PromptInputHoverCardTriggerProps = ComponentProps<  typeof HoverCardTrigger>;export const PromptInputHoverCardTrigger = (  props: PromptInputHoverCardTriggerProps) => <HoverCardTrigger {...props} />;export type PromptInputHoverCardContentProps = ComponentProps<  typeof HoverCardContent>;export const PromptInputHoverCardContent = ({  align = "start",  ...props}: PromptInputHoverCardContentProps) => (  <HoverCardContent align={align} {...props} />);export type PromptInputTabsListProps = HTMLAttributes<HTMLDivElement>;export const PromptInputTabsList = ({  className,  ...props}: PromptInputTabsListProps) => <div className={cn(className)} {...props} />;export type PromptInputTabProps = HTMLAttributes<HTMLDivElement>;export const PromptInputTab = ({  className,  ...props}: PromptInputTabProps) => <div className={cn(className)} {...props} />;export type PromptInputTabLabelProps = HTMLAttributes<HTMLHeadingElement>;export const PromptInputTabLabel = ({  className,  ...props}: PromptInputTabLabelProps) => (  <h3    className={cn(      "mb-2 px-3 font-medium text-muted-foreground text-xs",      className    )}    {...props}  />);export type PromptInputTabBodyProps = HTMLAttributes<HTMLDivElement>;export const PromptInputTabBody = ({  className,  ...props}: PromptInputTabBodyProps) => (  <div className={cn("space-y-1", className)} {...props} />);export type PromptInputTabItemProps = HTMLAttributes<HTMLDivElement>;export const PromptInputTabItem = ({  className,  ...props}: PromptInputTabItemProps) => (  <div    className={cn(      "flex items-center gap-2 px-3 py-2 text-xs hover:bg-accent",      className    )}    {...props}  />);export type PromptInputCommandProps = ComponentProps<typeof Command>;export const PromptInputCommand = ({  className,  ...props}: PromptInputCommandProps) => <Command className={cn(className)} {...props} />;export type PromptInputCommandInputProps = ComponentProps<typeof CommandInput>;export const PromptInputCommandInput = ({  className,  ...props}: PromptInputCommandInputProps) => (  <CommandInput className={cn(className)} {...props} />);export type PromptInputCommandListProps = ComponentProps<typeof CommandList>;export const PromptInputCommandList = ({  className,  ...props}: PromptInputCommandListProps) => (  <CommandList className={cn(className)} {...props} />);export type PromptInputCommandEmptyProps = ComponentProps<typeof CommandEmpty>;export const PromptInputCommandEmpty = ({  className,  ...props}: PromptInputCommandEmptyProps) => (  <CommandEmpty className={cn(className)} {...props} />);export type PromptInputCommandGroupProps = ComponentProps<typeof CommandGroup>;export const PromptInputCommandGroup = ({  className,  ...props}: PromptInputCommandGroupProps) => (  <CommandGroup className={cn(className)} {...props} />);export type PromptInputCommandItemProps = ComponentProps<typeof CommandItem>;export const PromptInputCommandItem = ({  className,  ...props}: PromptInputCommandItemProps) => (  <CommandItem className={cn(className)} {...props} />);export type PromptInputCommandSeparatorProps = ComponentProps<  typeof CommandSeparator>;export const PromptInputCommandSeparator = ({  className,  ...props}: PromptInputCommandSeparatorProps) => (  <CommandSeparator className={cn(className)} {...props} />);

Usage

import {
  PromptInput,
  PromptInputActionAddAttachments,
  PromptInputActionMenu,
  PromptInputActionMenuContent,
  PromptInputActionMenuItem,
  PromptInputActionMenuTrigger,
  PromptInputAttachment,
  PromptInputAttachments,
  PromptInputBody,
  PromptInputButton,
  PromptInputProvider,
  PromptInputSpeechButton,
  PromptInputSubmit,
  PromptInputTextarea,
  PromptInputFooter,
  PromptInputTools,
  usePromptInputAttachments,
} from '@/components/ai-elements/prompt-input';
import { GlobeIcon } from 'lucide-react';

<PromptInput onSubmit={() => {}} className="mt-4 relative">
  <PromptInputBody>
    <PromptInputAttachments>
      {(attachment) => (
        <PromptInputAttachment data={attachment} />
      )}
    </PromptInputAttachments>
    <PromptInputTextarea onChange={(e) => {}} value={''} />
  </PromptInputBody>
  <PromptInputFooter>
    <PromptInputTools>
      <PromptInputActionMenu>
        <PromptInputActionMenuTrigger />
        <PromptInputActionMenuContent>
          <PromptInputActionAddAttachments />
        </PromptInputActionMenuContent>
      </PromptInputActionMenu>
      <PromptInputSpeechButton />
      <PromptInputButton>
        <GlobeIcon size={16} />
        <span>Search</span>
      </PromptInputButton>
      <PromptInputModelSelect onValueChange={(value) => {}} value="gpt-4o">
        <PromptInputModelSelectTrigger>
          <PromptInputModelSelectValue />
        </PromptInputModelSelectTrigger>
        <PromptInputModelSelectContent>
          <PromptInputModelSelectItem value="gpt-4o">
            GPT-4o
          </PromptInputModelSelectItem>
          <PromptInputModelSelectItem value="claude-opus-4-20250514">
            Claude 4 Opus
          </PromptInputModelSelectItem>
        </PromptInputModelSelectContent>
      </PromptInputModelSelect>
    </PromptInputTools>
    <PromptInputSubmit
      disabled={false}
      status={'ready'}
    />
  </PromptInputFooter>
</PromptInput>

Usage with AI SDK

Built a fully functional chat app using PromptInput, Conversation with a model picker:

Add the following component to your frontend:

app/page.tsx
'use client';

import {
  PromptInput,
  PromptInputActionAddAttachments,
  PromptInputActionMenu,
  PromptInputActionMenuContent,
  PromptInputActionMenuTrigger,
  PromptInputAttachment,
  PromptInputAttachments,
  PromptInputBody,
  PromptInputButton,
  type PromptInputMessage,
  PromptInputModelSelect,
  PromptInputModelSelectContent,
  PromptInputModelSelectItem,
  PromptInputModelSelectTrigger,
  PromptInputModelSelectValue,
  PromptInputSpeechButton,
  PromptInputSubmit,
  PromptInputTextarea,
  PromptInputFooter,
  PromptInputTools,
} from '@/components/ai-elements/prompt-input';
import { GlobeIcon } from 'lucide-react';
import { useRef, useState } from 'react';
import { useChat } from '@ai-sdk/react';
import {
  Conversation,
  ConversationContent,
  ConversationScrollButton,
} from '@/components/ai-elements/conversation';
import { Message, MessageContent } from '@/components/ai-elements/message';
import { Response } from '@/components/ai-elements/response';

const models = [
  { id: 'gpt-4o', name: 'GPT-4o' },
  { id: 'claude-opus-4-20250514', name: 'Claude 4 Opus' },
];

const InputDemo = () => {
  const [text, setText] = useState<string>('');
  const [model, setModel] = useState<string>(models[0].id);
  const [useWebSearch, setUseWebSearch] = useState<boolean>(false);
  const textareaRef = useRef<HTMLTextAreaElement>(null);

  const { messages, status, sendMessage } = useChat();

  const handleSubmit = (message: PromptInputMessage) => {
    const hasText = Boolean(message.text);
    const hasAttachments = Boolean(message.files?.length);

    if (!(hasText || hasAttachments)) {
      return;
    }

    sendMessage(
      { 
        text: message.text || 'Sent with attachments',
        files: message.files 
      },
      {
        body: {
          model: model,
          webSearch: useWebSearch,
        },
      },
    );
    setText('');
  };

  return (
    <div className="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]">
      <div className="flex flex-col h-full">
        <Conversation>
          <ConversationContent>
            {messages.map((message) => (
              <Message from={message.role} key={message.id}>
                <MessageContent>
                  {message.parts.map((part, i) => {
                    switch (part.type) {
                      case 'text':
                        return (
                          <Response key={`${message.id}-${i}`}>
                            {part.text}
                          </Response>
                        );
                      default:
                        return null;
                    }
                  })}
                </MessageContent>
              </Message>
            ))}
          </ConversationContent>
          <ConversationScrollButton />
        </Conversation>

        <PromptInput onSubmit={handleSubmit} className="mt-4" globalDrop multiple>
          <PromptInputBody>
            <PromptInputAttachments>
              {(attachment) => <PromptInputAttachment data={attachment} />}
            </PromptInputAttachments>
            <PromptInputTextarea
              onChange={(e) => setText(e.target.value)}
              ref={textareaRef}
              value={text}
            />
          </PromptInputBody>
          <PromptInputFooter>
            <PromptInputTools>
              <PromptInputActionMenu>
                <PromptInputActionMenuTrigger />
                <PromptInputActionMenuContent>
                  <PromptInputActionAddAttachments />
                </PromptInputActionMenuContent>
              </PromptInputActionMenu>
              <PromptInputSpeechButton
                onTranscriptionChange={setText}
                textareaRef={textareaRef}
              />
              <PromptInputButton
                onClick={() => setUseWebSearch(!useWebSearch)}
                variant={useWebSearch ? 'default' : 'ghost'}
              >
                <GlobeIcon size={16} />
                <span>Search</span>
              </PromptInputButton>
              <PromptInputModelSelect
                onValueChange={(value) => {
                  setModel(value);
                }}
                value={model}
              >
                <PromptInputModelSelectTrigger>
                  <PromptInputModelSelectValue />
                </PromptInputModelSelectTrigger>
                <PromptInputModelSelectContent>
                  {models.map((model) => (
                    <PromptInputModelSelectItem key={model.id} value={model.id}>
                      {model.name}
                    </PromptInputModelSelectItem>
                  ))}
                </PromptInputModelSelectContent>
              </PromptInputModelSelect>
            </PromptInputTools>
            <PromptInputSubmit disabled={!text && !status} status={status} />
          </PromptInputFooter>
        </PromptInput>
      </div>
    </div>
  );
};

export default InputDemo;

Add the following route to your backend:

app/api/chat/route.ts
import { streamText, UIMessage, convertToModelMessages } from 'ai';

// Allow streaming responses up to 30 seconds
export const maxDuration = 30;

export async function POST(req: Request) {
  const { 
    model, 
    messages, 
    webSearch 
  }: { 
    messages: UIMessage[]; 
    model: string;
    webSearch?: boolean;
  } = await req.json();

  const result = streamText({
    model: webSearch ? 'perplexity/sonar' : model,
    messages: convertToModelMessages(messages),
  });

  return result.toUIMessageStreamResponse();
}

Features

  • Auto-resizing textarea that adjusts height based on content
  • File attachment support with drag-and-drop
  • Image preview for image attachments
  • Configurable file constraints (max files, max size, accepted types)
  • Automatic submit button icons based on status
  • Support for keyboard shortcuts (Enter to submit, Shift+Enter for new line)
  • Customizable min/max height for the textarea
  • Flexible toolbar with support for custom actions and tools
  • Built-in model selection dropdown
  • Built-in native speech recognition button (Web Speech API)
  • Optional provider for lifted state management
  • Form automatically resets on submit
  • Responsive design with mobile-friendly controls
  • Clean, modern styling with customizable themes
  • Form-based submission handling
  • Hidden file input sync for native form posts
  • Global document drop support (opt-in)

Examples

Cursor style

"use client";import {  PromptInput,  PromptInputAttachment,  PromptInputAttachments,  PromptInputBody,  PromptInputButton,  PromptInputCommand,  PromptInputCommandEmpty,  PromptInputCommandGroup,  PromptInputCommandInput,  PromptInputCommandItem,  PromptInputCommandList,  PromptInputCommandSeparator,  PromptInputFooter,  PromptInputHeader,  PromptInputHoverCard,  PromptInputHoverCardContent,  PromptInputHoverCardTrigger,  type PromptInputMessage,  PromptInputModelSelect,  PromptInputModelSelectContent,  PromptInputModelSelectItem,  PromptInputModelSelectTrigger,  PromptInputModelSelectValue,  PromptInputProvider,  PromptInputSubmit,  PromptInputTab,  PromptInputTabBody,  PromptInputTabItem,  PromptInputTabLabel,  PromptInputTextarea,  PromptInputTools,} from "@/components/ai-elements/elements/prompt-input";import { Button } from "@/components/ui/button";import {  AtSignIcon,  FilesIcon,  GlobeIcon,  ImageIcon,  RulerIcon,} from "lucide-react";import { useRef, useState } from "react";const models = [  { id: "gpt-4", name: "GPT-4" },  { id: "gpt-3.5-turbo", name: "GPT-3.5 Turbo" },  { id: "claude-2", name: "Claude 2" },  { id: "claude-instant", name: "Claude Instant" },  { id: "palm-2", name: "PaLM 2" },  { id: "llama-2-70b", name: "Llama 2 70B" },  { id: "llama-2-13b", name: "Llama 2 13B" },  { id: "cohere-command", name: "Command" },  { id: "mistral-7b", name: "Mistral 7B" },];const SUBMITTING_TIMEOUT = 200;const STREAMING_TIMEOUT = 2000;const sampleFiles = {  activeTabs: [{ path: "prompt-input.tsx", location: "packages/elements/src" }],  recents: [    { path: "queue.tsx", location: "apps/test/app/examples" },    { path: "queue.tsx", location: "packages/elements/src" },  ],  added: [    { path: "prompt-input.tsx", location: "packages/elements/src" },    { path: "queue.tsx", location: "apps/test/app/examples" },    { path: "queue.tsx", location: "packages/elements/src" },  ],  filesAndFolders: [    { path: "prompt-input.tsx", location: "packages/elements/src" },    { path: "queue.tsx", location: "apps/test/app/examples" },  ],  code: [{ path: "prompt-input.tsx", location: "packages/elements/src" }],  docs: [{ path: "README.md", location: "packages/elements" }],};const sampleTabs = {  active: [{ path: "packages/elements/src/task-queue-panel.tsx" }],  recents: [    { path: "apps/test/app/examples/task-queue-panel.tsx" },    { path: "apps/test/app/page.tsx" },    { path: "packages/elements/src/task.tsx" },    { path: "apps/test/app/examples/prompt-input.tsx" },    { path: "packages/elements/src/queue.tsx" },    { path: "apps/test/app/examples/queue.tsx" },  ],};const Example = () => {  const [model, setModel] = useState<string>(models[0].id);  const [status, setStatus] = useState<    "submitted" | "streaming" | "ready" | "error"  >("ready");  const textareaRef = useRef<HTMLTextAreaElement>(null);  const handleSubmit = (message: PromptInputMessage) => {    const hasText = Boolean(message.text);    const hasAttachments = Boolean(message.files?.length);    if (!(hasText || hasAttachments)) {      return;    }    setStatus("submitted");    console.log("Submitting message:", message);    setTimeout(() => {      setStatus("streaming");    }, SUBMITTING_TIMEOUT);    setTimeout(() => {      setStatus("ready");    }, STREAMING_TIMEOUT);  };  return (    <div className="flex size-full flex-col justify-end">      <PromptInputProvider>        <PromptInput globalDrop multiple onSubmit={handleSubmit}>          <PromptInputHeader>            <PromptInputHoverCard>              <PromptInputHoverCardTrigger>                <PromptInputButton                  className="!h-8"                  size="icon-sm"                  variant="outline"                >                  <AtSignIcon className="text-muted-foreground" size={12} />                </PromptInputButton>              </PromptInputHoverCardTrigger>              <PromptInputHoverCardContent className="w-[400px] p-0">                <PromptInputCommand>                  <PromptInputCommandInput                    className="border-none focus-visible:ring-0"                    placeholder="Add files, folders, docs..."                  />                  <PromptInputCommandList>                    <PromptInputCommandEmpty className="p-3 text-muted-foreground text-sm">                      No results found.                    </PromptInputCommandEmpty>                    <PromptInputCommandGroup heading="Added">                      <PromptInputCommandItem>                        <GlobeIcon />                        <span>Active Tabs</span>                        <span className="ml-auto text-muted-foreground">✓</span>                      </PromptInputCommandItem>                    </PromptInputCommandGroup>                    <PromptInputCommandSeparator />                    <PromptInputCommandGroup heading="Other Files">                      {sampleFiles.added.map((file, index) => (                        <PromptInputCommandItem key={`${file.path}-${index}`}>                          <GlobeIcon className="text-primary" />                          <div className="flex flex-col">                            <span className="font-medium text-sm">                              {file.path}                            </span>                            <span className="text-muted-foreground text-xs">                              {file.location}                            </span>                          </div>                        </PromptInputCommandItem>                      ))}                    </PromptInputCommandGroup>                  </PromptInputCommandList>                </PromptInputCommand>              </PromptInputHoverCardContent>            </PromptInputHoverCard>            <PromptInputHoverCard>              <PromptInputHoverCardTrigger>                <PromptInputButton size="sm" variant="outline">                  <RulerIcon className="text-muted-foreground" size={12} />                  <span>1</span>                </PromptInputButton>              </PromptInputHoverCardTrigger>              <PromptInputHoverCardContent className="divide-y overflow-hidden p-0">                <div className="space-y-2 p-3">                  <p className="font-medium text-muted-foreground text-sm">                    Attached Project Rules                  </p>                  <p className="ml-4 text-muted-foreground text-sm">                    Always Apply:                  </p>                  <p className="ml-8 text-sm">ultracite.mdc</p>                </div>                <p className="bg-sidebar px-4 py-3 text-muted-foreground text-sm">                  Click to manage                </p>              </PromptInputHoverCardContent>            </PromptInputHoverCard>            <PromptInputHoverCard>              <PromptInputHoverCardTrigger>                <PromptInputButton size="sm" variant="outline">                  <FilesIcon className="text-muted-foreground" size={12} />                  <span>1 Tab</span>                </PromptInputButton>              </PromptInputHoverCardTrigger>              <PromptInputHoverCardContent className="w-[300px] space-y-4 px-0 py-3">                <PromptInputTab>                  <PromptInputTabLabel>Active Tabs</PromptInputTabLabel>                  <PromptInputTabBody>                    {sampleTabs.active.map((tab) => (                      <PromptInputTabItem key={tab.path}>                        <GlobeIcon className="text-primary" size={16} />                        <span className="truncate" dir="rtl">                          {tab.path}                        </span>                      </PromptInputTabItem>                    ))}                  </PromptInputTabBody>                </PromptInputTab>                <PromptInputTab>                  <PromptInputTabLabel>Recents</PromptInputTabLabel>                  <PromptInputTabBody>                    {sampleTabs.recents.map((tab) => (                      <PromptInputTabItem key={tab.path}>                        <GlobeIcon className="text-primary" size={16} />                        <span className="truncate" dir="rtl">                          {tab.path}                        </span>                      </PromptInputTabItem>                    ))}                  </PromptInputTabBody>                </PromptInputTab>                <div className="border-t px-3 pt-2 text-muted-foreground text-xs">                  Only file paths are included                </div>              </PromptInputHoverCardContent>            </PromptInputHoverCard>          </PromptInputHeader>          <PromptInputBody>            <PromptInputAttachments>              {(attachment) => <PromptInputAttachment data={attachment} />}            </PromptInputAttachments>            <PromptInputTextarea              placeholder="Plan, search, build anything"              ref={textareaRef}            />          </PromptInputBody>          <PromptInputFooter>            <PromptInputTools>              <PromptInputModelSelect onValueChange={setModel} value={model}>                <PromptInputModelSelectTrigger>                  <PromptInputModelSelectValue />                </PromptInputModelSelectTrigger>                <PromptInputModelSelectContent>                  {models.map((modelOption) => (                    <PromptInputModelSelectItem                      key={modelOption.id}                      value={modelOption.id}                    >                      {modelOption.name}                    </PromptInputModelSelectItem>                  ))}                </PromptInputModelSelectContent>              </PromptInputModelSelect>            </PromptInputTools>            <div className="flex items-center gap-2">              <Button size="icon-sm" variant="ghost">                <ImageIcon className="text-muted-foreground" size={16} />              </Button>              <PromptInputSubmit className="!h-8" status={status} />            </div>          </PromptInputFooter>        </PromptInput>      </PromptInputProvider>    </div>  );};export default Example;

Props

<PromptInput />

Prop

Type

<PromptInputTextarea />

Prop

Type

<PromptInputFooter />

Prop

Type

<PromptInputTools />

Prop

Type

<PromptInputButton />

Prop

Type

<PromptInputSubmit />

Prop

Type

<PromptInputModelSelect />

Prop

Type

<PromptInputModelSelectTrigger />

Prop

Type

<PromptInputModelSelectContent />

Prop

Type

<PromptInputModelSelectItem />

Prop

Type

<PromptInputModelSelectValue />

Prop

Type

<PromptInputBody />

Prop

Type

<PromptInputAttachments />

Prop

Type

<PromptInputAttachment />

Prop

Type

<PromptInputActionMenu />

Prop

Type

<PromptInputActionMenuTrigger />

Prop

Type

<PromptInputActionMenuContent />

Prop

Type

<PromptInputActionMenuItem />

Prop

Type

<PromptInputActionAddAttachments />

Prop

Type

<PromptInputProvider />

Prop

Type

Optional global provider that lifts PromptInput state outside of PromptInput. When used, it allows you to access and control the input state from anywhere within the provider tree. If not used, PromptInput stays fully self-managed.

<PromptInputSpeechButton />

Prop

Type

Built-in button component that provides native speech recognition using the Web Speech API. The button will be disabled if speech recognition is not supported in the browser. Displays a microphone icon and pulses while actively listening.

Hooks

usePromptInputAttachments

Access and manage file attachments within a PromptInput context.

const attachments = usePromptInputAttachments();

// Available methods:
attachments.files // Array of current attachments
attachments.add(files) // Add new files
attachments.remove(id) // Remove an attachment by ID
attachments.clear() // Clear all attachments
attachments.openFileDialog() // Open file selection dialog

usePromptInputController

Access the full PromptInput controller from a PromptInputProvider. Only available when using the provider.

const controller = usePromptInputController();

// Available methods:
controller.textInput.value // Current text input value
controller.textInput.setInput(value) // Set text input value
controller.textInput.clear() // Clear text input
controller.attachments // Same as usePromptInputAttachments

useProviderAttachments

Access attachments context from a PromptInputProvider. Only available when using the provider.

const attachments = useProviderAttachments();

// Same interface as usePromptInputAttachments

<PromptInputHeader />

Prop

Type

<PromptInputHoverCard />

Prop

Type

<PromptInputHoverCardTrigger />

Prop

Type

<PromptInputHoverCardContent />

Prop

Type

<PromptInputTabsList />

Prop

Type

<PromptInputTab />

Prop

Type

<PromptInputTabLabel />

Prop

Type

<PromptInputTabBody />

Prop

Type

<PromptInputTabItem />

Prop

Type

<PromptInputCommand />

Prop

Type

<PromptInputCommandInput />

Prop

Type

<PromptInputCommandList />

Prop

Type

<PromptInputCommandEmpty />

Prop

Type

<PromptInputCommandGroup />

Prop

Type

<PromptInputCommandItem />

Prop

Type

<PromptInputCommandSeparator />

Prop

Type