Velocity UI
Loading…
Menu

AI Command Palette

A centralized, modal-based search bar (Cmd+K) that executes commands and features streaming AI text responses, keyboard navigation with spring animations, and a premium glass aesthetic.

Installation

Add this component to your project using the CLI:

terminal
npx vui-registry-cli-v1 add ai-command-palette

Source Code

ai-command-palette.tsx
'use client'

import React, { useState, useEffect, useRef } from 'react'
import { Command } from 'cmdk'
import { motion, AnimatePresence } from 'framer-motion'
import { 
  Search, 
  Command as CommandIcon, 
  Sparkles, 
  ArrowRight, 
  Zap,
  Github,
  Monitor,
  Moon,
  Sun,
  Laptop,
  MessageSquare,
  Wand2
} from 'lucide-react'
import { cn } from '@/lib/utils'

const USER_IMAGE = "https://i.postimg.cc/3xgQH76g/Whats-App-Image-2026-02-19-at-8-23-43-PM.jpg"

// Mock data for commands
const COMMANDS = [
  {
    heading: "Suggestions",
    items: [
      { id: 'ask-ai', icon: Sparkles, label: 'Ask AI Assistant', shortcut: 'A' },
      { id: 'search-docs', icon: Search, label: 'Search Documentation', shortcut: 'S' },
    ]
  },
  {
    heading: "System",
    items: [
      { id: 'toggle-theme', icon:  Sun, label: 'Toggle Theme', shortcut: 'T' },
      { id: 'settings', icon:  Monitor, label: 'Open Settings', shortcut: ',' },
    ]
  },
  {
    heading: "Navigation",
    items: [
      { id: 'home', icon:  Zap, label: 'Go to Dashboard', shortcut: 'G D' },
      { id: 'projects', icon:  Github, label: 'Go to Projects', shortcut: 'G P' },
    ]
  }
]

