| import { knowledgeAction } from './knowledge' | import { knowledgeAction } from './knowledge' | ||||
| import { pluginAction } from './plugin' | import { pluginAction } from './plugin' | ||||
| import { workflowNodesAction } from './workflow-nodes' | import { workflowNodesAction } from './workflow-nodes' | ||||
| import { ragPipelineNodesAction } from './rag-pipeline-nodes' | |||||
| import type { ActionItem, SearchResult } from './types' | import type { ActionItem, SearchResult } from './types' | ||||
| import { slashAction } from './commands' | import { slashAction } from './commands' | ||||
| import { slashCommandRegistry } from './commands/registry' | 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 = { | export const Actions = { | ||||
| slash: slashAction, | slash: slashAction, | ||||
| app: appAction, | app: appAction, | ||||
| locale: string, | locale: string, | ||||
| query: string, | query: string, | ||||
| actionItem?: ActionItem, | actionItem?: ActionItem, | ||||
| dynamicActions?: Record<string, ActionItem>, | |||||
| ): Promise<SearchResult[]> => { | ): Promise<SearchResult[]> => { | ||||
| if (actionItem) { | if (actionItem) { | ||||
| const searchTerm = query.replace(actionItem.key, '').replace(actionItem.shortcut, '').trim() | const searchTerm = query.replace(actionItem.key, '').replace(actionItem.shortcut, '').trim() | ||||
| if (query.startsWith('@') || query.startsWith('/')) | if (query.startsWith('@') || query.startsWith('/')) | ||||
| return [] | return [] | ||||
| const globalSearchActions = Object.values(Actions) | |||||
| const globalSearchActions = Object.values(dynamicActions || Actions) | |||||
| // Use Promise.allSettled to handle partial failures gracefully | // Use Promise.allSettled to handle partial failures gracefully | ||||
| const searchPromises = globalSearchActions.map(async (action) => { | const searchPromises = globalSearchActions.map(async (action) => { |
| 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 [] | |||||
| } | |||||
| }, | |||||
| } |
| title: 'Search Workflow Nodes', | title: 'Search Workflow Nodes', | ||||
| description: 'Find and jump to nodes in the current workflow by name or type', | description: 'Find and jump to nodes in the current workflow by name or type', | ||||
| searchFn: undefined, // Will be set by useWorkflowSearch hook | searchFn: undefined, // Will be set by useWorkflowSearch hook | ||||
| search: async (_, searchTerm = '', locale) => { | |||||
| search: async (_, searchTerm = '', _locale) => { | |||||
| try { | try { | ||||
| // Use the searchFn if available (set by useWorkflowSearch hook) | // Use the searchFn if available (set by useWorkflowSearch hook) | ||||
| if (workflowNodesAction.searchFn) | if (workflowNodesAction.searchFn) |
| * Whether the current page is a workflow page | * Whether the current page is a workflow page | ||||
| */ | */ | ||||
| isWorkflowPage: boolean | isWorkflowPage: boolean | ||||
| /** | |||||
| * Whether the current page is a RAG pipeline page | |||||
| */ | |||||
| isRagPipelinePage: boolean | |||||
| } | } | ||||
| // Create context with default values | // Create context with default values | ||||
| const GotoAnythingContext = createContext<GotoAnythingContextType>({ | const GotoAnythingContext = createContext<GotoAnythingContextType>({ | ||||
| isWorkflowPage: false, | isWorkflowPage: false, | ||||
| isRagPipelinePage: false, | |||||
| }) | }) | ||||
| /** | /** | ||||
| */ | */ | ||||
| export const GotoAnythingProvider: React.FC<GotoAnythingProviderProps> = ({ children }) => { | export const GotoAnythingProvider: React.FC<GotoAnythingProviderProps> = ({ children }) => { | ||||
| const [isWorkflowPage, setIsWorkflowPage] = useState(false) | const [isWorkflowPage, setIsWorkflowPage] = useState(false) | ||||
| const [isRagPipelinePage, setIsRagPipelinePage] = useState(false) | |||||
| const pathname = usePathname() | const pathname = usePathname() | ||||
| // Update context based on current pathname | |||||
| // Update context based on current pathname using more robust route matching | |||||
| useEffect(() => { | 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) | setIsWorkflowPage(isWorkflow) | ||||
| setIsRagPipelinePage(isRagPipeline) | |||||
| }, [pathname]) | }, [pathname]) | ||||
| return ( | return ( | ||||
| <GotoAnythingContext.Provider value={{ isWorkflowPage }}> | |||||
| <GotoAnythingContext.Provider value={{ isWorkflowPage, isRagPipelinePage }}> | |||||
| {children} | {children} | ||||
| </GotoAnythingContext.Provider> | </GotoAnythingContext.Provider> | ||||
| ) | ) |
| import { getKeyboardKeyCodeBySystem, isEventTargetInputArea, isMac } from '@/app/components/workflow/utils/common' | import { getKeyboardKeyCodeBySystem, isEventTargetInputArea, isMac } from '@/app/components/workflow/utils/common' | ||||
| import { selectWorkflowNode } from '@/app/components/workflow/utils/node-navigation' | import { selectWorkflowNode } from '@/app/components/workflow/utils/node-navigation' | ||||
| import { RiSearchLine } from '@remixicon/react' | 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 { GotoAnythingProvider, useGotoAnythingContext } from './context' | ||||
| import { slashCommandRegistry } from './actions/commands/registry' | import { slashCommandRegistry } from './actions/commands/registry' | ||||
| import { useQuery } from '@tanstack/react-query' | import { useQuery } from '@tanstack/react-query' | ||||
| }) => { | }) => { | ||||
| const router = useRouter() | const router = useRouter() | ||||
| const defaultLocale = useGetLanguage() | const defaultLocale = useGetLanguage() | ||||
| const { isWorkflowPage } = useGotoAnythingContext() | |||||
| const { isWorkflowPage, isRagPipelinePage } = useGotoAnythingContext() | |||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const [show, setShow] = useState<boolean>(false) | const [show, setShow] = useState<boolean>(false) | ||||
| const [searchQuery, setSearchQuery] = useState<string>('') | const [searchQuery, setSearchQuery] = useState<string>('') | ||||
| // Filter actions based on context | // Filter actions based on context | ||||
| const Actions = useMemo(() => { | 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>() | const [activePlugin, setActivePlugin] = useState<Plugin>() | ||||
| const query = searchQueryDebouncedValue.toLowerCase() | const query = searchQueryDebouncedValue.toLowerCase() | ||||
| const action = matchAction(query, Actions) | 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]) | }, [searchQueryDebouncedValue, Actions, isCommandsMode, searchQuery]) | ||||
| const { data: searchResults = [], isLoading, isError, error } = useQuery( | const { data: searchResults = [], isLoading, isError, error } = useQuery( | ||||
| searchQueryDebouncedValue, | searchQueryDebouncedValue, | ||||
| searchMode, | searchMode, | ||||
| isWorkflowPage, | isWorkflowPage, | ||||
| isRagPipelinePage, | |||||
| defaultLocale, | defaultLocale, | ||||
| Object.keys(Actions).sort().join(','), | Object.keys(Actions).sort().join(','), | ||||
| ], | ], | ||||
| queryFn: async () => { | queryFn: async () => { | ||||
| const query = searchQueryDebouncedValue.toLowerCase() | const query = searchQueryDebouncedValue.toLowerCase() | ||||
| const action = matchAction(query, Actions) | const action = matchAction(query, Actions) | ||||
| return await searchAnything(defaultLocale, query, action) | |||||
| return await searchAnything(defaultLocale, query, action, Actions) | |||||
| }, | }, | ||||
| enabled: !!searchQueryDebouncedValue && !isCommandsMode, | enabled: !!searchQueryDebouncedValue && !isCommandsMode, | ||||
| staleTime: 30000, | staleTime: 30000, | ||||
| ) : ( | ) : ( | ||||
| <> | <> | ||||
| <span className='opacity-60'> | <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> | ||||
| <span className='opacity-60'> | <span className='opacity-60'> | ||||
| {searchQuery.trim() || isCommandsMode | {searchQuery.trim() || isCommandsMode | ||||
| ? t('app.gotoAnything.tips') | ? t('app.gotoAnything.tips') | ||||
| : t('app.gotoAnything.pressEscToClose') | |||||
| } | |||||
| : t('app.gotoAnything.pressEscToClose')} | |||||
| </span> | </span> | ||||
| </> | </> | ||||
| )} | )} |
| } from '@/app/components/workflow/hooks' | } from '@/app/components/workflow/hooks' | ||||
| import { useEventEmitterContextContext } from '@/context/event-emitter' | import { useEventEmitterContextContext } from '@/context/event-emitter' | ||||
| import PublishToast from './publish-toast' | import PublishToast from './publish-toast' | ||||
| import { useRagPipelineSearch } from '../hooks/use-rag-pipeline-search' | |||||
| const RagPipelineChildren = () => { | const RagPipelineChildren = () => { | ||||
| const { eventEmitter } = useEventEmitterContextContext() | const { eventEmitter } = useEventEmitterContextContext() | ||||
| handleExportDSL, | handleExportDSL, | ||||
| } = useDSL() | } = useDSL() | ||||
| // Initialize RAG pipeline search functionality | |||||
| useRagPipelineSearch() | |||||
| eventEmitter?.useSubscription((v: any) => { | eventEmitter?.useSubscription((v: any) => { | ||||
| if (v.type === DSL_EXPORT_CHECK) | if (v.type === DSL_EXPORT_CHECK) | ||||
| setSecretEnvList(v.payload.data as EnvironmentVariable[]) | setSecretEnvList(v.payload.data as EnvironmentVariable[]) |
| '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 | |||||
| } |