| import { useState, useRef, useEffect } from 'react' | |
| export default function NodeEditor({ nodes, setNodes, connections, setConnections, selectedNode, setSelectedNode }) { | |
| const canvasRef = useRef(null) | |
| const [isConnecting, setIsConnecting] = useState(false) | |
| const [connectionStart, setConnectionStart] = useState(null) | |
| const [draggedNode, setDraggedNode] = useState(null) | |
| const [mousePos, setMousePos] = useState({ x: 0, y: 0 }) | |
| useEffect(() => { | |
| const handleDragOver = (e) => { | |
| e.preventDefault() | |
| setMousePos({ x: e.clientX, y: e.clientY }) | |
| } | |
| const handleDrop = (e) => { | |
| e.preventDefault() | |
| const nodeType = e.dataTransfer.getData('node-type') | |
| if (nodeType && canvasRef.current) { | |
| const rect = canvasRef.current.getBoundingClientRect() | |
| const newNode = { | |
| id: `node-${Date.now()}`, | |
| type: nodeType, | |
| x: e.clientX - rect.left - 75, | |
| y: e.clientY - rect.top - 30, | |
| properties: { | |
| name: nodeType, | |
| units: 128, | |
| activation: 'relu' | |
| } | |
| } | |
| setNodes(prev => [...prev, newNode]) | |
| } | |
| } | |
| const canvas = canvasRef.current | |
| if (canvas) { | |
| canvas.addEventListener('dragover', handleDragOver) | |
| canvas.addEventListener('drop', handleDrop) | |
| } | |
| return () => { | |
| if (canvas) { | |
| canvas.removeEventListener('dragover', handleDragOver) | |
| canvas.removeEventListener('drop', handleDrop) | |
| } | |
| } | |
| }, [setNodes]) | |
| const handleNodeMouseDown = (e, nodeId) => { | |
| if (e.target.dataset.handle === 'output') { | |
| setIsConnecting(true) | |
| setConnectionStart(nodeId) | |
| } else { | |
| setDraggedNode(nodeId) | |
| setSelectedNode(nodeId) | |
| } | |
| } | |
| const handleNodeMouseUp = (e, nodeId) => { | |
| if (isConnecting && connectionStart && connectionStart !== nodeId) { | |
| const newConnection = { | |
| id: `conn-${Date.now()}`, | |
| from: connectionStart, | |
| to: nodeId | |
| } | |
| setConnections(prev => [...prev, newConnection]) | |
| } | |
| setIsConnecting(false) | |
| setConnectionStart(null) | |
| } | |
| const handleMouseMove = (e) => { | |
| if (draggedNode && canvasRef.current) { | |
| const rect = canvasRef.current.getBoundingClientRect() | |
| setNodes(prev => prev.map(node => | |
| node.id === draggedNode | |
| ? { ...node, x: e.clientX - rect.left - 75, y: e.clientY - rect.top - 30 } | |
| : node | |
| )) | |
| } | |
| setMousePos({ x: e.clientX, y: e.clientY }) | |
| } | |
| const handleMouseUp = () => { | |
| setDraggedNode(null) | |
| setIsConnecting(false) | |
| setConnectionStart(null) | |
| } | |
| return ( | |
| <div | |
| ref={canvasRef} | |
| className="flex-1 bg-gray-100 relative overflow-hidden" | |
| data-tutorial-target="canvas" | |
| onMouseMove={handleMouseMove} | |
| onMouseUp={handleMouseUp} | |
| onMouseLeave={handleMouseUp} | |
| > | |
| {/* Grid Background */} | |
| <div className="absolute inset-0 opacity-10"> | |
| <div className="h-full w-full" style={{ | |
| backgroundImage: 'linear-gradient(#000 1px, transparent 1px), linear-gradient(90deg, #000 1px, transparent 1px)', | |
| backgroundSize: '20px 20px' | |
| }} /> | |
| </div> | |
| {/* Connections */} | |
| <svg className="absolute inset-0 pointer-events-none" style={{ width: '100%', height: '100%' }}> | |
| {connections.map(conn => { | |
| const fromNode = nodes.find(n => n.id === conn.from) | |
| const toNode = nodes.find(n => n.id === conn.to) | |
| if (!fromNode || !toNode) return null | |
| return ( | |
| <g key={conn.id}> | |
| <line | |
| x1={fromNode.x + 150} | |
| y1={fromNode.y + 30} | |
| x2={toNode.x} | |
| y2={toNode.y + 30} | |
| stroke="#3b82f6" | |
| strokeWidth="2" | |
| markerEnd="url(#arrowhead)" | |
| /> | |
| </g> | |
| ) | |
| })} | |
| {isConnecting && connectionStart && (() => { | |
| const fromNode = nodes.find(n => n.id === connectionStart) | |
| if (!fromNode) return null | |
| return ( | |
| <line | |
| x1={fromNode.x + 150} | |
| y1={fromNode.y + 30} | |
| x2={mousePos.x - canvasRef.current?.getBoundingClientRect().left} | |
| y2={mousePos.y - canvasRef.current?.getBoundingClientRect().top} | |
| stroke="#3b82f6" | |
| strokeWidth="2" | |
| strokeDasharray="5,5" | |
| /> | |
| ) | |
| })()} | |
| <defs> | |
| <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto"> | |
| <polygon points="0 0, 10 3.5, 0 7" fill="#3b82f6" /> | |
| </marker> | |
| </defs> | |
| </svg> | |
| {/* Nodes */} | |
| {nodes.map(node => ( | |
| <div | |
| key={node.id} | |
| className={`absolute bg-white rounded-lg shadow-lg border-2 cursor-move select-none ${ | |
| selectedNode === node.id ? 'border-primary-500' : 'border-gray-300' | |
| }`} | |
| style={{ left: node.x, top: node.y, width: '150px' }} | |
| onMouseDown={(e) => handleNodeMouseDown(e, node.id)} | |
| onMouseUp={(e) => handleNodeMouseUp(e, node.id)} | |
| > | |
| <div className="px-3 py-2 bg-gray-50 border-b border-gray-200 rounded-t-lg"> | |
| <h3 className="text-sm font-semibold text-gray-900">{node.properties.name}</h3> | |
| </div> | |
| <div className="p-3"> | |
| <div className="text-xs text-gray-600 mb-2">{node.type}</div> | |
| <div className="text-xs text-gray-500">Units: {node.properties.units}</div> | |
| </div> | |
| <div className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-2"> | |
| <div | |
| data-handle="input" | |
| className="w-4 h-4 bg-gray-400 rounded-full cursor-crosshair hover:bg-primary-500" | |
| /> | |
| </div> | |
| <div className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-2"> | |
| <div | |
| data-handle="output" | |
| className="w-4 h-4 bg-gray-400 rounded-full cursor-crosshair hover:bg-primary-500" | |
| /> | |
| </div> | |
| </div> | |
| ))} | |
| {/* Empty State */} | |
| {nodes.length === 0 && ( | |
| <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-center"> | |
| <div className="w-24 h-24 bg-gray-200 rounded-full flex items-center justify-center mx-auto mb-4"> | |
| <Layers className="w-12 h-12 text-gray-400" /> | |
| </div> | |
| <h3 className="text-xl font-semibold text-gray-700 mb-2">Start Building Your Architecture</h3> | |
| <p className="text-gray-500 mb-6">Drag layers from the toolbar onto the canvas</p> | |
| <button | |
| onClick={() => window.dispatchEvent(new CustomEvent('start-tutorial'))} | |
| className="px-6 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors" | |
| > | |
| Start Tutorial | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| } |