export default function AICommandPalette() {
  const [open, setOpen] = useState(false)
  const [search, setSearch] = useState('')
  const [selected, setSelected] = useState('')
  const [mode, setMode] = useState<'command' | 'ai'>('command')
  const [aiResponse, setAiResponse] = useState('')
  const [isTyping, setIsTyping] = useState(false)
  
  const [theme, setTheme] = useState('light')
  const [notification, setNotification] = useState<{message: string, visible: boolean}>({ message: '', visible: false })

  const showNotification = (message: string) => {
    setNotification({ message, visible: true })
    setTimeout(() => setNotification(prev => ({ ...prev, visible: false })), 2000)
  }

  const handleCommandSelect = (id: string) => {
    switch (id) {
      case 'ask-ai':
        setMode('ai')
        setSearch('')
        break
      case 'search-docs':
        showNotification('Opening documentation...')
        setTimeout(() => setOpen(false), 800)
        break
      case 'toggle-theme':
        setTheme(prev => prev === 'light' ? 'dark' : 'light')
        showNotification(`Theme switched to ${theme === 'light' ? 'Dark' : 'Light'}`)
        break
      case 'settings':
        showNotification('Opening settings panel...')
        setTimeout(() => setOpen(false), 800)
        break
      case 'home':
        showNotification('Navigating to Dashboard...')
        setTimeout(() => setOpen(false), 800)
        break
      case 'projects':
        showNotification('Loading Projects...')
        setTimeout(() => setOpen(false), 800)
        break
      default:
        console.log('Selected:', id)
        setOpen(false)
    }
  }
  
  // Toggle with Cmd+K
  useEffect(() => {
    const down = (e: KeyboardEvent) => {
      if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
        e.preventDefault()
        setOpen((open) => !open)
      }
    }
    document.addEventListener('keydown', down)
    return () => document.removeEventListener('keydown', down)
  }, [])

  // Simulate AI streaming response
  useEffect(() => {
    if (mode === 'ai' && search.length > 5 && !isTyping) {
        // Debounce simulation
        const timer = setTimeout(() => {
            setIsTyping(true)
            setAiResponse('')
            const response = "Based on your request, I can help you configure the component. The 'ProScheduler' supports custom event types, drag-and-drop interactions, and premium theming options. Would you like me to generate a configuration template?"
            let i = 0
            const interval = setInterval(() => {
                setAiResponse(prev => prev + response.charAt(i))
                i++
                if (i >= response.length) {
                    clearInterval(interval)
                    setIsTyping(false)
                }
            }, 30)
            return () => clearInterval(interval)
        }, 1000)
        return () => clearTimeout(timer)
    }
  }, [mode, search])

  // Reset when closing
  useEffect(() => {
    if (!open) {
        setTimeout(() => {
            setMode('command')
            setSearch('')
            setAiResponse('')
        }, 200)
    }
  }, [open])

  return (
    <div className="h-[600px] w-full flex flex-col items-center justify-center bg-neutral-100 dark:bg-neutral-950 p-4 relative overflow-hidden font-sans">
      
      {/* Background Elements */}
      <div className="absolute inset-0 bg-[linear-gradient(to_right,#80808008_1px,transparent_1px),linear-gradient(to_bottom,#80808008_1px,transparent_1px)] bg-[size:24px_24px]"></div>
      <div className="absolute left-0 right-0 top-0 -z-10 m-auto h-[310px] w-[310px] rounded-full bg-neutral-400/20 opacity-20 blur-[100px]"></div>

      {/* Trigger Button */}
      <motion.button
        whileHover={{ scale: 1.01, boxShadow: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)" }}
        whileTap={{ scale: 0.99 }}
        onClick={() => setOpen(true)}
        className="group relative flex items-center gap-3 rounded-xl bg-white dark:bg-neutral-900 px-8 py-4 text-left shadow-xl shadow-neutral-200/50 dark:shadow-black/50 border border-neutral-200 dark:border-white/10 transition-all hover:border-neutral-300 dark:hover:border-white/20"
      >
        <Search className="w-5 h-5 text-neutral-400 group-hover:text-neutral-800 dark:group-hover:text-white transition-colors" />
        <span className="text-sm font-medium text-neutral-500 group-hover:text-neutral-800 dark:text-neutral-400 dark:group-hover:text-white transition-colors">Search commands...</span>
        <kbd className="ml-12 pointer-events-none inline-flex h-6 select-none items-center gap-1 rounded-md border border-neutral-200 dark:border-white/10 bg-neutral-50 dark:bg-white/5 px-2 font-mono text-[11px] font-medium text-neutral-400 dark:text-neutral-500">
          <span className="text-xs"></span>K
        </kbd>
      </motion.button>

      <AnimatePresence>
        {open && (
          <div className="fixed inset-0 z-50 flex items-start justify-center pt-[20vh] px-4">
            {/* Backdrop */}
            <motion.div
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
              onClick={() => setOpen(false)}
              className="absolute inset-0 bg-neutral-950/20 backdrop-blur-[2px]"
            />
            
            {/* Notification Toast */}
            <AnimatePresence>
                {notification.visible && (
                    <motion.div
                        initial={{ opacity: 0, y: 20 }}
                        animate={{ opacity: 1, y: 0 }}
                        exit={{ opacity: 0, y: 20 }}
                        className="absolute bottom-12 z-50 px-4 py-2 bg-neutral-900 dark:bg-white text-white dark:text-black rounded-full text-xs font-medium shadow-lg"
                    >
                        {notification.message}
                    </motion.div>
                )}
            </AnimatePresence>

            {/* Command Palette */}
            <motion.div
              initial={{ opacity: 0, scale: 0.95, y: 20 }}
              animate={{ opacity: 1, scale: 1, y: 0 }}
              exit={{ opacity: 0, scale: 0.95, y: 20 }}
              transition={{ type: "spring", damping: 25, stiffness: 300 }}
              className="relative w-full max-w-2xl overflow-hidden rounded-2xl border border-neutral-200 dark:border-white/10 bg-white dark:bg-neutral-900 shadow-2xl shadow-neutral-500/10 dark:shadow-black/50"
            >
              <Command
                filter={(value, search) => {
                  if (mode === 'ai') return 1
                  if (value.toLowerCase().includes(search.toLowerCase())) return 1
                  return 0
                }}
                className="w-full bg-transparent"
                loop
              >
                {/* Input Area */}
                <div className="flex items-center border-b border-neutral-100 dark:border-white/5 px-4 py-4">
                    <AnimatePresence mode="wait">
                        {mode === 'ai' ? (
                            <motion.div 
                                initial={{ opacity: 0, scale: 0.5 }}
                                animate={{ opacity: 1, scale: 1 }}
                                exit={{ opacity: 0, scale: 0.5 }}
                                className="mr-3 flex items-center justify-center w-6 h-6 rounded-lg bg-black dark:bg-white"
                            >
                                <Sparkles className="w-3.5 h-3.5 text-white dark:text-black animate-pulse" />
                            </motion.div>
                        ) : (
                            <motion.div
                                initial={{ opacity: 0, scale: 0.5 }}
                                animate={{ opacity: 1, scale: 1 }}
                                exit={{ opacity: 0, scale: 0.5 }} 
                            >
                                <Search className="mr-3 h-5 w-5 text-neutral-400" />
                            </motion.div>
                        )}
                    </AnimatePresence>
                    
                    <Command.Input 
                        value={search}
                        onValueChange={setSearch}
                        placeholder={mode === 'ai' ? "Ask AI to generate code, explain concepts..." : "Type a command or search..."}
                        className="flex-1 bg-transparent text-base outline-none placeholder:text-neutral-400 dark:text-white dark:placeholder:text-neutral-600"
                        autoFocus
                    />
                    
                    {mode === 'command' && (
                        <div className="flex gap-2">
                             <button 
                                onClick={() => setMode('ai')}
                                className="px-2.5 py-1.5 text-[11px] font-medium bg-black/5 dark:bg-white/10 text-neutral-600 dark:text-neutral-300 rounded-md border border-black/5 dark:border-white/5 hover:bg-black/10 dark:hover:bg-white/20 transition-colors flex items-center gap-1.5"
                             >
                                <Sparkles className="w-3 h-3" />
                                Ask AI
                             </button>
                        </div>
                    )}
                    {mode === 'ai' && (
                         <button 
                            onClick={() => {
                                setMode('command')
                                setSearch('')
                                setAiResponse('')
                            }}
                            className="px-2 py-1 text-[10px] font-medium bg-neutral-100 dark:bg-white/10 text-neutral-500 dark:text-neutral-400 rounded-md hover:bg-neutral-200 dark:hover:bg-white/20 transition-colors"
                         >
                            Esc
                        </button>
                    )}
                </div>

                {/* Content Area */}
                <div className="relative min-h-[300px] bg-neutral-50/30 dark:bg-black/20">
                    {mode === 'command' && (
                        <Command.List className="max-h-[400px] overflow-y-auto p-2 scroll-py-2 custom-scrollbar">
                            <Command.Empty className="py-12 text-center text-sm text-neutral-400 flex flex-col items-center gap-2">
                                <Search className="w-8 h-8 opacity-20" />
                                No results found.
                            </Command.Empty>

                            {COMMANDS.map((group) => (
                                <Command.Group 
                                    key={group.heading} 
                                    heading={group.heading}
                                    className="text-[10px] font-semibold text-neutral-400 uppercase tracking-wider mb-2 px-2 mt-2 first:mt-0"
                                >
                                    {group.items.map((item) => (
                                        <Command.Item
                                            key={item.id}
                                            value={item.label}
                                            onSelect={() => handleCommandSelect(item.id)}
                                            className="group flex items-center justify-between rounded-lg px-3 py-3 text-sm text-neutral-600 dark:text-neutral-300 aria-selected:bg-white dark:aria-selected:bg-neutral-800 aria-selected:text-black dark:aria-selected:text-white aria-selected:shadow-md aria-selected:scale-[1.01] cursor-pointer transition-all duration-200 mb-1 border border-transparent aria-selected:border-neutral-200/50 dark:aria-selected:border-white/10"
                                        >
                                            <div className="flex items-center gap-3">
                                                <div className="flex items-center justify-center w-8 h-8 rounded-md bg-neutral-100 dark:bg-white/5 border border-neutral-200 dark:border-white/5 group-aria-selected:bg-neutral-50 dark:group-aria-selected:bg-white/10 transition-colors">
                                                    <item.icon className="w-4 h-4 text-neutral-500 dark:text-neutral-400 group-aria-selected:text-neutral-900 dark:group-aria-selected:text-white" />
                                                </div>
                                                <span className="font-medium">{item.label}</span>
                                            </div>
                                            {item.shortcut && (
                                                <span className="text-[10px] font-mono text-neutral-400 bg-neutral-100 dark:bg-white/5 px-1.5 py-0.5 rounded border border-neutral-200 dark:border-white/5 group-aria-selected:border-neutral-300 dark:group-aria-selected:border-white/10 transition-colors">
                                                    {item.shortcut}
                                                </span>
                                            )}
                                        </Command.Item>
                                    ))}
                                </Command.Group>
                            ))}
                        </Command.List>
                    )}

                    {mode === 'ai' && (
                        <div className="p-6 h-full flex flex-col">
                            {search.length === 0 ? (
                                <div className="flex-1 flex flex-col items-center justify-center text-center text-neutral-500">
                                    <div className="w-12 h-12 rounded-xl bg-black/5 dark:bg-white/5 flex items-center justify-center mb-4 border border-black/5 dark:border-white/5">
                                        <Wand2 className="w-5 h-5 text-neutral-700 dark:text-neutral-300" />
                                    </div>
                                    <h3 className="text-sm font-semibold text-neutral-900 dark:text-white mb-1">AI Assistant Ready</h3>
                                    <p className="text-xs max-w-[250px] text-neutral-400">Ask questions about your codebase, documentation, or generate code snippets.</p>
                                    
                                    <div className="mt-8 flex flex-wrap justify-center gap-2">
                                        {["How do I install this?", "Change theme colors", "Add new event type"].map((q) => (
                                            <button 
                                                key={q}
                                                onClick={() => setSearch(q)}
                                                className="px-3 py-1.5 rounded-full border border-neutral-200 dark:border-white/10 text-xs text-neutral-600 dark:text-neutral-400 hover:border-neutral-400 dark:hover:border-white/30 hover:text-neutral-900 dark:hover:text-white transition-all bg-white dark:bg-white/5 shadow-sm"
                                            >
                                                {q}
                                            </button>
                                        ))}
                                    </div>
                                </div>
                            ) : (
                                <div className="flex-1 overflow-y-auto custom-scrollbar">
                                    <div className="flex gap-4 mb-6">
                                        <div className="w-8 h-8 rounded-full bg-neutral-100 dark:bg-white/10 border border-neutral-200 dark:border-white/5 flex-shrink-0 flex items-center justify-center overflow-hidden">
                                            <img src={USER_IMAGE} alt="User" className="w-full h-full object-cover" />
                                        </div>
                                        <div className="flex-1 pt-1.5">
                                            <p className="text-sm text-neutral-800 dark:text-neutral-200 font-medium">{search}</p>
                                        </div>
                                    </div>
                                    
                                    {(aiResponse || isTyping) && (
                                        <div className="flex gap-4">
                                            <div className="w-8 h-8 rounded-full bg-black dark:bg-white flex-shrink-0 flex items-center justify-center shadow-lg shadow-black/10">
                                                <Sparkles className="w-4 h-4 text-white dark:text-black" />
                                            </div>
                                            <div className="flex-1 pt-1.5">
                                                <div className="text-sm text-neutral-600 dark:text-neutral-300 leading-relaxed">
                                                    {aiResponse}
                                                    {isTyping && (
                                                        <span className="inline-block w-1.5 h-4 ml-1 bg-neutral-400 animate-pulse align-middle"></span>
                                                    )}
                                                </div>
                                                
                                                {!isTyping && aiResponse && (
                                                    <motion.div 
                                                        initial={{ opacity: 0, y: 10 }}
                                                        animate={{ opacity: 1, y: 0 }}
                                                        className="mt-4 flex gap-2"
                                                    >
                                                        <button 
                                                            onClick={() => showNotification('Code copied to clipboard!')}
                                                            className="px-3 py-1.5 rounded-lg bg-white dark:bg-white/5 border border-neutral-200 dark:border-white/10 text-xs font-medium hover:bg-neutral-50 dark:hover:bg-white/10 transition-colors shadow-sm text-neutral-700 dark:text-neutral-300"
                                                        >
                                                            Copy Code
                                                        </button>
                                                        <button 
                                                            onClick={() => showNotification('Changes applied successfully!')}
                                                            className="px-3 py-1.5 rounded-lg bg-black dark:bg-white text-white dark:text-black text-xs font-medium hover:opacity-90 transition-opacity shadow-sm"
                                                        >
                                                            Apply Changes
                                                        </button>
                                                    </motion.div>
                                                )}
                                            </div>
                                        </div>
                                    )}
                                </div>
                            )}
                        </div>
                    )}
                </div>

                <div className="border-t border-neutral-100 dark:border-white/5 p-2.5 flex justify-between items-center bg-white dark:bg-neutral-900 text-[10px] text-neutral-400 font-medium">
                    <div className="flex gap-3">
                        <span className="flex items-center gap-1.5">
                            <kbd className="bg-neutral-100 dark:bg-white/10 px-1.5 py-0.5 rounded border border-neutral-200 dark:border-white/10 font-sans">↑↓</kbd> 
                            Navigate
                        </span>
                        <span className="flex items-center gap-1.5">
                            <kbd className="bg-neutral-100 dark:bg-white/10 px-1.5 py-0.5 rounded border border-neutral-200 dark:border-white/10 font-sans"></kbd> 
                            Select
                        </span>
                        {mode === 'ai' && (
                             <span className="flex items-center gap-1.5">
                                <kbd className="bg-neutral-100 dark:bg-white/10 px-1.5 py-0.5 rounded border border-neutral-200 dark:border-white/10 font-sans">esc</kbd> 
                                Back
                            </span>
                        )}
                    </div>
                    <div className="flex items-center gap-2 opacity-60">
                        <div className="w-1.5 h-1.5 rounded-full bg-black dark:bg-white"></div>
                        Velocity AI Ready
                    </div>
                </div>
              </Command>
            </motion.div>
          </div>
        )}
      </AnimatePresence>
    </div>
  )
}

Dependencies

  • framer-motion: latest
  • lucide-react: latest
  • cmdk: latest

Props

Component property reference.

NameTypeDefaultDescription
openbooleanfalseWhether the palette is visible.
onOpenChange(open: boolean) => voidundefinedCallback when visibility toggles.
mode'command' | 'ai''command'Initial mode of the palette.
commandsArray<{ heading: string; items: Array<{ id: string; icon: React.ComponentType<any>; label: string; shortcut?: string }> }>Built-in examplesCommand groups to render.
classNamestringundefinedAdditional CSS classes.
Context Worth Keeping In Orbit

Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.

I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.

I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.