All components

AI Instruction Panel

AI Instruction Panel is a React and Tailwind CSS component from Cult UI, licensed MIT. Copy it and paste it into your project.

Cult UI MIT other

What it is

AI Instruction Panel is a React and Tailwind CSS component from Cult UI, licensed MIT. Copy it and paste it into your project.

How to use it

Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.

Open in the catalogue

Five free copies a month. No card.

Needs

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

Licence

This component comes from Cult UI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.

AI Instruction Panel

apps/www/registry/default/ui/ai-instructions.tsx
"use client"

import {
  createContext,
  useCallback,
  useContext,
  useMemo,
  useState,
  type ComponentProps,
  type PropsWithChildren,
  type ReactNode,
} from "react"
import {
  CheckmarkSquare02Icon,
  PlusSignIcon,
  SettingsIcon,
} 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"

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

export interface Instruction {
  /** Unique identifier for the instruction */
  id: string
  /** Display title for the instruction */
  title: string
  /** Short description shown in the list */
  description: string
  /** Full instruction content shown in hover preview */
  content?: string
  /** Whether this is a user-created instruction */
  isCustom?: boolean
}

export interface InstructionsContextValue {
  /** Currently active instruction IDs */
  activeIds: string[]
  /** Toggle an instruction's active state */
  toggle: (id: string) => void
  /** Check if an instruction is active */
  isActive: (id: string) => boolean
  /** All available instructions */
  instructions: Instruction[]
  /** Add a custom instruction */
  addCustom: (instruction: Omit<Instruction, "id" | "isCustom">) => void
  /** Remove a custom instruction */
  removeCustom: (id: string) => void
  /** 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 InstructionsContext = createContext<InstructionsContextValue | null>(null)

/**
 * Hook to access the Instructions context
 * @throws Error if used outside of Instructions provider
 */
export const useInstructions = () => {
  const ctx = useContext(InstructionsContext)
  if (!ctx) {
    throw new Error(
      "useInstructions must be used within an Instructions 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 InstructionsProps = PropsWithChildren<{
  /** Controlled active instruction IDs */
  value?: string[]
  /** Callback when active instructions change */
  onValueChange?: (value: string[]) => void
  /** Default active instruction IDs (uncontrolled) */
  defaultValue?: string[]
  /** Initial set of instructions */
  instructions?: Instruction[]
  /** Callback when instructions change (for custom instructions) */
  onInstructionsChange?: (instructions: Instruction[]) => void
  /** Controlled open state for popover */
  open?: boolean
  /** Callback when open state changes */
  onOpenChange?: (open: boolean) => void
}>

/**
 * Root component for the Instructions widget.
 * Provides context for all child components.
 */
export function Instructions({
  value,
  onValueChange,
  defaultValue = [],
  instructions: controlledInstructions,
  onInstructionsChange,
  open: controlledOpen,
  onOpenChange,
  children,
}: InstructionsProps) {
  const [activeIds, setActiveIds] = useControllableState(
    value,
    defaultValue,
    onValueChange
  )

  const [internalInstructions, setInternalInstructions] = useState<
    Instruction[]
  >(controlledInstructions ?? [])

  const instructions = controlledInstructions ?? internalInstructions

  const [open, setOpen] = useControllableState(
    controlledOpen,
    false,
    onOpenChange
  )
  const [createDialogOpen, setCreateDialogOpen] = useState(false)

  const toggle = useCallback(
    (id: string) => {
      setActiveIds((prev) =>
        prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]
      )
    },
    [setActiveIds]
  )

  const isActive = useCallback(
    (id: string) => activeIds.includes(id),
    [activeIds]
  )

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

      if (controlledInstructions) {
        onInstructionsChange?.([...controlledInstructions, newInstruction])
      } else {
        setInternalInstructions((prev) => [...prev, newInstruction])
      }
    },
    [controlledInstructions, onInstructionsChange]
  )

  const removeCustom = useCallback(
    (id: string) => {
      // Also remove from active if it was active
      setActiveIds((prev) => prev.filter((i) => i !== id))

      if (controlledInstructions) {
        onInstructionsChange?.(
          controlledInstructions.filter((i) => i.id !== id)
        )
      } else {
        setInternalInstructions((prev) => prev.filter((i) => i.id !== id))
      }
    },
    [controlledInstructions, onInstructionsChange, setActiveIds]
  )

