Assign shortcuts by default to only the quick access items (#4622)

# Description of Changes
Change shortcuts to just be a limited set for Quick Access tools rather
than for everything to avoid breaking browser key commands by default.

Also fixes a bunch of types of variables that were representing
`ToolId`s (I stopped at `automate` because there's loads in there so
I've just introduced some `any` casts for now 😭)
This commit is contained in:
James Brunton 2025-10-08 17:18:05 +01:00 committed by GitHub
parent d714a1617f
commit 3090a85726
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 125 additions and 114 deletions

View File

@ -283,7 +283,8 @@
"capturing": "Press keys… (Esc to cancel)", "capturing": "Press keys… (Esc to cancel)",
"change": "Change shortcut", "change": "Change shortcut",
"reset": "Reset", "reset": "Reset",
"shortcut": "Shortcut" "shortcut": "Shortcut",
"noShortcut": "No shortcut set"
} }
}, },
"changeCreds": { "changeCreds": {

View File

@ -4,7 +4,9 @@ import { useTranslation } from 'react-i18next';
import { useToolWorkflow } from '../../../../contexts/ToolWorkflowContext'; import { useToolWorkflow } from '../../../../contexts/ToolWorkflowContext';
import { useHotkeys } from '../../../../contexts/HotkeyContext'; import { useHotkeys } from '../../../../contexts/HotkeyContext';
import HotkeyDisplay from '../../../hotkeys/HotkeyDisplay'; import HotkeyDisplay from '../../../hotkeys/HotkeyDisplay';
import { bindingEquals, eventToBinding } from '../../../../utils/hotkeys'; import { bindingEquals, eventToBinding, HotkeyBinding } from '../../../../utils/hotkeys';
import { ToolId } from 'src/types/toolId';
import { ToolRegistryEntry } from 'src/data/toolsTaxonomy';
const rowStyle: React.CSSProperties = { const rowStyle: React.CSSProperties = {
display: 'flex', display: 'flex',
@ -24,10 +26,10 @@ const HotkeysSection: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { toolRegistry } = useToolWorkflow(); const { toolRegistry } = useToolWorkflow();
const { hotkeys, defaults, updateHotkey, resetHotkey, pauseHotkeys, resumeHotkeys, getDisplayParts, isMac } = useHotkeys(); const { hotkeys, defaults, updateHotkey, resetHotkey, pauseHotkeys, resumeHotkeys, getDisplayParts, isMac } = useHotkeys();
const [editingTool, setEditingTool] = useState<string | null>(null); const [editingTool, setEditingTool] = useState<ToolId | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const tools = useMemo(() => Object.entries(toolRegistry), [toolRegistry]); const tools = useMemo(() => Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][], [toolRegistry]);
useEffect(() => { useEffect(() => {
if (!editingTool) { if (!editingTool) {
@ -64,7 +66,7 @@ const HotkeysSection: React.FC = () => {
return; return;
} }
const conflictEntry = Object.entries(hotkeys).find(([toolId, existing]) => ( const conflictEntry = (Object.entries(hotkeys) as [ToolId, HotkeyBinding][]).find(([toolId, existing]) => (
toolId !== editingTool && bindingEquals(existing, binding) toolId !== editingTool && bindingEquals(existing, binding)
)); ));
@ -85,7 +87,7 @@ const HotkeysSection: React.FC = () => {
}; };
}, [editingTool, hotkeys, toolRegistry, updateHotkey, t]); }, [editingTool, hotkeys, toolRegistry, updateHotkey, t]);
const handleStartCapture = (toolId: string) => { const handleStartCapture = (toolId: ToolId) => {
setEditingTool(toolId); setEditingTool(toolId);
setError(null); setError(null);
}; };

View File

@ -7,9 +7,10 @@ import { useToolSections } from '../../hooks/useToolSections';
import SubcategoryHeader from './shared/SubcategoryHeader'; import SubcategoryHeader from './shared/SubcategoryHeader';
import NoToolsFound from './shared/NoToolsFound'; import NoToolsFound from './shared/NoToolsFound';
import "./toolPicker/ToolPicker.css"; import "./toolPicker/ToolPicker.css";
import { ToolId } from 'src/types/toolId';
interface SearchResultsProps { interface SearchResultsProps {
filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>; filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>;
onSelect: (id: string) => void; onSelect: (id: string) => void;
searchQuery?: string; searchQuery?: string;
} }

View File

@ -6,11 +6,12 @@ import "./toolPicker/ToolPicker.css";
import { useToolSections } from "../../hooks/useToolSections"; import { useToolSections } from "../../hooks/useToolSections";
import NoToolsFound from "./shared/NoToolsFound"; import NoToolsFound from "./shared/NoToolsFound";
import { renderToolButtons } from "./shared/renderToolButtons"; import { renderToolButtons } from "./shared/renderToolButtons";
import { ToolId } from "src/types/toolId";
interface ToolPickerProps { interface ToolPickerProps {
selectedToolKey: string | null; selectedToolKey: string | null;
onSelect: (id: string) => void; onSelect: (id: string) => void;
filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>; filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>;
isSearching?: boolean; isSearching?: boolean;
} }

View File

@ -2,9 +2,10 @@ import { Suspense } from "react";
import { useToolWorkflow } from "../../contexts/ToolWorkflowContext"; import { useToolWorkflow } from "../../contexts/ToolWorkflowContext";
import { BaseToolProps } from "../../types/tool"; import { BaseToolProps } from "../../types/tool";
import ToolLoadingFallback from "./ToolLoadingFallback"; import ToolLoadingFallback from "./ToolLoadingFallback";
import { ToolId } from "src/types/toolId";
interface ToolRendererProps extends BaseToolProps { interface ToolRendererProps extends BaseToolProps {
selectedToolKey: string; selectedToolKey: ToolId;
} }

View File

@ -6,6 +6,7 @@ import { useToolSections } from '../../../hooks/useToolSections';
import { renderToolButtons } from '../shared/renderToolButtons'; import { renderToolButtons } from '../shared/renderToolButtons';
import ToolSearch from '../toolPicker/ToolSearch'; import ToolSearch from '../toolPicker/ToolSearch';
import ToolButton from '../toolPicker/ToolButton'; import ToolButton from '../toolPicker/ToolButton';
import { ToolId } from 'src/types/toolId';
interface ToolSelectorProps { interface ToolSelectorProps {
onSelect: (toolKey: string) => void; onSelect: (toolKey: string) => void;
@ -30,7 +31,7 @@ export default function ToolSelector({
// Filter out excluded tools (like 'automate' itself) and tools that don't support automation // Filter out excluded tools (like 'automate' itself) and tools that don't support automation
const baseFilteredTools = useMemo(() => { const baseFilteredTools = useMemo(() => {
return Object.entries(toolRegistry).filter(([key, tool]) => return (Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][]).filter(([key, tool]) =>
!excludeTools.includes(key) && getToolSupportsAutomate(tool) !excludeTools.includes(key) && getToolSupportsAutomate(tool)
); );
}, [toolRegistry, excludeTools]); }, [toolRegistry, excludeTools]);
@ -66,7 +67,7 @@ export default function ToolSelector({
}, [filteredTools]); }, [filteredTools]);
// Use the same tool sections logic as the main ToolPicker // Use the same tool sections logic as the main ToolPicker
const { sections, searchGroups } = useToolSections(transformedFilteredTools); const { sections, searchGroups } = useToolSections(transformedFilteredTools as any /* FIX ME */);
// Determine what to display: search results or organized sections // Determine what to display: search results or organized sections
const isSearching = searchTerm.trim().length > 0; const isSearching = searchTerm.trim().length > 0;
@ -156,7 +157,7 @@ export default function ToolSelector({
// Show selected tool in AutomationEntry style when tool is selected and dropdown closed // Show selected tool in AutomationEntry style when tool is selected and dropdown closed
<div onClick={handleSearchFocus} style={{ cursor: 'pointer', <div onClick={handleSearchFocus} style={{ cursor: 'pointer',
borderRadius: "var(--mantine-radius-lg)" }}> borderRadius: "var(--mantine-radius-lg)" }}>
<ToolButton id='tool' tool={toolRegistry[selectedValue]} isSelected={false} <ToolButton id={'tool' as any /* FIX ME */} tool={toolRegistry[selectedValue]} isSelected={false}
onSelect={()=>{}} rounded={true} disableNavigation={true}></ToolButton> onSelect={()=>{}} rounded={true} disableNavigation={true}></ToolButton>
</div> </div>
) : ( ) : (

View File

@ -5,13 +5,14 @@ import SubcategoryHeader from './SubcategoryHeader';
import { getSubcategoryLabel } from "../../../data/toolsTaxonomy"; import { getSubcategoryLabel } from "../../../data/toolsTaxonomy";
import { TFunction } from 'i18next'; import { TFunction } from 'i18next';
import { SubcategoryGroup } from '../../../hooks/useToolSections'; import { SubcategoryGroup } from '../../../hooks/useToolSections';
import { ToolId } from 'src/types/toolId';
// Helper function to render tool buttons for a subcategory // Helper function to render tool buttons for a subcategory
export const renderToolButtons = ( export const renderToolButtons = (
t: TFunction, t: TFunction,
subcategory: SubcategoryGroup, subcategory: SubcategoryGroup,
selectedToolKey: string | null, selectedToolKey: string | null,
onSelect: (id: string) => void, onSelect: (id: ToolId) => void,
showSubcategoryHeader: boolean = true, showSubcategoryHeader: boolean = true,
disableNavigation: boolean = false, disableNavigation: boolean = false,
searchResults?: Array<{ item: [string, any]; matchedText?: string }> searchResults?: Array<{ item: [string, any]; matchedText?: string }>

View File

@ -9,12 +9,13 @@ import { handleUnlessSpecialClick } from "../../../utils/clickHandlers";
import FitText from "../../shared/FitText"; import FitText from "../../shared/FitText";
import { useHotkeys } from "../../../contexts/HotkeyContext"; import { useHotkeys } from "../../../contexts/HotkeyContext";
import HotkeyDisplay from "../../hotkeys/HotkeyDisplay"; import HotkeyDisplay from "../../hotkeys/HotkeyDisplay";
import { ToolId } from "src/types/toolId";
interface ToolButtonProps { interface ToolButtonProps {
id: string; id: ToolId;
tool: ToolRegistryEntry; tool: ToolRegistryEntry;
isSelected: boolean; isSelected: boolean;
onSelect: (id: string) => void; onSelect: (id: ToolId) => void;
rounded?: boolean; rounded?: boolean;
disableNavigation?: boolean; disableNavigation?: boolean;
matchedSynonym?: string; matchedSynonym?: string;
@ -28,7 +29,7 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
const binding = hotkeys[id]; const binding = hotkeys[id];
const { getToolNavigation } = useToolNavigation(); const { getToolNavigation } = useToolNavigation();
const handleClick = (id: string) => { const handleClick = (id: ToolId) => {
if (isUnavailable) return; if (isUnavailable) return;
if (tool.link) { if (tool.link) {
// Open external link in new tab // Open external link in new tab
@ -47,12 +48,16 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
: ( : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
<span>{tool.description}</span> <span>{tool.description}</span>
{binding && ( <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.75rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.75rem' }}> {binding ? (
<>
<span style={{ color: 'var(--mantine-color-dimmed)', fontWeight: 500 }}>{t('settings.hotkeys.shortcut', 'Shortcut')}</span> <span style={{ color: 'var(--mantine-color-dimmed)', fontWeight: 500 }}>{t('settings.hotkeys.shortcut', 'Shortcut')}</span>
<HotkeyDisplay binding={binding} /> <HotkeyDisplay binding={binding} />
</div> </>
) : (
<span style={{ color: 'var(--mantine-color-dimmed)', fontWeight: 500, fontStyle: 'italic' }}>{t('settings.hotkeys.noShortcut', 'No shortcut set')}</span>
)} )}
</div>
</div> </div>
); );

View File

@ -2,14 +2,17 @@ import React, { createContext, useCallback, useContext, useEffect, useMemo, useS
import { HotkeyBinding, bindingEquals, bindingMatchesEvent, deserializeBindings, getDisplayParts, isMacLike, normalizeBinding, serializeBindings } from '../utils/hotkeys'; import { HotkeyBinding, bindingEquals, bindingMatchesEvent, deserializeBindings, getDisplayParts, isMacLike, normalizeBinding, serializeBindings } from '../utils/hotkeys';
import { useToolWorkflow } from './ToolWorkflowContext'; import { useToolWorkflow } from './ToolWorkflowContext';
import { ToolId } from '../types/toolId'; import { ToolId } from '../types/toolId';
import { ToolCategoryId, ToolRegistryEntry } from '../data/toolsTaxonomy';
type Bindings = Partial<Record<ToolId, HotkeyBinding>>;
interface HotkeyContextValue { interface HotkeyContextValue {
hotkeys: Record<string, HotkeyBinding>; hotkeys: Bindings;
defaults: Record<string, HotkeyBinding>; defaults: Bindings;
isMac: boolean; isMac: boolean;
updateHotkey: (toolId: string, binding: HotkeyBinding) => void; updateHotkey: (toolId: ToolId, binding: HotkeyBinding) => void;
resetHotkey: (toolId: string) => void; resetHotkey: (toolId: ToolId) => void;
isBindingAvailable: (binding: HotkeyBinding, excludeToolId?: string) => boolean; isBindingAvailable: (binding: HotkeyBinding, excludeToolId?: ToolId) => boolean;
pauseHotkeys: () => void; pauseHotkeys: () => void;
resumeHotkeys: () => void; resumeHotkeys: () => void;
areHotkeysPaused: boolean; areHotkeysPaused: boolean;
@ -20,46 +23,29 @@ const HotkeyContext = createContext<HotkeyContextValue | undefined>(undefined);
const STORAGE_KEY = 'stirlingpdf.hotkeys'; const STORAGE_KEY = 'stirlingpdf.hotkeys';
const KEY_ORDER: string[] = [ const generateDefaultHotkeys = (toolEntries: [ToolId, ToolRegistryEntry][], macLike: boolean): Bindings => {
'Digit1', 'Digit2', 'Digit3', 'Digit4', 'Digit5', 'Digit6', 'Digit7', 'Digit8', 'Digit9', 'Digit0', const defaults: Bindings = {};
'KeyQ', 'KeyW', 'KeyE', 'KeyR', 'KeyT', 'KeyY', 'KeyU', 'KeyI', 'KeyO', 'KeyP',
'KeyA', 'KeyS', 'KeyD', 'KeyF', 'KeyG', 'KeyH', 'KeyJ', 'KeyK', 'KeyL',
'KeyZ', 'KeyX', 'KeyC', 'KeyV', 'KeyB', 'KeyN', 'KeyM',
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
];
const generateDefaultHotkeys = (toolIds: string[], macLike: boolean): Record<string, HotkeyBinding> => { // Get Quick Access tools (RECOMMENDED_TOOLS category) from registry
const defaults: Record<string, HotkeyBinding> = {}; const quickAccessTools = toolEntries
let index = 0; .filter(([_, tool]) => tool.categoryId === ToolCategoryId.RECOMMENDED_TOOLS)
let useShift = false; .map(([toolId, _]) => toolId);
const nextBinding = (): HotkeyBinding => { // Assign Cmd+Option+Number (Mac) or Ctrl+Alt+Number (Windows) to Quick Access tools
if (index >= KEY_ORDER.length) { quickAccessTools.forEach((toolId, index) => {
index = 0; if (index < 9) { // Limit to Digit1-9
if (!useShift) { const digitNumber = index + 1;
useShift = true; defaults[toolId] = {
} else { code: `Digit${digitNumber}`,
// If we somehow run out of combinations, wrap back around (unlikely given tool count) alt: true,
useShift = false; shift: false,
} meta: macLike,
ctrl: !macLike,
};
} }
const code = KEY_ORDER[index];
index += 1;
return {
code,
alt: true,
shift: useShift,
meta: macLike,
ctrl: !macLike,
};
};
toolIds.forEach(toolId => {
defaults[toolId] = nextBinding();
}); });
// All other tools have no default (will be undefined in the record)
return defaults; return defaults;
}; };
@ -74,7 +60,7 @@ const shouldIgnoreTarget = (target: EventTarget | null): boolean => {
export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { toolRegistry, handleToolSelect } = useToolWorkflow(); const { toolRegistry, handleToolSelect } = useToolWorkflow();
const isMac = useMemo(() => isMacLike(), []); const isMac = useMemo(() => isMacLike(), []);
const [customBindings, setCustomBindings] = useState<Record<string, HotkeyBinding>>(() => { const [customBindings, setCustomBindings] = useState<Bindings>(() => {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return {}; return {};
} }
@ -82,16 +68,16 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
}); });
const [areHotkeysPaused, setHotkeysPaused] = useState(false); const [areHotkeysPaused, setHotkeysPaused] = useState(false);
const toolIds = useMemo(() => Object.keys(toolRegistry), [toolRegistry]); const toolEntries = useMemo(() => Object.entries(toolRegistry), [toolRegistry]) as [ToolId, ToolRegistryEntry][];
const defaults = useMemo(() => generateDefaultHotkeys(toolIds, isMac), [toolIds, isMac]); const defaults = useMemo(() => generateDefaultHotkeys(toolEntries, isMac), [toolRegistry, isMac]);
// Remove bindings for tools that are no longer present // Remove bindings for tools that are no longer present
useEffect(() => { useEffect(() => {
setCustomBindings(prev => { setCustomBindings(prev => {
const next: Record<string, HotkeyBinding> = {}; const next: Bindings = {};
let changed = false; let changed = false;
Object.entries(prev).forEach(([toolId, binding]) => { (Object.entries(prev) as [ToolId, HotkeyBinding][]).forEach(([toolId, binding]) => {
if (toolRegistry[toolId]) { if (toolRegistry[toolId]) {
next[toolId] = binding; next[toolId] = binding;
} else { } else {
@ -103,13 +89,21 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
}, [toolRegistry]); }, [toolRegistry]);
const resolved = useMemo(() => { const resolved = useMemo(() => {
const merged: Record<string, HotkeyBinding> = {}; const merged: Bindings = {};
toolIds.forEach(toolId => { toolEntries.forEach(([toolId, _]) => {
const custom = customBindings[toolId]; const custom = customBindings[toolId];
merged[toolId] = custom ? normalizeBinding(custom) : defaults[toolId]; const defaultBinding = defaults[toolId];
// Only add to resolved if there's a custom binding or a default binding
if (custom) {
merged[toolId] = normalizeBinding(custom);
} else if (defaultBinding) {
merged[toolId] = defaultBinding;
}
// If neither exists, don't add to merged (tool has no hotkey)
}); });
return merged; return merged;
}, [customBindings, defaults, toolIds]); }, [customBindings, defaults, toolEntries]);
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
@ -118,7 +112,7 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
window.localStorage.setItem(STORAGE_KEY, serializeBindings(customBindings)); window.localStorage.setItem(STORAGE_KEY, serializeBindings(customBindings));
}, [customBindings]); }, [customBindings]);
const isBindingAvailable = useCallback((binding: HotkeyBinding, excludeToolId?: string) => { const isBindingAvailable = useCallback((binding: HotkeyBinding, excludeToolId?: ToolId) => {
const normalized = normalizeBinding(binding); const normalized = normalizeBinding(binding);
return Object.entries(resolved).every(([toolId, existing]) => { return Object.entries(resolved).every(([toolId, existing]) => {
if (toolId === excludeToolId) { if (toolId === excludeToolId) {
@ -128,7 +122,7 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
}); });
}, [resolved]); }, [resolved]);
const updateHotkey = useCallback((toolId: string, binding: HotkeyBinding) => { const updateHotkey = useCallback((toolId: ToolId, binding: HotkeyBinding) => {
setCustomBindings(prev => { setCustomBindings(prev => {
const normalized = normalizeBinding(binding); const normalized = normalizeBinding(binding);
const defaultsForTool = defaults[toolId]; const defaultsForTool = defaults[toolId];
@ -142,7 +136,7 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
}); });
}, [defaults]); }, [defaults]);
const resetHotkey = useCallback((toolId: string) => { const resetHotkey = useCallback((toolId: ToolId) => {
setCustomBindings(prev => { setCustomBindings(prev => {
if (!(toolId in prev)) { if (!(toolId in prev)) {
return prev; return prev;
@ -165,12 +159,12 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
if (event.repeat) return; if (event.repeat) return;
if (shouldIgnoreTarget(event.target)) return; if (shouldIgnoreTarget(event.target)) return;
const entries = Object.entries(resolved) as Array<[string, HotkeyBinding]>; const entries = Object.entries(resolved) as [ToolId, HotkeyBinding][];
for (const [toolId, binding] of entries) { for (const [toolId, binding] of entries) {
if (bindingMatchesEvent(binding, event)) { if (bindingMatchesEvent(binding, event)) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
handleToolSelect(toolId as ToolId); handleToolSelect(toolId);
break; break;
} }
} }

View File

@ -73,10 +73,10 @@ function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowActio
// Context value interface // Context value interface
interface ToolWorkflowContextValue extends ToolWorkflowState { interface ToolWorkflowContextValue extends ToolWorkflowState {
// Tool management (from hook) // Tool management (from hook)
selectedToolKey: string | null; selectedToolKey: ToolId | null;
selectedTool: ToolRegistryEntry | null; selectedTool: ToolRegistryEntry | null;
toolRegistry: Record<string, ToolRegistryEntry>; toolRegistry: Partial<ToolRegistry>;
getSelectedTool: (toolId: string | null) => ToolRegistryEntry | null; getSelectedTool: (toolId: ToolId | null) => ToolRegistryEntry | null;
// UI Actions // UI Actions
setSidebarsVisible: (visible: boolean) => void; setSidebarsVisible: (visible: boolean) => void;
@ -101,7 +101,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
handleReaderToggle: () => void; handleReaderToggle: () => void;
// Computed values // Computed values
filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>; // Filtered by search filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>; // Filtered by search
isPanelVisible: boolean; isPanelVisible: boolean;
} }

View File

@ -824,7 +824,7 @@ export function useFlatToolRegistry(): ToolRegistry {
name: t("home.compare.title", "Compare"), name: t("home.compare.title", "Compare"),
component: null, component: null,
description: t("home.compare.desc", "Compare two PDF documents and highlight differences"), description: t("home.compare.desc", "Compare two PDF documents and highlight differences"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS /* TODO: Change to RECOMMENDED_TOOLS when component is implemented */,
subcategoryId: SubcategoryId.GENERAL, subcategoryId: SubcategoryId.GENERAL,
synonyms: getSynonyms(t, "compare"), synonyms: getSynonyms(t, "compare"),
supportsAutomate: false, supportsAutomate: false,

View File

@ -2,6 +2,7 @@ import { useCallback } from 'react';
import { useToolNavigation } from './useToolNavigation'; import { useToolNavigation } from './useToolNavigation';
import { useToolWorkflow } from '../contexts/ToolWorkflowContext'; import { useToolWorkflow } from '../contexts/ToolWorkflowContext';
import { handleUnlessSpecialClick } from '../utils/clickHandlers'; import { handleUnlessSpecialClick } from '../utils/clickHandlers';
import { ToolId } from 'src/types/toolId';
export interface SidebarNavigationProps { export interface SidebarNavigationProps {
/** Full URL for the navigation (for href attribute) */ /** Full URL for the navigation (for href attribute) */
@ -16,7 +17,7 @@ export interface SidebarNavigationProps {
*/ */
export function useSidebarNavigation(): { export function useSidebarNavigation(): {
getHomeNavigation: () => SidebarNavigationProps; getHomeNavigation: () => SidebarNavigationProps;
getToolNavigation: (toolId: string) => SidebarNavigationProps | null; getToolNavigation: (toolId: ToolId) => SidebarNavigationProps | null;
} { } {
const { getToolNavigation: getToolNavProps } = useToolNavigation(); const { getToolNavigation: getToolNavProps } = useToolNavigation();
const { getSelectedTool } = useToolWorkflow(); const { getSelectedTool } = useToolWorkflow();
@ -32,7 +33,7 @@ export function useSidebarNavigation(): {
return { href, onClick: defaultNavClick }; return { href, onClick: defaultNavClick };
}, [defaultNavClick]); }, [defaultNavClick]);
const getToolNavigation = useCallback((toolId: string): SidebarNavigationProps | null => { const getToolNavigation = useCallback((toolId: ToolId): SidebarNavigationProps | null => {
// Handle special nav sections that aren't tools // Handle special nav sections that aren't tools
if (toolId === 'read') return { href: '/read', onClick: defaultNavClick }; if (toolId === 'read') return { href: '/read', onClick: defaultNavClick };
if (toolId === 'automate') return { href: '/automate', onClick: defaultNavClick }; if (toolId === 'automate') return { href: '/automate', onClick: defaultNavClick };

View File

@ -1,16 +1,17 @@
import { useState, useCallback, useMemo } from 'react'; import { useState, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry"; import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
import { getAllEndpoints, type ToolRegistryEntry } from "../data/toolsTaxonomy"; import { getAllEndpoints, type ToolRegistryEntry, type ToolRegistry } from "../data/toolsTaxonomy";
import { useMultipleEndpointsEnabled } from "./useEndpointConfig"; import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
import { FileId } from '../types/file'; import { FileId } from '../types/file';
import { ToolId } from 'src/types/toolId';
interface ToolManagementResult { interface ToolManagementResult {
selectedTool: ToolRegistryEntry | null; selectedTool: ToolRegistryEntry | null;
toolSelectedFileIds: FileId[]; toolSelectedFileIds: FileId[];
toolRegistry: Record<string, ToolRegistryEntry>; toolRegistry: Partial<ToolRegistry>;
setToolSelectedFileIds: (fileIds: FileId[]) => void; setToolSelectedFileIds: (fileIds: FileId[]) => void;
getSelectedTool: (toolKey: string | null) => ToolRegistryEntry | null; getSelectedTool: (toolKey: ToolId | null) => ToolRegistryEntry | null;
} }
export const useToolManagement = (): ToolManagementResult => { export const useToolManagement = (): ToolManagementResult => {
@ -30,9 +31,9 @@ export const useToolManagement = (): ToolManagementResult => {
return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true); return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true);
}, [endpointsLoading, endpointStatus, baseRegistry]); }, [endpointsLoading, endpointStatus, baseRegistry]);
const toolRegistry: Record<string, ToolRegistryEntry> = useMemo(() => { const toolRegistry: Partial<ToolRegistry> = useMemo(() => {
const availableToolRegistry: Record<string, ToolRegistryEntry> = {}; const availableToolRegistry: Partial<ToolRegistry> = {};
Object.keys(baseRegistry).forEach(toolKey => { (Object.keys(baseRegistry) as ToolId[]).forEach(toolKey => {
if (isToolAvailable(toolKey)) { if (isToolAvailable(toolKey)) {
const baseTool = baseRegistry[toolKey as keyof typeof baseRegistry]; const baseTool = baseRegistry[toolKey as keyof typeof baseRegistry];
availableToolRegistry[toolKey] = { availableToolRegistry[toolKey] = {
@ -45,7 +46,7 @@ export const useToolManagement = (): ToolManagementResult => {
return availableToolRegistry; return availableToolRegistry;
}, [isToolAvailable, t, baseRegistry]); }, [isToolAvailable, t, baseRegistry]);
const getSelectedTool = useCallback((toolKey: string | null): ToolRegistryEntry | null => { const getSelectedTool = useCallback((toolKey: ToolId | null): ToolRegistryEntry | null => {
return toolKey ? toolRegistry[toolKey] || null : null; return toolKey ? toolRegistry[toolKey] || null : null;
}, [toolRegistry]); }, [toolRegistry]);

View File

@ -2,9 +2,10 @@ import { useMemo } from 'react';
import { SUBCATEGORY_ORDER, SubcategoryId, ToolCategoryId, ToolRegistryEntry } from '../data/toolsTaxonomy'; import { SUBCATEGORY_ORDER, SubcategoryId, ToolCategoryId, ToolRegistryEntry } from '../data/toolsTaxonomy';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ToolId } from 'src/types/toolId';
type SubcategoryIdMap = { type SubcategoryIdMap = {
[subcategoryId in SubcategoryId]: Array<{ id: string /* FIX ME: Should be ToolId */; tool: ToolRegistryEntry }>; [subcategoryId in SubcategoryId]: Array<{ id: ToolId; tool: ToolRegistryEntry }>;
} }
type GroupedTools = { type GroupedTools = {
@ -14,7 +15,7 @@ type GroupedTools = {
export interface SubcategoryGroup { export interface SubcategoryGroup {
subcategoryId: SubcategoryId; subcategoryId: SubcategoryId;
tools: { tools: {
id: string /* FIX ME: Should be ToolId */; id: ToolId;
tool: ToolRegistryEntry; tool: ToolRegistryEntry;
}[]; }[];
}; };
@ -28,7 +29,7 @@ export interface ToolSection {
}; };
export function useToolSections( export function useToolSections(
filteredTools: Array<{ item: [string /* FIX ME: Should be ToolId */, ToolRegistryEntry]; matchedText?: string }>, filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>,
searchQuery?: string searchQuery?: string
) { ) {
const { t } = useTranslation(); const { t } = useTranslation();
@ -107,9 +108,9 @@ export function useToolSections(
} }
const subMap = {} as SubcategoryIdMap; const subMap = {} as SubcategoryIdMap;
const seen = new Set<string /* FIX ME: Should be ToolId */>(); const seen = new Set<ToolId>();
filteredTools.forEach(({ item: [id, tool] }) => { filteredTools.forEach(({ item: [id, tool] }) => {
const toolId = id as string /* FIX ME: Should be ToolId */; const toolId = id as ToolId;
if (seen.has(toolId)) return; if (seen.has(toolId)) return;
seen.add(toolId); seen.add(toolId);
const sub = tool.subcategoryId; const sub = tool.subcategoryId;

View File

@ -1,8 +1,9 @@
import { ToolId } from "src/types/toolId";
import { ToolRegistryEntry } from "../data/toolsTaxonomy"; import { ToolRegistryEntry } from "../data/toolsTaxonomy";
import { scoreMatch, minScoreForQuery, normalizeForSearch } from "./fuzzySearch"; import { scoreMatch, minScoreForQuery, normalizeForSearch } from "./fuzzySearch";
export interface RankedToolItem { export interface RankedToolItem {
item: [string, ToolRegistryEntry]; item: [ToolId, ToolRegistryEntry];
matchedText?: string; matchedText?: string;
} }
@ -10,18 +11,18 @@ export function filterToolRegistryByQuery(
toolRegistry: Record<string, ToolRegistryEntry>, toolRegistry: Record<string, ToolRegistryEntry>,
query: string query: string
): RankedToolItem[] { ): RankedToolItem[] {
const entries = Object.entries(toolRegistry); const entries = Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][];
if (!query.trim()) { if (!query.trim()) {
return entries.map(([id, tool]) => ({ item: [id, tool] as [string, ToolRegistryEntry] })); return entries.map(([id, tool]) => ({ item: [id, tool] as [ToolId, ToolRegistryEntry] }));
} }
const nq = normalizeForSearch(query); const nq = normalizeForSearch(query);
const threshold = minScoreForQuery(query); const threshold = minScoreForQuery(query);
const exactName: Array<{ id: string; tool: ToolRegistryEntry; pos: number }> = []; const exactName: Array<{ id: ToolId; tool: ToolRegistryEntry; pos: number }> = [];
const exactSyn: Array<{ id: string; tool: ToolRegistryEntry; text: string; pos: number }> = []; const exactSyn: Array<{ id: ToolId; tool: ToolRegistryEntry; text: string; pos: number }> = [];
const fuzzyName: Array<{ id: string; tool: ToolRegistryEntry; score: number; text: string }> = []; const fuzzyName: Array<{ id: ToolId; tool: ToolRegistryEntry; score: number; text: string }> = [];
const fuzzySyn: Array<{ id: string; tool: ToolRegistryEntry; score: number; text: string }> = []; const fuzzySyn: Array<{ id: ToolId; tool: ToolRegistryEntry; score: number; text: string }> = [];
for (const [id, tool] of entries) { for (const [id, tool] of entries) {
const nameNorm = normalizeForSearch(tool.name || ''); const nameNorm = normalizeForSearch(tool.name || '');
@ -78,7 +79,7 @@ export function filterToolRegistryByQuery(
const seen = new Set<string>(); const seen = new Set<string>();
const ordered: RankedToolItem[] = []; const ordered: RankedToolItem[] = [];
const push = (id: string, tool: ToolRegistryEntry, matchedText?: string) => { const push = (id: ToolId, tool: ToolRegistryEntry, matchedText?: string) => {
if (seen.has(id)) return; if (seen.has(id)) return;
seen.add(id); seen.add(id);
ordered.push({ item: [id, tool], matchedText }); ordered.push({ item: [id, tool], matchedText });
@ -92,7 +93,7 @@ export function filterToolRegistryByQuery(
if (ordered.length > 0) return ordered; if (ordered.length > 0) return ordered;
// Fallback: return everything unchanged // Fallback: return everything unchanged
return entries.map(([id, tool]) => ({ item: [id, tool] as [string, ToolRegistryEntry] })); return entries.map(([id, tool]) => ({ item: [id, tool] as [ToolId, ToolRegistryEntry] }));
} }