import React, { useState, useEffect, useCallback } from 'react'; import { Box, IconButton, CircularProgress, Tooltip, Collapse, } from '@mui/material'; import { ChevronLeft, ChevronRight, ExpandMore, ChevronRight as ChevronRightIcon, Home as HomeIcon, CalendarToday, DateRange, Event, } from '@mui/icons-material'; import { useNavigationStore } from '../../stores/navigationStore'; import { useAuth } from '../../contexts/AuthContext'; import { NeoGraphNode } from '../../types/navigation'; import { logger } from '../../debugConfig'; interface TreeNode extends NeoGraphNode { has_children?: boolean; children?: TreeNode[]; } const NODE_ICONS: Record = { User: HomeIcon, CalendarYear: CalendarToday, CalendarMonth: DateRange, CalendarWeek: DateRange, CalendarDay: Event, }; const SIDEBAR_WIDTH = 220; interface TreeItemProps { node: TreeNode; depth: number; onSelect: (node: TreeNode) => void; onExpand: (node: TreeNode) => Promise; activeRoomId?: string; } function TreeItem({ node, depth, onSelect, onExpand, activeRoomId }: TreeItemProps) { const [expanded, setExpanded] = useState(false); const [children, setChildren] = useState(node.children || []); const [loading, setLoading] = useState(false); const Icon = NODE_ICONS[node.node_type] || HomeIcon; const canExpand = node.has_children !== false && node.node_type !== 'CalendarDay'; const handleToggle = async (e: React.MouseEvent) => { e.stopPropagation(); if (!expanded && children.length === 0 && canExpand) { setLoading(true); try { const loaded = await onExpand(node); setChildren(loaded); } finally { setLoading(false); } } setExpanded(v => !v); }; return ( onSelect(node)} sx={{ display: 'flex', alignItems: 'center', pl: depth * 1.5 + 0.5, pr: 0.5, py: 0.4, cursor: 'pointer', borderRadius: 1, mx: 0.5, fontSize: '0.78rem', minHeight: 28, bgcolor: 'transparent', '&:hover': { bgcolor: 'action.hover' }, }} > {canExpand && ( loading ? : ( {expanded ? : } ) )} {node.label} {canExpand && ( {children.map(child => ( ))} )} ); } interface GraphSidebarProps { open: boolean; onToggle: () => void; } export function GraphSidebar({ open, onToggle }: GraphSidebarProps) { const { accessToken } = useAuth(); const { navigateToNeoNode, context } = useNavigationStore(); const [tree, setTree] = useState(null); const [loading, setLoading] = useState(false); const apiBase = import.meta.env.VITE_API_BASE as string; const fetchTree = useCallback(async () => { if (!accessToken) return; setLoading(true); try { const res = await fetch(`${apiBase}/graph/tree`, { headers: { Authorization: `Bearer ${accessToken}` }, }); if (!res.ok) throw new Error(`Graph tree fetch failed: ${res.status}`); const data = await res.json(); setTree(data.tree); } catch (err) { logger.error('graph-sidebar', 'Failed to load graph tree', err); } finally { setLoading(false); } }, [accessToken, apiBase]); useEffect(() => { if (open && !tree && accessToken) fetchTree(); }, [open, tree, accessToken, fetchTree]); const handleExpand = useCallback(async (node: TreeNode): Promise => { if (!accessToken) return []; const params = new URLSearchParams({ neo4j_node_id: node.neo4j_node_id, neo4j_db_name: node.neo4j_db_name, node_type: node.node_type, }); try { const res = await fetch(`${apiBase}/graph/node/children?${params}`, { headers: { Authorization: `Bearer ${accessToken}` }, }); if (!res.ok) return []; const data = await res.json(); return data.children || []; } catch { return []; } }, [accessToken, apiBase]); return ( {loading ? ( ) : tree ? ( navigateToNeoNode(n)} onExpand={handleExpand} activeRoomId={context.node?.id} /> ) : null} {open ? : } ); }