You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

use-workflow-search.tsx 5.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. 'use client'
  2. import { useCallback, useEffect, useMemo } from 'react'
  3. import { useNodes } from 'reactflow'
  4. import { useNodesInteractions } from './use-nodes-interactions'
  5. import type { CommonNodeType } from '../types'
  6. import { workflowNodesAction } from '@/app/components/goto-anything/actions/workflow-nodes'
  7. import BlockIcon from '@/app/components/workflow/block-icon'
  8. import { setupNodeSelectionListener } from '../utils/node-navigation'
  9. import { BlockEnum } from '../types'
  10. import { useStore } from '../store'
  11. import type { Emoji } from '@/app/components/tools/types'
  12. import { CollectionType } from '@/app/components/tools/types'
  13. import { canFindTool } from '@/utils'
  14. /**
  15. * Hook to register workflow nodes search functionality
  16. */
  17. export const useWorkflowSearch = () => {
  18. const nodes = useNodes()
  19. const { handleNodeSelect } = useNodesInteractions()
  20. // Filter and process nodes for search
  21. const buildInTools = useStore(s => s.buildInTools)
  22. const customTools = useStore(s => s.customTools)
  23. const workflowTools = useStore(s => s.workflowTools)
  24. const mcpTools = useStore(s => s.mcpTools)
  25. const searchableNodes = useMemo(() => {
  26. const filteredNodes = nodes.filter((node) => {
  27. if (!node.id || !node.data || node.type === 'sticky') return false
  28. const nodeData = node.data as CommonNodeType
  29. const nodeType = nodeData?.type
  30. const internalStartNodes = ['iteration-start', 'loop-start']
  31. return !internalStartNodes.includes(nodeType)
  32. })
  33. const result = filteredNodes
  34. .map((node) => {
  35. const nodeData = node.data as CommonNodeType
  36. // compute tool icon if node is a Tool
  37. let toolIcon: string | Emoji | undefined
  38. if (nodeData?.type === BlockEnum.Tool) {
  39. let targetTools = workflowTools
  40. if (nodeData.provider_type === CollectionType.builtIn)
  41. targetTools = buildInTools
  42. else if (nodeData.provider_type === CollectionType.custom)
  43. targetTools = customTools
  44. else if (nodeData.provider_type === CollectionType.mcp)
  45. targetTools = mcpTools
  46. toolIcon = targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, nodeData.provider_id))?.icon
  47. }
  48. return {
  49. id: node.id,
  50. title: nodeData?.title || nodeData?.type || 'Untitled',
  51. type: nodeData?.type || '',
  52. desc: nodeData?.desc || '',
  53. blockType: nodeData?.type,
  54. nodeData,
  55. toolIcon,
  56. }
  57. })
  58. return result
  59. }, [nodes, buildInTools, customTools, workflowTools, mcpTools])
  60. // Create search function for workflow nodes
  61. const searchWorkflowNodes = useCallback((query: string) => {
  62. if (!searchableNodes.length) return []
  63. const searchTerm = query.toLowerCase().trim()
  64. const results = searchableNodes
  65. .map((node) => {
  66. const titleMatch = node.title.toLowerCase()
  67. const typeMatch = node.type.toLowerCase()
  68. const descMatch = node.desc?.toLowerCase() || ''
  69. let score = 0
  70. // If no search term, show all nodes with base score
  71. if (!searchTerm) {
  72. score = 1
  73. }
  74. else {
  75. // Score based on search relevance
  76. if (titleMatch.startsWith(searchTerm)) score += 100
  77. else if (titleMatch.includes(searchTerm)) score += 50
  78. else if (typeMatch === searchTerm) score += 80
  79. else if (typeMatch.includes(searchTerm)) score += 30
  80. else if (descMatch.includes(searchTerm)) score += 20
  81. }
  82. return score > 0
  83. ? {
  84. id: node.id,
  85. title: node.title,
  86. description: node.desc || node.type,
  87. type: 'workflow-node' as const,
  88. path: `#${node.id}`,
  89. icon: (
  90. <BlockIcon
  91. type={node.blockType}
  92. className="shrink-0"
  93. size="sm"
  94. toolIcon={node.toolIcon}
  95. />
  96. ),
  97. metadata: {
  98. nodeId: node.id,
  99. nodeData: node.nodeData,
  100. },
  101. // Add required data property for SearchResult type
  102. data: node.nodeData,
  103. }
  104. : null
  105. })
  106. .filter((node): node is NonNullable<typeof node> => node !== null)
  107. .sort((a, b) => {
  108. // If no search term, sort alphabetically
  109. if (!searchTerm)
  110. return a.title.localeCompare(b.title)
  111. // Sort by relevance when searching
  112. const aTitle = a.title.toLowerCase()
  113. const bTitle = b.title.toLowerCase()
  114. if (aTitle.startsWith(searchTerm) && !bTitle.startsWith(searchTerm)) return -1
  115. if (!aTitle.startsWith(searchTerm) && bTitle.startsWith(searchTerm)) return 1
  116. return 0
  117. })
  118. return results
  119. }, [searchableNodes])
  120. // Directly set the search function on the action object
  121. useEffect(() => {
  122. if (searchableNodes.length > 0) {
  123. // Set the search function directly on the action
  124. workflowNodesAction.searchFn = searchWorkflowNodes
  125. }
  126. return () => {
  127. // Clean up when component unmounts
  128. workflowNodesAction.searchFn = undefined
  129. }
  130. }, [searchableNodes, searchWorkflowNodes])
  131. // Set up node selection event listener using the utility function
  132. useEffect(() => {
  133. return setupNodeSelectionListener(handleNodeSelect)
  134. }, [handleNodeSelect])
  135. return null
  136. }