import { useState, useCallback, useEffect, useRef } from "react"; import { ReactFlowProvider, useReactFlow } from "reactflow"; import GraphEditor from "./components/Editor/GraphEditor"; import LeftPanel, { type LeftPanelRef } from "./components/Panels/LeftPanel"; import RightPanel from "./components/Panels/RightPanel"; import BottomPanel from "./components/Timeline/BottomPanel"; import DocumentTabs from "./components/Workspace/DocumentTabs"; import MenuBar from "./components/Menu/MenuBar"; import DocumentManager from "./components/Workspace/DocumentManager"; import KeyboardShortcutsHelp from "./components/Common/KeyboardShortcutsHelp"; import ToastContainer from "./components/Common/ToastContainer"; import { KeyboardShortcutProvider } from "./contexts/KeyboardShortcutContext"; import { useGlobalShortcuts } from "./hooks/useGlobalShortcuts"; import { useDocumentHistory } from "./hooks/useDocumentHistory"; import { useWorkspaceStore } from "./stores/workspaceStore"; import { usePanelStore } from "./stores/panelStore"; import { useCreateDocument } from "./hooks/useCreateDocument"; import type { Actor, Relation, Group } from "./types"; import type { ExportOptions } from "./utils/graphExport"; /** * App - Root application component * * Layout: * - Header with title * - Menu bar (File, Edit, View) with undo/redo controls * - Document tabs for multi-file support * - Main graph editor canvas * * Features: * - Responsive layout * - ReactFlowProvider wrapper for graph functionality * - Multi-document workspace with tabs * - Organized menu system for file and editing operations * - Per-document undo/redo with keyboard shortcuts * - Centralized keyboard shortcut management system */ /** Inner component that has access to ReactFlow context */ function AppContent() { const { undo, redo } = useDocumentHistory(); const { activeDocumentId } = useWorkspaceStore(); const { leftPanelVisible, rightPanelVisible, bottomPanelVisible } = usePanelStore(); const { handleNewDocument, NewDocumentDialog } = useCreateDocument(); const [showDocumentManager, setShowDocumentManager] = useState(false); const [showKeyboardHelp, setShowKeyboardHelp] = useState(false); // Ref for LeftPanel to call focusSearch const leftPanelRef = useRef(null); const [selectedNode, setSelectedNode] = useState(null); const [selectedEdge, setSelectedEdge] = useState(null); const [selectedGroup, setSelectedGroup] = useState(null); // Use refs for callbacks to avoid triggering re-renders const addNodeCallbackRef = useRef< ((nodeTypeId: string, position?: { x: number; y: number }) => void) | null >(null); const exportCallbackRef = useRef< ((format: "png" | "svg", options?: ExportOptions) => Promise) | null >(null); const { fitView } = useReactFlow(); // Listen for document manager open event from EmptyState useEffect(() => { const handleOpenDocumentManager = () => { setShowDocumentManager(true); }; window.addEventListener("openDocumentManager", handleOpenDocumentManager); return () => window.removeEventListener( "openDocumentManager", handleOpenDocumentManager, ); }, []); const handleFitView = useCallback(() => { fitView({ padding: 0.2, duration: 300 }); }, [fitView]); // Setup global keyboard shortcuts useGlobalShortcuts({ onUndo: undo, onRedo: redo, onNewDocument: handleNewDocument, onOpenDocumentManager: () => setShowDocumentManager(true), onOpenHelp: () => setShowKeyboardHelp(true), onFitView: handleFitView, onFocusSearch: () => leftPanelRef.current?.focusSearch(), }); // Escape key to close property panels useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // Escape: Close property panels if (e.key === "Escape") { if (selectedNode || selectedEdge || selectedGroup) { e.preventDefault(); setSelectedNode(null); setSelectedEdge(null); setSelectedGroup(null); } } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [selectedNode, selectedEdge, selectedGroup]); return (
{/* Header */}
Constellation Analyzer Logo

Constellation Analyzer

Visual editor for analyzing actors and their relationships
{/* Menu Bar */} setShowKeyboardHelp(true)} onFitView={handleFitView} onExport={exportCallbackRef.current || undefined} /> {/* Document Tabs */} {/* Main content area with side panels and bottom panel */}
{/* Top section: Left panel, graph editor, right panel */}
{/* Left Panel */} {leftPanelVisible && activeDocumentId && ( { setSelectedNode(null); setSelectedEdge(null); setSelectedGroup(null); }} onAddNode={addNodeCallbackRef.current || undefined} /> )} {/* Center: Graph Editor */}
{ setSelectedNode(node); // Only clear others if we're setting a node (not clearing) if (node) { setSelectedEdge(null); setSelectedGroup(null); } }} onEdgeSelect={(edge) => { setSelectedEdge(edge); // Only clear others if we're setting an edge (not clearing) if (edge) { setSelectedNode(null); setSelectedGroup(null); } }} onGroupSelect={(group) => { setSelectedGroup(group); // Only clear others if we're setting a group (not clearing) if (group) { setSelectedNode(null); setSelectedEdge(null); } }} onAddNodeRequest={( callback: ( nodeTypeId: string, position?: { x: number; y: number }, ) => void, ) => { addNodeCallbackRef.current = callback; }} onExportRequest={( callback: ( format: "png" | "svg", options?: ExportOptions, ) => Promise, ) => { exportCallbackRef.current = callback; }} />
{/* Right Panel */} {rightPanelVisible && activeDocumentId && ( { setSelectedNode(null); setSelectedEdge(null); setSelectedGroup(null); }} /> )}
{/* Bottom Panel (Timeline) - show when bottomPanelVisible and there's an active document */} {bottomPanelVisible && activeDocumentId && ( )}
{/* Document Manager Modal */} setShowDocumentManager(false)} /> {/* Keyboard Shortcuts Help Modal */} setShowKeyboardHelp(false)} /> {/* Toast Notifications */} {/* New Document Dialog */} {NewDocumentDialog}
); } function App() { return ( ); } export default App;