nodesworkflow / index.html
snapo's picture
looks greate, but when i connect 1 node to another gray temporary lines popup, this looks great but they are offset a lot to when the connection happens. this is a bug - Initial Deployment
5c8e55b verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lumiflow - Visual Workflow Builder</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.connection {
pointer-events: none;
}
.connection-delete-btn {
pointer-events: all;
}
.node {
transition: transform 0.1s ease;
}
.node:hover {
transform: scale(1.02);
}
.node-handle {
width: 12px;
height: 12px;
border-radius: 50%;
cursor: crosshair;
}
.node-handle.input {
left: -6px;
}
.node-handle.output {
right: -6px;
}
.connection-line {
stroke-width: 3;
fill: none;
}
.connection-line.temp {
stroke-dasharray: 5;
}
.flow-canvas {
background-image:
linear-gradient(#e5e7eb 1px, transparent 1px),
linear-gradient(90deg, #e5e7eb 1px, transparent 1px);
background-size: 20px 20px;
}
.dark .flow-canvas {
background-image:
linear-gradient(#374151 1px, transparent 1px),
linear-gradient(90deg, #374151 1px, transparent 1px);
}
.node-config-panel {
transition: all 0.3s ease;
}
.node-config-panel.hidden {
transform: translateX(100%);
opacity: 0;
}
.node-config-panel.visible {
transform: translateX(0);
opacity: 1;
}
</style>
</head>
<body class="bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200 transition-colors duration-300">
<div class="flex flex-col h-screen">
<!-- Header -->
<header class="bg-blue-600 dark:bg-blue-800 text-white p-4 shadow-md">
<div class="container mx-auto flex justify-between items-center">
<div class="flex items-center space-x-2">
<i class="fas fa-project-diagram text-2xl"></i>
<h1 class="text-2xl font-bold">Lumiflow</h1>
</div>
<div class="flex items-center space-x-4">
<button id="run-flow-btn" class="bg-green-500 hover:bg-green-600 px-4 py-2 rounded-md font-medium flex items-center space-x-2">
<i class="fas fa-play"></i>
<span>Run Flow</span>
</button>
<button id="theme-toggle" class="p-2 rounded-full hover:bg-blue-700 dark:hover:bg-blue-900">
<i class="fas fa-moon dark:hidden"></i>
<i class="fas fa-sun hidden dark:block"></i>
</button>
</div>
</div>
</header>
<!-- Main Content -->
<div class="flex flex-1 overflow-hidden">
<!-- Node Panel -->
<div class="w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 p-4 overflow-y-auto">
<h2 class="font-bold text-lg mb-4">Nodes</h2>
<div class="mb-6">
<h3 class="font-semibold text-sm uppercase text-gray-500 dark:text-gray-400 mb-2 flex items-center">
<i class="fas fa-sign-in-alt mr-2"></i>
Input
</h3>
<div class="space-y-2">
<div class="node-item bg-blue-50 dark:bg-blue-900/30 p-3 rounded-md cursor-move hover:bg-blue-100 dark:hover:bg-blue-900/50" draggable="true" data-type="inject">
<div class="flex items-center">
<i class="fas fa-bolt text-blue-500 mr-2"></i>
<span>Inject</span>
</div>
</div>
<div class="node-item bg-blue-50 dark:bg-blue-900/30 p-3 rounded-md cursor-move hover:bg-blue-100 dark:hover:bg-blue-900/50" draggable="true" data-type="http-in">
<div class="flex items-center">
<i class="fas fa-globe text-blue-500 mr-2"></i>
<span>HTTP In</span>
</div>
</div>
</div>
</div>
<div class="mb-6">
<h3 class="font-semibold text-sm uppercase text-gray-500 dark:text-gray-400 mb-2 flex items-center">
<i class="fas fa-cogs mr-2"></i>
Processing
</h3>
<div class="space-y-2">
<div class="node-item bg-purple-50 dark:bg-purple-900/30 p-3 rounded-md cursor-move hover:bg-purple-100 dark:hover:bg-purple-900/50" draggable="true" data-type="function">
<div class="flex items-center">
<i class="fas fa-code text-purple-500 mr-2"></i>
<span>Function</span>
</div>
</div>
<div class="node-item bg-purple-50 dark:bg-purple-900/30 p-3 rounded-md cursor-move hover:bg-purple-100 dark:hover:bg-purple-900/50" draggable="true" data-type="delay">
<div class="flex items-center">
<i class="fas fa-clock text-purple-500 mr-2"></i>
<span>Delay</span>
</div>
</div>
</div>
</div>
<div class="mb-6">
<h3 class="font-semibold text-sm uppercase text-gray-500 dark:text-gray-400 mb-2 flex items-center">
<i class="fas fa-sign-out-alt mr-2"></i>
Output
</h3>
<div class="space-y-2">
<div class="node-item bg-green-50 dark:bg-green-900/30 p-3 rounded-md cursor-move hover:bg-green-100 dark:hover:bg-green-900/50" draggable="true" data-type="debug">
<div class="flex items-center">
<i class="fas fa-bug text-green-500 mr-2"></i>
<span>Debug</span>
</div>
</div>
<div class="node-item bg-green-50 dark:bg-green-900/30 p-3 rounded-md cursor-move hover:bg-green-100 dark:hover:bg-green-900/50" draggable="true" data-type="http-out">
<div class="flex items-center">
<i class="fas fa-server text-green-500 mr-2"></i>
<span>HTTP Out</span>
</div>
</div>
</div>
</div>
</div>
<!-- Flow Canvas -->
<div class="flex-1 relative overflow-hidden">
<div id="flow-canvas" class="flow-canvas w-full h-full bg-white dark:bg-gray-900 relative overflow-auto">
<svg id="connections-svg" class="absolute top-0 left-0 w-full h-full pointer-events-none z-10"></svg>
</div>
</div>
<!-- Configuration Panel -->
<div id="config-panel" class="node-config-panel hidden w-80 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 p-4 overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h2 class="font-bold text-lg">Node Configuration</h2>
<button id="close-config-btn" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div id="node-config-content">
<div class="text-center py-10 text-gray-500">
<i class="fas fa-cog text-4xl mb-2"></i>
<p>Select a node to configure</p>
</div>
</div>
</div>
</div>
<!-- Status Bar -->
<footer class="bg-gray-200 dark:bg-gray-800 border-t border-gray-300 dark:border-gray-700 p-2 text-sm">
<div class="container mx-auto flex justify-between items-center">
<div>
<span id="node-count">0 nodes</span>
<span class="mx-2"></span>
<span id="connection-count">0 connections</span>
</div>
<div id="status-message" class="font-medium">Ready</div>
</div>
</footer>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Theme toggle
const themeToggle = document.getElementById('theme-toggle');
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
const currentTheme = localStorage.getItem('theme') || (prefersDarkScheme.matches ? 'dark' : 'light');
if (currentTheme === 'dark') {
document.body.classList.add('dark');
}
themeToggle.addEventListener('click', function() {
document.body.classList.toggle('dark');
const theme = document.body.classList.contains('dark') ? 'dark' : 'light';
localStorage.setItem('theme', theme);
});
// Flow state
const state = {
nodes: [],
connections: [],
selectedNode: null,
draggingNode: null,
creatingConnection: null,
tempConnection: null,
offset: { x: 0, y: 0 },
nextNodeId: 1,
nextConnectionId: 1
};
// DOM elements
const flowCanvas = document.getElementById('flow-canvas');
const connectionsSvg = document.getElementById('connections-svg');
const configPanel = document.getElementById('config-panel');
const nodeConfigContent = document.getElementById('node-config-content');
const nodeCountEl = document.getElementById('node-count');
const connectionCountEl = document.getElementById('connection-count');
const statusMessageEl = document.getElementById('status-message');
const runFlowBtn = document.getElementById('run-flow-btn');
const closeConfigBtn = document.getElementById('close-config-btn');
// Node templates
const nodeTemplates = {
'inject': {
name: 'Inject',
icon: 'bolt',
color: 'blue',
config: `
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1">Name</label>
<input type="text" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm font-medium mb-1">Payload</label>
<select class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700">
<option>Timestamp</option>
<option>String</option>
<option>Number</option>
<option>Boolean</option>
<option>JSON</option>
</select>
</div>
<div>
<label class="flex items-center">
<input type="checkbox" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-400">
<span class="ml-2 text-sm">Repeat</span>
</label>
</div>
<div class="repeat-options hidden">
<label class="block text-sm font-medium mb-1">Interval (ms)</label>
<input type="number" value="1000" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700">
</div>
</div>
`
},
'http-in': {
name: 'HTTP In',
icon: 'globe',
color: 'blue',
config: `
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1">Method</label>
<select class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700">
<option>GET</option>
<option>POST</option>
<option>PUT</option>
<option>DELETE</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1">URL</label>
<input type="text" placeholder="/api/endpoint" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700">
</div>
</div>
`
},
'function': {
name: 'Function',
icon: 'code',
color: 'purple',
config: `
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1">Function Code</label>
<textarea class="w-full h-40 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 font-mono text-sm">// Write your JavaScript code here
// The 'msg' object contains the incoming message
// Return the modified message object
msg.payload = "Processed: " + msg.payload;
return msg;</textarea>
</div>
</div>
`
},
'delay': {
name: 'Delay',
icon: 'clock',
color: 'purple',
config: `
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1">Delay (ms)</label>
<input type="number" value="1000" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700">
</div>
<div>
<label class="flex items-center">
<input type="checkbox" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-400">
<span class="ml-2 text-sm">Drop messages while delayed</span>
</label>
</div>
</div>
`
},
'debug': {
name: 'Debug',
icon: 'bug',
color: 'green',
config: `
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1">Output</label>
<select class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700">
<option>Complete message object</option>
<option>msg.payload</option>
<option>msg.topic</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1">To</label>
<select class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700">
<option>Debug console</option>
<option>System log</option>
</select>
</div>
</div>
`
},
'http-out': {
name: 'HTTP Out',
icon: 'server',
color: 'green',
config: `
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1">Status Code</label>
<input type="number" value="200" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm font-medium mb-1">Headers</label>
<textarea class="w-full h-20 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 font-mono text-sm">Content-Type: application/json</textarea>
</div>
</div>
`
}
};
// Initialize the app
function init() {
setupEventListeners();
updateCounters();
}
// Set up event listeners
function setupEventListeners() {
// Node panel drag events
document.querySelectorAll('.node-item').forEach(item => {
item.addEventListener('dragstart', handleNodeDragStart);
});
// Canvas events
flowCanvas.addEventListener('dragover', handleCanvasDragOver);
flowCanvas.addEventListener('drop', handleCanvasDrop);
flowCanvas.addEventListener('click', handleCanvasClick);
// Run flow button
runFlowBtn.addEventListener('click', runFlow);
// Close config panel
closeConfigBtn.addEventListener('click', closeConfigPanel);
// Keyboard events
document.addEventListener('keydown', handleKeyDown);
}
// Handle node drag start from panel
function handleNodeDragStart(e) {
const nodeType = e.target.getAttribute('data-type');
e.dataTransfer.setData('application/node-type', nodeType);
e.dataTransfer.effectAllowed = 'copy';
}
// Handle canvas drag over
function handleCanvasDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
}
// Handle canvas drop
function handleCanvasDrop(e) {
e.preventDefault();
const nodeType = e.dataTransfer.getData('application/node-type');
if (!nodeType) return;
const rect = flowCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
createNode(nodeType, x, y);
}
// Create a new node
function createNode(type, x, y) {
const template = nodeTemplates[type];
if (!template) return;
const nodeId = state.nextNodeId++;
const node = {
id: nodeId,
type: type,
x: x,
y: y,
connections: {
inputs: [],
outputs: []
}
};
const nodeEl = document.createElement('div');
nodeEl.className = `node absolute w-48 bg-white dark:bg-gray-700 rounded-lg shadow-md border border-${template.color}-300 dark:border-${template.color}-800 cursor-move z-20`;
nodeEl.style.transform = `translate(${x}px, ${y}px)`;
nodeEl.dataset.nodeId = nodeId;
nodeEl.innerHTML = `
<div class="bg-${template.color}-100 dark:bg-${template.color}-900/50 px-3 py-2 rounded-t-lg flex justify-between items-center">
<div class="flex items-center">
<i class="fas fa-${template.icon} text-${template.color}-500 mr-2"></i>
<span class="font-medium">${template.name}</span>
</div>
<button class="node-delete-btn p-1 rounded-full hover:bg-${template.color}-200 dark:hover:bg-${template.color}-800/50 text-gray-500 dark:text-gray-300">
<i class="fas fa-times text-xs"></i>
</button>
</div>
<div class="p-3">
<div class="text-xs text-gray-500 dark:text-gray-400">${type}</div>
</div>
<div class="node-handle input absolute top-1/2 bg-${template.color}-500 border-2 border-white dark:border-gray-700"></div>
<div class="node-handle output absolute top-1/2 bg-${template.color}-500 border-2 border-white dark:border-gray-700"></div>
`;
flowCanvas.appendChild(nodeEl);
node.element = nodeEl;
// Add event listeners to the node
nodeEl.addEventListener('mousedown', startNodeDrag);
nodeEl.querySelector('.node-delete-btn').addEventListener('click', (e) => {
e.stopPropagation();
deleteNode(nodeId);
});
// Add connection handle events
const inputHandle = nodeEl.querySelector('.node-handle.input');
const outputHandle = nodeEl.querySelector('.node-handle.output');
inputHandle.addEventListener('mousedown', (e) => startConnection(e, nodeId, 'input'));
outputHandle.addEventListener('mousedown', (e) => startConnection(e, nodeId, 'output'));
state.nodes.push(node);
updateCounters();
}
// Start dragging a node
function startNodeDrag(e) {
if (e.target.classList.contains('node-delete-btn') ||
e.target.classList.contains('node-handle')) {
return;
}
const nodeEl = e.currentTarget;
const nodeId = parseInt(nodeEl.dataset.nodeId);
const node = state.nodes.find(n => n.id === nodeId);
if (!node) return;
state.draggingNode = node;
state.offset = {
x: e.clientX - node.x,
y: e.clientY - node.y
};
document.addEventListener('mousemove', dragNode);
document.addEventListener('mouseup', stopNodeDrag);
nodeEl.style.zIndex = '30';
nodeEl.style.cursor = 'grabbing';
}
// Drag a node
function dragNode(e) {
if (!state.draggingNode) return;
const node = state.draggingNode;
const nodeEl = node.element;
node.x = e.clientX - state.offset.x;
node.y = e.clientY - state.offset.y;
nodeEl.style.transform = `translate(${node.x}px, ${node.y}px)`;
// Update all connections for this node
updateNodeConnections(node);
}
// Stop dragging a node
function stopNodeDrag() {
if (!state.draggingNode) return;
const node = state.draggingNode;
const nodeEl = node.element;
document.removeEventListener('mousemove', dragNode);
document.removeEventListener('mouseup', stopNodeDrag);
nodeEl.style.zIndex = '20';
nodeEl.style.cursor = 'move';
state.draggingNode = null;
}
// Delete a node
function deleteNode(nodeId) {
// Remove all connections to/from this node
const connectionsToRemove = state.connections.filter(conn =>
conn.sourceId === nodeId || conn.targetId === nodeId
);
connectionsToRemove.forEach(conn => {
deleteConnection(conn.id);
});
// Remove the node
const nodeIndex = state.nodes.findIndex(n => n.id === nodeId);
if (nodeIndex !== -1) {
const node = state.nodes[nodeIndex];
node.element.remove();
state.nodes.splice(nodeIndex, 1);
}
if (state.selectedNode && state.selectedNode.id === nodeId) {
closeConfigPanel();
}
updateCounters();
}
// Start creating a connection
function startConnection(e, nodeId, handleType) {
e.stopPropagation();
const node = state.nodes.find(n => n.id === nodeId);
if (!node) return;
state.creatingConnection = {
nodeId: nodeId,
handleType: handleType,
startX: e.clientX,
startY: e.clientY
};
// Create a temporary connection line
state.tempConnection = document.createElementNS('http://www.w3.org/2000/svg', 'path');
state.tempConnection.classList.add('connection-line', 'temp');
state.tempConnection.setAttribute('stroke', '#6b7280');
connectionsSvg.appendChild(state.tempConnection);
document.addEventListener('mousemove', updateTempConnection);
document.addEventListener('mouseup', finishConnection);
}
// Update temporary connection while dragging
function updateTempConnection(e) {
if (!state.creatingConnection || !state.tempConnection) return;
const node = state.nodes.find(n => n.id === state.creatingConnection.nodeId);
if (!node) return;
const nodeEl = node.element;
const rect = nodeEl.getBoundingClientRect();
const canvasRect = flowCanvas.getBoundingClientRect();
const scrollLeft = flowCanvas.scrollLeft;
const scrollTop = flowCanvas.scrollTop;
let startX, startY;
if (state.creatingConnection.handleType === 'input') {
startX = rect.left - canvasRect.left + scrollLeft;
startY = rect.top - canvasRect.top + rect.height / 2 + scrollTop;
} else {
startX = rect.left - canvasRect.left + rect.width + scrollLeft;
startY = rect.top - canvasRect.top + rect.height / 2 + scrollTop;
}
const endX = e.clientX - canvasRect.left + scrollLeft;
const endY = e.clientY - canvasRect.top + scrollTop;
// Calculate the control points for the bezier curve
const midX = (startX + endX) / 2;
const ctrlX1 = midX;
const ctrlX2 = midX;
// Create the path data
const pathData = `M${startX},${startY} C${ctrlX1},${startY} ${ctrlX2},${endY} ${endX},${endY}`;
state.tempConnection.setAttribute('d', pathData);
}
// Finish creating a connection
function finishConnection(e) {
if (!state.creatingConnection) return;
document.removeEventListener('mousemove', updateTempConnection);
document.removeEventListener('mouseup', finishConnection);
// Remove the temporary connection line
if (state.tempConnection) {
connectionsSvg.removeChild(state.tempConnection);
state.tempConnection = null;
}
// Check if we're hovering over a node handle
const canvasRect = flowCanvas.getBoundingClientRect();
const scrollLeft = flowCanvas.scrollLeft;
const scrollTop = flowCanvas.scrollTop;
const canvasX = e.clientX - canvasRect.left + scrollLeft;
const canvasY = e.clientY - canvasRect.top + scrollTop;
const elements = document.elementsFromPoint(e.clientX, e.clientY);
const targetHandle = elements.find(el => el.classList.contains('node-handle'));
if (!targetHandle) {
state.creatingConnection = null;
return;
}
const targetNodeEl = targetHandle.closest('.node');
if (!targetNodeEl) {
state.creatingConnection = null;
return;
}
const targetNodeId = parseInt(targetNodeEl.dataset.nodeId);
const targetNode = state.nodes.find(n => n.id === targetNodeId);
if (!targetNode) {
state.creatingConnection = null;
return;
}
// Determine if this is an input or output handle
const targetHandleType = targetHandle.classList.contains('input') ? 'input' : 'output';
// Create the connection if valid
if (state.creatingConnection.handleType === 'output' && targetHandleType === 'input') {
// Output to input connection
createConnection(state.creatingConnection.nodeId, targetNodeId);
} else if (state.creatingConnection.handleType === 'input' && targetHandleType === 'output') {
// Input to output connection (reverse)
createConnection(targetNodeId, state.creatingConnection.nodeId);
}
state.creatingConnection = null;
}
// Create a connection between two nodes
function createConnection(sourceId, targetId) {
// Don't allow connections to self
if (sourceId === targetId) return;
// Check if connection already exists
const existingConnection = state.connections.find(conn =>
conn.sourceId === sourceId && conn.targetId === targetId
);
if (existingConnection) return;
const connectionId = state.nextConnectionId++;
const connection = {
id: connectionId,
sourceId: sourceId,
targetId: targetId,
element: null
};
// Add to source and target nodes
const sourceNode = state.nodes.find(n => n.id === sourceId);
const targetNode = state.nodes.find(n => n.id === targetId);
if (sourceNode && targetNode) {
sourceNode.connections.outputs.push(connectionId);
targetNode.connections.inputs.push(connectionId);
}
// Create the SVG path element
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.classList.add('connection-line');
path.setAttribute('stroke', '#3b82f6');
path.setAttribute('marker-end', 'url(#arrowhead)');
connectionsSvg.appendChild(path);
// Create the delete button
const deleteBtn = document.createElement('div');
deleteBtn.classList.add('connection-delete-btn', 'absolute', 'bg-white', 'dark:bg-gray-700', 'rounded-full', 'w-5', 'h-5', 'flex', 'items-center', 'justify-center', 'shadow-sm', 'border', 'border-gray-300', 'dark:border-gray-600', 'cursor-pointer', 'z-30');
deleteBtn.innerHTML = '<i class="fas fa-times text-xs text-gray-600 dark:text-gray-300"></i>';
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteConnection(connectionId);
});
flowCanvas.appendChild(deleteBtn);
connection.path = path;
connection.deleteBtn = deleteBtn;
state.connections.push(connection);
// Update the connection position
updateConnection(connection);
updateCounters();
}
// Update a connection's position
function updateConnection(connection) {
const sourceNode = state.nodes.find(n => n.id === connection.sourceId);
const targetNode = state.nodes.find(n => n.id === connection.targetId);
if (!sourceNode || !targetNode) return;
const sourceEl = sourceNode.element;
const targetEl = targetNode.element;
const sourceRect = sourceEl.getBoundingClientRect();
const targetRect = targetEl.getBoundingClientRect();
const canvasRect = flowCanvas.getBoundingClientRect();
// Calculate positions relative to the canvas
const sourceX = sourceRect.left + sourceRect.width - canvasRect.left;
const sourceY = sourceRect.top + sourceRect.height / 2 - canvasRect.top;
const targetX = targetRect.left - canvasRect.left;
const targetY = targetRect.top + targetRect.height / 2 - canvasRect.top;
// Calculate the control points for the bezier curve
const midX = (sourceX + targetX) / 2;
const ctrlX1 = midX;
const ctrlX2 = midX;
// Create the path data
const pathData = `M${sourceX},${sourceY} C${ctrlX1},${sourceY} ${ctrlX2},${targetY} ${targetX},${targetY}`;
connection.path.setAttribute('d', pathData);
// Position the delete button at the midpoint
const btnX = (sourceX + targetX) / 2 - 10;
const btnY = (sourceY + targetY) / 2 - 10;
connection.deleteBtn.style.left = `${btnX}px`;
connection.deleteBtn.style.top = `${btnY}px`;
}
// Update all connections for a node
function updateNodeConnections(node) {
// Output connections
node.connections.outputs.forEach(connId => {
const connection = state.connections.find(c => c.id === connId);
if (connection) updateConnection(connection);
});
// Input connections
node.connections.inputs.forEach(connId => {
const connection = state.connections.find(c => c.id === connId);
if (connection) updateConnection(connection);
});
}
// Delete a connection
function deleteConnection(connectionId) {
const connectionIndex = state.connections.findIndex(c => c.id === connectionId);
if (connectionIndex === -1) return;
const connection = state.connections[connectionIndex];
// Remove from source and target nodes
const sourceNode = state.nodes.find(n => n.id === connection.sourceId);
const targetNode = state.nodes.find(n => n.id === connection.targetId);
if (sourceNode) {
const outputIndex = sourceNode.connections.outputs.indexOf(connectionId);
if (outputIndex !== -1) {
sourceNode.connections.outputs.splice(outputIndex, 1);
}
}
if (targetNode) {
const inputIndex = targetNode.connections.inputs.indexOf(connectionId);
if (inputIndex !== -1) {
targetNode.connections.inputs.splice(inputIndex, 1);
}
}
// Remove DOM elements
if (connection.path) {
connectionsSvg.removeChild(connection.path);
}
if (connection.deleteBtn) {
connection.deleteBtn.remove();
}
// Remove from state
state.connections.splice(connectionIndex, 1);
updateCounters();
}
// Handle canvas click
function handleCanvasClick(e) {
// Check if we clicked on a node
const nodeEl = e.target.closest('.node');
if (nodeEl) {
const nodeId = parseInt(nodeEl.dataset.nodeId);
const node = state.nodes.find(n => n.id === nodeId);
if (node) {
selectNode(node);
}
} else {
// Clicked on empty space, deselect
closeConfigPanel();
}
}
// Select a node and show its config
function selectNode(node) {
state.selectedNode = node;
// Highlight the selected node
document.querySelectorAll('.node').forEach(el => {
el.classList.remove('ring-2', 'ring-blue-500');
});
node.element.classList.add('ring-2', 'ring-blue-500');
// Show the config panel
showConfigPanel(node);
}
// Show config panel for a node
function showConfigPanel(node) {
const template = nodeTemplates[node.type];
if (!template) return;
nodeConfigContent.innerHTML = `
<div class="mb-4">
<h3 class="font-bold text-lg flex items-center">
<i class="fas fa-${template.icon} text-${template.color}-500 mr-2"></i>
${template.name}
</h3>
<div class="text-xs text-gray-500 dark:text-gray-400">ID: ${node.id}</div>
</div>
${template.config}
`;
// Add event listeners for any dynamic elements in the config
const repeatCheckbox = nodeConfigContent.querySelector('input[type="checkbox"]');
if (repeatCheckbox) {
repeatCheckbox.addEventListener('change', (e) => {
const repeatOptions = nodeConfigContent.querySelector('.repeat-options');
if (repeatOptions) {
repeatOptions.classList.toggle('hidden', !e.target.checked);
}
});
}
configPanel.classList.remove('hidden');
configPanel.classList.add('visible');
}
// Close config panel
function closeConfigPanel() {
if (state.selectedNode) {
state.selectedNode.element.classList.remove('ring-2', 'ring-blue-500');
state.selectedNode = null;
}
configPanel.classList.remove('visible');
configPanel.classList.add('hidden');
}
// Handle keyboard events
function handleKeyDown(e) {
// Delete key
if (e.key === 'Delete' && state.selectedNode) {
deleteNode(state.selectedNode.id);
}
}
// Run the flow
function runFlow() {
if (state.nodes.length === 0) {
statusMessageEl.textContent = 'No nodes to run';
return;
}
statusMessageEl.textContent = 'Running flow...';
// Simulate flow execution
setTimeout(() => {
statusMessageEl.textContent = 'Flow completed successfully';
// Reset after 2 seconds
setTimeout(() => {
statusMessageEl.textContent = 'Ready';
}, 2000);
}, 1500);
}
// Update node and connection counters
function updateCounters() {
nodeCountEl.textContent = `${state.nodes.length} node${state.nodes.length !== 1 ? 's' : ''}`;
connectionCountEl.textContent = `${state.connections.length} connection${state.connections.length !== 1 ? 's' : ''}`;
}
// Initialize the app
init();
// Add arrowhead marker to SVG for connection lines
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
marker.setAttribute('id', 'arrowhead');
marker.setAttribute('markerWidth', '10');
marker.setAttribute('markerHeight', '7');
marker.setAttribute('refX', '9');
marker.setAttribute('refY', '3.5');
marker.setAttribute('orient', 'auto');
const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
polygon.setAttribute('points', '0 0, 10 3.5, 0 7');
polygon.setAttribute('fill', '#3b82f6');
marker.appendChild(polygon);
defs.appendChild(marker);
connectionsSvg.appendChild(defs);
});
</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=snapo/nodesworkflow" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>