import { useState, useEffect, useRef } from 'react' import { Outlet, useLocation, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import Sidebar from './components/Sidebar' import OperationsBar from './components/OperationsBar' import { ToastContainer, useToast } from './components/Toast' import { systemApi } from './utils/api' import { useTheme } from './contexts/ThemeContext' import { useBranding } from './contexts/BrandingContext' import { useAuth } from './context/AuthContext' const COLLAPSED_KEY = 'localai_sidebar_collapsed' export default function App() { const [sidebarOpen, setSidebarOpen] = useState(false) const [sidebarCollapsed, setSidebarCollapsed] = useState(() => { try { return localStorage.getItem(COLLAPSED_KEY) === 'true' } catch (_) { return false } }) const { toasts, addToast, removeToast } = useToast() const [version, setVersion] = useState('') const location = useLocation() const navigate = useNavigate() const { theme, toggleTheme } = useTheme() const { authEnabled, user } = useAuth() const branding = useBranding() const { t } = useTranslation('nav') const hamburgerRef = useRef(null) const isChatRoute = location.pathname.match(/\/chat(\/|$)/) || location.pathname.match(/\/agents\/[^/]+\/chat/) useEffect(() => { systemApi.version() .then(data => setVersion(typeof data === 'string' ? data : (data?.version || ''))) .catch(() => {}) }, []) useEffect(() => { const handler = (e) => setSidebarCollapsed(e.detail.collapsed) window.addEventListener('sidebar-collapse', handler) return () => window.removeEventListener('sidebar-collapse', handler) }, []) // Scroll to top on route change useEffect(() => { window.scrollTo(0, 0) }, [location.pathname]) // Drawer polish: lock body scroll, close on Escape, return focus to the // hamburger when the drawer closes. Only engages when the drawer is open; // desktop and tablet rail mode are unaffected. useEffect(() => { if (!sidebarOpen) return const prevOverflow = document.body.style.overflow document.body.style.overflow = 'hidden' const onKey = (e) => { if (e.key === 'Escape') setSidebarOpen(false) } window.addEventListener('keydown', onKey) return () => { document.body.style.overflow = prevOverflow window.removeEventListener('keydown', onKey) // Restore focus to the trigger so keyboard users land back where // they invoked the drawer from. hamburgerRef.current?.focus() } }, [sidebarOpen]) const layoutClasses = [ 'app-layout', isChatRoute ? 'app-layout-chat' : '', sidebarCollapsed ? 'sidebar-is-collapsed' : '', ].filter(Boolean).join(' ') const showAvatar = authEnabled && user const accountLabel = user?.name || user?.email || t('account') const themeToggleLabel = theme === 'dark' ? t('switchToLightMode') : t('switchToDarkMode') return (