Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

use-workflow-search.tsx 6.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  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. import type { LLMNodeType } from '../nodes/llm/types'
  15. /**
  16. * Hook to register workflow nodes search functionality
  17. */
  18. export const useWorkflowSearch = () => {
  19. const nodes = useNodes()
  20. const { handleNodeSelect } = useNodesInteractions()
  21. // Filter and process nodes for search
  22. const buildInTools = useStore(s => s.buildInTools)
  23. const customTools = useStore(s => s.customTools)
  24. const workflowTools = useStore(s => s.workflowTools)
  25. const mcpTools = useStore(s => s.mcpTools)
  26. // Extract tool icon logic - clean separation of concerns
  27. const getToolIcon = useCallback((nodeData: CommonNodeType): string | Emoji | undefined => {
  28. if (nodeData?.type !== BlockEnum.Tool) return undefined
  29. const toolCollections: Record<string, any[]> = {
  30. [CollectionType.builtIn]: buildInTools,
  31. [CollectionType.custom]: customTools,
  32. [CollectionType.mcp]: mcpTools,
  33. }
  34. const targetTools = (nodeData.provider_type && toolCollections[nodeData.provider_type]) || workflowTools
  35. return targetTools.find((tool: any) => canFindTool(tool.id, nodeData.provider_id))?.icon
  36. }, [buildInTools, customTools, workflowTools, mcpTools])
  37. // Extract model info logic - clean extraction
  38. const getModelInfo = useCallback((nodeData: CommonNodeType) => {
  39. if (nodeData?.type !== BlockEnum.LLM) return {}
  40. const llmNodeData = nodeData as LLMNodeType
  41. return llmNodeData.model ? {
  42. provider: llmNodeData.model.provider,
  43. name: llmNodeData.model.name,
  44. mode: llmNodeData.model.mode,
  45. } : {}
  46. }, [])
  47. const searchableNodes = useMemo(() => {
  48. const filteredNodes = nodes.filter((node) => {
  49. if (!node.id || !node.data || node.type === 'sticky') return false
  50. const nodeData = node.data as CommonNodeType
  51. const nodeType = nodeData?.type
  52. const internalStartNodes = ['iteration-start', 'loop-start']
  53. return !internalStartNodes.includes(nodeType)
  54. })
  55. return filteredNodes.map((node) => {
  56. const nodeData = node.data as CommonNodeType
  57. return {
  58. id: node.id,
  59. title: nodeData?.title || nodeData?.type || 'Untitled',
  60. type: nodeData?.type || '',
  61. desc: nodeData?.desc || '',
  62. blockType: nodeData?.type,
  63. nodeData,
  64. toolIcon: getToolIcon(nodeData),
  65. modelInfo: getModelInfo(nodeData),
  66. }
  67. })
  68. }, [nodes, getToolIcon, getModelInfo])
  69. // Calculate search score - clean scoring logic
  70. const calculateScore = useCallback((node: {
  71. title: string;
  72. type: string;
  73. desc: string;
  74. modelInfo: { provider?: string; name?: string; mode?: string }
  75. }, searchTerm: string): number => {
  76. if (!searchTerm) return 1
  77. const titleMatch = node.title.toLowerCase()
  78. const typeMatch = node.type.toLowerCase()
  79. const descMatch = node.desc?.toLowerCase() || ''
  80. const modelProviderMatch = node.modelInfo?.provider?.toLowerCase() || ''
  81. const modelNameMatch = node.modelInfo?.name?.toLowerCase() || ''
  82. const modelModeMatch = node.modelInfo?.mode?.toLowerCase() || ''
  83. let score = 0
  84. // Title matching (exact prefix > partial match)
  85. if (titleMatch.startsWith(searchTerm)) score += 100
  86. else if (titleMatch.includes(searchTerm)) score += 50
  87. // Type matching (exact > partial)
  88. if (typeMatch === searchTerm) score += 80
  89. else if (typeMatch.includes(searchTerm)) score += 30
  90. // Description matching (additive)
  91. if (descMatch.includes(searchTerm)) score += 20
  92. // LLM model matching (additive - can combine multiple matches)
  93. if (modelNameMatch && modelNameMatch.includes(searchTerm)) score += 60
  94. if (modelProviderMatch && modelProviderMatch.includes(searchTerm)) score += 40
  95. if (modelModeMatch && modelModeMatch.includes(searchTerm)) score += 30
  96. return score
  97. }, [])
  98. // Create search function for workflow nodes
  99. const searchWorkflowNodes = useCallback((query: string) => {
  100. if (!searchableNodes.length) return []
  101. const searchTerm = query.toLowerCase().trim()
  102. const results = searchableNodes
  103. .map((node) => {
  104. const score = calculateScore(node, searchTerm)
  105. return score > 0 ? {
  106. id: node.id,
  107. title: node.title,
  108. description: node.desc || node.type,
  109. type: 'workflow-node' as const,
  110. path: `#${node.id}`,
  111. icon: (
  112. <BlockIcon
  113. type={node.blockType}
  114. className="shrink-0"
  115. size="sm"
  116. toolIcon={node.toolIcon}
  117. />
  118. ),
  119. metadata: {
  120. nodeId: node.id,
  121. nodeData: node.nodeData,
  122. },
  123. data: node.nodeData,
  124. score,
  125. } : null
  126. })
  127. .filter((node): node is NonNullable<typeof node> => node !== null)
  128. .sort((a, b) => {
  129. // If no search term, sort alphabetically
  130. if (!searchTerm) return a.title.localeCompare(b.title)
  131. // Sort by relevance score (higher score first)
  132. return (b.score || 0) - (a.score || 0)
  133. })
  134. return results
  135. }, [searchableNodes, calculateScore])
  136. // Directly set the search function on the action object
  137. useEffect(() => {
  138. if (searchableNodes.length > 0) {
  139. // Set the search function directly on the action
  140. workflowNodesAction.searchFn = searchWorkflowNodes
  141. }
  142. return () => {
  143. // Clean up when component unmounts
  144. workflowNodesAction.searchFn = undefined
  145. }
  146. }, [searchableNodes, searchWorkflowNodes])
  147. // Set up node selection event listener using the utility function
  148. useEffect(() => {
  149. return setupNodeSelectionListener(handleNodeSelect)
  150. }, [handleNodeSelect])
  151. return null
  152. }