import { useState, useRef, useEffect, useCallback } from 'react' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import type { SearchMode, SearchResult, VaultEntry } from '../types' import { invoke } from '@tauri-apps/api/core' import { isTauri, mockInvoke } from '../mock-tauri' interface SearchResultData { title: string path: string snippet: string score: number note_type: string | null } interface SearchResponseData { results: SearchResultData[] elapsed_ms: number } function searchCall(args: Record): Promise { return isTauri() ? invoke('search_vault', args) : mockInvoke('search_vault', args) } interface SearchPanelProps { open: boolean vaultPath: string entries: VaultEntry[] onSelectNote: (entry: VaultEntry) => void onClose: () => void } export function SearchPanel({ open, vaultPath, entries, onSelectNote, onClose }: SearchPanelProps) { const [query, setQuery] = useState('') const [mode, setMode] = useState('keyword') const [results, setResults] = useState([]) const [selectedIndex, setSelectedIndex] = useState(0) const [loading, setLoading] = useState(false) const [elapsedMs, setElapsedMs] = useState(null) const [searchError, setSearchError] = useState(null) const inputRef = useRef(null) const listRef = useRef(null) const debounceRef = useRef | null>(null) useEffect(() => { if (open) { setQuery('') setResults([]) setSelectedIndex(0) setElapsedMs(null) setSearchError(null) setTimeout(() => inputRef.current?.focus(), 50) } }, [open]) const performSearch = useCallback(async (q: string, m: SearchMode) => { if (!q.trim()) { setResults([]) setElapsedMs(null) setSearchError(null) return } setLoading(true) setSearchError(null) try { const response = await searchCall({ vaultPath, query: q, mode: m, limit: 20, }) const mapped = response.results.map((r: SearchResultData) => ({ title: r.title, path: r.path, snippet: r.snippet, score: r.score, noteType: r.note_type, })) setResults(mapped) setElapsedMs(response.elapsed_ms) setSelectedIndex(0) } catch (err) { setSearchError(String(err)) setResults([]) } finally { setLoading(false) } }, [vaultPath]) useEffect(() => { if (debounceRef.current) clearTimeout(debounceRef.current) if (!query.trim()) { setResults([]) setElapsedMs(null) return } debounceRef.current = setTimeout(() => { performSearch(query, mode) }, 200) return () => { if (debounceRef.current) clearTimeout(debounceRef.current) } }, [query, mode, performSearch]) useEffect(() => { if (!listRef.current) return const selected = listRef.current.children[selectedIndex] as HTMLElement | undefined selected?.scrollIntoView({ block: 'nearest' }) }, [selectedIndex]) const handleSelect = useCallback((result: SearchResult) => { const entry = entries.find(e => e.path === result.path) if (entry) { onSelectNote(entry) onClose() } }, [entries, onSelectNote, onClose]) useEffect(() => { if (!open) return const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault() onClose() } else if (e.key === 'ArrowDown') { e.preventDefault() setSelectedIndex(i => Math.min(i + 1, results.length - 1)) } else if (e.key === 'ArrowUp') { e.preventDefault() setSelectedIndex(i => Math.max(i - 1, 0)) } else if (e.key === 'Enter') { e.preventDefault() if (results[selectedIndex]) { handleSelect(results[selectedIndex]) } } else if (e.key === 'Tab') { e.preventDefault() setMode(m => m === 'keyword' ? 'semantic' : 'keyword') } } window.addEventListener('keydown', handleKey) return () => window.removeEventListener('keydown', handleKey) }, [open, results, selectedIndex, handleSelect, onClose]) if (!open) return null const entryTypeMap = new Map(entries.map(e => [e.path, e.isA])) return (
e.stopPropagation()} > {/* Search input row */}
setQuery(e.target.value)} />
{/* Content area */}
{loading && (
Searching...
)} {!loading && !query.trim() && (

Search across all note contents

Tab to toggle keyword/semantic · Enter to open · Esc to close

)} {!loading && query.trim() && results.length === 0 && !searchError && (

No results found

Try different keywords or switch to semantic search

)} {searchError && (

Search error

{searchError}

)} {!loading && results.length > 0 && ( <>
{results.length} result{results.length !== 1 ? 's' : ''}{elapsedMs !== null ? ` · ${elapsedMs}ms` : ''}
{results.map((result, i) => { const noteType = entryTypeMap.get(result.path) ?? result.noteType return (
handleSelect(result)} onMouseEnter={() => setSelectedIndex(i)} >
{result.title} {noteType && ( {noteType} )}
{result.snippet && (

{result.snippet}

)}
) })}
)}
) }