Todos os componentes

Prompt Library

Prompt Library é um componente para React e Tailwind CSS, da biblioteca Cult UI, com licença MIT. Copia e cola no teu projeto.

Cult UI MIT other

O que é

Prompt Library é um componente para React e Tailwind CSS, da biblioteca Cult UI, 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.

Abrir no catálogo

Cinco cópias grátis por mês. Sem cartão.

Precisa de

npm i @hugeicons/core-free-icons @hugeicons/react

Licença

Este componente vem de Cult UI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.

Prompt Library

apps/www/registry/default/ui/prompt-library.tsx
"use client"

import {
  createContext,
  useCallback,
  useContext,
  useMemo,
  useState,
  type ComponentProps,
  type PropsWithChildren,
  type ReactNode,
} from "react"
import { LibraryIcon, PlusSignIcon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
} from "@/components/ui/command"
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  type DialogDescriptionProps,
  type DialogTitleProps,
} from "@/components/ui/dialog"
import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card"
import { Input } from "@/components/ui/input"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"
import { Textarea } from "@/components/ui/textarea"
import { useOptionalPromptInputController } from "@/components/ai-elements/prompt-input"

// ============================================================================
// Types
// ============================================================================

export interface Prompt {
  /** Unique identifier for the prompt */
  id: string
  /** Display title for the prompt */
  title: string
  /** Short description shown in the list */
  description: string
  /** The actual prompt content to insert/copy */
  prompt: string
  /** Optional category for grouping */
  category?: string
  /** Whether this is a user-created prompt */
  isCustom?: boolean
}

export interface PromptLibraryContextValue {
  /** All available prompts */
  prompts: Prompt[]
  /** Add a custom prompt */
  addCustom: (prompt: Omit<Prompt, "id" | "isCustom">) => void
  /** Remove a custom prompt */
  removeCustom: (id: string) => void
  /** Select a prompt (insert or copy) */
  selectPrompt: (prompt: Prompt) => void
  /** Last selected prompt ID */
  lastSelectedId: string | null
  /** Open state for the popover */
  open: boolean
  /** Set open state for the popover */
  setOpen: (open: boolean) => void
  /** Open state for the create dialog */
  createDialogOpen: boolean
  /** Set open state for the create dialog */
  setCreateDialogOpen: (open: boolean) => void
}

// ============================================================================
// Context
// ============================================================================

const PromptLibraryContext = createContext<PromptLibraryContextValue | null>(
  null
)

/**
 * Hook to access the PromptLibrary context
 * @throws Error if used outside of PromptLibrary provider
 */
export const usePromptLibrary = () => {
  const ctx = useContext(PromptLibraryContext)
  if (!ctx) {
    throw new Error(
      "usePromptLibrary must be used within a PromptLibrary provider"
    )
  }
  return ctx
}

// ============================================================================
// Controllable State Hook
// ============================================================================

function useControllableState<T>(
  controlledValue: T | undefined,
  defaultValue: T,
  onChange?: (value: T) => void
): [T, (value: T | ((prev: T) => T)) => void] {
  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue)
  const isControlled = controlledValue !== undefined
  const value = isControlled ? controlledValue : uncontrolledValue

  const setValue = useCallback(
    (nextValue: T | ((prev: T) => T)) => {
      const resolvedValue =
        typeof nextValue === "function"
          ? (nextValue as (prev: T) => T)(value)
          : nextValue

      if (!isControlled) {
        setUncontrolledValue(resolvedValue)
      }
      onChange?.(resolvedValue)
    },
    [isControlled, onChange, value]
  )

  return [value, setValue]
}

// ============================================================================
// Root Component
// ============================================================================

export type PromptLibraryProps = PropsWithChildren<{
  /** Initial set of prompts */
  prompts?: Prompt[]
  /** Callback when prompts change (for custom prompts) */
  onPromptsChange?: (prompts: Prompt[]) => void
  /** Callback when a prompt is selected */
  onSelect?: (prompt: Prompt) => void
  /** Controlled open state for popover */
  open?: boolean
  /** Callback when open state changes */
  onOpenChange?: (open: boolean) => void
}>

/**
 * Root component for the PromptLibrary widget.
 * Provides context for all child components.
 * Integrates with PromptInput when used inside PromptInputProvider.
 */
