import { useState, useRef, useEffect } from 'react'; import { Navigate, Outlet, NavLink, Link, useLocation, useParams, useOutletContext } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import { useAuth } from '../lib/auth'; import { apiFetch } from '../lib/api'; import ThemeToggle from '../components/ThemeToggle'; import SettingsDialog from '../components/SettingsDialog'; import ConfirmDialog from '../components/ConfirmDialog'; type LayoutContext = { onOpenSettings: () => void }; export function useLayoutContext() { return useOutletContext(); } type ProjectSummary = { id: string; name: string; description: string | null; _count: { endpoints: number; modules: number }; }; function UserDropdown({ user, logout, onOpenSettings }: { user: { name: string; email: string }; logout: () => void; onOpenSettings: () => void }) { const [open, setOpen] = useState(false); const [confirmLogout, setConfirmLogout] = useState(false); const ref = useRef(null); const initials = user.name.split(' ').map(w => w[0]).join('').toUpperCase().slice(0, 2); useEffect(() => { if (!open) return; const handler = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [open]); return (
{open && (
{/* User info */}
{initials}
{user.name}
{user.email}
{/* Actions */}
)} { setConfirmLogout(false); logout(); }} onCancel={() => setConfirmLogout(false)} title="Sign Out" description="Are you sure you want to sign out?" confirmText="Sign Out" variant="warning" />
); } function ProjectSidebar() { const location = useLocation(); const params = useParams(); const activeProjectId = params.id; const { data: projects, isLoading } = useQuery({ queryKey: ['projects'], queryFn: () => apiFetch('/projects'), }); const isProjectsRoot = location.pathname === '/dashboard'; return ( ); } function OnboardingBanner({ onOpenSettings }: { onOpenSettings: () => void }) { const [dismissed, setDismissed] = useState(() => localStorage.getItem('agent-fox-onboarding-dismissed') === 'true'); const { data: keyStatus } = useQuery({ queryKey: ['api-key-status'], queryFn: () => apiFetch<{ hasKey: boolean }>('/auth/api-key/status'), }); if (dismissed || keyStatus?.hasKey) return null; // Still loading if (!keyStatus) return null; return (

Welcome! Generate an API key to start using MCP services.

You'll need an API key to connect your LLM client to your projects.

); } export default function Layout() { const { user, loading, logout } = useAuth(); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false); if (loading) { return (
); } if (!user) return ; return (
{/* Top Header — fixed */}
{/* Left: mobile menu + logo */}
AgentFox
{/* Right: theme toggle + user */}
setSettingsOpen(true)} />
{/* Body: sidebar + main — fills remaining height */}
{/* Mobile sidebar overlay */} {mobileMenuOpen && (
setMobileMenuOpen(false)} /> )} {/* Mobile sidebar */} {/* Desktop project sidebar — stays fixed, has its own scroll */} {/* Main content — only this area scrolls */}
setSettingsOpen(true)} /> setSettingsOpen(true) } satisfies LayoutContext} />
{/* Settings dialog */} setSettingsOpen(false)} />
); }