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 (
);
}