diff --git a/web/app/components/goto-anything/hooks/use-search.ts b/web/app/components/goto-anything/hooks/use-search.ts new file mode 100644 index 0000000000..f6dcae70f1 --- /dev/null +++ b/web/app/components/goto-anything/hooks/use-search.ts @@ -0,0 +1,93 @@ +import { keepPreviousData, useQuery } from '@tanstack/react-query' +import { useDebounce } from 'ahooks' +import { useMemo } from 'react' +import { useGetLanguage } from '@/context/i18n' +import { matchAction, searchAnything, useGotoAnythingScopes } from '../actions' +import { ACTION_KEYS } from '../constants' +import { useGotoAnythingContext } from '../context' + +export const useSearch = (searchQuery: string) => { + const defaultLocale = useGetLanguage() + const { isWorkflowPage, isRagPipelinePage } = useGotoAnythingContext() + + // Fetch scopes from registry based on context + const scopes = useGotoAnythingScopes({ isWorkflowPage, isRagPipelinePage }) + + const searchQueryDebouncedValue = useDebounce(searchQuery.trim(), { + wait: 300, + }) + + const isCommandsMode = searchQuery.trim() === '@' || searchQuery.trim() === '/' + || (searchQuery.trim().startsWith('@') && !matchAction(searchQuery.trim(), scopes)) + || (searchQuery.trim().startsWith('/') && !matchAction(searchQuery.trim(), scopes)) + + const searchMode = useMemo(() => { + if (isCommandsMode) { + // Distinguish between @ (scopes) and / (commands) mode + if (searchQuery.trim().startsWith('@')) + return 'scopes' + else if (searchQuery.trim().startsWith('/')) + return 'commands' + return 'commands' // default fallback + } + + const query = searchQueryDebouncedValue.toLowerCase() + const action = matchAction(query, scopes) + + if (!action) + return 'general' + + if (action.id === 'slash' || action.shortcut === ACTION_KEYS.SLASH) + return '@command' + + return action.shortcut + }, [searchQueryDebouncedValue, scopes, isCommandsMode, searchQuery]) + + const { data: searchResults = [], isLoading, isError, error } = useQuery( + { + queryKey: [ + 'goto-anything', + 'search-result', + searchQueryDebouncedValue, + searchMode, + isWorkflowPage, + isRagPipelinePage, + defaultLocale, + scopes.map(s => s.id).sort().join(','), + ], + queryFn: async () => { + const query = searchQueryDebouncedValue.toLowerCase() + const scope = matchAction(query, scopes) + return await searchAnything(defaultLocale, query, scope, scopes) + }, + enabled: !!searchQueryDebouncedValue && !isCommandsMode, + staleTime: 30000, + gcTime: 300000, + placeholderData: keepPreviousData, + }, + ) + + const dedupedResults = useMemo(() => { + if (!searchQuery.trim()) + return [] + + const seen = new Set() + return searchResults.filter((result) => { + const key = `${result.type}-${result.id}` + if (seen.has(key)) + return false + seen.add(key) + return true + }) + }, [searchResults, searchQuery]) + + return { + scopes, + searchResults: dedupedResults, + isLoading, + isError, + error, + searchMode, + isCommandsMode, + } +} diff --git a/web/app/components/goto-anything/index.tsx b/web/app/components/goto-anything/index.tsx index 1147174af2..7443d5f2f5 100644 --- a/web/app/components/goto-anything/index.tsx +++ b/web/app/components/goto-anything/index.tsx @@ -4,8 +4,7 @@ import type { FC } from 'react' import type { Plugin } from '../plugins/types' import type { SearchResult } from './actions' import { RiSearchLine } from '@remixicon/react' -import { keepPreviousData, useQuery } from '@tanstack/react-query' -import { useDebounce, useKeyPress } from 'ahooks' +import { useKeyPress } from 'ahooks' import { Command } from 'cmdk' import { useRouter } from 'next/navigation' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -15,14 +14,13 @@ import Modal from '@/app/components/base/modal' import { VIBE_COMMAND_EVENT } from '@/app/components/workflow/constants' import { getKeyboardKeyCodeBySystem, isEventTargetInputArea, isMac } from '@/app/components/workflow/utils/common' import { selectWorkflowNode } from '@/app/components/workflow/utils/node-navigation' -import { useGetLanguage } from '@/context/i18n' import InstallFromMarketplace from '../plugins/install-plugin/install-from-marketplace' -import { matchAction, searchAnything, useGotoAnythingScopes } from './actions' import { executeCommand, SlashCommandProvider } from './actions/commands' import { slashCommandRegistry } from './actions/commands/registry' import CommandSelector from './command-selector' -import { ACTION_KEYS, EMPTY_STATE_I18N_MAP, GROUP_HEADING_I18N_MAP } from './constants' -import { GotoAnythingProvider, useGotoAnythingContext } from './context' +import { EMPTY_STATE_I18N_MAP, GROUP_HEADING_I18N_MAP } from './constants' +import { GotoAnythingProvider } from './context' +import { useSearch } from './hooks/use-search' type Props = { onHide?: () => void @@ -31,16 +29,21 @@ const GotoAnything: FC = ({ onHide, }) => { const router = useRouter() - const defaultLocale = useGetLanguage() - const { isWorkflowPage, isRagPipelinePage } = useGotoAnythingContext() const { t } = useTranslation() const [show, setShow] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [cmdVal, setCmdVal] = useState('_') const inputRef = useRef(null) - // Fetch scopes from registry based on context - const scopes = useGotoAnythingScopes({ isWorkflowPage, isRagPipelinePage }) + const { + scopes, + searchResults: dedupedResults, + isLoading, + isError, + error, + searchMode, + isCommandsMode, + } = useSearch(searchQuery) const [activePlugin, setActivePlugin] = useState() @@ -72,60 +75,6 @@ const GotoAnything: FC = ({ } }) - const searchQueryDebouncedValue = useDebounce(searchQuery.trim(), { - wait: 300, - }) - - const isCommandsMode = searchQuery.trim() === '@' || searchQuery.trim() === '/' - || (searchQuery.trim().startsWith('@') && !matchAction(searchQuery.trim(), scopes)) - || (searchQuery.trim().startsWith('/') && !matchAction(searchQuery.trim(), scopes)) - - const searchMode = useMemo(() => { - if (isCommandsMode) { - // Distinguish between @ (scopes) and / (commands) mode - if (searchQuery.trim().startsWith('@')) - return 'scopes' - else if (searchQuery.trim().startsWith('/')) - return 'commands' - return 'commands' // default fallback - } - - const query = searchQueryDebouncedValue.toLowerCase() - const action = matchAction(query, scopes) - - if (!action) - return 'general' - - if (action.id === 'slash' || action.shortcut === ACTION_KEYS.SLASH) - return '@command' - - return action.shortcut - }, [searchQueryDebouncedValue, scopes, isCommandsMode, searchQuery]) - - const { data: searchResults = [], isLoading, isError, error } = useQuery( - { - queryKey: [ - 'goto-anything', - 'search-result', - searchQueryDebouncedValue, - searchMode, - isWorkflowPage, - isRagPipelinePage, - defaultLocale, - scopes.map(s => s.id).sort().join(','), - ], - queryFn: async () => { - const query = searchQueryDebouncedValue.toLowerCase() - const scope = matchAction(query, scopes) - return await searchAnything(defaultLocale, query, scope, scopes) - }, - enabled: !!searchQueryDebouncedValue && !isCommandsMode, - staleTime: 30000, - gcTime: 300000, - placeholderData: keepPreviousData, - }, - ) - // Prevent automatic selection of the first option when cmdVal is not set const clearSelection = () => { setCmdVal('_') @@ -197,20 +146,6 @@ const GotoAnything: FC = ({ } }, [router]) - const dedupedResults = useMemo(() => { - if (!searchQuery.trim()) - return [] - - const seen = new Set() - return searchResults.filter((result) => { - const key = `${result.type}-${result.id}` - if (seen.has(key)) - return false - seen.add(key) - return true - }) - }, [searchResults, searchQuery]) - // Group results by type const groupedResults = useMemo(() => dedupedResults.reduce((acc, result) => { if (!acc[result.type]) @@ -396,7 +331,7 @@ const GotoAnything: FC = ({
{t('app.gotoAnything.searchFailed')}
- {error.message} + {error?.message}