mirror of
https://github.com/langgenius/dify.git
synced 2026-02-18 13:01:00 -05:00
perf: reduce input lag in variable pickers
This commit is contained in:
@@ -84,7 +84,9 @@ const AgentNodeList: FC<Props> = ({
|
||||
}) => {
|
||||
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<Props> = ({
|
||||
}
|
||||
|
||||
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<Array<HTMLButtonElement | null>>([])
|
||||
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<Props> = ({
|
||||
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<Props> = ({
|
||||
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}
|
||||
|
||||
@@ -315,7 +315,9 @@ const VarReferenceVars: FC<Props> = ({
|
||||
}) => {
|
||||
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<Props> = ({
|
||||
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<Props> = ({
|
||||
}, [filteredVars])
|
||||
const [activeIndex, setActiveIndex] = useState(-1)
|
||||
const itemRefs = useRef<Array<HTMLDivElement | null>>([])
|
||||
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<Props> = ({
|
||||
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<Props> = ({
|
||||
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}
|
||||
|
||||
Reference in New Issue
Block a user