'use client' import React, { useEffect, useState, useRef } from 'react' import { Editor } from 'slate' import useFlowStore from '@/stores/flow-store' import { ServerIcon, Zap, Database, MessageSquare, Search, ArrowRightLeft, Layers } from 'lucide-react' import { getNameFromNode } from './name' interface NodeSuggestion { id: string type: string label: string } interface MentionSuggestionsProps { editor: Editor search: string onSelect: (suggestion: NodeSuggestion) => void onClose: () => void } const MentionSuggestions: React.FC = ({ editor, search, onSelect, onClose }) => { const { nodes } = useFlowStore() const [selectedIndex, setSelectedIndex] = useState(0) const suggestionsRef = useRef(null) // Filter nodes based on search const suggestions: NodeSuggestion[] = React.useMemo(() => { return nodes .filter(node => { // Show all nodes if search is empty, otherwise filter by name if (!search) return true const name = getNameFromNode(node) if (!name) return false const nameStr = String(name).toLowerCase() const searchLower = search.toLowerCase() return nameStr.includes(searchLower) }) .map(node => ({ id: node.id, type: node.type || 'unknown', label: String(getNameFromNode(node)) })) .slice(0, 10) // Limit to 10 suggestions }, [nodes, search]) // Handle keyboard navigation useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (!suggestions.length) return switch (e.key) { case 'ArrowDown': e.preventDefault() setSelectedIndex(prev => Math.min(prev + 1, suggestions.length - 1)) break case 'ArrowUp': e.preventDefault() setSelectedIndex(prev => Math.max(prev - 1, 0)) break case 'Enter': case 'Tab': e.preventDefault() if (suggestions[selectedIndex]) { onSelect(suggestions[selectedIndex]) } break case 'Escape': e.preventDefault() onClose() break } } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) }, [suggestions, selectedIndex, onSelect, onClose]) // Reset selected index when search changes useEffect(() => { setSelectedIndex(0) }, [search]) // Scroll selected item into view useEffect(() => { if (suggestionsRef.current && suggestions.length > 0) { const selectedElement = suggestionsRef.current.children[selectedIndex] as HTMLElement if (selectedElement) { selectedElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' }) } } }, [selectedIndex, suggestions.length]) // Position the dropdown near the cursor const [position, setPosition] = useState<{ top: number; left: number } | null>(null) useEffect(() => { try { const domSelection = window.getSelection() if (domSelection && domSelection.rangeCount > 0) { const range = domSelection.getRangeAt(0) const rect = range.getBoundingClientRect() const newPosition = { top: rect.bottom + window.scrollY + 4, left: rect.left + window.scrollX } setPosition(newPosition) } } catch (error) { console.warn('Could not position mention suggestions:', error) } }, [search]) if (!suggestions.length) return null // Use fallback position if positioning failed const finalPosition = position || { top: 100, left: 100 } // Get icon and color for node type const getNodeIcon = (type: string) => { switch (type) { case 'service': return { Icon: ServerIcon, color: 'text-pink-600' } case 'event': return { Icon: Zap, color: 'text-orange-600' } case 'data': return { Icon: Database, color: 'text-blue-600' } case 'command': return { Icon: MessageSquare, color: 'text-purple-600' } case 'query': return { Icon: Search, color: 'text-green-600' } case 'domain': return { Icon: Layers, color: 'text-indigo-600' } case 'channel': return { Icon: ArrowRightLeft, color: 'text-yellow-600' } default: return { Icon: ServerIcon, color: 'text-gray-600' } } } return (
{suggestions.map((suggestion, index) => { const { Icon, color } = getNodeIcon(suggestion.type) return ( ) })} {suggestions.length === 0 && (
No nodes found matching "{search}"
)}
) } export default MentionSuggestions