"use client"; import { ArrowRight, LifeBuoyIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, SidebarMenuButton, } from "../../../../shadcnui"; import { Modules } from "../../../../core"; import { DataListRetriever, useDataListRetriever, useDebounce } from "../../../../hooks"; import { HowTo } from "../../data/HowTo"; import { HowToInterface } from "../../data/HowToInterface"; import { HowToService } from "../../data/HowToService"; import HowToCommandViewer from "./HowToCommandViewer"; function matchPage(pathname: string, pattern: string): boolean { if (pattern.includes(":")) { const pathSegments = pathname.split("/").filter(Boolean); const patternSegments = pattern.split("/").filter(Boolean); if (pathSegments.length !== patternSegments.length) return false; return patternSegments.every((seg, i) => (seg.startsWith(":") ? true : seg === pathSegments[i])); } return pathname.toLowerCase().includes(pattern.toLowerCase()); } type HowToCommandProps = { /** Current pathname for page relevance matching */ pathname: string; /** Optional extra command groups to render when not searching */ extraGroups?: ReactNode; /** Called when user starts a chat from the viewer */ onStartChat?: () => void; }; export default function HowToCommand({ pathname, extraGroups, onStartChat }: HowToCommandProps) { const t = useTranslations(); const [dialogOpen, setDialogOpen] = useState(false); const [selectedHowTo, setSelectedHowTo] = useState(null); const searchTermRef = useRef(""); const [searchTerm, setSearchTerm] = useState(""); const data: DataListRetriever = useDataListRetriever({ retriever: (params) => { return HowToService.findMany(params); }, retrieverParams: {}, module: Modules.HowTo, }); // Split HowTos into relevant (matching current page) and others const { relevantHowTos, otherHowTos } = useMemo(() => { if (!data.data) return { relevantHowTos: [], otherHowTos: [] }; const relevant: HowToInterface[] = []; const other: HowToInterface[] = []; (data.data as HowToInterface[]).forEach((howTo) => { const pages = HowTo.parsePagesFromString(howTo.pages); const isRelevant = pages.some((page) => page && matchPage(pathname, page)); if (isRelevant) { relevant.push(howTo); } else { other.push(howTo); } }); return { relevantHowTos: relevant, otherHowTos: other }; }, [data.data, pathname]); const search = useCallback( async (searchedTerm: string) => { if (searchedTerm === searchTermRef.current) return; searchTermRef.current = searchedTerm; await data.search(searchedTerm); }, [searchTermRef, data], ); const updateSearchTerm = useDebounce(search, 500); useEffect(() => { updateSearchTerm(searchTerm); }, [updateSearchTerm, searchTerm]); // Reset search when dialog closes useEffect(() => { if (!dialogOpen) { setSearchTerm(""); searchTermRef.current = ""; } }, [dialogOpen]); // Keyboard shortcut: Cmd+K or Ctrl+K to toggle dialog useEffect(() => { const down = (e: KeyboardEvent) => { if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setDialogOpen((open) => !open); } }; document.addEventListener("keydown", down); return () => document.removeEventListener("keydown", down); }, []); const handleStartChat = () => { setDialogOpen(false); setSelectedHowTo(null); if (onStartChat) onStartChat(); }; return ( <> setDialogOpen(true)} className="text-muted-foreground" > {t(`howto.command.trigger`)} {/* Search HowTos Dialog */} { setDialogOpen(open); if (!open) setSelectedHowTo(null); }} modal={true} > {selectedHowTo ? selectedHowTo.name : t("howto.command.title")} {selectedHowTo ? ( setSelectedHowTo(null)} onStartChat={handleStartChat} /> ) : ( { if (e.key === "Escape") { setSearchTerm(""); searchTermRef.current = ""; } }} onValueChange={setSearchTerm} value={searchTerm} placeholder={t(`howto.search.placeholder`)} /> {t(`howto.command.empty`)} {/* App-specific extra groups (support, tours, etc.) */} {!searchTerm && extraGroups} {/* Relevant to this page */} {relevantHowTos.length > 0 && ( {relevantHowTos.map((howTo: HowToInterface) => ( setSelectedHowTo(howTo)} className="cursor-pointer"> {howTo.name} ))} )} {/* All HowTos / Search Results */} {otherHowTos.length > 0 && ( {otherHowTos.map((howTo: HowToInterface) => ( setSelectedHowTo(howTo)} className="cursor-pointer"> {howTo.name} ))} )} {/* Keyboard hints footer */}
{t(`howto.command.keyboard.select`)} ↑↓ {t(`howto.command.keyboard.navigate`)} {t(`howto.keyboard.esc`)} {t(`howto.command.keyboard.close`)}
)}
); }