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 b01fab0aa2..3bf0fab984 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,37 +97,65 @@ 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) + const resolvedActiveIndex = useMemo(() => { + if (!enableKeyboardNavigation || filteredNodes.length === 0) + return -1 + if (activeIndex < 0 || activeIndex >= filteredNodes.length) + return 0 + return activeIndex + }, [activeIndex, enableKeyboardNavigation, filteredNodes.length]) useEffect(() => { itemRefs.current = [] }, [filteredNodes.length]) useEffect(() => { - if (!enableKeyboardNavigation) { - setActiveIndex(-1) - return - } - if (filteredNodes.length === 0) { - setActiveIndex(-1) - return - } - setActiveIndex(0) - }, [enableKeyboardNavigation, filteredNodes.length, normalizedSearchText]) + filteredNodesRef.current = filteredNodes + }, [filteredNodes]) useEffect(() => { - if (!enableKeyboardNavigation || activeIndex < 0) + activeIndexRef.current = resolvedActiveIndex + }, [resolvedActiveIndex]) + + useEffect(() => { + onCloseRef.current = onClose + }, [onClose]) + + const handleHighlightIndex = useCallback((index: number, source: 'keyboard' | 'mouse' | 'filter') => { + lastInteractionRef.current = source + setActiveIndex(index) + }, []) + + useEffect(() => { + if (!enableKeyboardNavigation || filteredNodes.length === 0) { + lastInteractionRef.current = 'filter' return - const target = itemRefs.current[activeIndex] + } + if (activeIndex < 0 || activeIndex >= filteredNodes.length) + lastInteractionRef.current = 'filter' + }, [activeIndex, enableKeyboardNavigation, filteredNodes.length]) + + useEffect(() => { + if (!enableKeyboardNavigation || resolvedActiveIndex < 0) + return + if (lastInteractionRef.current !== 'keyboard') + return + const target = itemRefs.current[resolvedActiveIndex] if (target) target.scrollIntoView({ block: 'nearest' }) - }, [activeIndex, enableKeyboardNavigation, filteredNodes.length]) + lastInteractionRef.current = null + }, [enableKeyboardNavigation, filteredNodes.length, resolvedActiveIndex]) const handleSelectItem = useCallback((node: AgentNode) => { onSelect(node) @@ -135,34 +165,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 ( <> @@ -197,8 +227,8 @@ const AgentNodeList: FC = ({ key={node.id} node={node} onSelect={onSelect} - isHighlighted={enableKeyboardNavigation && index === activeIndex} - onSetHighlight={enableKeyboardNavigation ? () => setActiveIndex(index) : undefined} + isHighlighted={enableKeyboardNavigation && index === resolvedActiveIndex} + onSetHighlight={enableKeyboardNavigation ? () => handleHighlightIndex(index, 'mouse') : undefined} registerRef={enableKeyboardNavigation ? (element) => { itemRefs.current[index] = element 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 9b257518a0..176ff0a760 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,30 +379,58 @@ 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) + const resolvedActiveIndex = useMemo(() => { + if (!enableKeyboardNavigation || flatItems.length === 0) + return -1 + if (activeIndex < 0 || activeIndex >= flatItems.length) + return 0 + return activeIndex + }, [activeIndex, enableKeyboardNavigation, flatItems.length]) useEffect(() => { itemRefs.current = [] }, [flatItems.length]) useEffect(() => { - if (!enableKeyboardNavigation) { - setActiveIndex(-1) - return - } - if (flatItems.length === 0) { - setActiveIndex(-1) - return - } - setActiveIndex(0) - }, [enableKeyboardNavigation, flatItems.length, normalizedSearchText]) + flatItemsRef.current = flatItems + }, [flatItems]) useEffect(() => { - if (!enableKeyboardNavigation || activeIndex < 0) + activeIndexRef.current = resolvedActiveIndex + }, [resolvedActiveIndex]) + + useEffect(() => { + onCloseRef.current = onClose + }, [onClose]) + + const handleHighlightIndex = useCallback((index: number, source: 'keyboard' | 'mouse' | 'filter') => { + lastInteractionRef.current = source + setActiveIndex(index) + }, []) + + useEffect(() => { + if (!enableKeyboardNavigation || flatItems.length === 0) { + lastInteractionRef.current = 'filter' return - const target = itemRefs.current[activeIndex] + } + if (activeIndex < 0 || activeIndex >= flatItems.length) + lastInteractionRef.current = 'filter' + }, [activeIndex, enableKeyboardNavigation, flatItems.length]) + + useEffect(() => { + if (!enableKeyboardNavigation || resolvedActiveIndex < 0) + return + if (lastInteractionRef.current !== 'keyboard') + return + const target = itemRefs.current[resolvedActiveIndex] if (target) target.scrollIntoView({ block: 'nearest' }) - }, [activeIndex, enableKeyboardNavigation, flatItems.length]) + lastInteractionRef.current = null + }, [enableKeyboardNavigation, flatItems.length, resolvedActiveIndex]) const handleSelectItem = useCallback((item: { node: NodeOutPutVar, itemData: Var }) => { const isStructureOutput = item.itemData.type === VarType.object @@ -415,34 +452,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 @@ -529,8 +566,8 @@ const VarReferenceVars: FC = ({ isInCodeGeneratorInstructionEditor={isInCodeGeneratorInstructionEditor} zIndex={zIndex} preferSchemaType={preferSchemaType} - isHighlighted={enableKeyboardNavigation && itemIndex === activeIndex} - onSetHighlight={enableKeyboardNavigation ? () => setActiveIndex(itemIndex) : undefined} + isHighlighted={enableKeyboardNavigation && itemIndex === resolvedActiveIndex} + onSetHighlight={enableKeyboardNavigation ? () => handleHighlightIndex(itemIndex, 'mouse') : undefined} registerRef={enableKeyboardNavigation ? (element) => { itemRefs.current[itemIndex] = element