import React, { useMemo, useRef, useLayoutEffect, useState } from "react"; import { Box, Stack } from "@mantine/core"; import { useTranslation } from "react-i18next"; import { ToolRegistryEntry } from "../../data/toolsTaxonomy"; import "./toolPicker/ToolPicker.css"; import { useToolSections } from "../../hooks/useToolSections"; import NoToolsFound from "./shared/NoToolsFound"; import { renderToolButtons } from "./shared/renderToolButtons"; import Badge from "../shared/Badge"; import SubcategoryHeader from "./shared/SubcategoryHeader"; import ToolButton from "./toolPicker/ToolButton"; import { useToolWorkflow } from "../../contexts/ToolWorkflowContext"; import { ToolId } from "../../types/toolId"; interface ToolPickerProps { selectedToolKey: string | null; onSelect: (id: string) => void; filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>; isSearching?: boolean; } const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = false }: ToolPickerProps) => { const { t } = useTranslation(); const [quickHeaderHeight, setQuickHeaderHeight] = useState(0); const [allHeaderHeight, setAllHeaderHeight] = useState(0); const scrollableRef = useRef(null); const quickHeaderRef = useRef(null); const allHeaderRef = useRef(null); const quickAccessRef = useRef(null); const allToolsRef = useRef(null); // Keep header heights in sync with any dynamic size changes useLayoutEffect(() => { const update = () => { if (quickHeaderRef.current) { setQuickHeaderHeight(quickHeaderRef.current.offsetHeight || 0); } if (allHeaderRef.current) { setAllHeaderHeight(allHeaderRef.current.offsetHeight || 0); } }; update(); // Update on window resize window.addEventListener("resize", update); // Update on element resize (e.g., font load, badge count change, zoom) const observers: ResizeObserver[] = []; if (typeof ResizeObserver !== "undefined") { const observe = (el: HTMLDivElement | null, cb: () => void) => { if (!el) return; const ro = new ResizeObserver(() => cb()); ro.observe(el); observers.push(ro); }; observe(quickHeaderRef.current, update); observe(allHeaderRef.current, update); } return () => { window.removeEventListener("resize", update); observers.forEach(o => o.disconnect()); }; }, []); const { sections: visibleSections } = useToolSections(filteredTools); const { favoriteTools, toolRegistry } = useToolWorkflow(); const favoriteToolItems = useMemo(() => { return favoriteTools .map((toolId) => { const tool = (toolRegistry as any)[toolId as ToolId] as ToolRegistryEntry | undefined; return tool ? { id: toolId as string, tool } : null; }) .filter(Boolean) // Only include ready tools (component or link) and navigational exceptions .filter((item: any) => item && (item.tool.component || item.tool.link || item.id === 'read' || item.id === 'multiTool')) as Array<{ id: string; tool: ToolRegistryEntry }>; }, [favoriteTools, toolRegistry]); const quickSection = useMemo( () => visibleSections.find(s => s.key === 'quick'), [visibleSections] ); const recommendedItems = useMemo(() => { if (!quickSection) return [] as Array<{ id: string; tool: ToolRegistryEntry }>; const items: Array<{ id: string; tool: ToolRegistryEntry }> = []; quickSection.subcategories.forEach((sc: any) => sc.tools.forEach((toolEntry: any) => items.push(toolEntry))); return items.slice(0, 5); }, [quickSection]); const recommendedCount = useMemo(() => favoriteToolItems.length + recommendedItems.length, [favoriteToolItems.length, recommendedItems.length]); const allSection = useMemo( () => visibleSections.find(s => s.key === 'all'), [visibleSections] ); const scrollTo = (ref: React.RefObject) => { const container = scrollableRef.current; const target = ref.current; if (container && target) { const stackedOffset = ref === allToolsRef ? (quickHeaderHeight + allHeaderHeight) : quickHeaderHeight; const top = target.offsetTop - container.offsetTop - (stackedOffset || 0); container.scrollTo({ top: Math.max(0, top), behavior: "smooth" }); } }; // Build flat list by subcategory for search mode const { searchGroups } = useToolSections(isSearching ? filteredTools : []); return ( {isSearching ? ( {searchGroups.length === 0 ? ( ) : ( searchGroups.map(group => renderToolButtons(t, group, selectedToolKey, onSelect, true, false, filteredTools, true)) )} ) : ( <> {quickSection && ( <>
scrollTo(quickAccessRef)} > {t("toolPicker.quickAccess", "QUICK ACCESS")} {recommendedCount}
{favoriteToolItems.length > 0 && (
{favoriteToolItems.map(({ id, tool }) => ( ))}
)} {recommendedItems.length > 0 && (
{recommendedItems.map(({ id, tool }) => ( ))}
)}
)} {allSection && ( <>
scrollTo(allToolsRef)} > {t("toolPicker.allTools", "ALL TOOLS")} {allSection?.subcategories.reduce((acc, sc) => acc + sc.tools.length, 0)}
{allSection?.subcategories.map((sc: any) => renderToolButtons(t, sc, selectedToolKey, onSelect, true, false, undefined, true) )} )} {!quickSection && !allSection && } {/* bottom spacer to allow scrolling past the last row */}
)} ); }; export default ToolPicker;