  const contextValue = useMemo<InstructionsContextValue>(
    () => ({
      activeIds,
      toggle,
      isActive,
      instructions,
      addCustom,
      removeCustom,
      open,
      setOpen,
      createDialogOpen,
      setCreateDialogOpen,
    }),
    [
      activeIds,
      toggle,
      isActive,
      instructions,
      addCustom,
      removeCustom,
      open,
      setOpen,
      createDialogOpen,
    ]
  )

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

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

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

/**
 * Button that opens the instructions popover.
 * Shows active count badge when instructions are selected.
 */
export function InstructionsTrigger({
  className,
  label,
  children,
  ...props
}: InstructionsTriggerProps) {
  const { activeIds } = useInstructions()
  const activeCount = activeIds.length

  return (
    <PopoverTrigger asChild>
      <Button
        className={cn(
          "gap-1.5",
          activeCount > 0 && "text-foreground",
          className
        )}
        size="sm"
        type="button"
        variant="ghost"
        {...props}
      >
        {children ?? (
          <>
            <HugeiconsIcon
              className="size-3.5"
              icon={SettingsIcon}
              strokeWidth={2}
            />
            {label ?? "Instructions"}
            {activeCount > 0 && (
              <span
                className="inline-flex size-4 items-center justify-center rounded-full bg-primary text-[0.625rem] text-primary-foreground"
                data-slot="instructions-count"
              >
                {activeCount}
              </span>
            )}
          </>
        )}
      </Button>
    </PopoverTrigger>
  )
}

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

export type InstructionsContentProps = ComponentProps<typeof PopoverContent>

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

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

export type InstructionsSearchProps = ComponentProps<typeof CommandInput>

/**
 * Search input for filtering instructions.
 */
export function InstructionsSearch({
  placeholder = "Search instructions...",
  className,
  ...props
}: InstructionsSearchProps) {
  return (
    <CommandInput
      className={cn(className)}
      data-slot="instructions-search"
      placeholder={placeholder}
      {...props}
    />
  )
}

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

export type InstructionsListProps = ComponentProps<typeof CommandList>

/**
 * Scrollable list container for instructions.
 */
export function InstructionsList({
  className,
  children,
  ...props
}: InstructionsListProps) {
  return (
    <CommandList
      className={cn("max-h-64", className)}
      data-slot="instructions-list"
      {...props}
    >
      {children}
    </CommandList>
  )
}

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

export type InstructionsEmptyProps = ComponentProps<typeof CommandEmpty>

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

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

export type InstructionsGroupProps = ComponentProps<typeof CommandGroup>

/**
 * Group container for categorizing instructions.
 * Applies rounded corners to first/last items only.
 */
export function InstructionsGroup({
  className,
  ...props
}: InstructionsGroupProps) {
  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="instructions-group"
      {...props}
    />
  )
}

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

export type InstructionsSeparatorProps = ComponentProps<typeof CommandSeparator>

/**
 * Visual separator between instruction groups.
 */
export function InstructionsSeparator({
  className,
  ...props
}: InstructionsSeparatorProps) {
  return (
    <CommandSeparator
      className={cn(className)}
      data-slot="instructions-separator"
      {...props}
    />
  )
}

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

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

/**
 * Individual instruction item with toggle state and optional hover preview.
 */
export function InstructionsItem({
  instruction,
  disablePreview = false,
  className,
  children,
  ...props
}: InstructionsItemProps) {
  const { toggle, isActive } = useInstructions()
  const active = isActive(instruction.id)

  const handleSelect = () => {
    toggle(instruction.id)
  }

  const itemContent = (
    <CommandItem
      className={cn(
        "group/instruction-item flex cursor-pointer items-start gap-3 py-2",
        active && "bg-accent",
        className
      )}
      data-checked={active}
      data-slot="instructions-item"
      data-state={active ? "active" : "inactive"}
      onSelect={handleSelect}
      value={instruction.title}
      {...props}
    >
      <div
        className={cn(
          "mt-0.5 flex size-4 shrink-0 items-center justify-center border first:rounded-t-lg last:rounded-b-lg",
          active
            ? "border-primary bg-primary text-primary-foreground"
            : "border-muted-foreground/30"
        )}
        data-slot="instructions-item-checkbox"
      >
        {active && (
          <HugeiconsIcon
            className="size-3"
            icon={CheckmarkSquare02Icon}
            strokeWidth={2.5}
          />
        )}
      </div>
      <div className="flex min-w-0 flex-1 flex-col gap-0.5">
        {children ?? (
          <>
            <InstructionsItemTitle>{instruction.title}</InstructionsItemTitle>
            <InstructionsItemDescription>
              {instruction.description}
            </InstructionsItemDescription>
          </>
        )}
      </div>
    </CommandItem>
  )

  if (disablePreview || !instruction.content) {
    return itemContent
  }

  return (
    <InstructionsHoverCard instruction={instruction}>
      {itemContent}
    </InstructionsHoverCard>
  )
}

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

export type InstructionsItemTitleProps = ComponentProps<"span">

/**
 * Title text for an instruction item.
 */
