import React, { useState, useEffect } from 'react'; import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { BarChart3, LayoutDashboard, Database, FileText, MessageSquare, Settings, Sun, Moon, Menu, X, Home, Sparkles, LogOut, Brain, Shield, PanelLeftClose, PanelLeftOpen, Activity, GitBranch, Users, Terminal, Command, Camera, Rocket, Sliders, Lightbulb, Layers, ChevronRight, TrendingDown, Wifi, WifiOff, Cpu, Search, } from 'lucide-react'; import { useAuth } from '@/contexts/AuthContext'; import { useUserStore } from '@/store/userStore'; import LogoImage from '@/components/LogoImage'; import apiService, { api } from '@/services/api'; const AppLayout: React.FC = () => { const location = useLocation(); const navigate = useNavigate(); const { signOut } = useAuth(); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); useEffect(() => { // Dispatch resize event when sidebar collapses or expands so all charts resize smoothly const timer = setTimeout(() => { window.dispatchEvent(new Event('resize')); }, 150); return () => clearTimeout(timer); }, [isSidebarCollapsed]); const { isDark, toggleTheme } = useUserStore(); // V3: Command Palette State const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false); const [paletteSearch, setPaletteSearch] = useState(''); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); // WebSocket State const [wsConnected, setWsConnected] = useState(false); const [liveMetrics, setLiveMetrics] = useState(null); // Workspace State const { workspaces, setWorkspaces, activeWorkspaceId, setActiveWorkspace, activeWorkspaceRole } = useUserStore(); useEffect(() => { // Fetch user workspaces const fetchWorkspaces = async () => { try { const res = await api.get('/api/v1/collaboration/workspaces'); setWorkspaces(res.data.workspaces); // Auto-select personal workspace if none selected if (!activeWorkspaceId && res.data.workspaces.length > 0) { const personal = res.data.workspaces.find((w: any) => w.role === 'Owner'); if (personal) setActiveWorkspace(personal.id, personal.role); } } catch (err) { console.error("Failed to fetch workspaces:", err); } }; fetchWorkspaces(); }, []); useEffect(() => { // Connect to WebSocket const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; // When running locally, Vite proxies /api, but for WS it's better to use direct or proxied path const wsUrl = `${protocol}//${window.location.host}/api/v1/ws`; let ws: WebSocket; let reconnectTimer: NodeJS.Timeout; const connectWs = () => { ws = new WebSocket(wsUrl); ws.onopen = () => { setWsConnected(true); console.log("WebSocket connected!"); }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'REALTIME_METRICS') { setLiveMetrics(data.payload); } } catch (e) { // ignore } }; ws.onclose = () => { setWsConnected(false); // Attempt to reconnect after 3 seconds reconnectTimer = setTimeout(connectWs, 3000); }; ws.onerror = (err) => { ws.close(); }; }; connectWs(); return () => { clearTimeout(reconnectTimer); if (ws) ws.close(); }; }, []); // Debounced search useEffect(() => { if (paletteSearch.length < 2) { setSearchResults([]); return; } const delayDebounceFn = setTimeout(async () => { setIsSearching(true); try { const res = await apiService.globalSearch(paletteSearch); if (res.data && res.data.results) { setSearchResults(res.data.results); } } catch (error) { console.error("Global search error:", error); } finally { setIsSearching(false); } }, 300); return () => clearTimeout(delayDebounceFn); }, [paletteSearch]); useEffect(() => { // Sync class with store state if (!isDark) { document.documentElement.classList.add('light-theme'); } else { document.documentElement.classList.remove('light-theme'); } }, [isDark]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); setIsCommandPaletteOpen((prev) => !prev); } if (e.key === 'Escape') { setIsCommandPaletteOpen(false); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, []); // Close mobile menu on route change useEffect(() => { setIsMobileMenuOpen(false); }, [location.pathname]); // Check if ML results exist const hasMLResults = typeof window !== 'undefined' && localStorage.getItem('hasMLResults') === 'true'; const navGroups = [ { title: 'Data Foundation', items: [ { path: '/', label: 'Home', icon: Home }, { path: '/data-hub', label: 'Data Hub', icon: Database, roles: ['Owner', 'Admin', 'Analyst'] }, { path: '/lineage', label: 'Data Lineage', icon: GitBranch, roles: ['Owner', 'Admin'] }, ] }, { title: 'Intelligence & Analytics', items: [ { path: '/dashboard', label: 'Dashboard', icon: LayoutDashboard }, ] }, { title: 'Advanced AI Lab', items: [ { path: '/chat', label: 'AI Analyst', icon: MessageSquare }, { path: '/ml-predictions', label: 'AutoML & Predict', icon: Brain, roles: ['Owner', 'Admin', 'Analyst'] }, { path: '/computer-vision', label: 'Computer Vision', icon: Camera, roles: ['Owner', 'Admin', 'Analyst'] }, { path: '/simulator', label: 'Scenario Simulator', icon: Sliders, roles: ['Owner', 'Admin', 'Analyst'] }, ] }, { title: 'Output & Tools', items: [ { path: '/reports', label: 'Reports', icon: FileText }, { path: '/collaborate', label: 'Collaborate', icon: Users }, { path: '/developer', label: 'Developer', icon: Terminal, roles: ['Owner'] }, { path: '/settings', label: 'Settings', icon: Settings, roles: ['Owner', 'Admin'] }, ] } ]; // Filter navigation items based on current role const currentRole = activeWorkspaceRole || 'Owner'; const filteredNavGroups = navGroups.map(group => ({ ...group, items: group.items.filter(item => !item.roles || item.roles.includes(currentRole)) })).filter(group => group.items.length > 0); const navItems = filteredNavGroups.flatMap(group => group.items); const isActive = (path: string) => { if (path === '/') return location.pathname === '/'; return location.pathname.startsWith(path); }; return (
{/* Premium Sidebar */} {/* Logo Section */}
navigate('/')} >
DataVision
{/* Workspace Switcher */} {workspaces.length > 1 && (
Workspace
)} {/* Navigation */} {/* Footer - Sign Out Button */}
{/* Mobile Backdrop */} {isMobileMenuOpen && ( setIsMobileMenuOpen(false)} className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40 md:hidden" /> )} {/* Main Content Area */}
{/* Premium Header */}
{/* Mobile Menu Toggle */} {/* Page Title with Breadcrumb feel + Desktop Toggle */}
{/* Desktop Toggle Button */}
DataVision / {navItems.find(item => isActive(item.path))?.label || 'Overview'}
{/* Right Actions */}
{/* Search Trigger Button */} {/* Live Metrics Widget */}
{wsConnected ? : } {liveMetrics && wsConnected ? (
{liveMetrics.active_users} {liveMetrics.cpu_load}%
) : ( Connecting... )}
{/* Theme toggle visible on desktop in header (Hidden on Dashboard due to AI Themes) */} {location.pathname !== '/dashboard' && ( )}
{/* Page Content with Smooth Transitions */}
{/* V3 Command Palette Modal */} {isCommandPaletteOpen && (
{/* Backdrop */} setIsCommandPaletteOpen(false)} className="fixed inset-0 bg-black/70 backdrop-blur-md" /> {/* Palette Container */} {/* Search Bar */}
setPaletteSearch(e.target.value)} className="w-full py-4 text-sm bg-transparent focus:outline-none placeholder-gray-500" />
{/* Items List */}
{/* Navigation Items */}
Pages & Navigation
{navItems .filter(item => item.label.toLowerCase().includes(paletteSearch.toLowerCase())) .map((item) => ( ))} {/* Global Search Results from API */} {searchResults.length > 0 && ( <>
Global Search Results
{searchResults.map((result: any) => ( ))} )} {/* Custom Action Shortcuts */} {paletteSearch.trim() !== '' && ( <>
Actions
{/* Toggle Theme Action */} {"toggle theme".includes(paletteSearch.toLowerCase()) && ( )} {/* Sign Out Action */} {"sign out logout".includes(paletteSearch.toLowerCase()) && ( )} )}
)}
); }; export default AppLayout;