import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Search, FileText, CheckSquare, ShoppingCart, X, Loader2, ArrowRight, CornerDownLeft, } from 'lucide-react'; import { SearchHit } from '../lib/types'; import { api } from '../lib/api'; interface CommandPaletteProps { isOpen: boolean; onClose: () => void; onOpenDocument: (docId: string, blockId?: string) => void; onOpenTask: (taskId: string) => void; onOpenList: (listId: string) => void; } // Safely render snippet containing ... without dangerouslySetInnerHTML const SnippetRenderer: React.FC<{ snippet: string }> = ({ snippet }) => { const parts = useMemo(() => { if (!snippet) return []; return snippet.split(/([\s\S]*?<\/mark>)/g); }, [snippet]); return ( {parts.map((part, i) => { if (part.startsWith('') && part.endsWith('')) { const text = part.slice(6, -7); return ( {text} ); } return {part}; })} ); }; export const CommandPalette: React.FC = ({ isOpen, onClose, onOpenDocument, onOpenTask, onOpenList, }) => { const [query, setQuery] = useState(''); const [hits, setHits] = useState([]); const [selectedIndex, setSelectedIndex] = useState(0); const [loading, setLoading] = useState(false); const inputRef = useRef(null); const debounceTimerRef = useRef(null); // Group hits by category: Notizen, Aufgaben, Listen const groupedHits = useMemo(() => { const docs: SearchHit[] = []; const tasks: SearchHit[] = []; const lists: SearchHit[] = []; for (const h of hits) { if (h.type === 'document') { docs.push(h); } else if (h.type === 'thought') { tasks.push(h); } else if (h.type === 'list_item') { lists.push(h); } } return [ { key: 'documents', label: 'Notizen', icon: FileText, items: docs }, { key: 'tasks', label: 'Aufgaben', icon: CheckSquare, items: tasks }, { key: 'lists', label: 'Listen', icon: ShoppingCart, items: lists }, ].filter((g) => g.items.length > 0); }, [hits]); // Flattened array for simple up/down index navigation const flatHits = useMemo(() => { return groupedHits.flatMap((g) => g.items); }, [groupedHits]); // Focus input and reset on open useEffect(() => { if (isOpen) { setQuery(''); setHits([]); setSelectedIndex(0); setTimeout(() => inputRef.current?.focus(), 50); } }, [isOpen]); // 150ms debounce search useEffect(() => { if (debounceTimerRef.current !== null) { window.clearTimeout(debounceTimerRef.current); } const trimmed = query.trim(); if (!trimmed) { setHits([]); setLoading(false); setSelectedIndex(0); return; } setLoading(true); debounceTimerRef.current = window.setTimeout(async () => { try { const res = await api.searchAll(trimmed); setHits(res.hits || []); setSelectedIndex(0); } catch { setHits([]); } finally { setLoading(false); } }, 150); return () => { if (debounceTimerRef.current !== null) { window.clearTimeout(debounceTimerRef.current); } }; }, [query]); // Keyboard navigation: ↑, ↓, Enter, Escape useEffect(() => { if (!isOpen) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault(); onClose(); return; } if (flatHits.length === 0) return; if (e.key === 'ArrowDown') { e.preventDefault(); setSelectedIndex((prev) => (prev + 1) % flatHits.length); } else if (e.key === 'ArrowUp') { e.preventDefault(); setSelectedIndex((prev) => (prev - 1 + flatHits.length) % flatHits.length); } else if (e.key === 'Enter') { e.preventDefault(); const selected = flatHits[selectedIndex]; if (selected) { executeSelection(selected); } } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [isOpen, flatHits, selectedIndex]); const executeSelection = (hit: SearchHit) => { onClose(); if (hit.type === 'document') { onOpenDocument(hit.docId || hit.id, hit.blockId); } else if (hit.type === 'thought') { onOpenTask(hit.id); } else if (hit.type === 'list_item') { if (hit.listId) { onOpenList(hit.listId); } } }; if (!isOpen) return null; return (
{/* Search Modal Container */}
e.stopPropagation()} > {/* Top Input Bar */}
setQuery(e.target.value)} placeholder="Globale Suche in Notizen, Aufgaben und Listen…" className="flex-1 bg-transparent border-0 outline-none text-main placeholder-muted text-base sm:text-sm focus:ring-0 p-0" /> {loading && } {query && !loading && ( )}
{/* Results Container */}
{query.trim() === '' ? (

MindSift Globale Suche

Tippe ein Stichwort ein, um alle Inhalte in Sekundenschnelle zu durchsuchen.

) : flatHits.length === 0 && !loading ? (

Keine Treffer gefunden

Keine Notizen, Aufgaben oder Listen-Einträge stimmen mit „{query}“ überein.

) : ( groupedHits.map((group) => { const GroupIcon = group.icon; return (
{group.label}
{group.items.map((item) => { const overallIndex = flatHits.indexOf(item); const isSelected = overallIndex === selectedIndex; return (
executeSelection(item)} onMouseEnter={() => setSelectedIndex(overallIndex)} className={` px-3 py-2.5 rounded-btn flex items-start gap-2.5 transition-colors cursor-pointer border ${ isSelected ? 'bg-panel-elevated border-accent text-main' : 'bg-canvas/50 border-transparent text-main hover:bg-panel-elevated' } `} >
{item.icon || (item.type === 'document' ? '📝' : item.type === 'thought' ? '✅' : '🛒')}
{item.title || 'Unbenannt'} {isSelected && ( Öffnen )}
{item.snippet && (
)}
); })}
); }) )}
{/* Footer Shortcut Bar (Desktop) */}
↑{' '} ↓ Navigieren ↵ Auswählen Esc Schließen
{flatHits.length} Treffer
); };