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>
  );
}