"use client"; import type { Team } from "@stackframe/stack"; import { AlertTriangle, ArrowUpCircle, AudioLines, Brain, ChevronLeft, ChevronRight, CircleDollarSign, Database, FileText, Home, Key, LogOut, type LucideIcon, Megaphone, Phone, Settings, TrendingUp, Workflow, Wrench, } from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import React, { useRef } from "react"; import ThemeToggle from "@/components/ThemeSwitcher"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarRail, SidebarTrigger, useSidebar, } from "@/components/ui/sidebar"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useAppConfig } from "@/context/AppConfigContext"; import { useTelephonyConfigWarnings } from "@/context/TelephonyConfigWarningsContext"; import { useLatestReleaseVersion } from "@/hooks/useLatestReleaseVersion"; import type { LocalUser } from "@/lib/auth"; import { useAuth } from "@/lib/auth"; import { cn } from "@/lib/utils"; type SidebarNavItem = { title: string; url: string; icon: LucideIcon; showsTelephonyWarning?: boolean; }; type SidebarNavSection = { label?: string; items: SidebarNavItem[]; }; const TELEPHONY_WARNING_COPY = "Action required"; const NAV_SECTIONS: SidebarNavSection[] = [ { items: [ { title: "Overview", url: "/overview", icon: Home, }, ], }, { label: "BUILD", items: [ { title: "Voice Agents", url: "/workflow", icon: Workflow, }, { title: "Campaigns", url: "/campaigns", icon: Megaphone, }, { title: "Models", url: "/model-configurations", icon: Brain, }, { title: "Telephony", url: "/telephony-configurations", icon: Phone, showsTelephonyWarning: true, }, { title: "Tools", url: "/tools", icon: Wrench, }, { title: "Files", url: "/files", icon: Database, }, { title: "Recordings", url: "/recordings", icon: AudioLines, }, { title: "Developers", url: "/api-keys", icon: Key, }, ], }, { label: "OBSERVE", items: [ { title: "Agent Runs", url: "/usage", icon: TrendingUp, }, { title: "Billing", url: "/billing", icon: CircleDollarSign, }, { title: "Reports", url: "/reports", icon: FileText, }, ], }, ]; // Lazy load SelectedTeamSwitcher - we'll pass selectedTeam from our context const StackTeamSwitcher = React.lazy(() => import("@stackframe/stack").then((mod) => ({ default: mod.SelectedTeamSwitcher, })) ); export function AppSidebar() { const pathname = usePathname(); const router = useRouter(); const { state, isMobile, setOpenMobile } = useSidebar(); const { provider, getSelectedTeam, logout, user } = useAuth(); const { config } = useAppConfig(); const { telnyxMissingWebhookPublicKeyCount } = useTelephonyConfigWarnings(); const hasTelephonyWarning = telnyxMissingWebhookPublicKeyCount > 0; const isCollapsed = !isMobile && state === "collapsed"; // Get selected team for Stack auth (cast to Team type from Stack) // Stabilize the reference so SelectedTeamSwitcher only sees a change when the team ID changes, // preventing unnecessary PATCH calls to Stack Auth on every route navigation. const selectedTeamRef = useRef(null); const rawSelectedTeam = provider === "stack" && getSelectedTeam ? getSelectedTeam() as Team | null : null; if (rawSelectedTeam?.id !== selectedTeamRef.current?.id) { selectedTeamRef.current = rawSelectedTeam; } const selectedTeam = selectedTeamRef.current; // Version info from app config context const versionInfo = config ? { ui: config.uiVersion, api: config.apiVersion } : null; // Check for updates only on self-hosted (OSS) deployments — cloud is managed for the user. const { latest: latestRelease, isBehind, isLatest } = useLatestReleaseVersion( versionInfo?.ui, { enabled: config?.deploymentMode === "oss" }, ); const isActive = (path: string) => pathname.startsWith(path); const handleMobileNavClick = () => { if (isMobile) { setOpenMobile(false); } }; const SidebarLink = ({ item }: { item: SidebarNavItem }) => { const isItemActive = isActive(item.url); const Icon = item.icon; const showWarningDot = item.showsTelephonyWarning && hasTelephonyWarning; const tooltip = { children: (

{item.title}

{showWarningDot && (

{TELEPHONY_WARNING_COPY}

)}
), }; const warningIndicator = ( ); return ( {item.title} {showWarningDot && ( isCollapsed ? ( warningIndicator ) : ( {warningIndicator}

{TELEPHONY_WARNING_COPY}

) )}
); }; return (
Dograh {versionInfo && ( v{versionInfo.ui} )} {isBehind && latestRelease && ( Update

Latest: {latestRelease} — click to see the update guide

)} {isLatest && ( Latest

You're running the latest release

)}
{isCollapsed ? ( ) : ( )}
{provider === "stack" && (
} > { router.refresh(); }} />
)}
{NAV_SECTIONS.map((section, index) => ( {section.label && ( {section.label} )} {section.items.map((item) => ( ))} ))}
{provider !== "stack" && (
{(user as LocalUser | undefined)?.email && (

{(user as LocalUser).email}

)}
router.push("/settings")} className="cursor-pointer"> Platform Settings logout()} className="cursor-pointer"> Sign out
)} {provider === "stack" && (
{user?.displayName && (

{user.displayName}

)} {(user as { primaryEmail?: string })?.primaryEmail && (

{(user as { primaryEmail?: string }).primaryEmail}

)}
router.push("/handler/account-settings")} className="cursor-pointer"> Account settings router.push("/settings")} className="cursor-pointer"> Platform Settings router.push("/usage")} className="cursor-pointer"> Usage logout()} className="cursor-pointer"> Sign out
)}
{isCollapsed ? (

Toggle theme

) : (
)}
); }