"use client"; import { useIsMobile } from "@hooks/use-mobile"; import { fetchConsumerProProduct, fetchMemoriesFeature, } from "@repo/lib/queries"; import { Button } from "@repo/ui/components/button"; import { HeadingH2Bold } from "@repo/ui/text/heading/heading-h2-bold"; import { GlassMenuEffect } from "@ui/other/glass-effect"; import { useCustomer } from "autumn-js/react"; import { MessageSquareMore, Plus, Puzzle, User, X } from "lucide-react"; import { AnimatePresence, LayoutGroup, motion } from "motion/react"; import { useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; import { Drawer } from "vaul"; import { useMobilePanel } from "@/lib/mobile-panel-context"; import { TOUR_STEP_IDS } from "@/lib/tour-constants"; import { useChatOpen } from "@/stores"; import { ConnectAIModal } from "./connect-ai-modal"; import { ProjectSelector } from "./project-selector"; import { useTour } from "./tour"; import { AddMemoryExpandedView, AddMemoryView } from "./views/add-memory"; import { IntegrationsView } from "./views/integrations"; import { ProfileView } from "./views/profile"; const MCPIcon = ({ className }: { className?: string }) => { return ( ModelContextProtocol ); }; function Menu({ id }: { id?: string }) { const router = useRouter(); const searchParams = useSearchParams(); const openParam = searchParams.get("open"); // Valid view names that can be opened via URL parameter const validViews = [ "addUrl", "mcp", "projects", "profile", "integrations", ] as const; type ValidView = (typeof validViews)[number]; const [isHovered, setIsHovered] = useState(false); const [expandedView, setExpandedView] = useState< "addUrl" | "mcp" | "projects" | "profile" | "integrations" | null >(null); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isCollapsing, setIsCollapsing] = useState(false); const [showAddMemoryView, setShowAddMemoryView] = useState(false); const [showConnectAIModal, setShowConnectAIModal] = useState(false); const isMobile = useIsMobile(); const { activePanel, setActivePanel } = useMobilePanel(); const { setMenuExpanded } = useTour(); const autumn = useCustomer(); const { setIsOpen } = useChatOpen(); const { data: memoriesCheck } = fetchMemoriesFeature(autumn); const memoriesUsed = memoriesCheck?.usage ?? 0; const memoriesLimit = memoriesCheck?.included_usage ?? 0; const { data: proCheck } = fetchConsumerProProduct(autumn); useEffect(() => { if (memoriesCheck) { console.log({ memoriesCheck }); } if (proCheck) { console.log({ proCheck }); } }, [memoriesCheck, proCheck]); // Function to clear the 'open' parameter from URL const clearOpenParam = useCallback(() => { const newSearchParams = new URLSearchParams(searchParams.toString()); newSearchParams.delete("open"); const newUrl = `${window.location.pathname}${newSearchParams.toString() ? `?${newSearchParams.toString()}` : ""}`; router.replace(newUrl); }, [searchParams, router]); const isProUser = proCheck?.allowed ?? false; const shouldShowLimitWarning = !isProUser && memoriesUsed >= memoriesLimit * 0.8; // Map menu item keys to tour IDs const menuItemTourIds: Record = { addUrl: TOUR_STEP_IDS.MENU_ADD_MEMORY, projects: TOUR_STEP_IDS.MENU_PROJECTS, mcp: TOUR_STEP_IDS.MENU_MCP, integrations: "", // No tour ID for integrations yet }; const menuItems = [ { icon: Plus, text: "Add Memory", key: "addUrl" as const, disabled: false, }, { icon: MessageSquareMore, text: "Chat", key: "chat" as const, disabled: false, }, { icon: Puzzle, text: "Integrations", key: "integrations" as const, disabled: false, }, { icon: MCPIcon, text: "MCP", key: "mcp" as const, disabled: false, }, { icon: User, text: "Profile", key: "profile" as const, disabled: false, }, ]; const handleMenuItemClick = ( key: "chat" | "addUrl" | "mcp" | "projects" | "profile" | "integrations", ) => { if (key === "chat") { setIsOpen(true); setIsMobileMenuOpen(false); if (isMobile) { setActivePanel("chat"); } } else if (key === "mcp") { // Open ConnectAIModal directly for MCP setIsMobileMenuOpen(false); setExpandedView(null); setShowConnectAIModal(true); } else { if (expandedView === key) { setIsCollapsing(true); setExpandedView(null); } else if (key === "addUrl") { setShowAddMemoryView(true); setExpandedView(null); } else { setExpandedView(key); } if (isMobile) { setActivePanel("menu"); } } }; // Handle initial view opening based on URL parameter useEffect(() => { if (openParam) { if (openParam === "chat") { setIsOpen(true); setIsMobileMenuOpen(false); if (isMobile) { setActivePanel("chat"); } } else if (openParam === "mcp") { // Open ConnectAIModal directly for MCP setIsMobileMenuOpen(false); setExpandedView(null); setShowConnectAIModal(true); } else if (openParam === "addUrl") { setShowAddMemoryView(true); setExpandedView(null); if (isMobile) { setIsMobileMenuOpen(true); setActivePanel("menu"); } } else if (validViews.includes(openParam as ValidView)) { // For other valid views like "profile", "integrations" setExpandedView(openParam as ValidView); if (isMobile) { setIsMobileMenuOpen(true); setActivePanel("menu"); } } // Clear the parameter from URL after performing any action clearOpenParam(); } }, [ openParam, isMobile, setIsOpen, setActivePanel, validViews, clearOpenParam, ]); // Watch for active panel changes on mobile useEffect(() => { if (isMobile && activePanel !== "menu" && activePanel !== null) { // Another panel became active, close the menu setIsMobileMenuOpen(false); setExpandedView(null); } }, [isMobile, activePanel]); // Notify tour provider about expansion state changes useEffect(() => { const isExpanded = isMobile ? isMobileMenuOpen || !!expandedView : isHovered || !!expandedView; setMenuExpanded(isExpanded); }, [isMobile, isMobileMenuOpen, isHovered, expandedView, setMenuExpanded]); // Calculate width based on state const menuWidth = expandedView || isCollapsing ? 600 : isHovered ? 160 : 56; // Dynamic z-index for mobile based on active panel const mobileZIndex = isMobile && activePanel === "menu" ? "z-[70]" : "z-[100]"; return ( <> {/* Desktop Menu */} {!isMobile && ( !expandedView && setIsHovered(true)} onMouseLeave={() => !expandedView && setIsHovered(false)} transition={{ width: { duration: 0.2, ease: [0.4, 0, 0.2, 1], }, scale: { duration: 0.5, ease: [0.4, 0, 0.2, 1], }, layout: { duration: 0.2, ease: [0.4, 0, 0.2, 1], }, }} > {/* Glass effect background */} {/* Menu content */} setIsCollapsing(false)} > {!expandedView ? ( {menuItems.map((item, index) => ( handleMenuItemClick(item.key)} type="button" whileHover={{ scale: 1.02, transition: { duration: 0.1 }, }} whileTap={{ scale: 0.98 }} > {item.text} {index === 0 && ( )} ))} ) : ( {expandedView === "mcp" && "Model Context Protocol"} {expandedView === "profile" && "Profile"} {expandedView === "integrations" && "Integrations"} { setIsCollapsing(true); setExpandedView(null); }} size="icon" variant="ghost" > {expandedView === "profile" && } {expandedView === "integrations" && ( )} )} )} {/* Mobile Menu with Vaul Drawer */} {isMobile && ( { if (!open) { setIsMobileMenuOpen(false); setExpandedView(null); setActivePanel(null); } }} open={isMobileMenuOpen || !!expandedView} > {/* Menu Trigger Button */} {!isMobileMenuOpen && !expandedView && ( { setIsMobileMenuOpen(true); setActivePanel("menu"); }} transition={{ duration: 0.3, ease: [0.4, 0, 0.2, 1], }} type="button" whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} > {/* Glass effect background */} Open menu )} {expandedView === "addUrl" && "Add Memory"} {expandedView === "mcp" && "Model Context Protocol"} {expandedView === "profile" && "Profile"} {!expandedView && "Menu"} {/* Glass effect background */} {/* Drag Handle */} {/* Menu content */} setIsCollapsing(false)} > {!expandedView ? ( {/* Menu Items */} {menuItems.map((item, index) => ( { handleMenuItemClick(item.key); if ( item.key !== "mcp" && item.key !== "profile" && item.key !== "integrations" ) { setIsMobileMenuOpen(false); } }} type="button" whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} > {item.text} {/* Show warning indicator for Add Memory when limits approached */} {shouldShowLimitWarning && item.key === "addUrl" && ( {memoriesLimit - memoriesUsed} left )} {/* Add horizontal line after first item */} {index === 0 && ( )} ))} ) : ( {expandedView === "addUrl" && "Add Memory"} {expandedView === "mcp" && "Model Context Protocol"} {expandedView === "profile" && "Profile"} {expandedView === "integrations" && "Integrations"} { setIsCollapsing(true); setExpandedView(null); }} size="icon" variant="ghost" > {expandedView === "addUrl" && ( )} {expandedView === "profile" && } {expandedView === "integrations" && ( )} )} )} {showAddMemoryView && ( setShowAddMemoryView(false)} /> )} Connect AI Assistant > ); } export default Menu;