'use client' import type { FC } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useRouter } from 'next/navigation' import Modal from '@/app/components/base/modal' import Input from '@/app/components/base/input' 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 { GotoAnythingProvider, useGotoAnythingContext } from './context' import { useQuery } from '@tanstack/react-query' import { useGetLanguage } from '@/context/i18n' import { useTranslation } from 'react-i18next' import InstallFromMarketplace from '../plugins/install-plugin/install-from-marketplace' import type { Plugin } from '../plugins/types' import { Command } from 'cmdk' import CommandSelector from './command-selector' import { RunCommandProvider } from './actions/run' type Props = { onHide?: () => void } const GotoAnything: FC = ({ onHide, }) => { const router = useRouter() const defaultLocale = useGetLanguage() const { isWorkflowPage } = useGotoAnythingContext() const { t } = useTranslation() const [show, setShow] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [cmdVal, setCmdVal] = useState('_') const inputRef = useRef(null) const handleNavSearch = useCallback((q: string) => { setShow(true) setSearchQuery(q) setCmdVal('') requestAnimationFrame(() => inputRef.current?.focus()) }, []) // 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 { // Exclude node action on non-workflow pages const { app, knowledge, plugin, run } = AllActions return { app, knowledge, plugin, run } } }, [isWorkflowPage]) const [activePlugin, setActivePlugin] = useState() // Handle keyboard shortcuts const handleToggleModal = useCallback((e: KeyboardEvent) => { // Allow closing when modal is open, even if focus is in the search input if (!show && isEventTargetInputArea(e.target as HTMLElement)) return e.preventDefault() setShow((prev) => { if (!prev) { // Opening modal - reset search state setSearchQuery('') } return !prev }) }, [show]) useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.k`, handleToggleModal, { exactMatch: true, useCapture: true, }) useKeyPress(['esc'], (e) => { if (show) { e.preventDefault() setShow(false) setSearchQuery('') } }) const searchQueryDebouncedValue = useDebounce(searchQuery.trim(), { wait: 300, }) const isCommandsMode = searchQuery.trim() === '@' || (searchQuery.trim().startsWith('@') && !matchAction(searchQuery.trim(), Actions)) const searchMode = useMemo(() => { if (isCommandsMode) return 'commands' const query = searchQueryDebouncedValue.toLowerCase() const action = matchAction(query, Actions) return action ? action.key : 'general' }, [searchQueryDebouncedValue, Actions, isCommandsMode]) const { data: searchResults = [], isLoading, isError, error } = useQuery( { queryKey: [ 'goto-anything', 'search-result', searchQueryDebouncedValue, searchMode, isWorkflowPage, defaultLocale, Object.keys(Actions).sort().join(','), ], queryFn: async () => { const query = searchQueryDebouncedValue.toLowerCase() const action = matchAction(query, Actions) return await searchAnything(defaultLocale, query, action) }, enabled: !!searchQueryDebouncedValue && !isCommandsMode, staleTime: 30000, gcTime: 300000, }, ) // Prevent automatic selection of the first option when cmdVal is not set const clearSelection = () => { setCmdVal('_') } const handleCommandSelect = useCallback((commandKey: string) => { setSearchQuery(`${commandKey} `) clearSelection() setTimeout(() => { inputRef.current?.focus() }, 0) }, []) // Handle navigation to selected result const handleNavigate = useCallback((result: SearchResult) => { setShow(false) setSearchQuery('') switch (result.type) { case 'command': { const action = Object.values(Actions).find(a => a.key === '@run') action?.action?.(result) break } case 'plugin': setActivePlugin(result.data) break case 'workflow-node': // Handle workflow node selection and navigation if (result.metadata?.nodeId) selectWorkflowNode(result.metadata.nodeId, true) break default: if (result.path) router.push(result.path) } }, [router]) // Group results by type const groupedResults = useMemo(() => searchResults.reduce((acc, result) => { if (!acc[result.type]) acc[result.type] = [] acc[result.type].push(result) return acc }, {} as { [key: string]: SearchResult[] }), [searchResults]) const emptyResult = useMemo(() => { if (searchResults.length || !searchQuery.trim() || isLoading || isCommandsMode) return null const isCommandSearch = searchMode !== 'general' const commandType = isCommandSearch ? searchMode.replace('@', '') : '' if (isError) { return (
{t('app.gotoAnything.searchTemporarilyUnavailable')}
{t('app.gotoAnything.servicesUnavailableMessage')}
) } return (
{isCommandSearch ? (() => { const keyMap: Record = { app: 'app.gotoAnything.emptyState.noAppsFound', plugin: 'app.gotoAnything.emptyState.noPluginsFound', knowledge: 'app.gotoAnything.emptyState.noKnowledgeBasesFound', node: 'app.gotoAnything.emptyState.noWorkflowNodesFound', } return t(keyMap[commandType] || 'app.gotoAnything.noResults') })() : t('app.gotoAnything.noResults') }
{isCommandSearch ? t('app.gotoAnything.emptyState.tryDifferentTerm', { mode: searchMode }) : t('app.gotoAnything.emptyState.trySpecificSearch', { shortcuts: Object.values(Actions).map(action => action.shortcut).join(', ') }) }
) }, [searchResults, searchQuery, Actions, searchMode, isLoading, isError, isCommandsMode]) const defaultUI = useMemo(() => { if (searchQuery.trim()) return null return (
{t('app.gotoAnything.searchTitle')}
{t('app.gotoAnything.searchHint')}
{t('app.gotoAnything.commandHint')}
) }, [searchQuery, Actions]) useEffect(() => { if (show) { requestAnimationFrame(() => { inputRef.current?.focus() }) } }, [show]) return ( <> { setShow(false) setSearchQuery('') clearSelection() onHide?.() }} closable={false} className='!w-[480px] !p-0' highPriority={true} >
{ setSearchQuery(e.target.value) if (!e.target.value.startsWith('@')) clearSelection() }} className='flex-1 !border-0 !bg-transparent !shadow-none' wrapperClassName='flex-1 !border-0 !bg-transparent' autoFocus /> {searchMode !== 'general' && (
{searchMode.replace('@', '').toUpperCase()}
)}
{isMac() ? '⌘' : 'Ctrl'} K
{isLoading && (
{t('app.gotoAnything.searching')}
)} {isError && (
{t('app.gotoAnything.searchFailed')}
{error.message}
)} {!isLoading && !isError && ( <> {isCommandsMode ? ( ) : ( Object.entries(groupedResults).map(([type, results], groupIndex) => ( { const typeMap: Record = { 'app': 'app.gotoAnything.groups.apps', 'plugin': 'app.gotoAnything.groups.plugins', 'knowledge': 'app.gotoAnything.groups.knowledgeBases', 'workflow-node': 'app.gotoAnything.groups.workflowNodes', } return t(typeMap[type] || `${type}s`) })()} className='p-2 capitalize text-text-secondary'> {results.map(result => ( handleNavigate(result)} > {result.icon}
{result.title}
{result.description && (
{result.description}
)}
{result.type}
))}
)) )} {!isCommandsMode && emptyResult} {!isCommandsMode && defaultUI} )}
{(!!searchResults.length || isError) && (
{isError ? ( {t('app.gotoAnything.someServicesUnavailable')} ) : ( <> {t('app.gotoAnything.resultCount', { count: searchResults.length })} {searchMode !== 'general' && ( {t('app.gotoAnything.inScope', { scope: searchMode.replace('@', '') })} )} )} {searchMode !== 'general' ? t('app.gotoAnything.clearToSearchAll') : t('app.gotoAnything.useAtForSpecific') }
)}
{ activePlugin && ( setActivePlugin(undefined)} onSuccess={() => setActivePlugin(undefined)} /> ) } ) } /** * GotoAnything component with context provider */ const GotoAnythingWithContext: FC = (props) => { return ( ) } export default GotoAnythingWithContext