diff --git a/web/app/components/workflow/nodes/_base/components/agent-node-list/index.tsx b/web/app/components/workflow/nodes/_base/components/agent-node-list/index.tsx index c21510483f..a2f050bf7d 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-node-list/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-node-list/index.tsx @@ -84,7 +84,9 @@ const AgentNodeList: FC = ({ }) => { const { t } = useTranslation() const [searchText, setSearchText] = useState('') - const normalizedSearchText = externalSearchText === undefined ? searchText : externalSearchText.trim() + const normalizedSearchText = externalSearchText === undefined ? searchText : externalSearchText + const normalizedSearchTextTrimmed = normalizedSearchText.trim() + const normalizedSearchTextLower = normalizedSearchTextTrimmed.toLowerCase() const shouldShowSearchInput = !hideSearch && externalSearchText === undefined const handleKeyDown = (e: React.KeyboardEvent) => { @@ -95,36 +97,63 @@ const AgentNodeList: FC = ({ } const filteredNodes = useMemo(() => nodes.filter((node) => { - if (!normalizedSearchText) + if (!normalizedSearchTextTrimmed) return true - return node.title.toLowerCase().includes(normalizedSearchText.toLowerCase()) - }), [nodes, normalizedSearchText]) + return node.title.toLowerCase().includes(normalizedSearchTextLower) + }), [nodes, normalizedSearchTextLower, normalizedSearchTextTrimmed]) const [activeIndex, setActiveIndex] = useState(-1) const itemRefs = useRef>([]) + const lastInteractionRef = useRef<'keyboard' | 'mouse' | 'filter' | null>(null) + const filteredNodesRef = useRef(filteredNodes) + const activeIndexRef = useRef(activeIndex) + const onCloseRef = useRef(onClose) useEffect(() => { itemRefs.current = [] }, [filteredNodes.length]) + useEffect(() => { + filteredNodesRef.current = filteredNodes + }, [filteredNodes]) + + useEffect(() => { + activeIndexRef.current = activeIndex + }, [activeIndex]) + + useEffect(() => { + onCloseRef.current = onClose + }, [onClose]) + + const handleHighlightIndex = useCallback((index: number, source: 'keyboard' | 'mouse' | 'filter') => { + lastInteractionRef.current = source + setActiveIndex(index) + }, []) + useEffect(() => { if (!enableKeyboardNavigation) { - setActiveIndex(-1) + if (activeIndex !== -1) + setActiveIndex(-1) return } if (filteredNodes.length === 0) { - setActiveIndex(-1) + if (activeIndex !== -1) + setActiveIndex(-1) return } - setActiveIndex(0) - }, [enableKeyboardNavigation, filteredNodes.length, normalizedSearchText]) + if (activeIndex < 0 || activeIndex >= filteredNodes.length) + handleHighlightIndex(0, 'filter') + }, [enableKeyboardNavigation, filteredNodes.length, activeIndex, handleHighlightIndex]) useEffect(() => { if (!enableKeyboardNavigation || activeIndex < 0) return + if (lastInteractionRef.current !== 'keyboard') + return const target = itemRefs.current[activeIndex] if (target) target.scrollIntoView({ block: 'nearest' }) + lastInteractionRef.current = null }, [activeIndex, enableKeyboardNavigation, filteredNodes.length]) const handleSelectItem = useCallback((node: AgentNode) => { @@ -135,34 +164,34 @@ const AgentNodeList: FC = ({ if (!enableKeyboardNavigation) return const handleKeyDown = (event: KeyboardEvent) => { - if (filteredNodes.length === 0) + const nodes = filteredNodesRef.current + if (nodes.length === 0) return if (!['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(event.key)) return event.preventDefault() event.stopPropagation() if (event.key === 'Escape') { - onClose?.() + onCloseRef.current?.() return } if (event.key === 'Enter') { - if (activeIndex < 0 || activeIndex >= filteredNodes.length) + const index = activeIndexRef.current + if (index < 0 || index >= nodes.length) return - handleSelectItem(filteredNodes[activeIndex]) + handleSelectItem(nodes[index]) return } const delta = event.key === 'ArrowDown' ? 1 : -1 - setActiveIndex((prev) => { - const baseIndex = prev < 0 ? 0 : prev - const nextIndex = Math.min(Math.max(baseIndex + delta, 0), filteredNodes.length - 1) - return nextIndex - }) + const baseIndex = activeIndexRef.current < 0 ? 0 : activeIndexRef.current + const nextIndex = Math.min(Math.max(baseIndex + delta, 0), nodes.length - 1) + handleHighlightIndex(nextIndex, 'keyboard') } document.addEventListener('keydown', handleKeyDown, true) return () => { document.removeEventListener('keydown', handleKeyDown, true) } - }, [activeIndex, enableKeyboardNavigation, filteredNodes, handleSelectItem, onClose]) + }, [enableKeyboardNavigation, handleHighlightIndex, handleSelectItem]) return ( <> @@ -198,7 +227,7 @@ const AgentNodeList: FC = ({ node={node} onSelect={onSelect} isHighlighted={enableKeyboardNavigation && index === activeIndex} - onSetHighlight={enableKeyboardNavigation ? () => setActiveIndex(index) : undefined} + onSetHighlight={enableKeyboardNavigation ? () => handleHighlightIndex(index, 'mouse') : undefined} registerRef={enableKeyboardNavigation ? (element) => { itemRefs.current[index] = element } : undefined} diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx index 066ca7c67b..5e6bcc16d2 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx @@ -315,7 +315,9 @@ const VarReferenceVars: FC = ({ }) => { const { t } = useTranslation() const [searchText, setSearchText] = useState('') - const normalizedSearchText = externalSearchText === undefined ? searchText : externalSearchText.trim() + const normalizedSearchText = externalSearchText === undefined ? searchText : externalSearchText + const normalizedSearchTextTrimmed = normalizedSearchText.trim() + const normalizedSearchTextLower = normalizedSearchTextTrimmed.toLowerCase() const shouldShowSearchInput = !hideSearch && externalSearchText === undefined const handleKeyDown = (e: React.KeyboardEvent) => { @@ -332,32 +334,39 @@ const VarReferenceVars: FC = ({ onClose?.() } - const filteredVars = useMemo(() => { - return vars.filter((v) => { - const children = v.vars.filter(v => checkKeys([v.variable], false).isValid || isSpecialVar(v.variable.split('.')[0])) - return children.length > 0 - }).filter((node) => { - if (!normalizedSearchText) - return node - const searchTextLower = normalizedSearchText.toLowerCase() - const children = node.vars.filter((v) => { - return v.variable.toLowerCase().includes(searchTextLower) || node.title.toLowerCase().includes(searchTextLower) - }) - return children.length > 0 - }).map((node) => { - let vars = node.vars.filter(v => checkKeys([v.variable], false).isValid || isSpecialVar(v.variable.split('.')[0])) - if (normalizedSearchText) { - const searchTextLower = normalizedSearchText.toLowerCase() - if (!node.title.toLowerCase().includes(searchTextLower)) - vars = vars.filter(v => v.variable.toLowerCase().includes(searchTextLower)) - } - - return { + const validatedVars = useMemo(() => { + const res: NodeOutPutVar[] = [] + vars.forEach((node) => { + const nodeVars = node.vars.filter(v => checkKeys([v.variable], false).isValid || isSpecialVar(v.variable.split('.')[0])) + if (nodeVars.length === 0) + return + res.push({ ...node, - vars, - } + vars: nodeVars, + }) }) - }, [normalizedSearchText, vars]) + return res + }, [vars]) + + const filteredVars = useMemo(() => { + if (!normalizedSearchTextTrimmed) + return validatedVars + const res: NodeOutPutVar[] = [] + validatedVars.forEach((node) => { + const titleLower = node.title.toLowerCase() + const matchedByTitle = titleLower.includes(normalizedSearchTextLower) + const nodeVars = matchedByTitle + ? node.vars + : node.vars.filter(v => v.variable.toLowerCase().includes(normalizedSearchTextLower)) + if (nodeVars.length === 0) + return + res.push({ + ...node, + vars: nodeVars, + }) + }) + return res + }, [normalizedSearchTextLower, normalizedSearchTextTrimmed, validatedVars]) const flatItems = useMemo(() => { const items: Array<{ node: NodeOutPutVar, itemData: Var }> = [] @@ -370,29 +379,56 @@ const VarReferenceVars: FC = ({ }, [filteredVars]) const [activeIndex, setActiveIndex] = useState(-1) const itemRefs = useRef>([]) + const lastInteractionRef = useRef<'keyboard' | 'mouse' | 'filter' | null>(null) + const flatItemsRef = useRef(flatItems) + const activeIndexRef = useRef(activeIndex) + const onCloseRef = useRef(onClose) useEffect(() => { itemRefs.current = [] }, [flatItems.length]) + useEffect(() => { + flatItemsRef.current = flatItems + }, [flatItems]) + + useEffect(() => { + activeIndexRef.current = activeIndex + }, [activeIndex]) + + useEffect(() => { + onCloseRef.current = onClose + }, [onClose]) + + const handleHighlightIndex = useCallback((index: number, source: 'keyboard' | 'mouse' | 'filter') => { + lastInteractionRef.current = source + setActiveIndex(index) + }, []) + useEffect(() => { if (!enableKeyboardNavigation) { - setActiveIndex(-1) + if (activeIndex !== -1) + setActiveIndex(-1) return } if (flatItems.length === 0) { - setActiveIndex(-1) + if (activeIndex !== -1) + setActiveIndex(-1) return } - setActiveIndex(0) - }, [enableKeyboardNavigation, flatItems.length, normalizedSearchText]) + if (activeIndex < 0 || activeIndex >= flatItems.length) + handleHighlightIndex(0, 'filter') + }, [enableKeyboardNavigation, flatItems.length, activeIndex, handleHighlightIndex]) useEffect(() => { if (!enableKeyboardNavigation || activeIndex < 0) return + if (lastInteractionRef.current !== 'keyboard') + return const target = itemRefs.current[activeIndex] if (target) target.scrollIntoView({ block: 'nearest' }) + lastInteractionRef.current = null }, [activeIndex, enableKeyboardNavigation, flatItems.length]) const handleSelectItem = useCallback((item: { node: NodeOutPutVar, itemData: Var }) => { @@ -415,34 +451,34 @@ const VarReferenceVars: FC = ({ if (!enableKeyboardNavigation) return const handleKeyDown = (event: KeyboardEvent) => { - if (flatItems.length === 0) + const items = flatItemsRef.current + if (items.length === 0) return if (!['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(event.key)) return event.preventDefault() event.stopPropagation() if (event.key === 'Escape') { - onClose?.() + onCloseRef.current?.() return } if (event.key === 'Enter') { - if (activeIndex < 0 || activeIndex >= flatItems.length) + const index = activeIndexRef.current + if (index < 0 || index >= items.length) return - handleSelectItem(flatItems[activeIndex]) + handleSelectItem(items[index]) return } const delta = event.key === 'ArrowDown' ? 1 : -1 - setActiveIndex((prev) => { - const baseIndex = prev < 0 ? 0 : prev - const nextIndex = Math.min(Math.max(baseIndex + delta, 0), flatItems.length - 1) - return nextIndex - }) + const baseIndex = activeIndexRef.current < 0 ? 0 : activeIndexRef.current + const nextIndex = Math.min(Math.max(baseIndex + delta, 0), items.length - 1) + handleHighlightIndex(nextIndex, 'keyboard') } document.addEventListener('keydown', handleKeyDown, true) return () => { document.removeEventListener('keydown', handleKeyDown, true) } - }, [activeIndex, enableKeyboardNavigation, flatItems, handleSelectItem, onClose]) + }, [enableKeyboardNavigation, handleHighlightIndex, handleSelectItem]) let runningIndex = -1 @@ -511,7 +547,7 @@ const VarReferenceVars: FC = ({ zIndex={zIndex} preferSchemaType={preferSchemaType} isHighlighted={enableKeyboardNavigation && itemIndex === activeIndex} - onSetHighlight={enableKeyboardNavigation ? () => setActiveIndex(itemIndex) : undefined} + onSetHighlight={enableKeyboardNavigation ? () => handleHighlightIndex(itemIndex, 'mouse') : undefined} registerRef={enableKeyboardNavigation ? (element) => { itemRefs.current[itemIndex] = element } : undefined}