import React, { useState } from 'react'; import SessionModal from './SessionModal'; import { useContextMenu } from '../hooks/useContextMenu'; import { renameSession, deleteSession, updateSession } from '../api/orchestration'; const { logger } = require('@nexusai/shared'); export default function Sidebar({ sessions, activeSession, onSelectSession, onNewChat, onNewProject, isOpen, onToggle, onSessionsChange, onNavigate, projects, onProjectsChange, onSelectProject }) { const [chatsOpen, setChatsOpen] = useState(true); const [projectsOpen, setProjectsOpen] = useState(true); const [modalSession, setModalSession] = useState(null); const [modalMode, setModalMode] = useState('settings'); const [hoveredId, setHoveredId] = useState(null); const { menu, open: openMenu, close: closeMenu } = useContextMenu(); // ── Handlers ──────────────────────────────────────────── async function handleRename(session, name, projectId) { try { await updateSession(session.external_id, { name, projectId }); onSessionsChange(); } catch (err) { logger.error('[Sidebar] Rename failed:', err.message); } } async function handleDelete(session) { try { await deleteSession(session.external_id); onSessionsChange(session); } catch (err) { logger.error('[Sidebar] Delete failed:', err.message); } } // ── Collapsed rail ─────────────────────────────────────── if (!isOpen) { return (
{/* Expand toggle */}
{/* New Chat */} {/* New Project */} {/* All Chats */} {/* Spacer */}
{/* Settings */}
); } // ── Expanded sidebar ───────────────────────────────────── const recentSessions = sessions.slice(0, 10); // Group recent sessions by project const grouped = {}; const unassigned = []; for (const session of recentSessions) { if (session.project_id) { if (!grouped[session.project_id]) grouped[session.project_id] = []; grouped[session.project_id].push(session); } else { unassigned.push(session); } } const sessionRowProps = (session) => ({ session, isActive: activeSession?.external_id === session.external_id, isHovered: hoveredId === session.external_id, onHover: setHoveredId, onSelect: () => { onSelectSession(session); onNavigate('chat'); }, onRename: () => { setModalMode('settings'); setModalSession(session); }, onDelete: () => { setModalMode('confirm-delete'); setModalSession(session); }, onContextMenu: e => !session.isNew && openMenu(e, session), }); return ( <>
{/* Header */}
NexusAI
{/* Action buttons */}
{/* Scrollable content */}
{/* ── Projects section ── */} setProjectsOpen(o => !o)} /> {projectsOpen && (
{!projects?.length ? (
No projects yet
) : (
{projects.slice(0, 6).map(project => ( ))}
)}
)}
{/* ── Recent Chats section ── */} setChatsOpen(o => !o)} /> {chatsOpen && ( <> {recentSessions.length === 0 && (
No conversations yet
)} {/* Project groups */} {Object.entries(grouped).map(([projectId, projectSessions]) => { const project = projects?.find(p => p.id === Number(projectId)); return (
{/* Project group label */}
{project?.name ?? 'Project'}
{projectSessions.map(session => ( ))}
); })} {/* Unassigned sessions */} {unassigned.length > 0 && ( <> {Object.keys(grouped).length > 0 && (
Other
)} {unassigned.map(session => ( ))} )} {sessions.length > 0 && ( )} )}
{/* Settings — pinned to bottom */}
{/* Context menu */} {menu && (
e.stopPropagation()} style={{ position: 'fixed', top: menu.y, left: menu.x, background: 'var(--bg-elevated)', border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', padding: '4px', zIndex: 50, minWidth: '140px', }} > { setModalMode('settings'); setModalSession(menu.session); closeMenu(); }} >✎ Rename { setModalMode('confirm-delete'); setModalSession(menu.session); closeMenu(); }} danger >✕ Delete
)} {/* Session modal */} {modalSession && ( setModalSession(null)} projects={projects} /> )} ); } // ── Sub-components ─────────────────────────────────────────── function SectionHeader({ label, isOpen, onToggle }) { return ( ); } function SessionRow({ session, isActive, isHovered, onHover, onSelect, onRename, onDelete, onContextMenu }) { return (
onHover(session.external_id)} onMouseLeave={() => onHover(null)} onContextMenu={onContextMenu} style={{ position: 'relative', display: 'flex', alignItems: 'stretch', background: isActive || isHovered ? 'var(--bg-elevated)' : 'transparent', borderLeft: isActive ? '2px solid var(--accent)' : '2px solid transparent', transition: 'background 0.1s', overflow: 'hidden', width: '100%', boxSizing: 'border-box', }} >
); } function ContextMenuItem({ children, onClick, danger }) { return ( ); }