diff --git a/frontend/src/components/dashboard/CommandPalette.tsx b/frontend/src/components/dashboard/CommandPalette.tsx new file mode 100644 index 0000000..5ec69da --- /dev/null +++ b/frontend/src/components/dashboard/CommandPalette.tsx @@ -0,0 +1,376 @@ +import { useState, useEffect, useRef, useMemo } from 'react' +import { useNavigate } from 'react-router-dom' +import { useAuth } from '../../contexts/AuthContext' +import { API_URL } from '../../config/api' + +interface CommandPaletteProps { + isOpen: boolean + onClose: () => void +} + +interface Repository { + id: string + name: string + branch: string + status: string +} + +interface CommandItem { + id: string + type: 'repo' | 'action' | 'navigation' + title: string + subtitle?: string + icon: string + shortcut?: string + action: () => void +} + +export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) { + const [query, setQuery] = useState('') + const [selectedIndex, setSelectedIndex] = useState(0) + const [repos, setRepos] = useState([]) + const inputRef = useRef(null) + const navigate = useNavigate() + const { session, signOut } = useAuth() + + // Fetch repos for search + useEffect(() => { + if (isOpen && session?.access_token) { + fetchRepos() + } + }, [isOpen, session]) + + // Focus input when opened + useEffect(() => { + if (isOpen) { + setQuery('') + setSelectedIndex(0) + setTimeout(() => inputRef.current?.focus(), 10) + } + }, [isOpen]) + + // Handle keyboard navigation + useEffect(() => { + if (!isOpen) return + + const handleKeyDown = (e: KeyboardEvent) => { + switch (e.key) { + case 'ArrowDown': + e.preventDefault() + setSelectedIndex(i => Math.min(i + 1, filteredItems.length - 1)) + break + case 'ArrowUp': + e.preventDefault() + setSelectedIndex(i => Math.max(i - 1, 0)) + break + case 'Enter': + e.preventDefault() + if (filteredItems[selectedIndex]) { + filteredItems[selectedIndex].action() + onClose() + } + break + case 'Escape': + e.preventDefault() + onClose() + break + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [isOpen, selectedIndex, query]) + + const fetchRepos = async () => { + try { + const response = await fetch(`${API_URL}/api/repos`, { + headers: { 'Authorization': `Bearer ${session?.access_token}` } + }) + const data = await response.json() + setRepos(data.repositories || []) + } catch (error) { + console.error('Error fetching repos:', error) + } + } + + // Build command items + const allItems: CommandItem[] = useMemo(() => { + const items: CommandItem[] = [] + + // Repositories + repos.forEach(repo => { + items.push({ + id: `repo-${repo.id}`, + type: 'repo', + title: repo.name, + subtitle: `${repo.branch} • ${repo.status}`, + icon: '📦', + action: () => navigate(`/dashboard/repo/${repo.id}`) + }) + }) + + // Actions + items.push({ + id: 'action-add-repo', + type: 'action', + title: 'Add Repository', + subtitle: 'Clone and index a new repository', + icon: '➕', + action: () => { + // Trigger add repo modal - dispatch custom event + window.dispatchEvent(new CustomEvent('openAddRepo')) + navigate('/dashboard') + } + }) + + items.push({ + id: 'action-refresh', + type: 'action', + title: 'Refresh Repositories', + subtitle: 'Reload the repository list', + icon: '🔄', + action: () => { + window.location.reload() + } + }) + + // Navigation + items.push({ + id: 'nav-dashboard', + type: 'navigation', + title: 'Go to Dashboard', + subtitle: 'View all repositories', + icon: '🏠', + action: () => navigate('/dashboard') + }) + + items.push({ + id: 'nav-settings', + type: 'navigation', + title: 'Settings', + subtitle: 'Account and preferences', + icon: '⚙️', + action: () => navigate('/dashboard/settings') + }) + + items.push({ + id: 'nav-docs', + type: 'navigation', + title: 'Documentation', + subtitle: 'Learn how to use CodeIntel', + icon: '📚', + action: () => window.open('/docs', '_blank') + }) + + items.push({ + id: 'action-signout', + type: 'action', + title: 'Sign Out', + subtitle: 'Log out of your account', + icon: '🚪', + action: () => signOut() + }) + + return items + }, [repos, navigate, signOut]) + + // Filter items based on query + const filteredItems = useMemo(() => { + if (!query.trim()) return allItems + + const lowerQuery = query.toLowerCase() + return allItems.filter(item => + item.title.toLowerCase().includes(lowerQuery) || + item.subtitle?.toLowerCase().includes(lowerQuery) + ) + }, [allItems, query]) + + // Reset selection when filter changes + useEffect(() => { + setSelectedIndex(0) + }, [query]) + + // Group items by type + const groupedItems = useMemo(() => { + const groups: { [key: string]: CommandItem[] } = { + repo: [], + action: [], + navigation: [] + } + + filteredItems.forEach(item => { + groups[item.type].push(item) + }) + + return groups + }, [filteredItems]) + + if (!isOpen) return null + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Search Input */} +
+ + + + setQuery(e.target.value)} + placeholder="Search commands, repos, actions..." + className="flex-1 bg-transparent text-white placeholder:text-gray-500 outline-none text-base" + /> + + ESC + +
+ + {/* Results */} +
+ {filteredItems.length === 0 ? ( +
+ No results found for "{query}" +
+ ) : ( + <> + {/* Repositories */} + {groupedItems.repo.length > 0 && ( +
+
+ Repositories +
+ {groupedItems.repo.map((item, idx) => { + const globalIndex = filteredItems.indexOf(item) + return ( + { + item.action() + onClose() + }} + onMouseEnter={() => setSelectedIndex(globalIndex)} + /> + ) + })} +
+ )} + + {/* Actions */} + {groupedItems.action.length > 0 && ( +
+
+ Actions +
+ {groupedItems.action.map((item) => { + const globalIndex = filteredItems.indexOf(item) + return ( + { + item.action() + onClose() + }} + onMouseEnter={() => setSelectedIndex(globalIndex)} + /> + ) + })} +
+ )} + + {/* Navigation */} + {groupedItems.navigation.length > 0 && ( +
+
+ Navigation +
+ {groupedItems.navigation.map((item) => { + const globalIndex = filteredItems.indexOf(item) + return ( + { + item.action() + onClose() + }} + onMouseEnter={() => setSelectedIndex(globalIndex)} + /> + ) + })} +
+ )} + + )} +
+ + {/* Footer */} +
+
+ + ↑↓ + navigate + + + + select + +
+ CodeIntel +
+
+
+ ) +} + +// Individual command item component +function CommandItem({ + item, + isSelected, + onClick, + onMouseEnter +}: { + item: CommandItem + isSelected: boolean + onClick: () => void + onMouseEnter: () => void +}) { + return ( + + ) +} diff --git a/frontend/src/components/dashboard/DashboardLayout.tsx b/frontend/src/components/dashboard/DashboardLayout.tsx index d272b12..4257971 100644 --- a/frontend/src/components/dashboard/DashboardLayout.tsx +++ b/frontend/src/components/dashboard/DashboardLayout.tsx @@ -2,7 +2,9 @@ import { useState } from 'react' import { Outlet } from 'react-router-dom' import { Sidebar } from './Sidebar' import { TopNav } from './TopNav' +import { CommandPalette } from './CommandPalette' import { Toaster } from '@/components/ui/sonner' +import { useKeyboardShortcut, SHORTCUTS } from '../../hooks/useKeyboardShortcut' interface DashboardLayoutProps { children?: React.ReactNode @@ -10,6 +12,16 @@ interface DashboardLayoutProps { export function DashboardLayout({ children }: DashboardLayoutProps) { const [sidebarCollapsed, setSidebarCollapsed] = useState(false) + const [commandPaletteOpen, setCommandPaletteOpen] = useState(false) + + // Keyboard shortcuts + useKeyboardShortcut(SHORTCUTS.COMMAND_PALETTE, () => { + setCommandPaletteOpen(true) + }) + + useKeyboardShortcut(SHORTCUTS.TOGGLE_SIDEBAR, () => { + setSidebarCollapsed(prev => !prev) + }) return (
@@ -17,6 +29,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) { setSidebarCollapsed(!sidebarCollapsed)} sidebarCollapsed={sidebarCollapsed} + onOpenCommandPalette={() => setCommandPaletteOpen(true)} />
@@ -38,6 +51,12 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
+ {/* Command Palette */} + setCommandPaletteOpen(false)} + /> + void sidebarCollapsed: boolean + onOpenCommandPalette?: () => void } // Icons @@ -32,7 +33,7 @@ const CodeIntelLogo = () => (
) -export function TopNav({ onToggleSidebar, sidebarCollapsed }: TopNavProps) { +export function TopNav({ onToggleSidebar, sidebarCollapsed, onOpenCommandPalette }: TopNavProps) { const { session, signOut } = useAuth() const [showUserMenu, setShowUserMenu] = useState(false) @@ -61,11 +62,7 @@ export function TopNav({ onToggleSidebar, sidebarCollapsed }: TopNavProps) { {/* Center - Command Palette Trigger */}