import { useCallback, useRef, useState, type DragEvent } from 'react'; import { ReactFlow, Background, Controls, MiniMap, type Connection, type Node, type Edge, BackgroundVariant, type OnNodesChange, type OnEdgesChange, useReactFlow, ReactFlowProvider, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import TransactionNodeComponent from './TransactionNode'; import { Save, GitBranch, ShieldCheck, FlaskConical, Play, AlertTriangle, CheckCircle2, DollarSign, Clock, Globe, Undo2, Redo2, Copy, Trash2, Plus, X, SplitSquareHorizontal, ZoomIn, ZoomOut, Maximize } from 'lucide-react'; import type { ComponentItem, TransactionTab, SessionMode } from '../types'; const nodeTypes = { transactionNode: TransactionNodeComponent }; interface CanvasProps { nodes: Node[]; edges: Edge[]; setNodes: (updater: Node[] | ((prev: Node[]) => Node[])) => void; setEdges: (updater: Edge[] | ((prev: Edge[]) => Edge[])) => void; onNodesChange: OnNodesChange; onEdgesChange: OnEdgesChange; onConnect: (params: Connection) => void; onSelectionChange: (params: { nodes: Node[] }) => void; onDropComponent: (item: ComponentItem, position: { x: number; y: number }) => void; onValidate: () => void; onSimulate: () => void; onExecute: () => void; transactionName: string; onRenameTransaction: (name: string) => void; isSimulating: boolean; simulationResults: string | null; onDismissSimulation: () => void; mode: SessionMode; canUndo: boolean; canRedo: boolean; onUndo: () => void; onRedo: () => void; selectedNodeIds: Set; onDeleteSelected: () => void; onDuplicateSelected: () => void; transactionTabs: TransactionTab[]; activeTransactionId: string; onSwitchTab: (id: string) => void; onAddTab: () => void; onCloseTab: (id: string) => void; splitView: boolean; onToggleSplitView: () => void; pushHistory: (nodes: Node[], edges: Edge[]) => void; } function CanvasInner({ nodes, edges, onNodesChange, onEdgesChange, onConnect, onSelectionChange, onDropComponent, onValidate, onSimulate, onExecute, transactionName, onRenameTransaction, isSimulating, simulationResults, onDismissSimulation, mode, canUndo, canRedo, onUndo, onRedo, selectedNodeIds, onDeleteSelected, onDuplicateSelected, transactionTabs, activeTransactionId, onSwitchTab, onAddTab, onCloseTab, splitView, onToggleSplitView, }: CanvasProps) { const reactFlowWrapper = useRef(null); const [isEditingName, setIsEditingName] = useState(false); const [editName, setEditName] = useState(transactionName); const [zoomLevel, setZoomLevel] = useState(100); const reactFlowInstance = useReactFlow(); const onDragOver = useCallback((event: DragEvent) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; }, []); const onDrop = useCallback( (event: DragEvent) => { event.preventDefault(); const data = event.dataTransfer.getData('application/transactflow-component'); if (!data) return; const item: ComponentItem = JSON.parse(data); const wrapperBounds = reactFlowWrapper.current?.getBoundingClientRect(); if (!wrapperBounds) return; const position = reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY, }); onDropComponent(item, position); }, [onDropComponent, reactFlowInstance] ); const onMoveEnd = useCallback(() => { const zoom = reactFlowInstance.getZoom(); setZoomLevel(Math.round(zoom * 100)); }, [reactFlowInstance]); const handleZoomIn = () => { reactFlowInstance.zoomIn(); setZoomLevel(Math.round(reactFlowInstance.getZoom() * 100)); }; const handleZoomOut = () => { reactFlowInstance.zoomOut(); setZoomLevel(Math.round(reactFlowInstance.getZoom() * 100)); }; const handleFitView = () => { reactFlowInstance.fitView(); setTimeout(() => setZoomLevel(Math.round(reactFlowInstance.getZoom() * 100)), 100); }; const errorCount = nodes.filter(n => (n.data as Record).status === 'error').length; const warningCount = nodes.filter(n => (n.data as Record).status === 'warning').length; const commitName = () => { setIsEditingName(false); if (editName.trim()) onRenameTransaction(editName.trim()); else setEditName(transactionName); }; return (
{/* Transaction tabs */}
{transactionTabs.map(tab => (
onSwitchTab(tab.id)} > {tab.name} {transactionTabs.length > 1 && ( )}
))}
{isEditingName ? ( setEditName(e.target.value)} onBlur={commitName} onKeyDown={e => { if (e.key === 'Enter') commitName(); if (e.key === 'Escape') { setEditName(transactionName); setIsEditingName(false); } }} autoFocus /> ) : ( { setIsEditingName(true); setEditName(transactionName); }} title="Click to rename" > {transactionName} )} v1.0 Saved
{ const d = n.data as Record; return (d?.color as string) || '#3b82f6'; }} maskColor="rgba(0,0,0,0.7)" />
{splitView && ( <>

Comparison View

Select a saved version or branch to compare

)}
{nodes.length === 0 && !splitView && (

Start Building

Drag components from the left panel onto the canvas to compose your transaction flow

or press Ctrl+K to search components

)} {/* Simulation overlay */} {isSimulating && (
Running simulation...
)} {simulationResults && (
Simulation Results
{simulationResults}
)}
{nodes.length} nodes
{edges.length} connections
0 ? '#ef4444' : '#555'} /> {errorCount} errors
0 ? '#eab308' : '#555'} /> {warningCount} warnings
Est. fees: {nodes.length > 0 ? '$0.02%' : '—'}
Settlement: {nodes.length > 0 ? 'T+1' : '—'}
Jurisdictions: {nodes.length > 0 ? 'Multi' : '—'}
{selectedNodeIds.size > 0 && ( <>
{selectedNodeIds.size} selected
)}
); } export default function Canvas(props: CanvasProps) { return ( ); }