'use client' import { useEffect, useState, useCallback, useMemo } from 'react' import { useRouter } from 'next/navigation' import { useCreateDialogs } from '@/lib/hooks/use-create-dialogs' import { useNotebooks } from '@/lib/hooks/use-notebooks' import { useTheme } from '@/lib/stores/theme-store' import { CommandDialog, CommandInput, CommandList, CommandGroup, CommandItem, CommandSeparator, } from '@/components/ui/command' import { Book, Search, Mic, Bot, Shuffle, Settings, FileText, Wrench, MessageCircleQuestion, Plus, Sun, Moon, Monitor, Loader2, } from 'lucide-react' const navigationItems = [ { name: 'Sources', href: '/sources', icon: FileText, keywords: ['files', 'documents', 'upload'] }, { name: 'Notebooks', href: '/notebooks', icon: Book, keywords: ['notes', 'research', 'projects'] }, { name: 'Ask and Search', href: '/search', icon: Search, keywords: ['find', 'query'] }, { name: 'Podcasts', href: '/podcasts', icon: Mic, keywords: ['audio', 'episodes', 'generate'] }, { name: 'Models', href: '/models', icon: Bot, keywords: ['ai', 'llm', 'providers', 'openai', 'anthropic'] }, { name: 'Transformations', href: '/transformations', icon: Shuffle, keywords: ['prompts', 'templates', 'actions'] }, { name: 'Settings', href: '/settings', icon: Settings, keywords: ['preferences', 'config', 'options'] }, { name: 'Advanced', href: '/advanced', icon: Wrench, keywords: ['debug', 'system', 'tools'] }, ] const createItems = [ { name: 'Create Source', action: 'source', icon: FileText }, { name: 'Create Notebook', action: 'notebook', icon: Book }, { name: 'Create Podcast', action: 'podcast', icon: Mic }, ] const themeItems = [ { name: 'Light Theme', value: 'light' as const, icon: Sun, keywords: ['bright', 'day'] }, { name: 'Dark Theme', value: 'dark' as const, icon: Moon, keywords: ['night'] }, { name: 'System Theme', value: 'system' as const, icon: Monitor, keywords: ['auto', 'default'] }, ] export function CommandPalette() { const [open, setOpen] = useState(false) const [query, setQuery] = useState('') const router = useRouter() const { openSourceDialog, openNotebookDialog, openPodcastDialog } = useCreateDialogs() const { setTheme } = useTheme() const { data: notebooks, isLoading: notebooksLoading } = useNotebooks(false) // Global keyboard listener for ⌘K / Ctrl+K useEffect(() => { const down = (e: KeyboardEvent) => { // Skip if focus is inside editable elements const target = e.target as HTMLElement | null if ( target && (target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName)) ) { return } if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { e.preventDefault() e.stopPropagation() setOpen((open) => !open) } } // Use capture phase to intercept before other handlers document.addEventListener('keydown', down, true) return () => document.removeEventListener('keydown', down, true) }, []) // Reset query when dialog closes useEffect(() => { if (!open) { setQuery('') } }, [open]) const handleSelect = useCallback((callback: () => void) => { setOpen(false) setQuery('') // Use setTimeout to ensure dialog closes before action setTimeout(callback, 0) }, []) const handleNavigate = useCallback((href: string) => { handleSelect(() => router.push(href)) }, [handleSelect, router]) const handleSearch = useCallback(() => { if (!query.trim()) return handleSelect(() => router.push(`/search?q=${encodeURIComponent(query)}&mode=search`)) }, [handleSelect, router, query]) const handleAsk = useCallback(() => { if (!query.trim()) return handleSelect(() => router.push(`/search?q=${encodeURIComponent(query)}&mode=ask`)) }, [handleSelect, router, query]) const handleCreate = useCallback((action: string) => { handleSelect(() => { if (action === 'source') openSourceDialog() else if (action === 'notebook') openNotebookDialog() else if (action === 'podcast') openPodcastDialog() }) }, [handleSelect, openSourceDialog, openNotebookDialog, openPodcastDialog]) const handleTheme = useCallback((theme: 'light' | 'dark' | 'system') => { handleSelect(() => setTheme(theme)) }, [handleSelect, setTheme]) // Check if query matches any command (navigation, create, theme, or notebook) const queryLower = query.toLowerCase().trim() const hasCommandMatch = useMemo(() => { if (!queryLower) return false return ( navigationItems.some(item => item.name.toLowerCase().includes(queryLower) || item.keywords.some(k => k.includes(queryLower)) ) || createItems.some(item => item.name.toLowerCase().includes(queryLower) ) || themeItems.some(item => item.name.toLowerCase().includes(queryLower) || item.keywords.some(k => k.includes(queryLower)) ) || (notebooks?.some(nb => nb.name.toLowerCase().includes(queryLower) || (nb.description && nb.description.toLowerCase().includes(queryLower)) ) ?? false) ) }, [queryLower, notebooks]) // Determine if we should show the Search/Ask section at the top const showSearchFirst = query.trim() && !hasCommandMatch return ( {/* Search/Ask - show FIRST when there's a query with no command match */} {showSearchFirst && ( Search for “{query}” Ask about “{query}” )} {/* Navigation */} {navigationItems.map((item) => ( handleNavigate(item.href)} > {item.name} ))} {/* Notebooks */} {notebooksLoading ? ( Loading notebooks... ) : notebooks && notebooks.length > 0 ? ( notebooks.map((notebook) => ( handleNavigate(`/notebooks/${notebook.id}`)} > {notebook.name} )) ) : null} {/* Create */} {createItems.map((item) => ( handleCreate(item.action)} > {item.name} ))} {/* Theme */} {themeItems.map((item) => ( handleTheme(item.value)} > {item.name} ))} {/* Search/Ask - show at bottom when there IS a command match */} {query.trim() && hasCommandMatch && ( <> Search for “{query}” Ask about “{query}” )} ) }