mirror of
				https://github.com/langgenius/dify.git
				synced 2025-11-03 20:33:00 +00:00 
			
		
		
		
	feat(goto-anything): add RAG pipeline node search (#25948)
This commit is contained in:
		
							parent
							
								
									4047a6bb12
								
							
						
					
					
						commit
						ab910c736c
					
				@ -167,10 +167,39 @@ import { appAction } from './app'
 | 
			
		||||
import { knowledgeAction } from './knowledge'
 | 
			
		||||
import { pluginAction } from './plugin'
 | 
			
		||||
import { workflowNodesAction } from './workflow-nodes'
 | 
			
		||||
import { ragPipelineNodesAction } from './rag-pipeline-nodes'
 | 
			
		||||
import type { ActionItem, SearchResult } from './types'
 | 
			
		||||
import { slashAction } from './commands'
 | 
			
		||||
import { slashCommandRegistry } from './commands/registry'
 | 
			
		||||
 | 
			
		||||
// Create dynamic Actions based on context
 | 
			
		||||
export const createActions = (isWorkflowPage: boolean, isRagPipelinePage: boolean) => {
 | 
			
		||||
  const baseActions = {
 | 
			
		||||
    slash: slashAction,
 | 
			
		||||
    app: appAction,
 | 
			
		||||
    knowledge: knowledgeAction,
 | 
			
		||||
    plugin: pluginAction,
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Add appropriate node search based on context
 | 
			
		||||
  if (isRagPipelinePage) {
 | 
			
		||||
    return {
 | 
			
		||||
      ...baseActions,
 | 
			
		||||
      node: ragPipelineNodesAction,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  else if (isWorkflowPage) {
 | 
			
		||||
    return {
 | 
			
		||||
      ...baseActions,
 | 
			
		||||
      node: workflowNodesAction,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Default actions without node search
 | 
			
		||||
  return baseActions
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Legacy export for backward compatibility
 | 
			
		||||
export const Actions = {
 | 
			
		||||
  slash: slashAction,
 | 
			
		||||
  app: appAction,
 | 
			
		||||
@ -183,6 +212,7 @@ export const searchAnything = async (
 | 
			
		||||
  locale: string,
 | 
			
		||||
  query: string,
 | 
			
		||||
  actionItem?: ActionItem,
 | 
			
		||||
  dynamicActions?: Record<string, ActionItem>,
 | 
			
		||||
): Promise<SearchResult[]> => {
 | 
			
		||||
  if (actionItem) {
 | 
			
		||||
    const searchTerm = query.replace(actionItem.key, '').replace(actionItem.shortcut, '').trim()
 | 
			
		||||
@ -198,7 +228,7 @@ export const searchAnything = async (
 | 
			
		||||
  if (query.startsWith('@') || query.startsWith('/'))
 | 
			
		||||
    return []
 | 
			
		||||
 | 
			
		||||
  const globalSearchActions = Object.values(Actions)
 | 
			
		||||
  const globalSearchActions = Object.values(dynamicActions || Actions)
 | 
			
		||||
 | 
			
		||||
  // Use Promise.allSettled to handle partial failures gracefully
 | 
			
		||||
  const searchPromises = globalSearchActions.map(async (action) => {
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,24 @@
 | 
			
		||||
import type { ActionItem } from './types'
 | 
			
		||||
 | 
			
		||||
// Create the RAG pipeline nodes action
 | 
			
		||||
export const ragPipelineNodesAction: ActionItem = {
 | 
			
		||||
  key: '@node',
 | 
			
		||||
  shortcut: '@node',
 | 
			
		||||
  title: 'Search RAG Pipeline Nodes',
 | 
			
		||||
  description: 'Find and jump to nodes in the current RAG pipeline by name or type',
 | 
			
		||||
  searchFn: undefined, // Will be set by useRagPipelineSearch hook
 | 
			
		||||
  search: async (_, searchTerm = '', _locale) => {
 | 
			
		||||
    try {
 | 
			
		||||
      // Use the searchFn if available (set by useRagPipelineSearch hook)
 | 
			
		||||
      if (ragPipelineNodesAction.searchFn)
 | 
			
		||||
        return ragPipelineNodesAction.searchFn(searchTerm)
 | 
			
		||||
 | 
			
		||||
      // If not in RAG pipeline context, return empty array
 | 
			
		||||
      return []
 | 
			
		||||
    }
 | 
			
		||||
    catch (error) {
 | 
			
		||||
      console.warn('RAG pipeline nodes search failed:', error)
 | 
			
		||||
      return []
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
@ -7,7 +7,7 @@ export const workflowNodesAction: ActionItem = {
 | 
			
		||||
  title: 'Search Workflow Nodes',
 | 
			
		||||
  description: 'Find and jump to nodes in the current workflow by name or type',
 | 
			
		||||
  searchFn: undefined, // Will be set by useWorkflowSearch hook
 | 
			
		||||
  search: async (_, searchTerm = '', locale) => {
 | 
			
		||||
  search: async (_, searchTerm = '', _locale) => {
 | 
			
		||||
    try {
 | 
			
		||||
      // Use the searchFn if available (set by useWorkflowSearch hook)
 | 
			
		||||
      if (workflowNodesAction.searchFn)
 | 
			
		||||
 | 
			
		||||
@ -12,11 +12,16 @@ type GotoAnythingContextType = {
 | 
			
		||||
   * Whether the current page is a workflow page
 | 
			
		||||
   */
 | 
			
		||||
  isWorkflowPage: boolean
 | 
			
		||||
  /**
 | 
			
		||||
   * Whether the current page is a RAG pipeline page
 | 
			
		||||
   */
 | 
			
		||||
  isRagPipelinePage: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create context with default values
 | 
			
		||||
const GotoAnythingContext = createContext<GotoAnythingContextType>({
 | 
			
		||||
  isWorkflowPage: false,
 | 
			
		||||
  isRagPipelinePage: false,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -33,17 +38,28 @@ type GotoAnythingProviderProps = {
 | 
			
		||||
 */
 | 
			
		||||
export const GotoAnythingProvider: React.FC<GotoAnythingProviderProps> = ({ children }) => {
 | 
			
		||||
  const [isWorkflowPage, setIsWorkflowPage] = useState(false)
 | 
			
		||||
  const [isRagPipelinePage, setIsRagPipelinePage] = useState(false)
 | 
			
		||||
  const pathname = usePathname()
 | 
			
		||||
 | 
			
		||||
  // Update context based on current pathname
 | 
			
		||||
  // Update context based on current pathname using more robust route matching
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // Check if current path contains workflow
 | 
			
		||||
    const isWorkflow = pathname?.includes('/workflow') || false
 | 
			
		||||
    if (!pathname) {
 | 
			
		||||
      setIsWorkflowPage(false)
 | 
			
		||||
      setIsRagPipelinePage(false)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Workflow pages: /app/[appId]/workflow or /workflow/[token] (shared)
 | 
			
		||||
    const isWorkflow = /^\/app\/[^/]+\/workflow$/.test(pathname) || /^\/workflow\/[^/]+$/.test(pathname)
 | 
			
		||||
    // RAG Pipeline pages: /datasets/[datasetId]/pipeline
 | 
			
		||||
    const isRagPipeline = /^\/datasets\/[^/]+\/pipeline$/.test(pathname)
 | 
			
		||||
 | 
			
		||||
    setIsWorkflowPage(isWorkflow)
 | 
			
		||||
    setIsRagPipelinePage(isRagPipeline)
 | 
			
		||||
  }, [pathname])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <GotoAnythingContext.Provider value={{ isWorkflowPage }}>
 | 
			
		||||
    <GotoAnythingContext.Provider value={{ isWorkflowPage, isRagPipelinePage }}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </GotoAnythingContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ import { useDebounce, useKeyPress } from 'ahooks'
 | 
			
		||||
import { getKeyboardKeyCodeBySystem, isEventTargetInputArea, isMac } from '@/app/components/workflow/utils/common'
 | 
			
		||||
import { selectWorkflowNode } from '@/app/components/workflow/utils/node-navigation'
 | 
			
		||||
import { RiSearchLine } from '@remixicon/react'
 | 
			
		||||
import { Actions as AllActions, type SearchResult, matchAction, searchAnything } from './actions'
 | 
			
		||||
import { type SearchResult, createActions, matchAction, searchAnything } from './actions'
 | 
			
		||||
import { GotoAnythingProvider, useGotoAnythingContext } from './context'
 | 
			
		||||
import { slashCommandRegistry } from './actions/commands/registry'
 | 
			
		||||
import { useQuery } from '@tanstack/react-query'
 | 
			
		||||
@ -29,7 +29,7 @@ const GotoAnything: FC<Props> = ({
 | 
			
		||||
}) => {
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const defaultLocale = useGetLanguage()
 | 
			
		||||
  const { isWorkflowPage } = useGotoAnythingContext()
 | 
			
		||||
  const { isWorkflowPage, isRagPipelinePage } = useGotoAnythingContext()
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
  const [show, setShow] = useState<boolean>(false)
 | 
			
		||||
  const [searchQuery, setSearchQuery] = useState<string>('')
 | 
			
		||||
@ -38,16 +38,9 @@ const GotoAnything: FC<Props> = ({
 | 
			
		||||
 | 
			
		||||
  // Filter actions based on context
 | 
			
		||||
  const Actions = useMemo(() => {
 | 
			
		||||
    // Create a filtered copy of actions based on current page context
 | 
			
		||||
    if (isWorkflowPage) {
 | 
			
		||||
      // Include all actions on workflow pages
 | 
			
		||||
      return AllActions
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      const { app, knowledge, plugin, slash } = AllActions
 | 
			
		||||
      return { app, knowledge, plugin, slash }
 | 
			
		||||
    }
 | 
			
		||||
  }, [isWorkflowPage])
 | 
			
		||||
    // Create actions based on current page context
 | 
			
		||||
    return createActions(isWorkflowPage, isRagPipelinePage)
 | 
			
		||||
  }, [isWorkflowPage, isRagPipelinePage])
 | 
			
		||||
 | 
			
		||||
  const [activePlugin, setActivePlugin] = useState<Plugin>()
 | 
			
		||||
 | 
			
		||||
@ -99,9 +92,11 @@ const GotoAnything: FC<Props> = ({
 | 
			
		||||
 | 
			
		||||
    const query = searchQueryDebouncedValue.toLowerCase()
 | 
			
		||||
    const action = matchAction(query, Actions)
 | 
			
		||||
    return action
 | 
			
		||||
      ? (action.key === '/' ? '@command' : action.key)
 | 
			
		||||
      : 'general'
 | 
			
		||||
 | 
			
		||||
    if (!action)
 | 
			
		||||
      return 'general'
 | 
			
		||||
 | 
			
		||||
    return action.key === '/' ? '@command' : action.key
 | 
			
		||||
  }, [searchQueryDebouncedValue, Actions, isCommandsMode, searchQuery])
 | 
			
		||||
 | 
			
		||||
  const { data: searchResults = [], isLoading, isError, error } = useQuery(
 | 
			
		||||
@ -112,13 +107,14 @@ const GotoAnything: FC<Props> = ({
 | 
			
		||||
        searchQueryDebouncedValue,
 | 
			
		||||
        searchMode,
 | 
			
		||||
        isWorkflowPage,
 | 
			
		||||
        isRagPipelinePage,
 | 
			
		||||
        defaultLocale,
 | 
			
		||||
        Object.keys(Actions).sort().join(','),
 | 
			
		||||
      ],
 | 
			
		||||
      queryFn: async () => {
 | 
			
		||||
        const query = searchQueryDebouncedValue.toLowerCase()
 | 
			
		||||
        const action = matchAction(query, Actions)
 | 
			
		||||
        return await searchAnything(defaultLocale, query, action)
 | 
			
		||||
        return await searchAnything(defaultLocale, query, action, Actions)
 | 
			
		||||
      },
 | 
			
		||||
      enabled: !!searchQueryDebouncedValue && !isCommandsMode,
 | 
			
		||||
      staleTime: 30000,
 | 
			
		||||
@ -446,18 +442,20 @@ const GotoAnything: FC<Props> = ({
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <>
 | 
			
		||||
                    <span className='opacity-60'>
 | 
			
		||||
                      {isCommandsMode
 | 
			
		||||
                        ? t('app.gotoAnything.selectToNavigate')
 | 
			
		||||
                        : searchQuery.trim()
 | 
			
		||||
                          ? t('app.gotoAnything.searching')
 | 
			
		||||
                          : t('app.gotoAnything.startTyping')
 | 
			
		||||
                      }
 | 
			
		||||
                      {(() => {
 | 
			
		||||
                        if (isCommandsMode)
 | 
			
		||||
                          return t('app.gotoAnything.selectToNavigate')
 | 
			
		||||
 | 
			
		||||
                        if (searchQuery.trim())
 | 
			
		||||
                          return t('app.gotoAnything.searching')
 | 
			
		||||
 | 
			
		||||
                        return t('app.gotoAnything.startTyping')
 | 
			
		||||
                      })()}
 | 
			
		||||
                    </span>
 | 
			
		||||
                    <span className='opacity-60'>
 | 
			
		||||
                      {searchQuery.trim() || isCommandsMode
 | 
			
		||||
                        ? t('app.gotoAnything.tips')
 | 
			
		||||
                        : t('app.gotoAnything.pressEscToClose')
 | 
			
		||||
                      }
 | 
			
		||||
                        : t('app.gotoAnything.pressEscToClose')}
 | 
			
		||||
                    </span>
 | 
			
		||||
                  </>
 | 
			
		||||
                )}
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@ import {
 | 
			
		||||
} from '@/app/components/workflow/hooks'
 | 
			
		||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
 | 
			
		||||
import PublishToast from './publish-toast'
 | 
			
		||||
import { useRagPipelineSearch } from '../hooks/use-rag-pipeline-search'
 | 
			
		||||
 | 
			
		||||
const RagPipelineChildren = () => {
 | 
			
		||||
  const { eventEmitter } = useEventEmitterContextContext()
 | 
			
		||||
@ -30,6 +31,9 @@ const RagPipelineChildren = () => {
 | 
			
		||||
    handleExportDSL,
 | 
			
		||||
  } = useDSL()
 | 
			
		||||
 | 
			
		||||
  // Initialize RAG pipeline search functionality
 | 
			
		||||
  useRagPipelineSearch()
 | 
			
		||||
 | 
			
		||||
  eventEmitter?.useSubscription((v: any) => {
 | 
			
		||||
    if (v.type === DSL_EXPORT_CHECK)
 | 
			
		||||
      setSecretEnvList(v.payload.data as EnvironmentVariable[])
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,168 @@
 | 
			
		||||
'use client'
 | 
			
		||||
 | 
			
		||||
import { useCallback, useEffect, useMemo } from 'react'
 | 
			
		||||
import { useNodes } from 'reactflow'
 | 
			
		||||
import { useNodesInteractions } from '@/app/components/workflow/hooks/use-nodes-interactions'
 | 
			
		||||
import type { CommonNodeType } from '@/app/components/workflow/types'
 | 
			
		||||
import { ragPipelineNodesAction } from '@/app/components/goto-anything/actions/rag-pipeline-nodes'
 | 
			
		||||
import BlockIcon from '@/app/components/workflow/block-icon'
 | 
			
		||||
import { setupNodeSelectionListener } from '@/app/components/workflow/utils/node-navigation'
 | 
			
		||||
import { BlockEnum } from '@/app/components/workflow/types'
 | 
			
		||||
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
 | 
			
		||||
import type { ToolNodeType } from '@/app/components/workflow/nodes/tool/types'
 | 
			
		||||
import type { KnowledgeRetrievalNodeType } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
 | 
			
		||||
import { useGetToolIcon } from '@/app/components/workflow/hooks/use-tool-icon'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Hook to register RAG pipeline nodes search functionality
 | 
			
		||||
 */
 | 
			
		||||
export const useRagPipelineSearch = () => {
 | 
			
		||||
  const nodes = useNodes()
 | 
			
		||||
  const { handleNodeSelect } = useNodesInteractions()
 | 
			
		||||
  const getToolIcon = useGetToolIcon()
 | 
			
		||||
 | 
			
		||||
  // Process nodes to create searchable data structure
 | 
			
		||||
  const searchableNodes = useMemo(() => {
 | 
			
		||||
    return nodes.map((node) => {
 | 
			
		||||
      const nodeData = node.data as CommonNodeType
 | 
			
		||||
      const title = nodeData.title || nodeData.type || 'Untitled Node'
 | 
			
		||||
      let desc = nodeData.desc || ''
 | 
			
		||||
 | 
			
		||||
      // Keep the original node title for consistency with workflow display
 | 
			
		||||
      // Only enhance description for better search context
 | 
			
		||||
      if (nodeData.type === BlockEnum.Tool) {
 | 
			
		||||
        const toolData = nodeData as ToolNodeType
 | 
			
		||||
        desc = toolData.tool_description || toolData.tool_label || desc
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (nodeData.type === BlockEnum.LLM) {
 | 
			
		||||
        const llmData = nodeData as LLMNodeType
 | 
			
		||||
        if (llmData.model?.provider && llmData.model?.name)
 | 
			
		||||
          desc = `${llmData.model.name} (${llmData.model.provider}) - ${llmData.model.mode || desc}`
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (nodeData.type === BlockEnum.KnowledgeRetrieval) {
 | 
			
		||||
        const knowledgeData = nodeData as KnowledgeRetrievalNodeType
 | 
			
		||||
        if (knowledgeData.dataset_ids?.length)
 | 
			
		||||
          desc = `Knowledge Retrieval with ${knowledgeData.dataset_ids.length} datasets - ${desc}`
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        id: node.id,
 | 
			
		||||
        title,
 | 
			
		||||
        desc,
 | 
			
		||||
        type: nodeData.type,
 | 
			
		||||
        blockType: nodeData.type,
 | 
			
		||||
        nodeData,
 | 
			
		||||
        toolIcon: getToolIcon(nodeData),
 | 
			
		||||
        modelInfo: nodeData.type === BlockEnum.LLM ? {
 | 
			
		||||
          provider: (nodeData as LLMNodeType).model?.provider,
 | 
			
		||||
          name: (nodeData as LLMNodeType).model?.name,
 | 
			
		||||
          mode: (nodeData as LLMNodeType).model?.mode,
 | 
			
		||||
        } : {
 | 
			
		||||
          provider: undefined,
 | 
			
		||||
          name: undefined,
 | 
			
		||||
          mode: undefined,
 | 
			
		||||
        },
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }, [nodes, getToolIcon])
 | 
			
		||||
 | 
			
		||||
  // Calculate relevance score for search results
 | 
			
		||||
  const calculateScore = useCallback((node: {
 | 
			
		||||
    title: string;
 | 
			
		||||
    type: string;
 | 
			
		||||
    desc: string;
 | 
			
		||||
    modelInfo: { provider?: string; name?: string; mode?: string }
 | 
			
		||||
  }, searchTerm: string): number => {
 | 
			
		||||
    if (!searchTerm) return 1
 | 
			
		||||
 | 
			
		||||
    let score = 0
 | 
			
		||||
    const term = searchTerm.toLowerCase()
 | 
			
		||||
 | 
			
		||||
    // Title match (highest priority)
 | 
			
		||||
    if (node.title.toLowerCase().includes(term))
 | 
			
		||||
      score += 10
 | 
			
		||||
 | 
			
		||||
    // Type match
 | 
			
		||||
    if (node.type.toLowerCase().includes(term))
 | 
			
		||||
      score += 8
 | 
			
		||||
 | 
			
		||||
    // Description match
 | 
			
		||||
    if (node.desc.toLowerCase().includes(term))
 | 
			
		||||
      score += 5
 | 
			
		||||
 | 
			
		||||
    // Model info matches (for LLM nodes)
 | 
			
		||||
    if (node.modelInfo.provider?.toLowerCase().includes(term))
 | 
			
		||||
      score += 6
 | 
			
		||||
    if (node.modelInfo.name?.toLowerCase().includes(term))
 | 
			
		||||
      score += 6
 | 
			
		||||
    if (node.modelInfo.mode?.toLowerCase().includes(term))
 | 
			
		||||
      score += 4
 | 
			
		||||
 | 
			
		||||
    return score
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  // Create search function for RAG pipeline nodes
 | 
			
		||||
  const searchRagPipelineNodes = 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
 | 
			
		||||
      ragPipelineNodesAction.searchFn = searchRagPipelineNodes
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      // Clean up when component unmounts
 | 
			
		||||
      ragPipelineNodesAction.searchFn = undefined
 | 
			
		||||
    }
 | 
			
		||||
  }, [searchableNodes, searchRagPipelineNodes])
 | 
			
		||||
 | 
			
		||||
  // Set up node selection event listener using the utility function
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    return setupNodeSelectionListener(handleNodeSelect)
 | 
			
		||||
  }, [handleNodeSelect])
 | 
			
		||||
 | 
			
		||||
  return null
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user