'use client'; import { useState, useEffect } from 'react'; import { cn } from '@/lib/utils'; import { ConversationHistorySidebar } from '@/components/chat/conversation-history-sidebar'; import { MessageCircle, X, Bot, Brain, Globe, Database, Menu, LogOut, ChevronLeft, ChevronRight, History, Search, MoreHorizontal, Clock, Filter, BarChart3, Users } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { VersionDisplay } from '@/components/ui/version-display'; import Link from 'next/link'; import Image from 'next/image'; import { usePathname } from 'next/navigation'; import { User } from '@/types'; import { useAuthStore } from '@/stores/auth-store'; import { useChatStore } from '@/stores/chat-store'; import { getInitials } from '@/lib/utils'; import { getAuthToken, getTenantInfo } from '@/services/auth'; import { getUserRole } from '@/lib/permissions'; interface SidebarProps { user: User | null; onCollapseChange?: (collapsed: boolean) => void; onSelectConversation?: (conversationId: string) => void; } export function Sidebar({ user, onCollapseChange, onSelectConversation }: SidebarProps) { const pathname = usePathname(); const { logout } = useAuthStore(); const { unreadCounts } = useChatStore(); const [showUserMenu, setShowUserMenu] = useState(false); const [tenantInfo, setTenantInfo] = useState<{name: string; domain: string} | null>(null); const [availableAgents, setAvailableAgents] = useState<{id: string, name: string}[]>([]); const [isCollapsed, setIsCollapsed] = useState(() => { // Load saved state from localStorage on initial render if (typeof window !== 'undefined') { const saved = localStorage.getItem('gt-sidebar-collapsed'); return saved ? JSON.parse(saved) : false; } return false; }); // Track if user has ever interacted with the sidebar const [hasUserInteracted, setHasUserInteracted] = useState(() => { if (typeof window !== 'undefined') { return localStorage.getItem('gt-sidebar-collapsed') !== null; } return false; }); const [showPulse, setShowPulse] = useState(false); const [isTabHovered, setIsTabHovered] = useState(false); const [isNarrowScreen, setIsNarrowScreen] = useState(false); const [userManuallyExpanded, setUserManuallyExpanded] = useState(false); // Fetch tenant display name from control panel (via Next.js API route) // Cache in sessionStorage to persist across navigation useEffect(() => { const fetchTenantDisplayName = async () => { if (typeof window === 'undefined') return; try { const localTenantInfo = getTenantInfo(); if (!localTenantInfo) return; // Check if we have cached tenant display name in sessionStorage const cachedTenantName = sessionStorage.getItem('gt2_tenant_display_name'); if (cachedTenantName) { // Use cached value immediately setTenantInfo({ domain: localTenantInfo.domain, name: cachedTenantName, id: localTenantInfo.id }); return; } // Fetch tenant display name via Next.js API route (avoids CORS) const response = await fetch('/api/tenant-info'); if (response.ok) { const data = await response.json(); const displayName = data.name || localTenantInfo.name; // Cache the display name in sessionStorage sessionStorage.setItem('gt2_tenant_display_name', displayName); // Update with correct display name from control panel setTenantInfo({ domain: localTenantInfo.domain, name: displayName, id: localTenantInfo.id }); } else { // If API fails, use localStorage value setTenantInfo(localTenantInfo); } } catch (error) { console.error('Failed to fetch tenant display name:', error); // On error, use localStorage value const tenant = getTenantInfo(); if (tenant) { setTenantInfo(tenant); } } }; fetchTenantDisplayName(); }, []); // Check screen width and auto-collapse on narrow screens useEffect(() => { const checkScreenWidth = () => { const screenWidth = window.innerWidth; const sidebarWidth = 320; // w-80 = 320px const minMainContentWidth = 400; // Minimum space needed for main content const collisionPoint = sidebarWidth + minMainContentWidth; // 720px const isNarrow = screenWidth < 1024; // lg breakpoint const isVeryNarrow = screenWidth < 768; // md breakpoint const wouldOverlap = screenWidth < collisionPoint; // Hard boundary at 720px setIsNarrowScreen(isNarrow); // Auto-collapse on narrow screens if user has never interacted with the sidebar if (isNarrow && !isCollapsed && !hasUserInteracted) { setIsCollapsed(true); } // Force collapse on very narrow screens regardless of user interaction if (isVeryNarrow && !isCollapsed) { setIsCollapsed(true); setUserManuallyExpanded(false); // Reset manual override on very narrow screens } // HARD BOUNDARY: Force collapse when sidebar would overlap main content if (wouldOverlap && !isCollapsed) { setIsCollapsed(true); setUserManuallyExpanded(false); // Reset manual override when hitting boundary } // Reset manual override when going back to wide screen if (!isNarrow) { setUserManuallyExpanded(false); } }; if (typeof window !== 'undefined') { // Only run checkScreenWidth on mount and resize, not on every render checkScreenWidth(); window.addEventListener('resize', checkScreenWidth); return () => window.removeEventListener('resize', checkScreenWidth); } }, []); // Remove dependencies to prevent running on state changes // Helper to check if any navigation page is active const isNavActive = ['/chat', '/agents', '/datasets', '/teams', '/observability'].some(path => pathname === path || pathname.startsWith(path + '/') ); // Profile menu is no longer used for navigation, only for sign out // Save collapse state to localStorage whenever it changes useEffect(() => { if (typeof window !== 'undefined') { localStorage.setItem('gt-sidebar-collapsed', JSON.stringify(isCollapsed)); onCollapseChange?.(isCollapsed); } }, [isCollapsed, onCollapseChange]); // Load user's available agents - ONLY on chat page for performance useEffect(() => { const loadUserAgents = async () => { try { const token = getAuthToken(); if (!token) return; // Use lightweight minimal endpoint for better performance const response = await fetch('/api/v1/agents/minimal', { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, }); if (response.ok) { const agents = await response.json(); console.log('🤖 Loaded minimal agents for filtering:', agents); setAvailableAgents(agents); } } catch (error) { console.error('Error loading user agents:', error); } }; // Load agents once on first mount, cache for entire session // No pathname check - agents needed for filter dropdown on all pages if (availableAgents.length === 0) { loadUserAgents(); } }, [availableAgents.length]); const handleCollapseToggle = () => { setShowPulse(true); const newCollapsedState = !isCollapsed; const screenWidth = window.innerWidth; const sidebarWidth = 320; // w-80 = 320px const minMainContentWidth = 400; // Minimum space needed for main content const collisionPoint = sidebarWidth + minMainContentWidth; // 720px const isVeryNarrow = screenWidth < 768; // md breakpoint const wouldOverlap = screenWidth < collisionPoint; // Hard boundary // Prevent expansion on very narrow screens if (!newCollapsedState && isVeryNarrow) { setTimeout(() => setShowPulse(false), 300); return; // Don't expand on very narrow screens } // HARD BOUNDARY: Prevent expansion when it would overlap main content if (!newCollapsedState && wouldOverlap) { setTimeout(() => setShowPulse(false), 300); return; // Don't expand when it would cause overlap } setIsCollapsed(newCollapsedState); setIsTabHovered(false); // Reset hover state when toggling setHasUserInteracted(true); // Mark that user has interacted with sidebar // If user is expanding on a narrow screen, mark as manual override if (!newCollapsedState && isNarrowScreen && !isVeryNarrow && !wouldOverlap) { setUserManuallyExpanded(true); } setTimeout(() => setShowPulse(false), 300); }; return (
{tenantInfo.name}
{user?.full_name || 'Unknown User'}
{user?.user_type?.replace('_', ' ') || 'User'}
GT AI OS Community | v2.0.33