| import { Outlet, NavLink, useLocation } from 'react-router-dom'; |
| import { useEffect } from 'react'; |
| import { |
| LayoutDashboard, |
| Layers, |
| BarChart3, |
| Settings, |
| Cpu, |
| HardDrive, |
| Zap, |
| Github, |
| Menu, |
| X, |
| Sun, |
| Moon |
| } from 'lucide-react'; |
| import { useSystemStore, useUIStore, useModelStore } from '../store'; |
| import { motion, AnimatePresence } from 'framer-motion'; |
|
|
| |
| |
| |
| export default function Layout() { |
| const { sidebarOpen, toggleSidebar, theme, toggleTheme } = useUIStore(); |
| const systemInfo = useSystemStore((state) => state.systemInfo); |
| const checkLoadedModel = useModelStore((state) => state.checkLoadedModel); |
| const location = useLocation(); |
|
|
| |
| useEffect(() => { |
| checkLoadedModel(); |
| }, []); |
|
|
| const navItems = [ |
| { path: '/dashboard', label: 'Dashboard', icon: LayoutDashboard }, |
| { path: '/quantize', label: 'Quantizer', icon: Layers }, |
| { path: '/analysis', label: 'Analysis', icon: BarChart3 }, |
| { path: '/models', label: 'Models', icon: HardDrive }, |
| ]; |
|
|
| return ( |
| <div className="app-layout"> |
| {/* Sidebar */} |
| <aside className={`sidebar ${sidebarOpen ? '' : 'closed'}`}> |
| {/* Logo */} |
| <div className="sidebar-header"> |
| <div className="logo"> |
| <div className="logo-icon"> |
| <Zap size={24} /> |
| </div> |
| <div className="logo-text"> |
| <span className="logo-title">Quantizer</span> |
| <span className="logo-subtitle">Neural Network</span> |
| </div> |
| </div> |
| <button className="btn btn-ghost btn-icon mobile-menu" onClick={toggleSidebar}> |
| <X size={20} /> |
| </button> |
| </div> |
| |
| {/* Navigation */} |
| <nav className="sidebar-nav"> |
| {navItems.map((item) => ( |
| <NavLink |
| key={item.path} |
| to={item.path} |
| className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`} |
| > |
| <item.icon size={20} /> |
| <span>{item.label}</span> |
| </NavLink> |
| ))} |
| </nav> |
| |
| {/* System Status */} |
| <div className="sidebar-footer"> |
| <div className="system-status glass-card no-hover"> |
| <div className="status-header"> |
| <Cpu size={16} /> |
| <span>System Status</span> |
| </div> |
| {systemInfo ? ( |
| <div className="status-details"> |
| <div className="status-item"> |
| <span className="status-label">GPU</span> |
| <span className={`badge ${systemInfo.cuda_available ? 'badge-success' : 'badge-warning'}`}> |
| {systemInfo.cuda_available ? 'CUDA' : systemInfo.mps_available ? 'MPS' : 'CPU'} |
| </span> |
| </div> |
| {systemInfo.gpus?.length > 0 && ( |
| <div className="status-item"> |
| <span className="status-label">{systemInfo.gpus[0].name}</span> |
| <span className="text-xs text-muted">{systemInfo.gpus[0].total_memory_gb}GB</span> |
| </div> |
| )} |
| <div className="status-item"> |
| <span className="status-label">RAM</span> |
| <span className="text-xs text-muted"> |
| {systemInfo.ram_available_gb?.toFixed(1)}GB / {systemInfo.ram_total_gb?.toFixed(1)}GB |
| </span> |
| </div> |
| </div> |
| ) : ( |
| <div className="status-loading"> |
| <div className="spinner"></div> |
| <span className="text-xs text-muted">Detecting...</span> |
| </div> |
| )} |
| </div> |
| |
| <button className="nav-item w-full" onClick={toggleTheme}> |
| {theme === 'dark' ? <Sun size={20} /> : <Moon size={20} />} |
| <span>{theme === 'dark' ? 'Light Mode' : 'Dark Mode'}</span> |
| </button> |
| |
| <a |
| href="https://github.com" |
| target="_blank" |
| rel="noopener noreferrer" |
| className="nav-item github-link" |
| > |
| <Github size={20} /> |
| <span>GitHub</span> |
| </a> |
| </div> |
| </aside> |
| |
| {/* Mobile menu button */} |
| <button className="mobile-menu-btn btn btn-secondary btn-icon" onClick={toggleSidebar}> |
| <Menu size={20} /> |
| </button> |
| |
| {/* Main Content */} |
| <main className="main-content"> |
| <AnimatePresence mode="wait"> |
| <motion.div |
| key={location.pathname} |
| initial={{ opacity: 0, y: 10 }} |
| animate={{ opacity: 1, y: 0 }} |
| exit={{ opacity: 0, y: -10 }} |
| transition={{ duration: 0.2 }} |
| > |
| <Outlet /> |
| </motion.div> |
| </AnimatePresence> |
| </main> |
| |
| <style>{` |
| .sidebar { |
| display: flex; |
| flex-direction: column; |
| } |
| |
| .sidebar-header { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| margin-bottom: var(--space-xl); |
| } |
| |
| .logo { |
| display: flex; |
| align-items: center; |
| gap: var(--space-md); |
| } |
| |
| .logo-icon { |
| width: 40px; |
| height: 40px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| background: var(--gradient-primary); |
| border-radius: var(--radius-lg); |
| color: white; |
| } |
| |
| .logo-text { |
| display: flex; |
| flex-direction: column; |
| } |
| |
| .logo-title { |
| font-size: var(--text-lg); |
| font-weight: 700; |
| color: var(--text-primary); |
| line-height: 1.2; |
| } |
| |
| .logo-subtitle { |
| font-size: var(--text-xs); |
| color: var(--text-tertiary); |
| } |
| |
| .mobile-menu { |
| display: none; |
| } |
| |
| .mobile-menu-btn { |
| display: none; |
| position: fixed; |
| top: var(--space-md); |
| left: var(--space-md); |
| z-index: 99; |
| } |
| |
| .sidebar-nav { |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| gap: var(--space-xs); |
| } |
| |
| .nav-item { |
| display: flex; |
| align-items: center; |
| gap: var(--space-md); |
| padding: var(--space-sm) var(--space-md); |
| border-radius: var(--radius-lg); |
| color: var(--text-secondary); |
| text-decoration: none; |
| transition: all var(--transition-fast); |
| } |
| |
| .nav-item:hover { |
| background: var(--glass-bg); |
| color: var(--text-primary); |
| } |
| |
| .nav-item.active { |
| background: var(--gradient-primary); |
| color: white; |
| box-shadow: var(--shadow-md); |
| } |
| |
| .sidebar-footer { |
| margin-top: auto; |
| display: flex; |
| flex-direction: column; |
| gap: var(--space-md); |
| } |
| |
| .system-status { |
| padding: var(--space-md); |
| } |
| |
| .status-header { |
| display: flex; |
| align-items: center; |
| gap: var(--space-sm); |
| font-size: var(--text-sm); |
| font-weight: 500; |
| color: var(--text-primary); |
| margin-bottom: var(--space-sm); |
| } |
| |
| .status-details { |
| display: flex; |
| flex-direction: column; |
| gap: var(--space-xs); |
| } |
| |
| .status-item { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| font-size: var(--text-xs); |
| } |
| |
| .status-label { |
| color: var(--text-secondary); |
| } |
| |
| .status-loading { |
| display: flex; |
| align-items: center; |
| gap: var(--space-sm); |
| } |
| |
| .github-link { |
| opacity: 0.7; |
| } |
| |
| .github-link:hover { |
| opacity: 1; |
| } |
| |
| @media (max-width: 768px) { |
| .mobile-menu { |
| display: flex; |
| } |
| |
| .mobile-menu-btn { |
| display: flex; |
| } |
| |
| .sidebar.closed { |
| transform: translateX(-100%); |
| } |
| } |
| `}</style> |
| </div> |
| ); |
| } |
|
|