diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx index 6178dda44..1d7824aa4 100644 --- a/frontend/src/contexts/ToolWorkflowContext.tsx +++ b/frontend/src/contexts/ToolWorkflowContext.tsx @@ -11,7 +11,7 @@ import { useNavigationActions, useNavigationState } from './NavigationContext'; import { ToolId, isValidToolId } from '../types/toolId'; import { useNavigationUrlSync } from '../hooks/useUrlSync'; import { getDefaultWorkbench } from '../types/workbench'; -import { idToWords, scoreMatch, minScoreForQuery } from '../utils/fuzzySearch'; +import { filterToolRegistryByQuery } from '../utils/toolSearch'; // State interface interface ToolWorkflowState { @@ -219,54 +219,10 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { setReaderMode(true); }, [setReaderMode]); - // Filter tools based on search query with fuzzy matching (name and description) + // Filter tools based on search query with fuzzy matching (name, description, id, synonyms) const filteredTools = useMemo(() => { if (!toolRegistry) return []; - const entries = Object.entries(toolRegistry); - if (!state.searchQuery.trim()) { - // Return in the new format even when not searching - return entries.map(([id, tool]) => ({ item: [id, tool] as [string, ToolRegistryEntry] })); - } - - const threshold = minScoreForQuery(state.searchQuery); - const results: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string; score: number }> = []; - - for (const [id, tool] of entries) { - let best = 0; - let matchedText = ''; - - const candidates: string[] = [ - idToWords(id), - tool.name || '', - tool.description || '' - ]; - for (const value of candidates) { - if (!value) continue; - const s = scoreMatch(state.searchQuery, value); - if (s > best) { - best = s; - matchedText = value; - } - } - - if (Array.isArray(tool.synonyms)) { - for (const synonym of tool.synonyms) { - if (!synonym) continue; - const s = scoreMatch(state.searchQuery, synonym); - if (s > best) { - best = s; - matchedText = synonym; - } - } - } - - if (best >= threshold) { - results.push({ item: [id, tool] as [string, ToolRegistryEntry], matchedText, score: best }); - } - } - - results.sort((a, b) => b.score - a.score); - return results.map(({ item, matchedText }) => ({ item, matchedText })); + return filterToolRegistryByQuery(toolRegistry as Record, state.searchQuery); }, [toolRegistry, state.searchQuery]); const isPanelVisible = useMemo(() => diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index 009783300..d3cd8c85a 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -12,6 +12,7 @@ import RemoveBlanks from "../tools/RemoveBlanks"; import RemovePages from "../tools/RemovePages"; import RemovePassword from "../tools/RemovePassword"; import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy"; +import { mergeSynonyms } from "../utils/toolSynonyms"; import AddWatermark from "../tools/AddWatermark"; import Merge from '../tools/Merge'; import Repair from "../tools/Repair"; @@ -146,31 +147,6 @@ export const CONVERT_SUPPORTED_FORMATS = [ "pdf", ]; -// Helper function to get translated synonyms for a tool -const getTranslatedSynonyms = (t: any, toolId: string): string[] => { - try { - const tagsKey = `${toolId}.tags`; - const tags = t(tagsKey); - - // If the translation key doesn't exist or returns the key itself, return empty array - if (!tags || tags === tagsKey) { - return []; - } - - // Split by comma and clean up the tags - return tags.split(',').map((tag: string) => tag.trim()).filter((tag: string) => tag.length > 0); - } catch (error) { - console.warn(`Failed to get translated synonyms for tool ${toolId}:`, error); - return []; - } -}; - -// Helper function to merge translated synonyms with existing synonyms -const mergeSynonyms = (t: any, toolId: string, existingSynonyms: string[] = []): string[] => { - const translatedSynonyms = getTranslatedSynonyms(t, toolId); - return [...translatedSynonyms, ...existingSynonyms]; -}; - // Hook to get the translated tool registry export function useFlatToolRegistry(): ToolRegistry { const { t } = useTranslation(); diff --git a/frontend/src/utils/fuzzySearch.ts b/frontend/src/utils/fuzzySearch.ts index 40b77c891..e8e8bdf01 100644 --- a/frontend/src/utils/fuzzySearch.ts +++ b/frontend/src/utils/fuzzySearch.ts @@ -96,6 +96,9 @@ export function rankByFuzzy(items: T[], query: string, getters: Array<(item: best = s; matchedText = value; } + if (best >= 95) { + break; + } } if (best >= threshold) results.push({ item, score: best, matchedText }); } diff --git a/frontend/src/utils/toolSearch.ts b/frontend/src/utils/toolSearch.ts new file mode 100644 index 000000000..95c431a72 --- /dev/null +++ b/frontend/src/utils/toolSearch.ts @@ -0,0 +1,61 @@ +import { ToolRegistryEntry } from "../data/toolsTaxonomy"; +import { idToWords, scoreMatch, minScoreForQuery } from "./fuzzySearch"; + +export interface RankedToolItem { + item: [string, ToolRegistryEntry]; + matchedText?: string; +} + +export function filterToolRegistryByQuery( + toolRegistry: Record, + query: string +): RankedToolItem[] { + const entries = Object.entries(toolRegistry); + if (!query.trim()) { + return entries.map(([id, tool]) => ({ item: [id, tool] as [string, ToolRegistryEntry] })); + } + + const threshold = minScoreForQuery(query); + const results: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string; score: number }> = []; + + for (const [id, tool] of entries) { + let best = 0; + let matchedText = ''; + + const candidates: string[] = [ + idToWords(id), + tool.name || '', + tool.description || '' + ]; + for (const value of candidates) { + if (!value) continue; + const s = scoreMatch(query, value); + if (s > best) { + best = s; + matchedText = value; + } + if (best >= 95) break; + } + + if (Array.isArray(tool.synonyms)) { + for (const synonym of tool.synonyms) { + if (!synonym) continue; + const s = scoreMatch(query, synonym); + if (s > best) { + best = s; + matchedText = synonym; + } + if (best >= 95) break; + } + } + + if (best >= threshold) { + results.push({ item: [id, tool] as [string, ToolRegistryEntry], matchedText, score: best }); + } + } + + results.sort((a, b) => b.score - a.score); + return results.map(({ item, matchedText }) => ({ item, matchedText })); +} + + diff --git a/frontend/src/utils/toolSynonyms.ts b/frontend/src/utils/toolSynonyms.ts new file mode 100644 index 000000000..f3660c7a5 --- /dev/null +++ b/frontend/src/utils/toolSynonyms.ts @@ -0,0 +1,36 @@ +import { TFunction } from 'i18next'; + +// Helper function to get translated synonyms for a tool +export const getTranslatedSynonyms = (t: TFunction, toolId: string): string[] => { + try { + const tagsKey = `${toolId}.tags`; + const tags = t(tagsKey) as unknown as string; + + // If the translation key doesn't exist or returns the key itself, return empty array + if (!tags || tags === tagsKey) { + return []; + } + + // Split by comma and clean up the tags + return tags + .split(',') + .map((tag: string) => tag.trim()) + .filter((tag: string) => tag.length > 0); + } catch (error) { + // eslint-disable-next-line no-console + console.warn(`Failed to get translated synonyms for tool ${toolId}:`, error); + return []; + } +}; + +// Helper function to merge translated synonyms with existing synonyms +export const mergeSynonyms = ( + t: TFunction, + toolId: string, + existingSynonyms: string[] = [] +): string[] => { + const translatedSynonyms = getTranslatedSynonyms(t, toolId); + return [...translatedSynonyms, ...existingSynonyms]; +}; + +