Spaces:
Sleeping
Sleeping
File size: 7,025 Bytes
cf47145 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | 'use client';
import { useState } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import {
Brain,
Activity,
ListTodo,
GitBranch,
Database,
Settings,
ChevronLeft,
ChevronRight,
Rocket,
Github,
X,
Menu,
} from 'lucide-react';
const NAV_ITEMS = [
{ href: '/', icon: Brain, label: 'Agent', desc: 'Run AI tasks' },
{ href: '/health', icon: Activity, label: 'Health', desc: 'System status' },
{ href: '/queue', icon: ListTodo, label: 'Queue', desc: 'Job queue' },
{ href: '/memory', icon: Database, label: 'Memory', desc: 'Episodic memory' },
{ href: '/workspace', icon: GitBranch, label: 'Workspace', desc: 'Projects' },
];
interface AppShellProps {
children: React.ReactNode;
}
export default function AppShell({ children }: AppShellProps) {
const pathname = usePathname();
const [collapsed, setCollapsed] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
return (
<div className="flex min-h-screen">
{/* ββ Mobile overlay ββ */}
{mobileOpen && (
<div
className="fixed inset-0 z-30 bg-black/60 lg:hidden"
onClick={() => setMobileOpen(false)}
/>
)}
{/* ββ Left Sidebar ββ */}
<aside
className={`
fixed top-0 left-0 z-40 h-full flex flex-col
bg-slate-900 border-r border-white/5
transition-all duration-200 ease-in-out
${collapsed ? 'w-16' : 'w-56'}
${mobileOpen ? 'translate-x-0' : '-translate-x-full'}
lg:translate-x-0 lg:static lg:h-auto lg:min-h-screen
`}
>
{/* Logo */}
<div className={`flex items-center gap-2 px-3 py-4 border-b border-white/5 ${collapsed ? 'justify-center' : ''}`}>
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-brand-600/30 text-brand-400 shrink-0">
<Rocket className="h-4 w-4" />
</div>
{!collapsed && (
<div className="min-w-0">
<p className="text-sm font-bold truncate">OpenHands</p>
<p className="text-[10px] text-slate-500 truncate">Genspark AI OS</p>
</div>
)}
{/* Collapse toggle β desktop only */}
<button
onClick={() => setCollapsed(v => !v)}
className="ml-auto hidden lg:flex items-center justify-center w-6 h-6 rounded hover:bg-white/5 text-slate-500 hover:text-slate-300"
>
{collapsed ? <ChevronRight className="h-3.5 w-3.5" /> : <ChevronLeft className="h-3.5 w-3.5" />}
</button>
{/* Mobile close */}
<button
onClick={() => setMobileOpen(false)}
className="ml-auto lg:hidden flex items-center justify-center w-6 h-6 rounded hover:bg-white/5 text-slate-500"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
{/* Phase badge */}
{!collapsed && (
<div className="mx-3 my-2">
<span className="inline-flex items-center gap-1 text-[10px] bg-brand-600/20 text-brand-300 border border-brand-600/30 px-2 py-0.5 rounded-full w-full justify-center">
β
Phase 4 Live
</span>
</div>
)}
{/* Nav items */}
<nav className="flex-1 px-2 py-2 space-y-0.5 overflow-y-auto">
{NAV_ITEMS.map(({ href, icon: Icon, label, desc }) => {
const active = pathname === href || (href !== '/' && pathname.startsWith(href));
return (
<Link
key={href}
href={href}
onClick={() => setMobileOpen(false)}
className={`
flex items-center gap-3 rounded-lg px-2 py-2 text-sm transition-colors
${active
? 'bg-brand-600/20 text-brand-300 border border-brand-600/20'
: 'text-slate-400 hover:text-slate-200 hover:bg-white/5'
}
${collapsed ? 'justify-center' : ''}
`}
title={collapsed ? label : undefined}
>
<Icon className={`h-4 w-4 shrink-0 ${active ? 'text-brand-400' : ''}`} />
{!collapsed && (
<div className="min-w-0">
<p className="font-medium leading-none">{label}</p>
<p className="text-[10px] text-slate-500 mt-0.5 truncate">{desc}</p>
</div>
)}
</Link>
);
})}
</nav>
{/* Bottom β Settings link */}
<div className="border-t border-white/5 px-2 py-2 space-y-0.5">
<Link
href="/settings"
onClick={() => setMobileOpen(false)}
className={`
flex items-center gap-3 rounded-lg px-2 py-2 text-sm transition-colors
${pathname === '/settings'
? 'bg-slate-700/50 text-slate-200'
: 'text-slate-500 hover:text-slate-300 hover:bg-white/5'
}
${collapsed ? 'justify-center' : ''}
`}
title={collapsed ? 'Settings' : undefined}
>
<Settings className="h-4 w-4 shrink-0" />
{!collapsed && <span className="font-medium">Settings</span>}
</Link>
{!collapsed && (
<a
href="https://github.com/pyaesonegtckglay-dotcom/Onehands-development"
target="_blank"
rel="noreferrer"
className="flex items-center gap-3 rounded-lg px-2 py-1.5 text-xs text-slate-600 hover:text-slate-400 hover:bg-white/5 transition-colors"
>
<Github className="h-3.5 w-3.5 shrink-0" />
<span>GitHub</span>
</a>
)}
</div>
</aside>
{/* ββ Main content area ββ */}
<div className="flex-1 flex flex-col min-w-0">
{/* Mobile top bar */}
<header className="lg:hidden flex items-center gap-3 px-4 py-3 border-b border-white/5 bg-slate-900/80 backdrop-blur sticky top-0 z-20">
<button
onClick={() => setMobileOpen(true)}
className="flex items-center justify-center w-8 h-8 rounded-lg hover:bg-white/5 text-slate-400"
>
<Menu className="h-5 w-5" />
</button>
<div className="flex items-center gap-2">
<Rocket className="h-4 w-4 text-brand-400" />
<span className="font-bold text-sm">OpenHands</span>
</div>
<div className="ml-auto">
<span className="text-[10px] text-brand-400 border border-brand-600/30 bg-brand-600/10 px-2 py-0.5 rounded-full">
Phase 4
</span>
</div>
</header>
{/* Page content */}
<main className="flex-1 overflow-auto">
{children}
</main>
</div>
</div>
);
}
|