Spaces:
Sleeping
Sleeping
| import React, { useState, useRef, useEffect } from 'react'; | |
| import { config } from '../config'; | |
| import TopNavBar from './TopNavBar'; | |
| import SideNavBar from './SideNavBar'; | |
| /* βββ Terminal Panel βββ */ | |
| const COMMANDS = { | |
| help: () => ['Commands: help | status | clear | echo <text>'], | |
| status: () => ['Agent A (INV-01): STANDBY', 'Agent B (VAL-01): STANDBY', `WebSocket: ${config.WS_URL} β CONNECTED`, 'Episode: None active'], | |
| }; | |
| const TerminalDrawer = ({ onClose }) => { | |
| const [input, setInput] = useState(''); | |
| const [lines, setLines] = useState([{ type: 'system', text: '// NEXUS Terminal v2.0 β type "help" for commands' }]); | |
| const [history, setHistory] = useState([]); | |
| const [histIdx, setHistIdx] = useState(-1); | |
| const endRef = useRef(null); | |
| const inputRef = useRef(null); | |
| useEffect(() => { endRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [lines]); | |
| useEffect(() => { inputRef.current?.focus(); }, []); | |
| const run = (e) => { | |
| e.preventDefault(); | |
| const cmd = input.trim(); | |
| if (!cmd) return; | |
| setHistory(h => [cmd, ...h].slice(0, 50)); | |
| setHistIdx(-1); | |
| if (cmd.toLowerCase() === 'clear') { setLines([]); setInput(''); return; } | |
| const parts = cmd.toLowerCase().split(' '); | |
| let output, type; | |
| if (parts[0] === 'echo') { output = [cmd.slice(5) || '']; type = 'output'; } | |
| else if (COMMANDS[parts[0]]) { output = COMMANDS[parts[0]](); type = 'output'; } | |
| else { output = [`Command not found: ${parts[0]}. Type "help".`]; type = 'error'; } | |
| setLines(l => [...l, { type: 'input', text: `nexus@terminal:~$ ${cmd}` }, ...output.map(t => ({ type, text: t }))]); | |
| setInput(''); | |
| }; | |
| const handleKey = (e) => { | |
| if (e.key === 'ArrowUp') { const i = Math.min(histIdx + 1, history.length - 1); setHistIdx(i); setInput(history[i] ?? ''); e.preventDefault(); } | |
| if (e.key === 'ArrowDown') { const i = Math.max(histIdx - 1, -1); setHistIdx(i); setInput(i === -1 ? '' : history[i]); e.preventDefault(); } | |
| }; | |
| const colorMap = { system: 'text-slate-600 italic', input: 'text-primary', output: 'text-on-surface/80', error: 'text-error' }; | |
| return ( | |
| <div className="flex flex-col h-full" onClick={() => inputRef.current?.focus()}> | |
| <div className="flex-1 p-3 font-mono text-xs overflow-y-auto space-y-0.5 bg-surface-container-lowest cursor-text"> | |
| {lines.map((l, i) => <div key={i} className={colorMap[l.type]}>{l.text}</div>)} | |
| <div ref={endRef} /> | |
| </div> | |
| <form onSubmit={run} className="flex items-center gap-2 px-3 py-2 border-t border-white/5 bg-surface-container-lowest shrink-0"> | |
| <span className="text-primary font-mono text-xs shrink-0">nexus@terminal:~$</span> | |
| <input ref={inputRef} value={input} onChange={e => setInput(e.target.value)} onKeyDown={handleKey} | |
| className="flex-1 bg-transparent font-mono text-xs text-on-surface focus:outline-none placeholder:text-slate-700" | |
| placeholder="type a command and press Enter..." /> | |
| </form> | |
| </div> | |
| ); | |
| }; | |
| /* βββ Communication Panel βββ */ | |
| const CommunicationDrawer = () => ( | |
| <div className="flex flex-col h-full p-4 font-mono text-xs space-y-2 bg-surface-container-lowest overflow-y-auto"> | |
| {[ | |
| { agent: 'AGENT_A', msg: 'Awaiting objective. Standing by for episode_start event.', time: 'β', color: 'text-primary' }, | |
| { agent: 'AGENT_B', msg: 'Validation module idle. Ready to receive investigator output.', time: 'β', color: 'text-secondary' }, | |
| { agent: 'SYSTEM', msg: 'No active episode. Use START to begin.', time: 'β', color: 'text-outline-variant' }, | |
| ].map((m, i) => ( | |
| <div key={i} className="flex gap-3 py-1.5 border-b border-white/5"> | |
| <span className={`${m.color} font-bold shrink-0 w-20`}>[{m.agent}]</span> | |
| <span className="text-on-surface/70">{m.msg}</span> | |
| <span className="text-slate-600 ml-auto shrink-0">{m.time}</span> | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| /* βββ Reward Analytics Panel βββ */ | |
| const AnalyticsDrawer = () => { | |
| const stats = [ | |
| { label: 'Avg Reward', value: 'β', color: 'text-primary' }, | |
| { label: 'Best Step', value: 'β', color: 'text-tertiary' }, | |
| { label: 'Root Cause', value: 'β', color: 'text-tertiary' }, | |
| { label: 'Steps Run', value: 'β', color: 'text-on-surface' }, | |
| { label: 'Episodes', value: 'β', color: 'text-on-surface' }, | |
| { label: 'Success Rate', value: 'β', color: 'text-secondary' }, | |
| ]; | |
| return ( | |
| <div className="flex h-full"> | |
| {/* Reward chart placeholder */} | |
| <div className="flex-1 p-4 flex flex-col"> | |
| <p className="text-[9px] font-mono text-outline-variant uppercase mb-2">Cumulative Reward Over Steps</p> | |
| <div className="flex-1 flex items-end gap-1 border-l border-b border-outline-variant/20 px-2 pb-1"> | |
| {[12, 24, 18, 36, 30, 48, 42, 60].map((h, i) => ( | |
| <div key={i} className="flex-1 flex flex-col items-center justify-end"> | |
| <div className="w-full bg-primary/30 rounded-sm transition-all" style={{ height: `${h}%` }}></div> | |
| </div> | |
| ))} | |
| </div> | |
| <p className="text-[9px] font-mono text-outline-variant/40 italic mt-1">No live data β connect to episode to populate</p> | |
| </div> | |
| {/* Stat grid */} | |
| <div className="w-48 shrink-0 p-3 border-l border-white/5 grid grid-cols-2 gap-2 content-start"> | |
| {stats.map(s => ( | |
| <div key={s.label} className="bg-surface-container p-2 rounded border border-white/5"> | |
| <span className="text-[8px] font-mono text-outline-variant block uppercase truncate">{s.label}</span> | |
| <span className={`text-sm font-bold font-mono ${s.color}`}>{s.value}</span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| /* βββ Layout βββ */ | |
| const TABS = [ | |
| { id: 'communication', label: 'Communication', icon: 'forum' }, | |
| { id: 'terminal', label: 'Terminal', icon: 'code' }, | |
| ]; | |
| const Layout = ({ children }) => { | |
| const [activeTab, setActiveTab] = useState(null); // null = closed | |
| const toggle = (id) => setActiveTab(prev => prev === id ? null : id); | |
| /* drawer height when open */ | |
| const drawerH = 'h-64'; | |
| return ( | |
| <div className="min-h-screen flex flex-col"> | |
| <TopNavBar /> | |
| <SideNavBar /> | |
| {/* Main scrollable area β leave room for fixed footer + optional drawer */} | |
| <main className={`ml-20 pt-16 flex-1 transition-all ${activeTab ? 'pb-[calc(48px+256px)]' : 'pb-12'}`}> | |
| <div className="p-8 max-w-[1600px] mx-auto"> | |
| {children} | |
| </div> | |
| </main> | |
| {/* Sliding drawer */} | |
| {activeTab && ( | |
| <div className={`fixed bottom-12 left-20 right-0 ${drawerH} z-40 bg-surface border-t border-primary/20 shadow-[0_-10px_40px_rgba(0,0,0,0.6)] flex flex-col`}> | |
| {/* Drawer title bar */} | |
| <div className="flex items-center justify-between px-5 py-2 bg-surface-container border-b border-white/5 shrink-0"> | |
| <div className="flex items-center gap-2"> | |
| <span className="material-symbols-outlined text-primary text-sm"> | |
| {TABS.find(t => t.id === activeTab)?.icon} | |
| </span> | |
| <span className="font-mono text-xs text-primary uppercase tracking-widest"> | |
| {TABS.find(t => t.id === activeTab)?.label} | |
| </span> | |
| </div> | |
| <button onClick={() => setActiveTab(null)} className="text-slate-500 hover:text-white transition-colors"> | |
| <span className="material-symbols-outlined text-sm">keyboard_arrow_down</span> | |
| </button> | |
| </div> | |
| {/* Drawer content */} | |
| <div className="flex-1 overflow-hidden"> | |
| {activeTab === 'terminal' && <TerminalDrawer onClose={() => setActiveTab(null)} />} | |
| {activeTab === 'communication' && <CommunicationDrawer />} | |
| {activeTab === 'analytics' && <AnalyticsDrawer />} | |
| </div> | |
| </div> | |
| )} | |
| {/* Footer tab bar */} | |
| <footer className="fixed bottom-0 left-0 w-full h-12 bg-background/90 backdrop-blur-2xl z-50 flex items-center border-t border-primary/15 px-8 shadow-[0_-10px_30px_rgba(0,0,0,0.5)]"> | |
| {/* Left: ticker */} | |
| <div className="flex-1 hidden md:flex items-center gap-2 overflow-hidden"> | |
| <span className="text-[9px] font-mono text-outline-variant italic uppercase tracking-tight whitespace-nowrap"> | |
| SYSTEM_INITIALIZED: STANDBY FOR AGENT HANDSHAKE... | |
| </span> | |
| </div> | |
| {/* Centre: tabs */} | |
| <div className="flex items-center gap-1 shrink-0"> | |
| {TABS.map(tab => ( | |
| <button | |
| key={tab.id} | |
| onClick={() => toggle(tab.id)} | |
| className={`flex items-center gap-2 px-4 h-12 transition-all border-t-2 font-mono text-[10px] tracking-widest uppercase ${activeTab === tab.id | |
| ? 'border-primary text-primary bg-primary/10' | |
| : 'border-transparent text-slate-500 hover:text-primary hover:bg-white/5' | |
| }`} | |
| > | |
| <span className="material-symbols-outlined text-base">{tab.icon}</span> | |
| {tab.label} | |
| </button> | |
| ))} | |
| </div> | |
| {/* Right: session info */} | |
| <div className="flex-1 hidden md:flex items-center justify-end gap-2 text-[9px] font-mono text-outline-variant/50"> | |
| <span>SESSION: IDLE</span> | |
| </div> | |
| </footer> | |
| </div> | |
| ); | |
| }; | |
| export default Layout; | |