Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>NodeVis - Interactive Node Visualization</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/konva@8.3.2/konva.min.js"></script> | |
| <style> | |
| .tooltip { | |
| position: relative; | |
| display: inline-block; | |
| } | |
| .tooltip .tooltiptext { | |
| visibility: hidden; | |
| width: 120px; | |
| background-color: #555; | |
| color: #fff; | |
| text-align: center; | |
| border-radius: 6px; | |
| padding: 5px; | |
| position: absolute; | |
| z-index: 1; | |
| bottom: 125%; | |
| left: 50%; | |
| margin-left: -60px; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| .tooltip:hover .tooltiptext { | |
| visibility: visible; | |
| opacity: 1; | |
| } | |
| #container { | |
| box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <header class="flex flex-col md:flex-row justify-between items-center mb-8"> | |
| <div class="flex items-center mb-4 md:mb-0"> | |
| <div class="w-10 h-10 rounded-full bg-blue-500 flex items-center justify-center mr-3"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> | |
| </svg> | |
| </div> | |
| <h1 class="text-3xl font-bold text-gray-800">NodeVis</h1> | |
| </div> | |
| <div class="flex space-x-4"> | |
| <button id="addNodeBtn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg flex items-center transition"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" viewBox="0 0 20 20" fill="currentColor"> | |
| <path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" /> | |
| </svg> | |
| Add Node | |
| </button> | |
| <button id="clearBtn" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg flex items-center transition"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" viewBox="0 0 20 20" fill="currentColor"> | |
| <path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" /> | |
| </svg> | |
| Clear All | |
| </button> | |
| <div class="tooltip"> | |
| <button id="helpBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded-lg flex items-center transition"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> | |
| <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" /> | |
| </svg> | |
| </button> | |
| <span class="tooltiptext">Click to add nodes, drag to connect them</span> | |
| </div> | |
| </div> | |
| </header> | |
| <div class="bg-white rounded-xl p-4 shadow-lg mb-6"> | |
| <div class="flex flex-wrap gap-4 mb-4"> | |
| <div class="flex items-center"> | |
| <div class="w-4 h-4 rounded-full bg-blue-500 mr-2"></div> | |
| <span class="text-sm">Nodes</span> | |
| </div> | |
| <div class="flex items-center"> | |
| <div class="w-4 h-4 rounded-full bg-green-500 mr-2"></div> | |
| <span class="text-sm">Start Node</span> | |
| </div> | |
| <div class="flex items-center"> | |
| <div class="w-4 h-4 rounded-full bg-red-500 mr-2"></div> | |
| <span class="text-sm">End Node</span> | |
| </div> | |
| <div class="flex items-center"> | |
| <svg height="20" width="20"> | |
| <line x1="0" y1="10" x2="20" y2="10" style="stroke:gray;stroke-width:2" /> | |
| </svg> | |
| <span class="text-sm ml-2">Connections</span> | |
| </div> | |
| </div> | |
| <div id="nodeCounter" class="text-gray-600 text-sm">0 nodes created</div> | |
| </div> | |
| <div id="container" class="w-full h-96 bg-white rounded-xl overflow-hidden"></div> | |
| <div class="mt-6 grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <div class="bg-white p-4 rounded-lg shadow"> | |
| <h3 class="font-semibold text-lg mb-2">Node Properties</h3> | |
| <div id="nodeProps" class="text-gray-600">Select a node to edit properties</div> | |
| </div> | |
| <div class="bg-white p-4 rounded-lg shadow"> | |
| <h3 class="font-semibold text-lg mb-2">Connection Info</h3> | |
| <div id="connectionInfo" class="text-gray-600">Click on a connection for details</div> | |
| </div> | |
| <div class="bg-white p-4 rounded-lg shadow"> | |
| <h3 class="font-semibold text-lg mb-2">Export/Import</h3> | |
| <div class="flex space-x-2"> | |
| <button id="exportBtn" class="bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded text-sm">Export JSON</button> | |
| <button id="importBtn" class="bg-purple-500 hover:bg-purple-600 text-white px-3 py-1 rounded text-sm">Import JSON</button> | |
| <input type="file" id="fileInput" class="hidden" accept=".json"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Initialize Konva stage | |
| const width = document.getElementById('container').offsetWidth; | |
| const height = 600; | |
| const stage = new Konva.Stage({ | |
| container: 'container', | |
| width: width, | |
| height: height | |
| }); | |
| const layer = new Konva.Layer(); | |
| stage.add(layer); | |
| // State variables | |
| let nodes = []; | |
| let connections = []; | |
| let selectedNode = null; | |
| let drawingConnection = false; | |
| let tempLine = null; | |
| let startNode = null; | |
| let nodeCounter = 0; | |
| // Update node counter display | |
| function updateNodeCounter() { | |
| document.getElementById('nodeCounter').textContent = | |
| `${nodes.length} node${nodes.length !== 1 ? 's' : ''} created, ${connections.length} connection${connections.length !== 1 ? 's' : ''}`; | |
| } | |
| // Create a new node | |
| function createNode(x, y) { | |
| nodeCounter++; | |
| const nodeId = `node-${nodeCounter}`; | |
| const nodeGroup = new Konva.Group({ | |
| x: x, | |
| y: y, | |
| id: nodeId, | |
| draggable: true | |
| }); | |
| const circle = new Konva.Circle({ | |
| radius: 30, | |
| fill: '#3B82F6', | |
| stroke: '#1D4ED8', | |
| strokeWidth: 2, | |
| shadowColor: 'black', | |
| shadowBlur: 10, | |
| shadowOpacity: 0.2, | |
| shadowOffset: { x: 2, y: 2 } | |
| }); | |
| const text = new Konva.Text({ | |
| text: nodeCounter.toString(), | |
| fontSize: 18, | |
| fontFamily: 'Arial', | |
| fill: 'white', | |
| align: 'center', | |
| verticalAlign: 'middle', | |
| width: circle.radius() * 2, | |
| height: circle.radius() * 2, | |
| offsetX: circle.radius(), | |
| offsetY: circle.radius() / 1.5 | |
| }); | |
| nodeGroup.add(circle); | |
| nodeGroup.add(text); | |
| layer.add(nodeGroup); | |
| layer.draw(); | |
| nodes.push({ | |
| id: nodeId, | |
| group: nodeGroup, | |
| connections: [], | |
| isStart: false, | |
| isEnd: false | |
| }); | |
| updateNodeCounter(); | |
| // Add event listeners | |
| nodeGroup.on('dragstart', function() { | |
| this.moveToTop(); | |
| layer.draw(); | |
| }); | |
| nodeGroup.on('dragmove', function() { | |
| // Update all connections from/to this node | |
| updateConnectionsForNode(this); | |
| }); | |
| nodeGroup.on('click tap', function(e) { | |
| e.cancelBubble = true; | |
| // Deselect previously selected node | |
| if (selectedNode) { | |
| selectedNode.group.children[0].stroke('#1D4ED8'); | |
| selectedNode.group.children[0].strokeWidth(2); | |
| } | |
| // Select this node | |
| selectedNode = nodes.find(n => n.id === this.id()); | |
| this.children[0].stroke('#F59E0B'); | |
| this.children[0].strokeWidth(3); | |
| // Update properties panel | |
| updateNodePropertiesPanel(); | |
| layer.draw(); | |
| }); | |
| return nodeGroup; | |
| } | |
| // Update connections when a node is moved | |
| function updateConnectionsForNode(nodeGroup) { | |
| const nodeId = nodeGroup.id(); | |
| // Update connections where this node is the start | |
| connections.forEach(conn => { | |
| if (conn.startNode.id() === nodeId) { | |
| conn.line.points([ | |
| nodeGroup.x(), | |
| nodeGroup.y(), | |
| conn.endNode.x(), | |
| conn.endNode.y() | |
| ]); | |
| } | |
| }); | |
| // Update connections where this node is the end | |
| connections.forEach(conn => { | |
| if (conn.endNode.id() === nodeId) { | |
| conn.line.points([ | |
| conn.startNode.x(), | |
| conn.startNode.y(), | |
| nodeGroup.x(), | |
| nodeGroup.y() | |
| ]); | |
| } | |
| }); | |
| layer.draw(); | |
| } | |
| // Start drawing a connection | |
| function startConnection(nodeGroup) { | |
| if (drawingConnection) return; | |
| drawingConnection = true; | |
| startNode = nodeGroup; | |
| tempLine = new Konva.Line({ | |
| points: [nodeGroup.x(), nodeGroup.y(), nodeGroup.x(), nodeGroup.y()], | |
| stroke: 'gray', | |
| strokeWidth: 2, | |
| lineCap: 'round', | |
| lineJoin: 'round', | |
| dash: [10, 5] | |
| }); | |
| layer.add(tempLine); | |
| layer.draw(); | |
| } | |
| // Update temporary connection line while drawing | |
| function updateTempConnection(pos) { | |
| if (!drawingConnection || !tempLine) return; | |
| tempLine.points([ | |
| startNode.x(), | |
| startNode.y(), | |
| pos.x, | |
| pos.y | |
| ]); | |
| layer.draw(); | |
| } | |
| // Complete the connection | |
| function completeConnection(endNode) { | |
| if (!drawingConnection || !startNode || startNode.id() === endNode.id()) { | |
| cancelConnection(); | |
| return; | |
| } | |
| // Check if connection already exists | |
| const existingConnection = connections.find(conn => | |
| (conn.startNode.id() === startNode.id() && conn.endNode.id() === endNode.id()) || | |
| (conn.startNode.id() === endNode.id() && conn.endNode.id() === startNode.id()) | |
| ); | |
| if (existingConnection) { | |
| cancelConnection(); | |
| return; | |
| } | |
| // Create the connection line | |
| const line = new Konva.Line({ | |
| points: [ | |
| startNode.x(), | |
| startNode.y(), | |
| endNode.x(), | |
| endNode.y() | |
| ], | |
| stroke: 'gray', | |
| strokeWidth: 2, | |
| lineCap: 'round', | |
| lineJoin: 'round' | |
| }); | |
| layer.add(line); | |
| // Store the connection | |
| connections.push({ | |
| line: line, | |
| startNode: startNode, | |
| endNode: endNode | |
| }); | |
| // Add to nodes' connection lists | |
| const startNodeData = nodes.find(n => n.id === startNode.id()); | |
| const endNodeData = nodes.find(n => n.id === endNode.id()); | |
| if (startNodeData && endNodeData) { | |
| startNodeData.connections.push(endNode.id()); | |
| endNodeData.connections.push(startNode.id()); | |
| } | |
| // Add click event to connection | |
| line.on('click tap', function() { | |
| document.getElementById('connectionInfo').innerHTML = ` | |
| <div class="mb-2"><strong>Connection:</strong> ${startNodeData.group.children[1].text()} ↔ ${endNodeData.group.children[1].text()}</div> | |
| <div><strong>Length:</strong> ${Math.sqrt( | |
| Math.pow(endNode.x() - startNode.x(), 2) + | |
| Math.pow(endNode.y() - startNode.y(), 2) | |
| ).toFixed(1)}px</div> | |
| `; | |
| }); | |
| updateNodeCounter(); | |
| cancelConnection(); | |
| layer.draw(); | |
| } | |
| // Cancel the current connection drawing | |
| function cancelConnection() { | |
| drawingConnection = false; | |
| startNode = null; | |
| if (tempLine) { | |
| tempLine.destroy(); | |
| tempLine = null; | |
| } | |
| layer.draw(); | |
| } | |
| // Update node properties panel | |
| function updateNodePropertiesPanel() { | |
| if (!selectedNode) return; | |
| const node = selectedNode.group; | |
| const nodeData = nodes.find(n => n.id === node.id()); | |
| document.getElementById('nodeProps').innerHTML = ` | |
| <div class="mb-2"><strong>Node ID:</strong> ${node.children[1].text()}</div> | |
| <div class="mb-2"><strong>Position:</strong> (${node.x().toFixed(0)}, ${node.y().toFixed(0)})</div> | |
| <div class="mb-3"><strong>Connections:</strong> ${nodeData.connections.length}</div> | |
| <div class="flex space-x-2 mb-3"> | |
| <button onclick="setAsStartNode('${node.id()}')" class="bg-green-500 hover:bg-green-600 text-white px-2 py-1 rounded text-sm ${nodeData.isStart ? 'opacity-50 cursor-not-allowed' : ''}"> | |
| Set as Start | |
| </button> | |
| <button onclick="setAsEndNode('${node.id()}')" class="bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded text-sm ${nodeData.isEnd ? 'opacity-50 cursor-not-allowed' : ''}"> | |
| Set as End | |
| </button> | |
| </div> | |
| <button onclick="deleteNode('${node.id()}')" class="bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded text-sm w-full"> | |
| Delete Node | |
| </button> | |
| `; | |
| } | |
| // Set node as start node | |
| window.setAsStartNode = function(nodeId) { | |
| // Reset previous start node | |
| nodes.forEach(n => { | |
| if (n.isStart) { | |
| n.isStart = false; | |
| n.group.children[0].fill('#3B82F6'); | |
| } | |
| }); | |
| // Set new start node | |
| const node = nodes.find(n => n.id === nodeId); | |
| if (node) { | |
| node.isStart = true; | |
| node.group.children[0].fill('#10B981'); | |
| if (selectedNode && selectedNode.id === nodeId) { | |
| updateNodePropertiesPanel(); | |
| } | |
| layer.draw(); | |
| } | |
| }; | |
| // Set node as end node | |
| window.setAsEndNode = function(nodeId) { | |
| // Reset previous end node | |
| nodes.forEach(n => { | |
| if (n.isEnd) { | |
| n.isEnd = false; | |
| n.group.children[0].fill('#3B82F6'); | |
| } | |
| }); | |
| // Set new end node | |
| const node = nodes.find(n => n.id === nodeId); | |
| if (node) { | |
| node.isEnd = true; | |
| node.group.children[0].fill('#EF4444'); | |
| if (selectedNode && selectedNode.id === nodeId) { | |
| updateNodePropertiesPanel(); | |
| } | |
| layer.draw(); | |
| } | |
| }; | |
| // Delete a node | |
| window.deleteNode = function(nodeId) { | |
| const nodeIndex = nodes.findIndex(n => n.id === nodeId); | |
| if (nodeIndex === -1) return; | |
| const node = nodes[nodeIndex]; | |
| // Remove all connections to this node | |
| const connectionsToRemove = connections.filter(conn => | |
| conn.startNode.id() === nodeId || conn.endNode.id() === nodeId | |
| ); | |
| connectionsToRemove.forEach(conn => { | |
| // Remove from the other node's connections list | |
| const otherNodeId = conn.startNode.id() === nodeId ? conn.endNode.id() : conn.startNode.id(); | |
| const otherNode = nodes.find(n => n.id === otherNodeId); | |
| if (otherNode) { | |
| otherNode.connections = otherNode.connections.filter(id => id !== nodeId); | |
| } | |
| // Remove the connection line | |
| conn.line.destroy(); | |
| // Remove from connections array | |
| connections = connections.filter(c => c !== conn); | |
| }); | |
| // Remove the node | |
| node.group.destroy(); | |
| nodes.splice(nodeIndex, 1); | |
| // Reset selection if needed | |
| if (selectedNode && selectedNode.id === nodeId) { | |
| selectedNode = null; | |
| document.getElementById('nodeProps').textContent = 'Select a node to edit properties'; | |
| } | |
| updateNodeCounter(); | |
| layer.draw(); | |
| }; | |
| // Clear all nodes and connections | |
| document.getElementById('clearBtn').addEventListener('click', function() { | |
| if (confirm('Are you sure you want to clear all nodes and connections?')) { | |
| // Remove all connections | |
| connections.forEach(conn => conn.line.destroy()); | |
| connections = []; | |
| // Remove all nodes | |
| nodes.forEach(node => node.group.destroy()); | |
| nodes = []; | |
| // Reset state | |
| selectedNode = null; | |
| drawingConnection = false; | |
| startNode = null; | |
| if (tempLine) { | |
| tempLine.destroy(); | |
| tempLine = null; | |
| } | |
| document.getElementById('nodeProps').textContent = 'Select a node to edit properties'; | |
| document.getElementById('connectionInfo').textContent = 'Click on a connection for details'; | |
| updateNodeCounter(); | |
| layer.draw(); | |
| } | |
| }); | |
| // Add new node on button click | |
| document.getElementById('addNodeBtn').addEventListener('click', function() { | |
| const x = Math.random() * (width - 100) + 50; | |
| const y = Math.random() * (height - 100) + 50; | |
| createNode(x, y); | |
| }); | |
| // Export to JSON | |
| document.getElementById('exportBtn').addEventListener('click', function() { | |
| const data = { | |
| nodes: nodes.map(node => ({ | |
| id: node.id, | |
| x: node.group.x(), | |
| y: node.group.y(), | |
| text: node.group.children[1].text(), | |
| isStart: node.isStart, | |
| isEnd: node.isEnd, | |
| connections: node.connections | |
| })), | |
| connections: connections.map(conn => ({ | |
| startNodeId: conn.startNode.id(), | |
| endNodeId: conn.endNode.id() | |
| })) | |
| }; | |
| const dataStr = JSON.stringify(data, null, 2); | |
| const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); | |
| const exportFileDefaultName = 'nodevis-export.json'; | |
| const linkElement = document.createElement('a'); | |
| linkElement.setAttribute('href', dataUri); | |
| linkElement.setAttribute('download', exportFileDefaultName); | |
| linkElement.click(); | |
| }); | |
| // Import from JSON | |
| document.getElementById('importBtn').addEventListener('click', function() { | |
| document.getElementById('fileInput').click(); | |
| }); | |
| document.getElementById('fileInput').addEventListener('change', function(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| try { | |
| const data = JSON.parse(e.target.result); | |
| // Clear existing nodes and connections | |
| document.getElementById('clearBtn').click(); | |
| // Create nodes | |
| data.nodes.forEach(nodeData => { | |
| const nodeGroup = new Konva.Group({ | |
| x: nodeData.x, | |
| y: nodeData.y, | |
| id: nodeData.id, | |
| draggable: true | |
| }); | |
| const circle = new Konva.Circle({ | |
| radius: 30, | |
| fill: nodeData.isStart ? '#10B981' : | |
| nodeData.isEnd ? '#EF4444' : '#3B82F6', | |
| stroke: '#1D4ED8', | |
| strokeWidth: 2 | |
| }); | |
| const text = new Konva.Text({ | |
| text: nodeData.text, | |
| fontSize: 18, | |
| fontFamily: 'Arial', | |
| fill: 'white', | |
| align: 'center', | |
| verticalAlign: 'middle', | |
| width: circle.radius() * 2, | |
| height: circle.radius() * 2, | |
| offsetX: circle.radius(), | |
| offsetY: circle.radius() / 1.5 | |
| }); | |
| nodeGroup.add(circle); | |
| nodeGroup.add(text); | |
| layer.add(nodeGroup); | |
| nodes.push({ | |
| id: nodeData.id, | |
| group: nodeGroup, | |
| connections: nodeData.connections, | |
| isStart: nodeData.isStart, | |
| isEnd: nodeData.isEnd | |
| }); | |
| // Add event listeners | |
| nodeGroup.on('dragstart', function() { | |
| this.moveToTop(); | |
| layer.draw(); | |
| }); | |
| nodeGroup.on('dragmove', function() { | |
| updateConnectionsForNode(this); | |
| }); | |
| nodeGroup.on('click tap', function(e) { | |
| e.cancelBubble = true; | |
| if (selectedNode) { | |
| selectedNode.group.children[0].stroke('#1D4ED8'); | |
| selectedNode.group.children[0].strokeWidth(2); | |
| } | |
| selectedNode = nodes.find(n => n.id === this.id()); | |
| this.children[0].stroke('#F59E0B'); | |
| this.children[0].strokeWidth(3); | |
| updateNodePropertiesPanel(); | |
| layer.draw(); | |
| }); | |
| }); | |
| // Create connections | |
| data.connections.forEach(connData => { | |
| const startNode = nodes.find(n => n.id === connData.startNodeId).group; | |
| const endNode = nodes.find(n => n.id === connData.endNodeId).group; | |
| const line = new Konva.Line({ | |
| points: [ | |
| startNode.x(), | |
| startNode.y(), | |
| endNode.x(), | |
| endNode.y() | |
| ], | |
| stroke: 'gray', | |
| strokeWidth: 2, | |
| lineCap: 'round', | |
| lineJoin: 'round' | |
| }); | |
| layer.add(line); | |
| connections.push({ | |
| line: line, | |
| startNode: startNode, | |
| endNode: endNode | |
| }); | |
| // Add click event to connection | |
| line.on('click tap', function() { | |
| const startNodeData = nodes.find(n => n.id === startNode.id()); | |
| const endNodeData = nodes.find(n => n.id === endNode.id()); | |
| document.getElementById('connectionInfo').innerHTML = ` | |
| <div class="mb-2"><strong>Connection:</strong> ${startNodeData.group.children[1].text()} ↔ ${endNodeData.group.children[1].text()}</div> | |
| <div><strong>Length:</strong> ${Math.sqrt( | |
| Math.pow(endNode.x() - startNode.x(), 2) + | |
| Math.pow(endNode.y() - startNode.y(), 2) | |
| ).toFixed(1)}px</div> | |
| `; | |
| }); | |
| }); | |
| nodeCounter = nodes.length; | |
| updateNodeCounter(); | |
| layer.draw(); | |
| } catch (error) { | |
| alert('Error importing file: ' + error.message); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| }); | |
| // Stage event listeners | |
| stage.on('click tap', function(e) { | |
| // Clicked on empty space - deselect node | |
| if (e.target === stage) { | |
| if (selectedNode) { | |
| selectedNode.group.children[0].stroke('#1D4ED8'); | |
| selectedNode.group.children[0].strokeWidth(2); | |
| selectedNode = null; | |
| document.getElementById('nodeProps').textContent = 'Select a node to edit properties'; | |
| layer.draw(); | |
| } | |
| if (drawingConnection) { | |
| cancelConnection(); | |
| } | |
| } | |
| }); | |
| stage.on('mousemove', function(e) { | |
| if (drawingConnection) { | |
| updateTempConnection(stage.getPointerPosition()); | |
| } | |
| }); | |
| // Make stage responsive | |
| function resizeStage() { | |
| const container = document.getElementById('container'); | |
| const newWidth = container.offsetWidth; | |
| stage.width(newWidth); | |
| stage.height(height); | |
| layer.draw(); | |
| } | |
| window.addEventListener('resize', resizeStage); | |
| // Initial setup | |
| updateNodeCounter(); | |
| // Add initial nodes for demo | |
| const centerX = width / 2; | |
| const centerY = height / 2; | |
| const node1 = createNode(centerX - 100, centerY - 100); | |
| const node2 = createNode(centerX + 100, centerY - 100); | |
| const node3 = createNode(centerX, centerY + 100); | |
| // Set node1 as start node | |
| setAsStartNode(node1.id()); | |
| // Create some initial connections | |
| setTimeout(() => { | |
| startConnection(node1); | |
| completeConnection(node2); | |
| startConnection(node2); | |
| completeConnection(node3); | |
| startConnection(node3); | |
| completeConnection(node1); | |
| // Select the first node | |
| node1.fire('click'); | |
| }, 100); | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=senangh/v1-nodevis" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |