import { AgentSidebar } from "@agent-native/core/client/agent-chat"; import { agentNativePath, appPath } from "@agent-native/core/client/api-path"; import { DevDatabaseLink } from "@agent-native/core/client/db-admin"; import { LanguagePicker, useT } from "@agent-native/core/client/i18n"; import { openCommandMenu } from "@agent-native/core/client/navigation"; import { OrgSwitcher } from "@agent-native/core/client/org"; import { FeedbackButton } from "@agent-native/core/client/ui"; import { HeaderActionsProvider, SidebarFooterActions, } from "@agent-native/toolkit/app-shell"; import { IconFlame, IconLoader2, IconChartBar, IconSettings, IconLayoutSidebarLeftCollapse, IconLayoutSidebarLeftExpand, IconSearch, } from "@tabler/icons-react"; import { useIsFetching, useIsMutating, useQuery, useQueryClient, } from "@tanstack/react-query"; import { useState, useEffect } from "react"; import { Link, useLocation, useNavigate } from "react-router"; import { Button } from "@/components/ui/button"; import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { apiFetch } from "@/lib/api"; import { cn } from "@/lib/utils"; import { Header } from "./Header"; const navItems = [ { icon: IconFlame, labelKey: "navigation.entry", href: "/" }, { icon: IconChartBar, labelKey: "navigation.analytics", href: "/analytics" }, ]; const bottomNavItems = [ { icon: IconSettings, labelKey: "navigation.settings", href: "/settings" }, ]; export function AppLayout({ children }: { children: React.ReactNode }) { const location = useLocation(); const navigate = useNavigate(); const queryClient = useQueryClient(); const t = useT(); const [sidebarOpen, setSidebarOpen] = useState(false); const [desktopSidebarCollapsed, setDesktopSidebarCollapsed] = useState(() => { if (typeof window === "undefined") return false; return window.localStorage.getItem("macros:left-sidebar-collapsed") === "1"; }); const isAnalytics = location.pathname === "/analytics"; const isSettings = location.pathname.startsWith("/settings"); const isAgent = location.pathname.startsWith("/agent"); // Auto-close sidebar on route change (mobile) useEffect(() => { setSidebarOpen(false); }, [location.pathname]); useEffect(() => { window.localStorage.setItem( "macros:left-sidebar-collapsed", desktopSidebarCollapsed ? "1" : "0", ); }, [desktopSidebarCollapsed]); // Navigation state sync - write current view to application state useEffect(() => { const view = isAgent ? "agent" : isSettings ? "settings" : isAnalytics ? "analytics" : "entry"; apiFetch(agentNativePath("/_agent-native/application-state/navigation"), { method: "PUT", body: JSON.stringify({ view, path: location.pathname }), }).catch(() => {}); }, [location.pathname, isAgent, isAnalytics, isSettings]); // useDbSync invalidates this key when the agent writes a navigate command. const { data: navCommand } = useQuery({ queryKey: ["navigate-command"], queryFn: async () => { try { const res = await fetch( agentNativePath("/_agent-native/application-state/navigate"), ); if (!res.ok) return null; return await res.json(); } catch { return null; } }, }); useEffect(() => { if (navCommand) { const commandValue = "value" in navCommand ? navCommand.value : navCommand; const cmd = typeof commandValue === "string" ? JSON.parse(commandValue) : commandValue; if (cmd.view === "analytics") { navigate("/analytics"); } else if (cmd.view === "settings") { navigate("/settings"); } else if (cmd.view === "agent") { navigate("/agent"); } else if (cmd.view === "entry") { navigate("/"); } // Clear the command fetch(agentNativePath("/_agent-native/application-state/navigate"), { method: "DELETE", }).catch(() => {}); queryClient.setQueryData(["navigate-command"], null); } }, [navCommand, navigate, queryClient]); return (
{/* Desktop sidebar */} {/* Mobile sidebar sheet */} {t("sidebar.navigation")} {/* Page content */}
setSidebarOpen(true)} />
{children}
); } function SidebarContent({ pathname, collapsed = false, onToggleCollapsed, }: { pathname: string; collapsed?: boolean; onToggleCollapsed?: () => void; }) { const t = useT(); const ToggleIcon = collapsed ? IconLayoutSidebarLeftExpand : IconLayoutSidebarLeftCollapse; const collapseButton = onToggleCollapsed ? ( {collapsed ? t("sidebar.expand") : t("sidebar.collapse")} ) : null; const searchButton = ( {t("root.search")} ); const translateButton = ( ); const feedbackButton = ( ); return (
{!collapsed && (
{t("navigation.brand")}
)}
{!collapsed && ( <>
)}
); } function SyncIndicator({ sidebarCollapsed }: { sidebarCollapsed: boolean }) { const t = useT(); const refetchingActions = useIsFetching({ predicate: (query) => query.queryKey[0] === "action" && query.state.dataUpdatedAt > 0, }); const mutatingActions = useIsMutating(); const [agentToolRuns, setAgentToolRuns] = useState(0); useEffect(() => { const trackedTools = new Set(["log-meal", "log-exercise", "log-weight"]); const handleStart = (event: Event) => { const tool = (event as CustomEvent).detail?.tool; if (trackedTools.has(tool)) setAgentToolRuns((count) => count + 1); }; const handleDone = (event: Event) => { const tool = (event as CustomEvent).detail?.tool; if (!trackedTools.has(tool)) return; setTimeout(() => { setAgentToolRuns((count) => Math.max(0, count - 1)); }, 400); }; window.addEventListener("agent-native:tool-start", handleStart); window.addEventListener("agent-native:tool-done", handleDone); return () => { window.removeEventListener("agent-native:tool-start", handleStart); window.removeEventListener("agent-native:tool-done", handleDone); }; }, []); const isSyncing = refetchingActions > 0 || mutatingActions > 0 || agentToolRuns > 0; if (!isSyncing) return null; return (
{t("sidebar.syncing")}
); }