"use client" import { useIsMobile } from "@hooks/use-mobile" import { fetchConsumerProProduct, fetchMemoriesFeature, } from "@repo/lib/queries" import { Button } from "@repo/ui/components/button" import { ConnectAIModal } from "./connect-ai-modal" import { HeadingH2Bold } from "@repo/ui/text/heading/heading-h2-bold" import { GlassMenuEffect } from "@ui/other/glass-effect" import { useCustomer } from "autumn-js/react" import { 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 { useChatOpen } from "@/stores" import { ProjectSelector } from "./project-selector" import { AddMemoryExpandedView, AddMemoryView } from "./views/add-memory" import { IntegrationsView } from "./views/integrations" import { ProfileView } from "./views/profile" export 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 autumn = useCustomer() const { setIsOpen } = useChatOpen() const { data: memoriesCheck } = fetchMemoriesFeature( autumn, !autumn.isLoading, ) 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 const menuItems = [ { icon: Plus, text: "Add Memory", key: "addUrl" 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]) // 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], }, }} > {/* 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"} {expandedView === "profile" && } {expandedView === "integrations" && ( )} )}
)} {/* Mobile Menu with Vaul Drawer */} {isMobile && ( { if (!open) { setIsMobileMenuOpen(false) setExpandedView(null) setActivePanel(null) } }} > {/* 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"}
{expandedView === "addUrl" && ( )} {expandedView === "profile" && } {expandedView === "integrations" && ( )}
)}
)} {showAddMemoryView && ( setShowAddMemoryView(false)} /> )} ) } export default Menu