| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- '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'
- import { BlockEnum } from '../types'
- import { useStore } from '../store'
- import type { Emoji } from '@/app/components/tools/types'
- import { CollectionType } from '@/app/components/tools/types'
- import { canFindTool } from '@/utils'
- import type { LLMNodeType } from '../nodes/llm/types'
-
- /**
- * Hook to register workflow nodes search functionality
- */
- export const useWorkflowSearch = () => {
- const nodes = useNodes()
- const { handleNodeSelect } = useNodesInteractions()
-
- // Filter and process nodes for search
- const buildInTools = useStore(s => s.buildInTools)
- const customTools = useStore(s => s.customTools)
- const workflowTools = useStore(s => s.workflowTools)
- const mcpTools = useStore(s => s.mcpTools)
-
- // Extract tool icon logic - clean separation of concerns
- const getToolIcon = useCallback((nodeData: CommonNodeType): string | Emoji | undefined => {
- if (nodeData?.type !== BlockEnum.Tool) return undefined
-
- const toolCollections: Record<string, any[]> = {
- [CollectionType.builtIn]: buildInTools,
- [CollectionType.custom]: customTools,
- [CollectionType.mcp]: mcpTools,
- }
-
- const targetTools = (nodeData.provider_type && toolCollections[nodeData.provider_type]) || workflowTools
- return targetTools.find((tool: any) => canFindTool(tool.id, nodeData.provider_id))?.icon
- }, [buildInTools, customTools, workflowTools, mcpTools])
-
- // Extract model info logic - clean extraction
- const getModelInfo = useCallback((nodeData: CommonNodeType) => {
- if (nodeData?.type !== BlockEnum.LLM) return {}
-
- const llmNodeData = nodeData as LLMNodeType
- return llmNodeData.model ? {
- provider: llmNodeData.model.provider,
- name: llmNodeData.model.name,
- mode: llmNodeData.model.mode,
- } : {}
- }, [])
-
- 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)
- })
-
- return 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,
- toolIcon: getToolIcon(nodeData),
- modelInfo: getModelInfo(nodeData),
- }
- })
- }, [nodes, getToolIcon, getModelInfo])
-
- // Calculate search score - clean scoring logic
- const calculateScore = useCallback((node: {
- title: string;
- type: string;
- desc: string;
- modelInfo: { provider?: string; name?: string; mode?: string }
- }, searchTerm: string): number => {
- if (!searchTerm) return 1
-
- const titleMatch = node.title.toLowerCase()
- const typeMatch = node.type.toLowerCase()
- const descMatch = node.desc?.toLowerCase() || ''
- const modelProviderMatch = node.modelInfo?.provider?.toLowerCase() || ''
- const modelNameMatch = node.modelInfo?.name?.toLowerCase() || ''
- const modelModeMatch = node.modelInfo?.mode?.toLowerCase() || ''
-
- let score = 0
-
- // Title matching (exact prefix > partial match)
- if (titleMatch.startsWith(searchTerm)) score += 100
- else if (titleMatch.includes(searchTerm)) score += 50
-
- // Type matching (exact > partial)
- if (typeMatch === searchTerm) score += 80
- else if (typeMatch.includes(searchTerm)) score += 30
-
- // Description matching (additive)
- if (descMatch.includes(searchTerm)) score += 20
-
- // LLM model matching (additive - can combine multiple matches)
- if (modelNameMatch && modelNameMatch.includes(searchTerm)) score += 60
- if (modelProviderMatch && modelProviderMatch.includes(searchTerm)) score += 40
- if (modelModeMatch && modelModeMatch.includes(searchTerm)) score += 30
-
- return score
- }, [])
-
- // Create search function for workflow nodes
- const searchWorkflowNodes = useCallback((query: string) => {
- if (!searchableNodes.length) return []
-
- const searchTerm = query.toLowerCase().trim()
-
- const results = searchableNodes
- .map((node) => {
- const score = calculateScore(node, searchTerm)
-
- 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"
- toolIcon={node.toolIcon}
- />
- ),
- metadata: {
- nodeId: node.id,
- nodeData: node.nodeData,
- },
- data: node.nodeData,
- score,
- } : null
- })
- .filter((node): node is NonNullable<typeof node> => node !== null)
- .sort((a, b) => {
- // If no search term, sort alphabetically
- if (!searchTerm) return a.title.localeCompare(b.title)
- // Sort by relevance score (higher score first)
- return (b.score || 0) - (a.score || 0)
- })
-
- return results
- }, [searchableNodes, calculateScore])
-
- // 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
- }
|