export function PromptLibrary({
  prompts: controlledPrompts,
  onPromptsChange,
  onSelect,
  open: controlledOpen,
  onOpenChange,
  children,
}: PromptLibraryProps) {
  const [internalPrompts, setInternalPrompts] = useState<Prompt[]>(
    controlledPrompts ?? []
  )

  const prompts = controlledPrompts ?? internalPrompts

  const [open, setOpen] = useControllableState(
    controlledOpen,
    false,
    onOpenChange
  )
  const [createDialogOpen, setCreateDialogOpen] = useState(false)
  const [lastSelectedId, setLastSelectedId] = useState<string | null>(null)

  // Try to get PromptInput controller for integration
  const promptInputController = useOptionalPromptInputController()

  const selectPrompt = useCallback(
    async (prompt: Prompt) => {
      setLastSelectedId(prompt.id)

      if (promptInputController) {
        // Insert directly into PromptInput textarea
        promptInputController.textInput.setInput(prompt.prompt)
      } else {
        // Fallback: copy to clipboard
        try {
          await navigator.clipboard.writeText(prompt.prompt)
        } catch (err) {
          console.error("Failed to copy to clipboard:", err)
        }
      }

      // Close popover after selection
      setOpen(false)

      // Call onSelect callback
      onSelect?.(prompt)
    },
    [promptInputController, setOpen, onSelect]
  )

  const addCustom = useCallback(
    (prompt: Omit<Prompt, "id" | "isCustom">) => {
      const newPrompt: Prompt = {
        ...prompt,
        id: `custom-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
        isCustom: true,
      }

      if (controlledPrompts) {
        onPromptsChange?.([...controlledPrompts, newPrompt])
      } else {
        setInternalPrompts((prev) => [...prev, newPrompt])
      }
    },
    [controlledPrompts, onPromptsChange]
  )

  const removeCustom = useCallback(
    (id: string) => {
      if (controlledPrompts) {
        onPromptsChange?.(controlledPrompts.filter((p) => p.id !== id))
      } else {
        setInternalPrompts((prev) => prev.filter((p) => p.id !== id))
      }
    },
    [controlledPrompts, onPromptsChange]
  )

  const contextValue = useMemo<PromptLibraryContextValue>(
    () => ({
      prompts,
      addCustom,
      removeCustom,
      selectPrompt,
      lastSelectedId,
      open,
      setOpen,
      createDialogOpen,
      setCreateDialogOpen,
    }),
    [
      prompts,
      addCustom,
      removeCustom,
      selectPrompt,
      lastSelectedId,
      open,
      setOpen,
      createDialogOpen,
    ]
  )

  return (
    <PromptLibraryContext.Provider value={contextValue}>
      <Popover onOpenChange={setOpen} open={open}>
        {children}
      </Popover>
    </PromptLibraryContext.Provider>
  )
}

// ============================================================================
// Trigger Component
// ============================================================================

export type PromptLibraryTriggerProps = ComponentProps<typeof Button> & {
  /** Custom label for the trigger button */
  label?: ReactNode
}

/**
 * Button that opens the prompt library popover.
 */
export function PromptLibraryTrigger({
  className,
  label,
  children,
  ...props
}: PromptLibraryTriggerProps) {
  return (
    <PopoverTrigger asChild>
      <Button
        className={cn("gap-1.5", className)}
        size="sm"
        type="button"
        variant="ghost"
        {...props}
      >
        {children ?? (
          <>
            <HugeiconsIcon
              className="size-3.5"
              icon={LibraryIcon}
              strokeWidth={2}
            />
            {label ?? "Prompts"}
          </>
        )}
      </Button>
    </PopoverTrigger>
  )
}

// ============================================================================
// Content Component
// ============================================================================

export type PromptLibraryContentProps = ComponentProps<typeof PopoverContent>

/**
 * Popover content container for the prompt library list.
 */
export function PromptLibraryContent({
  className,
  children,
  ...props
}: PromptLibraryContentProps) {
  return (
    <PopoverContent
      align="start"
      className={cn("w-80 p-0", className)}
      side="top"
      sideOffset={8}
      {...props}
    >
      <Command className="rounded-lg" data-slot="prompt-library-content">
        {children}
      </Command>
    </PopoverContent>
  )
}

// ============================================================================
// Search Component
// ============================================================================

export type PromptLibrarySearchProps = ComponentProps<typeof CommandInput>

/**
 * Search input for filtering prompts.
 */
export function PromptLibrarySearch({
  placeholder = "Search prompts...",
  className,
  ...props
}: PromptLibrarySearchProps) {
  return (
    <CommandInput
      className={cn(className)}
      data-slot="prompt-library-search"
      placeholder={placeholder}
      {...props}
    />
  )
}

// ============================================================================
// List Component
// ============================================================================

export type PromptLibraryListProps = ComponentProps<typeof CommandList>

/**
 * Scrollable list container for prompts.
 */
export function PromptLibraryList({
  className,
  children,
  ...props
}: PromptLibraryListProps) {
  return (
    <CommandList
      className={cn("max-h-64", className)}
      data-slot="prompt-library-list"
      {...props}
    >
      {children}
    </CommandList>
  )
}

// ============================================================================
// Empty Component
// ============================================================================

export type PromptLibraryEmptyProps = ComponentProps<typeof CommandEmpty>

/**
 * Empty state shown when no prompts match the search.
 */
export function PromptLibraryEmpty({
  children = "No prompts found.",
  className,
  ...props
}: PromptLibraryEmptyProps) {
  return (
    <CommandEmpty
      className={cn(className)}
      data-slot="prompt-library-empty"
      {...props}
    >
      {children}
    </CommandEmpty>
  )
}

// ============================================================================
// Group Component
// ============================================================================

export type PromptLibraryGroupProps = ComponentProps<typeof CommandGroup>

/**
 * Group container for categorizing prompts.
 */
export function PromptLibraryGroup({
  className,
  ...props
}: PromptLibraryGroupProps) {
  return (
    <CommandGroup
      className={cn(
        // Remove default rounding from all items
        "**:[[cmdk-item]]:rounded-none",
        // First child in group gets rounded top
        "[&_[cmdk-group-items]>:first-child_[cmdk-item]]:rounded-t-md",
        // Last child in group gets rounded bottom
        "[&_[cmdk-group-items]>:last-child_[cmdk-item]]:rounded-b-md",
        className
      )}
      data-slot="prompt-library-group"
      {...props}
    />
  )
}

// ============================================================================
// Separator Component
// ============================================================================

export type PromptLibrarySeparatorProps = ComponentProps<
  typeof CommandSeparator
>

/**
 * Visual separator between prompt groups.
 */
export function PromptLibrarySeparator({
  className,
  ...props
}: PromptLibrarySeparatorProps) {
  return (
    <CommandSeparator
      className={cn(className)}
      data-slot="prompt-library-separator"
      {...props}
    />
  )
}

// ============================================================================
// Item Component
// ============================================================================

export type PromptLibraryItemProps = Omit<
  ComponentProps<typeof CommandItem>,
  "value" | "onSelect"
> & {
  /** The prompt data */
  prompt: Prompt
  /** Disable hover card preview */
  disablePreview?: boolean
}

/**
 * Individual prompt item with click-to-select and optional hover preview.
 */
export function PromptLibraryItem({
  prompt,
  disablePreview = false,
  className,
  children,
  ...props
}: PromptLibraryItemProps) {
  const { selectPrompt, lastSelectedId } = usePromptLibrary()
  const isLastSelected = lastSelectedId === prompt.id

  const handleSelect = () => {
    selectPrompt(prompt)
  }

  const itemContent = (
    <CommandItem
      className={cn(
        "group/prompt-item flex cursor-pointer items-start gap-3 py-2",
        isLastSelected && "bg-accent/50",
        className
      )}
      data-slot="prompt-library-item"
      data-state={isLastSelected ? "selected" : "idle"}
      onSelect={handleSelect}
      value={prompt.title}
      {...props}
    >
      <div className="flex min-w-0 flex-1 flex-col gap-0.5">
        {children ?? (
          <>
            <PromptLibraryItemTitle>{prompt.title}</PromptLibraryItemTitle>
            <PromptLibraryItemDescription>
              {prompt.description}
            </PromptLibraryItemDescription>
          </>
        )}
      </div>
    </CommandItem>
  )

  if (disablePreview || !prompt.prompt) {
    return itemContent
  }

  return (
    <PromptLibraryHoverCard prompt={prompt}>
      {itemContent}
    </PromptLibraryHoverCard>
  )
}

// ============================================================================
// Item Title Component
// ============================================================================

export type PromptLibraryItemTitleProps = ComponentProps<"span">

/**
 * Title text for a prompt item.
 */
export function PromptLibraryItemTitle({
  className,
  ...props
}: PromptLibraryItemTitleProps) {
  return (
    <span
      className={cn("font-medium text-foreground text-xs", className)}
      data-slot="prompt-library-item-title"
      {...props}
    />
  )
}

// ============================================================================
// Item Description Component
// ============================================================================

export type PromptLibraryItemDescriptionProps = ComponentProps<"span">

/**
 * Description text for a prompt item.
 */
export function PromptLibraryItemDescription({
  className,
  ...props
}: PromptLibraryItemDescriptionProps) {
  return (
    <span
      className={cn("line-clamp-2 text-muted-foreground text-xs", className)}
      data-slot="prompt-library-item-description"
      {...props}
    />
  )
}

// ============================================================================
// Hover Card Component
// ============================================================================

export interface PromptLibraryHoverCardProps {
  /** The prompt to preview */
  prompt: Prompt
  /** The trigger element (prompt item) */
  children: ReactNode
}

/**
 * Hover card that shows the full prompt content on hover.
 */
export function PromptLibraryHoverCard({
  prompt,
  children,
}: PromptLibraryHoverCardProps) {
  return (
    <HoverCard openDelay={300} closeDelay={100}>
      <HoverCardTrigger>{children}</HoverCardTrigger>
      <HoverCardContent
        align="start"
        className="w-80"
        data-slot="prompt-library-hover-card"
        side="right"
        sideOffset={8}
      >
        <div className="flex flex-col gap-2">
          <div className="flex flex-col gap-1">
            <span className="font-medium text-sm">{prompt.title}</span>
            <span className="text-muted-foreground text-xs">
              {prompt.description}
            </span>
          </div>
          {prompt.prompt && (
            <div className="rounded-md bg-muted/50 p-2">
              <p className="line-clamp-6 whitespace-pre-wrap text-muted-foreground text-xs">
                {prompt.prompt}
              </p>
            </div>
          )}
          {prompt.category && (
            <span className="text-[0.625rem] text-muted-foreground">
              Category: {prompt.category}
            </span>
          )}
          {prompt.isCustom && (
            <span className="text-[0.625rem] text-muted-foreground">
              Custom prompt
            </span>
          )}
        </div>
      </HoverCardContent>
    </HoverCard>
  )
}

// ============================================================================
// Footer Component
// ============================================================================

export type PromptLibraryFooterProps = ComponentProps<"div">

/**
 * Footer container that stays fixed at the bottom of the popover.
 * Use this to place the create trigger outside the scrollable list.
 */
export function PromptLibraryFooter({
  className,
  children,
  ...props
}: PromptLibraryFooterProps) {
  return (
    <div
      className={cn("border-t p-1", className)}
      data-slot="prompt-library-footer"
      {...props}
    >
      {children}
    </div>
  )
}

// ============================================================================
// Create Trigger Component
// ============================================================================

export type PromptLibraryCreateTriggerProps = Omit<
  ComponentProps<typeof CommandItem>,
  "onSelect"
> & {
  /** Custom label for the create button */
  label?: ReactNode
}

/**
 * Button that opens the create prompt dialog.
 */
export function PromptLibraryCreateTrigger({
  label = "New Prompt",
  className,
  children,
  ...props
}: PromptLibraryCreateTriggerProps) {
  const { setCreateDialogOpen, setOpen } = usePromptLibrary()

  const handleSelect = () => {
    setOpen(false)
    setCreateDialogOpen(true)
  }

  return (
    <CommandItem
      className={cn(
        "flex cursor-pointer items-center gap-2 text-muted-foreground",
        className
      )}
      data-slot="prompt-library-create-trigger"
      onSelect={handleSelect}
      value="__new_prompt__"
      {...props}
    >
      {children ?? (
        <>
          <HugeiconsIcon
            className="size-3.5"
            icon={PlusSignIcon}
            strokeWidth={2}
          />
          {label}
        </>
      )}
    </CommandItem>
  )
}

// ============================================================================
// Create Dialog Component
// ============================================================================

export type PromptLibraryCreateDialogProps = Omit<
  ComponentProps<typeof Dialog>,
  "open" | "onOpenChange" | "children"
> & {
  /** Dialog title */
  title?: DialogTitleProps["children"]
  /** Dialog description */
  description?: DialogDescriptionProps["children"]
  /** Custom form content to replace the default form */
  children?: ReactNode
}

/**
 * Modal dialog for creating custom prompts.
 */
export function PromptLibraryCreateDialog({
  title = "Create Prompt",
  description = "Create a custom prompt template for quick access.",
  children,
  ...props
}: PromptLibraryCreateDialogProps) {
  const { createDialogOpen, setCreateDialogOpen, addCustom } =
    usePromptLibrary()
  const [formTitle, setFormTitle] = useState("")
  const [formDescription, setFormDescription] = useState("")
  const [formPrompt, setFormPrompt] = useState("")
  const [formCategory, setFormCategory] = useState("")

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()

    if (!(formTitle.trim() && formDescription.trim() && formPrompt.trim())) {
      return
    }

    addCustom({
      title: formTitle.trim(),
      description: formDescription.trim(),
      prompt: formPrompt.trim(),
      category: formCategory.trim() || undefined,
    })

    // Reset form
    setFormTitle("")
    setFormDescription("")
    setFormPrompt("")
    setFormCategory("")
    setCreateDialogOpen(false)
  }

  const handleOpenChange = (open: boolean) => {
    setCreateDialogOpen(open)
    if (!open) {
      // Reset form when closing
      setFormTitle("")
      setFormDescription("")
      setFormPrompt("")
      setFormCategory("")
    }
  }

  return (
    <Dialog
      data-slot="prompt-library-create-dialog"
      onOpenChange={handleOpenChange}
      open={createDialogOpen}
      {...props}
    >
      <DialogContent className="sm:max-w-md">
        <DialogHeader>
          <DialogTitle>{title}</DialogTitle>
          <DialogDescription>{description}</DialogDescription>
        </DialogHeader>
        {children ?? (
          <form className="flex flex-col gap-4" onSubmit={handleSubmit}>
            <div className="flex flex-col gap-2">
              <label className="font-medium text-xs" htmlFor="prompt-title">
                Title
              </label>
              <Input
                id="prompt-title"
                onChange={(e) => setFormTitle(e.target.value)}
                placeholder="e.g., Code Review"
                required
                value={formTitle}
              />
            </div>
            <div className="flex flex-col gap-2">
              <label
                className="font-medium text-xs"
                htmlFor="prompt-description"
              >
                Description
              </label>
              <Input
                id="prompt-description"
                onChange={(e) => setFormDescription(e.target.value)}
                placeholder="e.g., Review code for best practices"
                required
                value={formDescription}
              />
            </div>
            <div className="flex flex-col gap-2">
              <label className="font-medium text-xs" htmlFor="prompt-content">
                Prompt
              </label>
              <Textarea
                className="min-h-24 resize-none"
                id="prompt-content"
                onChange={(e) => setFormPrompt(e.target.value)}
                placeholder="Enter the prompt text..."
                required
                value={formPrompt}
              />
            </div>
            <div className="flex flex-col gap-2">
              <label className="font-medium text-xs" htmlFor="prompt-category">
                Category (optional)
              </label>
              <Input
                id="prompt-category"
                onChange={(e) => setFormCategory(e.target.value)}
                placeholder="e.g., Development"
                value={formCategory}
              />
            </div>
            <DialogFooter>
              <DialogClose asChild>
                <Button type="button" variant="outline">
                  Cancel
                </Button>
              </DialogClose>
              <Button type="submit">Create</Button>
            </DialogFooter>
          </form>
        )}
      </DialogContent>
    </Dialog>
  )
}

// ============================================================================
// Compound Export
// ============================================================================

export const PromptLibraryWidget = Object.assign(PromptLibrary, {
  Trigger: PromptLibraryTrigger,
  Content: PromptLibraryContent,
  Search: PromptLibrarySearch,
  List: PromptLibraryList,
  Empty: PromptLibraryEmpty,
  Group: PromptLibraryGroup,
  Separator: PromptLibrarySeparator,
  Item: PromptLibraryItem,
  ItemTitle: PromptLibraryItemTitle,
  ItemDescription: PromptLibraryItemDescription,
  HoverCard: PromptLibraryHoverCard,
  Footer: PromptLibraryFooter,
  CreateTrigger: PromptLibraryCreateTrigger,
  CreateDialog: PromptLibraryCreateDialog,
})

Componentes parecidos