v1-nodevis / index.html
senangh's picture
undefined - Initial Deployment
6abfbcd verified
<!DOCTYPE html>
<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>