import React, { useState, useEffect } from 'react'; import { MessageSquare, SquarePen, Search, MoreVertical, Trash2, LogOut, User, UserCircle, DatabaseZap, KeyRound, PanelLeft, FileText, ChevronRight, Clock } from 'lucide-react'; import * as LucideIcons from 'lucide-react'; import { useAppConfig } from '../contexts/AppConfigContext'; import UserAvatarPicker from './UserAvatarPicker'; import CopyrightNotice from './CopyrightNotice'; import '../styles/Sidebar.css'; const Sidebar = ({ user, currentSessionId, onSelectSession, onNewChat, onSignOut, authToken, onSidebarToggle, isMobileOpen = false, onMobileToggle, refreshTrigger, onCurrentSessionDeleted, pageContext = 'chat', canvasItems = [], canvasSubview = 'workspace', widgetGroups = [], deliverableProjects = [], insightSections = [], userAvatarId, onAvatarChange, onOpenProfile, onOpenAccount, onOpenClearData, }) => { const { config } = useAppConfig(); const isOnCanvas = pageContext === 'canvas'; const [showAvatarPicker, setShowAvatarPicker] = useState(false); const avatarOptions = config?.app?.user_avatars || []; const currentAvatar = avatarOptions.find(a => a.id === userAvatarId); const AvatarIcon = currentAvatar ? (LucideIcons[currentAvatar.icon] || User) : User; const [expanded, setExpanded] = useState(() => { try { return JSON.parse(localStorage.getItem('sidebar-expanded-v1') || '{}'); } catch { return {}; } }); const toggleExpanded = (key) => { setExpanded(prev => { const next = { ...prev, [key]: !prev[key] }; localStorage.setItem('sidebar-expanded-v1', JSON.stringify(next)); return next; }); }; const [chatSessions, setChatSessions] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const [isLoading, setIsLoading] = useState(true); const [showUserMenu, setShowUserMenu] = useState(false); const [isCollapsed, setIsCollapsed] = useState(false); const [isCreatingNewChat, setIsCreatingNewChat] = useState(false); useEffect(() => { if (authToken) { fetchChatSessions(); } }, [authToken]); useEffect(() => { const handleOverlayClick = (e) => { // Only close if clicking the overlay itself, not the sidebar if (e.target.classList.contains('mobile-sidebar-overlay')) { onMobileToggle(false); } }; if (isMobileOpen) { document.addEventListener('click', handleOverlayClick); return () => document.removeEventListener('click', handleOverlayClick); } }, [isMobileOpen, onMobileToggle]); // Notify parent when sidebar state changes useEffect(() => { if (onSidebarToggle) { onSidebarToggle(isCollapsed); } }, [isCollapsed, onSidebarToggle]); // Add effect to refresh when currentSessionId changes (new session created) useEffect(() => { if (currentSessionId && authToken) { // Small delay to ensure the session is saved to database const timer = setTimeout(() => { fetchChatSessions(); }, 200); return () => clearTimeout(timer); } }, [currentSessionId, authToken]); // Refresh session list when parent signals a message exchange completed useEffect(() => { if (refreshTrigger > 0 && authToken) { fetchChatSessions(); } }, [refreshTrigger]); const fetchChatSessions = async () => { try { const response = await fetch(`${process.env.REACT_APP_API_URL}/api/chat-sessions`, { headers: { 'Authorization': `Bearer ${authToken}`, 'Content-Type': 'application/json' } }); if (response.ok) { const sessions = await response.json(); setChatSessions(sessions); } else { console.error('Failed to fetch chat sessions'); } } catch (error) { console.error('Error fetching chat sessions:', error); } finally { setIsLoading(false); } }; const handleNewChat = async () => { setIsCreatingNewChat(true); try { // Call the parent's new chat handler and wait for it to complete await onNewChat(); // Refresh the sessions list immediately after new chat is created // The parent should have updated currentSessionId by now await fetchChatSessions(); } catch (error) { console.error('Error creating new chat:', error); // Optionally show an error message to the user } finally { setIsCreatingNewChat(false); } }; const handleDeleteSession = async (sessionId, event) => { event.stopPropagation(); if (window.confirm('Are you sure you want to delete this chat?')) { try { const response = await fetch(`${process.env.REACT_APP_API_URL}/api/chat-sessions/${sessionId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${authToken}`, 'Content-Type': 'application/json' } }); if (response.ok) { setChatSessions(prev => prev.filter(session => session.id !== sessionId)); if (currentSessionId === sessionId) { onCurrentSessionDeleted?.(); } } } catch (error) { console.error('Error deleting chat session:', error); } } }; const toggleSidebar = () => { setIsCollapsed(!isCollapsed); // Close user menu when collapsing if (!isCollapsed) { setShowUserMenu(false); } }; const filteredSessions = chatSessions.filter(session => session.title.toLowerCase().includes(searchTerm.toLowerCase()) ); const formatDate = (dateString) => { const date = new Date(dateString); const now = new Date(); const diffTime = Math.abs(now - date); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); if (diffDays === 1) return 'Today'; if (diffDays === 2) return 'Yesterday'; if (diffDays <= 7) return `${diffDays - 1} days ago`; return date.toLocaleDateString(); }; return ( <>
{/* Header */}
{!isCollapsed && ( <>
onAvatarChange && setShowAvatarPicker(true)} style={{ cursor: onAvatarChange ? 'pointer' : undefined, backgroundColor: currentAvatar?.bg || undefined, color: currentAvatar?.color || undefined, }} title={onAvatarChange ? 'Change avatar' : undefined} >
{user.firstName} {user.lastName} {user.email}
{/* Toggle button next to user menu when expanded */}
{showUserMenu && (
)}
)} {isCollapsed && (
{/* Toggle button replaces user avatar when collapsed */}
)}
{/* Search + New Chat - only show when expanded */} {!isCollapsed && (
setSearchTerm(e.target.value)} className="search-input" />
{!isOnCanvas && ( )}
)} {/* Canvas sidebar โ€” subview-aware (Insights / Workspace / Deliverables) */} {isOnCanvas ? (
{!isCollapsed && (() => { const q = searchTerm.toLowerCase(); // ---------- DELIVERABLES: project list with expandable section dropdown ---------- if (canvasSubview === 'deliverables') { const projects = deliverableProjects.filter(p => !q || p.name.toLowerCase().includes(q) || p.sections.some(s => s.name.toLowerCase().includes(q)) ); if (projects.length === 0) { return (
{searchTerm ? 'No drafts match' : 'No drafts yet โ€” create one in Documents'}
); } return projects.map(p => { const open = expanded[`p-${p.id}`] ?? p.isActive; const totalWords = p.sections.reduce((s, x) => s + x.wc, 0); return (
{open && (
{p.sections.map(s => ( ))}
{p.versions} version{p.versions === 1 ? '' : 's'} ยท auto-saved
{totalWords} words total
)}
); }); } // ---------- WORKSPACE: widgets grouped by category ---------- if (canvasSubview === 'workspace') { const groups = widgetGroups .map(g => ({ ...g, items: g.items.filter(it => !q || it.label.toLowerCase().includes(q)), })) .filter(g => g.items.length > 0); if (groups.length === 0) { return (
{searchTerm ? 'No widgets match' : 'Workspace is empty โ€” add widgets'}
); } return groups.map(g => { const open = expanded[`g-${g.id}`] ?? true; return (
{open && (
{g.items.map(it => ( ))}
)}
); }); } // ---------- INSIGHTS: section list with confidence badges ---------- if (canvasSubview === 'insights') { const sections = insightSections.filter(s => !q || s.name.toLowerCase().includes(q)); if (sections.length === 0) { return
{searchTerm ? 'No sections match' : 'No insights yet'}
; } return (
Sections {sections.length}
{sections.map(s => { const complete = s.taskCount > 0 && s.doneCount === s.taskCount; return ( ); })}
); } // ---------- Fallback: flat list (legacy) ---------- const items = canvasItems.filter(it => !q || it.label.toLowerCase().includes(q)); if (items.length === 0) { return
{searchTerm ? 'No matches' : 'Nothing here yet'}
; } return (
{items.map((it) => (
{it.label}
{it.sub &&
{it.sub}
}
))}
); })()}
) : ( /* Chat Sessions */
{isLoading ? (
{!isCollapsed && Loading chats...}
) : isCreatingNewChat ? (
{!isCollapsed && Creating new chat...}
) : filteredSessions.length === 0 ? (
{!isCollapsed && (searchTerm ? 'No chats found' : 'No chats yet')}
) : (
{filteredSessions.map((session) => (
onSelectSession(session.id)} title={isCollapsed ? session.title : ''} >
{!isCollapsed && (
{session.title}
{formatDate(session.updated_at)} {session.message_count} messages
)}
{!isCollapsed && ( )}
))}
)}
)} {/* Footer */}
{!isCollapsed && Neon.ai} {!isCollapsed && }
{showAvatarPicker && ( { onAvatarChange?.(id); setShowAvatarPicker(false); }} onClose={() => setShowAvatarPicker(false)} /> )} {isMobileOpen && (
onMobileToggle(false)} /> )} ); }; export default Sidebar;