import React, { useState, useEffect, useCallback } from 'react'; import { useDebounce } from 'use-debounce'; import { useHotkeys } from 'react-hotkeys-hook'; import { useSearchMessages, type SearchResult } from './hooks/useSearch'; import { Input } from './ui/input'; import { ScrollArea } from './ui/scroll-area'; import { Search, MessageSquare, User, Bot, Settings, ChevronRight, RefreshCw } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Badge } from './ui/badge'; interface GlobalSearchModalProps { isOpen: boolean; onClose: () => void; onNavigateToSession: (sessionId: string, messageIndex?: number) => void; } export default function GlobalSearchModal({ isOpen, onClose, onNavigateToSession, }: GlobalSearchModalProps) { const [searchQuery, setSearchQuery] = useState(''); const [debouncedQuery] = useDebounce(searchQuery, 300); const [selectedIndex, setSelectedIndex] = useState(0); // Use TanStack Query for search with debouncing const { data, isLoading, error } = useSearchMessages(debouncedQuery, undefined, 10, isOpen); const results = data?.results || []; const searchError = error?.message ?? null; // Clamp selectedIndex when results change to prevent out-of-bounds selection useEffect(() => { if (selectedIndex >= results.length && results.length > 0) { setSelectedIndex(results.length - 1); } else if (results.length === 0) { setSelectedIndex(0); } }, [results.length, selectedIndex]); const handleResultClick = useCallback( (result: SearchResult) => { onNavigateToSession(result.sessionId, result.messageIndex); onClose(); }, [onNavigateToSession, onClose] ); // Reset when modal opens/closes useEffect(() => { if (isOpen) { setSearchQuery(''); setSelectedIndex(0); } }, [isOpen]); // Keyboard navigation (using react-hotkeys-hook) // ArrowDown to navigate down in results useHotkeys( 'down', () => { setSelectedIndex((prev) => Math.min(prev + 1, results.length - 1)); }, { enabled: isOpen, preventDefault: true }, [isOpen, results.length] ); // ArrowUp to navigate up in results useHotkeys( 'up', () => { setSelectedIndex((prev) => Math.max(prev - 1, 0)); }, { enabled: isOpen, preventDefault: true }, [isOpen] ); // Enter to select current result useHotkeys( 'enter', () => { if (results[selectedIndex]) { handleResultClick(results[selectedIndex]); setSelectedIndex(0); } }, { enabled: isOpen, preventDefault: true }, [isOpen, results, selectedIndex, handleResultClick] ); // Escape to close modal useHotkeys( 'escape', () => { onClose(); }, { enabled: isOpen, preventDefault: true }, [isOpen, onClose] ); const getRoleIcon = (role: string) => { switch (role) { case 'user': return ; case 'assistant': return ; case 'system': return ; default: return ; } }; const getRoleColor = (role: string) => { switch (role) { case 'user': return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'; case 'assistant': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'; case 'system': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'; default: return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200'; } }; const highlightText = (text: string, query: string) => { if (!query) return text; const escapedQuery = query.replace(/[.*+?^${}()|\[\]\\]/g, '\\$&'); const regex = new RegExp(`(${escapedQuery})`, 'gi'); const parts = text.split(regex); return parts.map((part, index) => part.toLowerCase() === query.toLowerCase() ? ( {part} ) : ( part ) ); }; if (!isOpen) return null; return ( <> {/* Backdrop */}
{/* Search popover */}
{/* Search Header */}
setSearchQuery(e.target.value)} className="pl-12 text-lg border-0 shadow-none focus-visible:ring-0 bg-transparent" autoFocus />
{/* Results */}
{isLoading ? (
Searching...
) : error ? (

Search Error

{searchError}

Try again or check your connection.

) : (
{results.length > 0 ? (
{results.map((result, index) => (
handleResultClick(result)} >
{getRoleIcon(result.message.role)}
{result.sessionId.length > 20 ? `${result.sessionId.slice(0, 20)}...` : result.sessionId} {result.message.role}
{highlightText( result.context, debouncedQuery )}
))}
) : debouncedQuery ? (

No messages found matching your search.

Try different keywords.

) : (

Start typing to search your conversations.

to navigate
Enter to select
Esc to close
)}
)}
); }