"use client"; import { ChevronRight, type LucideIcon } from "lucide-react"; import { usePathname } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useMemo, useState } from "react"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, } from "@/components/ui/sidebar"; interface NavItem { title: string; url: string; icon: LucideIcon; isActive?: boolean; items?: { title: string; url: string; }[]; } interface NavMainProps { items: NavItem[]; onSourcesExpandedChange?: (expanded: boolean) => void; } export function NavMain({ items, onSourcesExpandedChange }: NavMainProps) { const t = useTranslations("nav_menu"); const pathname = usePathname(); // Translation function that handles both exact matches and fallback to original const translateTitle = (title: string): string => { const titleMap: Record = { Researcher: "researcher", "Manage LLMs": "manage_llms", Sources: "sources", "Add Sources": "add_sources", "Manage Documents": "manage_documents", "Manage Connectors": "manage_connectors", Podcasts: "podcasts", Logs: "logs", Platform: "platform", Team: "team", }; const key = titleMap[title]; return key ? t(key) : title; }; // Check if an item is active based on pathname const isItemActive = useCallback( (item: NavItem): boolean => { if (!pathname) return false; // For items without sub-items, check if pathname matches or starts with the URL if (!item.items?.length) { // Chat item: active ONLY when on new-chat page without a specific chat ID // (i.e., exactly /dashboard/{id}/new-chat, not /dashboard/{id}/new-chat/123) if (item.url.includes("/new-chat")) { // Match exactly the new-chat base URL (ends with /new-chat) return pathname.endsWith("/new-chat"); } // Logs item: active when on logs page if (item.url.includes("/logs")) { return pathname.includes("/logs"); } // Check exact match or prefix match return pathname === item.url || pathname.startsWith(`${item.url}/`); } // For items with sub-items (like Sources), check if any sub-item URL matches return item.items.some( (subItem) => pathname === subItem.url || pathname.startsWith(subItem.url) ); }, [pathname] ); // Memoize items to prevent unnecessary re-renders const memoizedItems = useMemo(() => items, [items]); // Track expanded state for items with sub-menus (like Sources) const [expandedItems, setExpandedItems] = useState>(() => { const initial: Record = {}; items.forEach((item) => { if (item.items?.length) { initial[item.title] = item.isActive ?? false; } }); return initial; }); // Handle collapsible state change const handleOpenChange = useCallback( (title: string, isOpen: boolean) => { setExpandedItems((prev) => ({ ...prev, [title]: isOpen })); // Notify parent when Sources is expanded/collapsed if (title === "Sources" && onSourcesExpandedChange) { onSourcesExpandedChange(isOpen); } }, [onSourcesExpandedChange] ); return ( {translateTitle("Platform")} {memoizedItems.map((item, index) => { const translatedTitle = translateTitle(item.title); const hasSub = !!item.items?.length; const isActive = isItemActive(item); const isItemOpen = expandedItems[item.title] ?? isActive ?? false; return ( handleOpenChange(item.title, open) : undefined} defaultOpen={!hasSub ? isActive : undefined} > {hasSub ? ( // When the item has children, make the whole row a collapsible trigger <> Toggle submenu {item.items?.map((subItem, subIndex) => { const translatedSubTitle = translateTitle(subItem.title); return ( {translatedSubTitle} ); })} ) : ( // Leaf item: treat as a normal link {translatedTitle} )} ); })} ); }