import React, { useRef, useEffect, useState } from 'react'; import { NodeTemplate, NodeTemplates } from '@esengine/behavior-tree'; import { Search, X, LucideIcon, ChevronDown, ChevronRight } from 'lucide-react'; interface QuickCreateMenuProps { visible: boolean; position: { x: number; y: number }; searchText: string; selectedIndex: number; mode: 'create' | 'replace'; iconMap: Record; onSearchChange: (text: string) => void; onIndexChange: (index: number) => void; onNodeSelect: (template: NodeTemplate) => void; onClose: () => void; } interface CategoryGroup { category: string; templates: NodeTemplate[]; isExpanded: boolean; } export const QuickCreateMenu: React.FC = ({ visible, position, searchText, selectedIndex, iconMap, onSearchChange, onIndexChange, onNodeSelect, onClose }) => { const selectedNodeRef = useRef(null); const [expandedCategories, setExpandedCategories] = useState>(new Set()); const [shouldAutoScroll, setShouldAutoScroll] = useState(false); const allTemplates = NodeTemplates.getAllTemplates(); const searchTextLower = searchText.toLowerCase(); const filteredTemplates = searchTextLower ? allTemplates.filter((t: NodeTemplate) => { const className = t.className || ''; return t.displayName.toLowerCase().includes(searchTextLower) || t.description.toLowerCase().includes(searchTextLower) || t.category.toLowerCase().includes(searchTextLower) || className.toLowerCase().includes(searchTextLower); }) : allTemplates; const categoryGroups: CategoryGroup[] = React.useMemo(() => { const groups = new Map(); filteredTemplates.forEach((template: NodeTemplate) => { const category = template.category || '未分类'; if (!groups.has(category)) { groups.set(category, []); } groups.get(category)!.push(template); }); return Array.from(groups.entries()).map(([category, templates]) => ({ category, templates, isExpanded: searchTextLower ? true : expandedCategories.has(category) })).sort((a, b) => a.category.localeCompare(b.category)); }, [filteredTemplates, expandedCategories, searchTextLower]); const flattenedTemplates = React.useMemo(() => { return categoryGroups.flatMap(group => group.isExpanded ? group.templates : [] ); }, [categoryGroups]); const toggleCategory = (category: string) => { setExpandedCategories(prev => { const newSet = new Set(prev); if (newSet.has(category)) { newSet.delete(category); } else { newSet.add(category); } return newSet; }); }; useEffect(() => { if (allTemplates.length > 0 && expandedCategories.size === 0) { const categories = new Set(allTemplates.map(t => t.category || '未分类')); setExpandedCategories(categories); } }, [allTemplates, expandedCategories.size]); useEffect(() => { if (shouldAutoScroll && selectedNodeRef.current) { selectedNodeRef.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); setShouldAutoScroll(false); } }, [selectedIndex, shouldAutoScroll]); if (!visible) return null; let globalIndex = -1; return ( <>
e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} > {/* 搜索框 */}
{ onSearchChange(e.target.value); onIndexChange(0); }} onKeyDown={(e) => { if (e.key === 'Escape') { onClose(); } else if (e.key === 'ArrowDown') { e.preventDefault(); setShouldAutoScroll(true); onIndexChange(Math.min(selectedIndex + 1, flattenedTemplates.length - 1)); } else if (e.key === 'ArrowUp') { e.preventDefault(); setShouldAutoScroll(true); onIndexChange(Math.max(selectedIndex - 1, 0)); } else if (e.key === 'Enter' && flattenedTemplates.length > 0) { e.preventDefault(); const selectedTemplate = flattenedTemplates[selectedIndex]; if (selectedTemplate) { onNodeSelect(selectedTemplate); } } }} style={{ flex: 1, background: 'transparent', border: 'none', outline: 'none', color: '#ccc', fontSize: '14px', padding: '4px' }} />
{/* 节点列表 */}
{categoryGroups.length === 0 ? (
未找到匹配的节点
) : ( categoryGroups.map((group) => { return (
toggleCategory(group.category)} style={{ display: 'flex', alignItems: 'center', gap: '6px', padding: '8px 12px', backgroundColor: '#1e1e1e', borderRadius: '3px', cursor: 'pointer', userSelect: 'none' }} > {group.isExpanded ? ( ) : ( )} {group.category} {group.templates.length}
{group.isExpanded && (
{group.templates.map((template: NodeTemplate) => { globalIndex++; const IconComponent = template.icon ? iconMap[template.icon] : null; const className = template.className || ''; const isSelected = globalIndex === selectedIndex; return (
onNodeSelect(template)} onMouseEnter={() => onIndexChange(globalIndex)} style={{ padding: '8px 12px', marginBottom: '4px', backgroundColor: isSelected ? '#0e639c' : '#1e1e1e', borderLeft: `3px solid ${template.color || '#666'}`, borderRadius: '3px', cursor: 'pointer', transition: 'all 0.15s', transform: isSelected ? 'translateX(2px)' : 'translateX(0)' }} >
{IconComponent && ( )}
{template.displayName}
{className && (
{className}
)}
{template.description}
); })}
)}
); }) )}
); };