
Co-authored-by: GuanMu <ballmanjq@gmail.com> Co-authored-by: zhujiruo <zhujiruo@foxmail.com> Co-authored-by: Matri Qi <matrixdom@126.com> Co-authored-by: croatialu <wuli.croatia@foxmail.com> Co-authored-by: HyaCinth <88471803+HyaCiovo@users.noreply.github.com> Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com>
124 lines
3.8 KiB
TypeScript
124 lines
3.8 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useEffect, useMemo } from 'react'
|
|
import { useNodes } from 'reactflow'
|
|
import { useNodesInteractions } from './use-nodes-interactions'
|
|
import type { CommonNodeType } from '../types'
|
|
import { workflowNodesAction } from '@/app/components/goto-anything/actions/workflow-nodes'
|
|
import BlockIcon from '@/app/components/workflow/block-icon'
|
|
import { setupNodeSelectionListener } from '../utils/node-navigation'
|
|
|
|
/**
|
|
* Hook to register workflow nodes search functionality
|
|
*/
|
|
export const useWorkflowSearch = () => {
|
|
const nodes = useNodes()
|
|
const { handleNodeSelect } = useNodesInteractions()
|
|
|
|
// Filter and process nodes for search
|
|
const searchableNodes = useMemo(() => {
|
|
const filteredNodes = nodes.filter((node) => {
|
|
if (!node.id || !node.data || node.type === 'sticky') return false
|
|
|
|
const nodeData = node.data as CommonNodeType
|
|
const nodeType = nodeData?.type
|
|
|
|
const internalStartNodes = ['iteration-start', 'loop-start']
|
|
return !internalStartNodes.includes(nodeType)
|
|
})
|
|
|
|
const result = filteredNodes
|
|
.map((node) => {
|
|
const nodeData = node.data as CommonNodeType
|
|
|
|
return {
|
|
id: node.id,
|
|
title: nodeData?.title || nodeData?.type || 'Untitled',
|
|
type: nodeData?.type || '',
|
|
desc: nodeData?.desc || '',
|
|
blockType: nodeData?.type,
|
|
nodeData,
|
|
}
|
|
})
|
|
|
|
return result
|
|
}, [nodes])
|
|
|
|
// Create search function for workflow nodes
|
|
const searchWorkflowNodes = useCallback((query: string) => {
|
|
if (!searchableNodes.length || !query.trim()) return []
|
|
|
|
const searchTerm = query.toLowerCase()
|
|
|
|
const results = searchableNodes
|
|
.map((node) => {
|
|
const titleMatch = node.title.toLowerCase()
|
|
const typeMatch = node.type.toLowerCase()
|
|
const descMatch = node.desc?.toLowerCase() || ''
|
|
|
|
let score = 0
|
|
|
|
if (titleMatch.startsWith(searchTerm)) score += 100
|
|
else if (titleMatch.includes(searchTerm)) score += 50
|
|
else if (typeMatch === searchTerm) score += 80
|
|
else if (typeMatch.includes(searchTerm)) score += 30
|
|
else if (descMatch.includes(searchTerm)) score += 20
|
|
|
|
return score > 0
|
|
? {
|
|
id: node.id,
|
|
title: node.title,
|
|
description: node.desc || node.type,
|
|
type: 'workflow-node' as const,
|
|
path: `#${node.id}`,
|
|
icon: (
|
|
<BlockIcon
|
|
type={node.blockType}
|
|
className="shrink-0"
|
|
size="sm"
|
|
/>
|
|
),
|
|
metadata: {
|
|
nodeId: node.id,
|
|
nodeData: node.nodeData,
|
|
},
|
|
// Add required data property for SearchResult type
|
|
data: node.nodeData,
|
|
}
|
|
: null
|
|
})
|
|
.filter((node): node is NonNullable<typeof node> => node !== null)
|
|
.sort((a, b) => {
|
|
const aTitle = a.title.toLowerCase()
|
|
const bTitle = b.title.toLowerCase()
|
|
|
|
if (aTitle.startsWith(searchTerm) && !bTitle.startsWith(searchTerm)) return -1
|
|
if (!aTitle.startsWith(searchTerm) && bTitle.startsWith(searchTerm)) return 1
|
|
|
|
return 0
|
|
})
|
|
|
|
return results
|
|
}, [searchableNodes])
|
|
|
|
// Directly set the search function on the action object
|
|
useEffect(() => {
|
|
if (searchableNodes.length > 0) {
|
|
// Set the search function directly on the action
|
|
workflowNodesAction.searchFn = searchWorkflowNodes
|
|
}
|
|
|
|
return () => {
|
|
// Clean up when component unmounts
|
|
workflowNodesAction.searchFn = undefined
|
|
}
|
|
}, [searchableNodes, searchWorkflowNodes])
|
|
|
|
// Set up node selection event listener using the utility function
|
|
useEffect(() => {
|
|
return setupNodeSelectionListener(handleNodeSelect)
|
|
}, [handleNodeSelect])
|
|
|
|
return null
|
|
}
|