export function InstructionsItemTitle({
  className,
  ...props
}: InstructionsItemTitleProps) {
  return (
    <span
      className={cn("font-medium text-foreground text-xs", className)}
      data-slot="instructions-item-title"
      {...props}
    />
  )
}

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

export type InstructionsItemDescriptionProps = ComponentProps<"span">

/**
 * Description text for an instruction item.
 */
export function InstructionsItemDescription({
  className,
  ...props
}: InstructionsItemDescriptionProps) {
  return (
    <span
      className={cn("line-clamp-2 text-muted-foreground text-xs", className)}
      data-slot="instructions-item-description"
      {...props}
    />
  )
}

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

export interface InstructionsHoverCardProps {
  /** The instruction to preview */
  instruction: Instruction
  /** The trigger element (instruction item) */
  children: ReactNode
}

/**
 * Hover card that shows the full instruction content on hover.
 */
export function InstructionsHoverCard({
  instruction,
  children,
}: InstructionsHoverCardProps) {
  return (
    <HoverCard openDelay={300} closeDelay={100}>
      <HoverCardTrigger>{children}</HoverCardTrigger>
      <HoverCardContent
        align="start"
        className="w-72"
        data-slot="instructions-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">{instruction.title}</span>
            <span className="text-muted-foreground text-xs">
              {instruction.description}
            </span>
          </div>
          {instruction.content && (
            <div className="rounded-md bg-muted/50 p-2">
              <p className="whitespace-pre-wrap text-muted-foreground text-xs">
                {instruction.content}
              </p>
            </div>
          )}
          {instruction.isCustom && (
            <span className="text-[0.625rem] text-muted-foreground">
              Custom instruction
            </span>
          )}
        </div>
      </HoverCardContent>
    </HoverCard>
  )
}

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

export type InstructionsFooterProps = 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 InstructionsFooter({
  className,
  children,
  ...props
}: InstructionsFooterProps) {
  return (
    <div
      className={cn("border-t p-1", className)}
      data-slot="instructions-footer"
      {...props}
    >
      {children}
    </div>
  )
}

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

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

/**
 * Button that opens the create instruction dialog.
 */
export function InstructionsCreateTrigger({
  label = "New Instruction",
  className,
  children,
  ...props
}: InstructionsCreateTriggerProps) {
  const { setCreateDialogOpen, setOpen } = useInstructions()

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

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

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

export type InstructionsCreateDialogProps = 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 instructions.
 */
export function InstructionsCreateDialog({
  title = "Create Instruction",
  description = "Create a custom instruction to guide the AI's behavior.",
  children,
  ...props
}: InstructionsCreateDialogProps) {
  const { createDialogOpen, setCreateDialogOpen, addCustom } = useInstructions()
  const [formTitle, setFormTitle] = useState("")
  const [formDescription, setFormDescription] = useState("")
  const [formContent, setFormContent] = useState("")

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

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

    addCustom({
      title: formTitle.trim(),
      description: formDescription.trim(),
      content: formContent.trim() || undefined,
    })

    // Reset form
    setFormTitle("")
    setFormDescription("")
    setFormContent("")
    setCreateDialogOpen(false)
  }

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

  return (
    <Dialog
      data-slot="instructions-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="instruction-title"
              >
                Title
              </label>
              <Input
                id="instruction-title"
                onChange={(e) => setFormTitle(e.target.value)}
                placeholder="e.g., Be Concise"
                required
                value={formTitle}
              />
            </div>
            <div className="flex flex-col gap-2">
              <label
                className="font-medium text-xs"
                htmlFor="instruction-description"
              >
                Description
              </label>
              <Input
                id="instruction-description"
                onChange={(e) => setFormDescription(e.target.value)}
                placeholder="e.g., Keep responses short and to the point"
                required
                value={formDescription}
              />
            </div>
            <div className="flex flex-col gap-2">
              <label
                className="font-medium text-xs"
                htmlFor="instruction-content"
              >
                Full Instruction (optional)
              </label>
              <Textarea
                className="min-h-24 resize-none"
                id="instruction-content"
                onChange={(e) => setFormContent(e.target.value)}
                placeholder="Detailed instruction text that will be shown in the preview..."
                value={formContent}
              />
            </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 InstructionsWidget = Object.assign(Instructions, {
  Trigger: InstructionsTrigger,
  Content: InstructionsContent,
  Search: InstructionsSearch,
  List: InstructionsList,
  Empty: InstructionsEmpty,
  Group: InstructionsGroup,
  Separator: InstructionsSeparator,
  Item: InstructionsItem,
  ItemTitle: InstructionsItemTitle,
  ItemDescription: InstructionsItemDescription,
  HoverCard: InstructionsHoverCard,
  Footer: InstructionsFooter,
  CreateTrigger: InstructionsCreateTrigger,
  CreateDialog: InstructionsCreateDialog,
})

Similar components