import * as React from "react" import * as DialogPrimitive from "@radix-ui/react-dialog" import { Command } from "cmdk" import { Dialog, DialogPortal, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog" import { CommandGroup } from "./command-items" import { CommandAction, CommandItemType, createCommandGroups } from "./command-data" import { useAccount } from "@/lib/providers/jazz-provider" import { searchSafeRegExp } from "@/lib/utils" import { GraphNode } from "@/components/routes/public/PublicHomeRoute" import { useCommandActions } from "./hooks/use-command-actions" import { atom, useAtom } from "jotai" const graph_data_promise = import("@/components/routes/public/graph-data.json").then(a => a.default) const filterItems = (items: CommandItemType[], searchRegex: RegExp) => items.filter(item => searchRegex.test(item.value)).slice(0, 10) export const commandPaletteOpenAtom = atom(false) export function CommandPalette() { const { me } = useAccount({ root: { personalLinks: [], personalPages: [] } }) const dialogRef = React.useRef(null) const [inputValue, setInputValue] = React.useState("") const [activePage, setActivePage] = React.useState("home") const [open, setOpen] = useAtom(commandPaletteOpenAtom) const actions = useCommandActions() const commandGroups = React.useMemo(() => me && createCommandGroups(actions, me), [actions, me]) const raw_graph_data = React.use(graph_data_promise) as GraphNode[] React.useEffect(() => { const down = (e: KeyboardEvent) => { if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault() setOpen(prev => !prev) } } document.addEventListener("keydown", down) return () => document.removeEventListener("keydown", down) }, [setOpen]) const bounce = React.useCallback(() => { if (dialogRef.current) { dialogRef.current.style.transform = "scale(0.99) translateX(-50%)" setTimeout(() => { if (dialogRef.current) { dialogRef.current.style.transform = "" } }, 100) } }, []) const handleKeyDown = React.useCallback( (e: React.KeyboardEvent) => { if (e.key === "Enter") { bounce() } if (activePage !== "home" && !inputValue && e.key === "Backspace") { e.preventDefault() setActivePage("home") setInputValue("") bounce() } }, [activePage, inputValue, bounce] ) const topics = React.useMemo( () => ({ heading: "Topics", items: raw_graph_data.map(topic => ({ icon: "Circle" as const, value: topic?.prettyName || "", label: topic?.prettyName || "", action: () => actions.navigateTo(`/${topic?.name}`) })) }), [raw_graph_data, actions] ) const personalLinks = React.useMemo( () => ({ heading: "Personal Links", items: me?.root.personalLinks?.map(link => ({ icon: "Link" as const, value: link?.title || "Untitled", label: link?.title || "Untitled", action: () => actions.openLinkInNewTab(link?.url || "#") })) || [] }), [me?.root.personalLinks, actions] ) const personalPages = React.useMemo( () => ({ heading: "Personal Pages", items: me?.root.personalPages?.map(page => ({ icon: "FileText" as const, value: page?.title || "Untitled", label: page?.title || "Untitled", action: () => actions.navigateTo(`/pages/${page?.id}`) })) || [] }), [me?.root.personalPages, actions] ) const getFilteredCommands = React.useCallback(() => { if (!commandGroups) return [] const searchRegex = searchSafeRegExp(inputValue) if (activePage === "home") { if (!inputValue) { // Only show items from the home object when there's no search input return commandGroups.home } // When there's a search input, search across all categories const allGroups = [...Object.values(commandGroups).flat(), personalLinks, personalPages, topics] return allGroups .map(group => ({ heading: group.heading, items: filterItems(group.items, searchRegex) })) .filter(group => group.items.length > 0) } // Handle other active pages (searchLinks, searchPages, etc.) switch (activePage) { case "searchLinks": return [...commandGroups.searchLinks, { items: filterItems(personalLinks.items, searchRegex) }] case "searchPages": return [...commandGroups.searchPages, { items: filterItems(personalPages.items, searchRegex) }] default: const pageCommands = commandGroups[activePage] if (!inputValue) return pageCommands return pageCommands .map(group => ({ heading: group.heading, items: filterItems(group.items, searchRegex) })) .filter(group => group.items.length > 0) } }, [inputValue, activePage, commandGroups, personalLinks, personalPages, topics]) const handleAction = React.useCallback( (action: CommandAction, payload?: any) => { const closeDialog = () => { setOpen(false) } if (typeof action === "function") { action() closeDialog() return } switch (action) { case "CHANGE_PAGE": if (payload) { setActivePage(payload) setInputValue("") bounce() } else { console.error(`Invalid page: ${payload}`) } break default: console.log(`Unhandled action: ${action}`) closeDialog() } }, [bounce, setOpen] ) const filteredCommands = React.useMemo(() => getFilteredCommands(), [getFilteredCommands]) const commandKey = React.useMemo(() => { return filteredCommands .map(group => { const itemsKey = group.items.map(item => `${item.label}-${item.action}`).join("|") return `${group.heading}:${itemsKey}` }) .join("__") }, [filteredCommands]) if (!me) return null return ( Command Palette Search for commands and actions
No results found. {filteredCommands.map((group, index, array) => ( ))}
) }