|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>FlowForge - Visual Workflow Automation</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> |
|
.node { |
|
cursor: grab; |
|
transition: all 0.2s ease; |
|
} |
|
.node:hover { |
|
transform: translateY(-2px); |
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); |
|
} |
|
.node.selected { |
|
border-color: #3b82f6; |
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5); |
|
} |
|
.connection-path { |
|
stroke: #94a3b8; |
|
stroke-width: 2; |
|
fill: none; |
|
} |
|
.workflow-canvas { |
|
background-color: #f8fafc; |
|
background-image: |
|
linear-gradient(to right, #e2e8f0 1px, transparent 1px), |
|
linear-gradient(to bottom, #e2e8f0 1px, transparent 1px); |
|
background-size: 20px 20px; |
|
} |
|
.resize-handle { |
|
width: 10px; |
|
height: 10px; |
|
background-color: #3b82f6; |
|
position: absolute; |
|
right: 0; |
|
bottom: 0; |
|
cursor: nwse-resize; |
|
border-radius: 2px; |
|
} |
|
.node-port { |
|
width: 12px; |
|
height: 12px; |
|
border-radius: 50%; |
|
background-color: #64748b; |
|
cursor: pointer; |
|
} |
|
.node-port.input { |
|
left: -6px; |
|
} |
|
.node-port.output { |
|
right: -6px; |
|
} |
|
.sidebar { |
|
scrollbar-width: thin; |
|
scrollbar-color: #cbd5e1 #f1f5f9; |
|
} |
|
.sidebar::-webkit-scrollbar { |
|
width: 6px; |
|
} |
|
.sidebar::-webkit-scrollbar-track { |
|
background: #f1f5f9; |
|
} |
|
.sidebar::-webkit-scrollbar-thumb { |
|
background-color: #cbd5e1; |
|
border-radius: 3px; |
|
} |
|
.tutorial-overlay { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
right: 0; |
|
bottom: 0; |
|
background-color: rgba(0, 0, 0, 0.7); |
|
z-index: 100; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
} |
|
.tutorial-card { |
|
background-color: white; |
|
border-radius: 0.5rem; |
|
width: 90%; |
|
max-width: 600px; |
|
max-height: 90vh; |
|
overflow-y: auto; |
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); |
|
} |
|
.tutorial-highlight { |
|
position: absolute; |
|
border-radius: 0.5rem; |
|
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5); |
|
z-index: 101; |
|
pointer-events: none; |
|
} |
|
.tutorial-tooltip { |
|
position: absolute; |
|
background-color: white; |
|
padding: 0.5rem 1rem; |
|
border-radius: 0.5rem; |
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); |
|
z-index: 102; |
|
max-width: 300px; |
|
} |
|
.animate-pulse { |
|
animation: pulse 2s infinite; |
|
} |
|
@keyframes pulse { |
|
0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); } |
|
70% { box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); } |
|
100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); } |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-50 h-screen flex flex-col overflow-hidden"> |
|
|
|
<header class="bg-white border-b border-gray-200 px-4 py-3 flex items-center justify-between"> |
|
<div class="flex items-center space-x-2"> |
|
<div class="bg-blue-500 text-white p-2 rounded-lg"> |
|
<i class="fas fa-project-diagram text-lg"></i> |
|
</div> |
|
<h1 class="text-xl font-bold text-gray-800">FlowForge</h1> |
|
</div> |
|
<div class="flex items-center space-x-4"> |
|
<button id="start-tutorial" class="px-4 py-2 bg-purple-500 text-white rounded-md hover:bg-purple-600 transition flex items-center space-x-2"> |
|
<i class="fas fa-graduation-cap"></i> |
|
<span>Start Tutorial</span> |
|
</button> |
|
<button class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition flex items-center space-x-2"> |
|
<i class="fas fa-play"></i> |
|
<span>Execute</span> |
|
</button> |
|
<button class="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition flex items-center space-x-2"> |
|
<i class="fas fa-save"></i> |
|
<span>Save</span> |
|
</button> |
|
<div class="relative"> |
|
<button class="p-2 rounded-full hover:bg-gray-100"> |
|
<i class="fas fa-user-circle text-xl text-gray-600"></i> |
|
</button> |
|
</div> |
|
</div> |
|
</header> |
|
|
|
|
|
<div class="flex flex-1 overflow-hidden"> |
|
|
|
<div class="w-64 bg-white border-r border-gray-200 flex flex-col sidebar overflow-y-auto"> |
|
<div class="p-4 border-b border-gray-200"> |
|
<div class="relative"> |
|
<input type="text" placeholder="Search nodes..." class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i> |
|
</div> |
|
</div> |
|
<div class="p-4"> |
|
<h3 class="font-medium text-gray-700 mb-2">Triggers</h3> |
|
<div class="space-y-2"> |
|
<div class="node-item bg-white border border-gray-200 rounded-md p-3 cursor-pointer hover:bg-blue-50" data-type="trigger" data-node="webhook"> |
|
<div class="flex items-center space-x-3"> |
|
<div class="bg-red-100 p-2 rounded-md"> |
|
<i class="fas fa-bolt text-red-500"></i> |
|
</div> |
|
<div> |
|
<h4 class="font-medium">Webhook</h4> |
|
<p class="text-xs text-gray-500">Trigger workflow with HTTP request</p> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="node-item bg-white border border-gray-200 rounded-md p-3 cursor-pointer hover:bg-blue-50" data-type="trigger" data-node="schedule"> |
|
<div class="flex items-center space-x-3"> |
|
<div class="bg-purple-100 p-2 rounded-md"> |
|
<i class="fas fa-clock text-purple-500"></i> |
|
</div> |
|
<div> |
|
<h4 class="font-medium">Schedule</h4> |
|
<p class="text-xs text-gray-500">Trigger workflow on schedule</p> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<h3 class="font-medium text-gray-700 mt-6 mb-2">Actions</h3> |
|
<div class="space-y-2"> |
|
<div class="node-item bg-white border border-gray-200 rounded-md p-3 cursor-pointer hover:bg-blue-50" data-type="action" data-node="http"> |
|
<div class="flex items-center space-x-3"> |
|
<div class="bg-blue-100 p-2 rounded-md"> |
|
<i class="fas fa-globe text-blue-500"></i> |
|
</div> |
|
<div> |
|
<h4 class="font-medium">HTTP Request</h4> |
|
<p class="text-xs text-gray-500">Make HTTP request to any URL</p> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="node-item bg-white border border-gray-200 rounded-md p-3 cursor-pointer hover:bg-blue-50" data-type="action" data-node="email"> |
|
<div class="flex items-center space-x-3"> |
|
<div class="bg-green-100 p-2 rounded-md"> |
|
<i class="fas fa-envelope text-green-500"></i> |
|
</div> |
|
<div> |
|
<h4 class="font-medium">Email</h4> |
|
<p class="text-xs text-gray-500">Send an email</p> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="node-item bg-white border border-gray-200 rounded-md p-3 cursor-pointer hover:bg-blue-50" data-type="action" data-node="delay"> |
|
<div class="flex items-center space-x-3"> |
|
<div class="bg-yellow-100 p-2 rounded-md"> |
|
<i class="fas fa-hourglass-half text-yellow-500"></i> |
|
</div> |
|
<div> |
|
<h4 class="font-medium">Delay</h4> |
|
<p class="text-xs text-gray-500">Delay workflow execution</p> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<h3 class="font-medium text-gray-700 mt-6 mb-2">Logic</h3> |
|
<div class="space-y-2"> |
|
<div class="node-item bg-white border border-gray-200 rounded-md p-3 cursor-pointer hover:bg-blue-50" data-type="logic" data-node="if"> |
|
<div class="flex items-center space-x-3"> |
|
<div class="bg-indigo-100 p-2 rounded-md"> |
|
<i class="fas fa-code-branch text-indigo-500"></i> |
|
</div> |
|
<div> |
|
<h4 class="font-medium">IF Condition</h4> |
|
<p class="text-xs text-gray-500">Branch workflow based on condition</p> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="node-item bg-white border border-gray-200 rounded-md p-3 cursor-pointer hover:bg-blue-50" data-type="logic" data-node="switch"> |
|
<div class="flex items-center space-x-3"> |
|
<div class="bg-pink-100 p-2 rounded-md"> |
|
<i class="fas fa-random text-pink-500"></i> |
|
</div> |
|
<div> |
|
<h4 class="font-medium">Switch</h4> |
|
<p class="text-xs text-gray-500">Route workflow based on value</p> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="flex-1 relative overflow-hidden"> |
|
<div id="workflow-canvas" class="workflow-canvas w-full h-full relative"> |
|
<svg id="connections-svg" class="absolute top-0 left-0 w-full h-full pointer-events-none" style="z-index: 1;"></svg> |
|
<div id="nodes-container" class="absolute top-0 left-0 w-full h-full" style="z-index: 2;"></div> |
|
</div> |
|
|
|
|
|
<div class="absolute bottom-4 right-4 bg-white rounded-md shadow-md p-2 flex flex-col space-y-2"> |
|
<button id="zoom-in" class="p-2 hover:bg-gray-100 rounded"> |
|
<i class="fas fa-search-plus text-gray-600"></i> |
|
</button> |
|
<button id="zoom-reset" class="p-2 hover:bg-gray-100 rounded"> |
|
<span class="text-sm font-medium text-gray-600">100%</span> |
|
</button> |
|
<button id="zoom-out" class="p-2 hover:bg-gray-100 rounded"> |
|
<i class="fas fa-search-minus text-gray-600"></i> |
|
</button> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="w-80 bg-white border-l border-gray-200 flex flex-col sidebar overflow-y-auto"> |
|
<div class="p-4 border-b border-gray-200"> |
|
<h3 class="font-medium text-gray-700">Node Configuration</h3> |
|
<p class="text-sm text-gray-500" id="selected-node-name">No node selected</p> |
|
</div> |
|
<div class="p-4 flex-1" id="node-configuration"> |
|
<div class="text-center py-10 text-gray-400"> |
|
<i class="fas fa-mouse-pointer text-3xl mb-2"></i> |
|
<p>Select a node to configure</p> |
|
</div> |
|
</div> |
|
<div class="p-4 border-t border-gray-200"> |
|
<button id="delete-node" class="w-full py-2 bg-red-500 text-white rounded-md hover:bg-red-600 transition flex items-center justify-center space-x-2"> |
|
<i class="fas fa-trash"></i> |
|
<span>Delete Node</span> |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="tutorial-overlay" class="tutorial-overlay hidden"> |
|
<div class="tutorial-card"> |
|
<div class="p-6"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h2 class="text-2xl font-bold text-gray-800">FlowForge Tutorial</h2> |
|
<button id="close-tutorial" class="text-gray-500 hover:text-gray-700"> |
|
<i class="fas fa-times"></i> |
|
</button> |
|
</div> |
|
<div id="tutorial-content"> |
|
<div class="tutorial-step active" data-step="1"> |
|
<h3 class="text-lg font-medium mb-2">Welcome to FlowForge!</h3> |
|
<p class="text-gray-600 mb-4">FlowForge is a visual workflow automation tool that helps you connect different services and automate tasks without writing code.</p> |
|
<p class="text-gray-600 mb-6">This tutorial will guide you through creating your first workflow.</p> |
|
<div class="flex justify-end"> |
|
<button class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition next-step"> |
|
Next <i class="fas fa-arrow-right ml-1"></i> |
|
</button> |
|
</div> |
|
</div> |
|
<div class="tutorial-step hidden" data-step="2"> |
|
<h3 class="text-lg font-medium mb-2">Understanding the Interface</h3> |
|
<p class="text-gray-600 mb-4">The workspace has three main areas:</p> |
|
<ul class="list-disc pl-5 text-gray-600 mb-4 space-y-2"> |
|
<li><strong>Left Sidebar:</strong> Contains all available nodes you can add to your workflow</li> |
|
<li><strong>Canvas:</strong> Where you build your workflow by connecting nodes</li> |
|
<li><strong>Right Sidebar:</strong> Configure individual nodes when selected</li> |
|
</ul> |
|
<div class="flex justify-between"> |
|
<button class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition prev-step"> |
|
<i class="fas fa-arrow-left mr-1"></i> Back |
|
</button> |
|
<button class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition next-step"> |
|
Next <i class="fas fa-arrow-right ml-1"></i> |
|
</button> |
|
</div> |
|
</div> |
|
<div class="tutorial-step hidden" data-step="3"> |
|
<h3 class="text-lg font-medium mb-2">Creating Your First Workflow</h3> |
|
<p class="text-gray-600 mb-4">We'll create a simple workflow that:</p> |
|
<ol class="list-decimal pl-5 text-gray-600 mb-4 space-y-2"> |
|
<li>Triggers on a schedule</li> |
|
<li>Makes an HTTP request to get data</li> |
|
<li>Sends an email with the results</li> |
|
</ol> |
|
<div class="flex justify-between"> |
|
<button class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition prev-step"> |
|
<i class="fas fa-arrow-left mr-1"></i> Back |
|
</button> |
|
<button class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition next-step"> |
|
Start Building <i class="fas fa-arrow-right ml-1"></i> |
|
</button> |
|
</div> |
|
</div> |
|
<div class="tutorial-step hidden" data-step="4"> |
|
<h3 class="text-lg font-medium mb-2">Step 1: Add a Trigger</h3> |
|
<p class="text-gray-600 mb-4">Every workflow needs a trigger to start. Let's add a Schedule trigger that will run our workflow daily.</p> |
|
<p class="text-gray-600 mb-4 font-medium">👉 Click and drag the "Schedule" node from the left sidebar to the canvas</p> |
|
<div class="bg-blue-50 border border-blue-200 rounded-md p-4 mb-4"> |
|
<div class="flex items-center space-x-3"> |
|
<div class="bg-purple-100 p-2 rounded-md"> |
|
<i class="fas fa-clock text-purple-500"></i> |
|
</div> |
|
<div> |
|
<h4 class="font-medium">Schedule</h4> |
|
<p class="text-xs text-gray-500">Trigger workflow on schedule</p> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="flex justify-between"> |
|
<button class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition prev-step"> |
|
<i class="fas fa-arrow-left mr-1"></i> Back |
|
</button> |
|
<button class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition next-step" disabled id="trigger-added-btn"> |
|
Next <i class="fas fa-arrow-right ml-1"></i> |
|
</button> |
|
</div> |
|
</div> |
|
<div class="tutorial-step hidden" data-step="5"> |
|
<h3 class="text-lg font-medium mb-2">Step 2: Add an HTTP Request</h3> |
|
<p class="text-gray-600 mb-4">Now let's add a node that will fetch data from an API when the workflow is triggered.</p> |
|
<p class="text-gray-600 mb-4 font-medium">👉 Click and drag the "HTTP Request" node to the right of the Schedule node</p> |
|
<div class="bg-blue-50 border border-blue-200 rounded-md p-4 mb-4"> |
|
<div class="flex items-center space-x-3"> |
|
<div class="bg-blue-100 p-2 rounded-md"> |
|
<i class="fas fa-globe text-blue-500"></i> |
|
</div> |
|
<div> |
|
<h4 class="font-medium">HTTP Request</h4> |
|
<p class="text-xs text-gray-500">Make HTTP request to any URL</p> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="flex justify-between"> |
|
<button class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition prev-step"> |
|
<i class="fas fa-arrow-left mr-1"></i> Back |
|
</button> |
|
<button class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition next-step" disabled id="http-added-btn"> |
|
Next <i class="fas fa-arrow-right ml-1"></i> |
|
</button> |
|
</div> |
|
</div> |
|
<div class="tutorial-step hidden" data-step="6"> |
|
<h3 class="text-lg font-medium mb-2">Step 3: Connect the Nodes</h3> |
|
<p class="text-gray-600 mb-4">Now we need to connect the Schedule node to the HTTP Request node so the workflow knows the order of operations.</p> |
|
<p class="text-gray-600 mb-4 font-medium">👉 Click and drag from the output port (right side) of the Schedule node to the input port (left side) of the HTTP Request node</p> |
|
<div class="flex justify-between"> |
|
<button class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition prev-step"> |
|
<i class="fas fa-arrow-left mr-1"></i> Back |
|
</button> |
|
<button class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition next-step" disabled id="nodes-connected-btn"> |
|
Next <i class="fas fa-arrow-right ml-1"></i> |
|
</button> |
|
</div> |
|
</div> |
|
<div class="tutorial-step hidden" data-step="7"> |
|
<h3 class="text-lg font-medium mb-2">Step 4: Add an Email Node</h3> |
|
<p class="text-gray-600 mb-4">Finally, let's add a node that will email us the results from the API request.</p> |
|
<p class="text-gray-600 mb-4 font-medium">👉 Add an "Email" node to the right of the HTTP Request node and connect them</p> |
|
<div class="bg-blue-50 border border-blue-200 rounded-md p-4 mb-4"> |
|
<div class="flex items-center space-x-3"> |
|
<div class="bg-green-100 p-2 rounded-md"> |
|
<i class="fas fa-envelope text-green-500"></i> |
|
</div> |
|
<div> |
|
<h4 class="font-medium">Email</h4> |
|
<p class="text-xs text-gray-500">Send an email</p> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="flex justify-between"> |
|
<button class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition prev-step"> |
|
<i class="fas fa-arrow-left mr-1"></i> Back |
|
</button> |
|
<button class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition next-step" disabled id="email-added-btn"> |
|
Next <i class="fas fa-arrow-right ml-1"></i> |
|
</button> |
|
</div> |
|
</div> |
|
<div class="tutorial-step hidden" data-step="8"> |
|
<h3 class="text-lg font-medium mb-2">Step 5: Configure the Nodes</h3> |
|
<p class="text-gray-600 mb-4">Now let's configure each node:</p> |
|
<ol class="list-decimal pl-5 text-gray-600 mb-4 space-y-2"> |
|
<li><strong>Schedule:</strong> Double-click to set it to run daily at 9am</li> |
|
<li><strong>HTTP Request:</strong> Set the URL to an API endpoint</li> |
|
<li><strong>Email:</strong> Enter your email and a subject/message</li> |
|
</ol> |
|
<p class="text-gray-600 mb-4 font-medium">👉 Double-click each node to configure it</p> |
|
<div class="flex justify-between"> |
|
<button class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition prev-step"> |
|
<i class="fas fa-arrow-left mr-1"></i> Back |
|
</button> |
|
<button class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition next-step"> |
|
Next <i class="fas fa-arrow-right ml-1"></i> |
|
</button> |
|
</div> |
|
</div> |
|
<div class="tutorial-step hidden" data-step="9"> |
|
<h3 class="text-lg font-medium mb-2">Step 6: Execute Your Workflow</h3> |
|
<p class="text-gray-600 mb-4">Your workflow is now ready to run! You can:</p> |
|
<ul class="list-disc pl-5 text-gray-600 mb-4 space-y-2"> |
|
<li>Click "Execute" to run it manually</li> |
|
<li>Wait for the scheduled time (if you configured one)</li> |
|
<li>Trigger it via webhook (if you used a webhook trigger)</li> |
|
</ul> |
|
<p class="text-gray-600 mb-6">Congratulations! You've built your first workflow automation.</p> |
|
<div class="flex justify-between"> |
|
<button class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition prev-step"> |
|
<i class="fas fa-arrow-left mr-1"></i> Back |
|
</button> |
|
<button class="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition finish-tutorial"> |
|
Finish Tutorial <i class="fas fa-check ml-1"></i> |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
const state = { |
|
nodes: [], |
|
connections: [], |
|
selectedNode: null, |
|
draggingNode: null, |
|
draggingPort: null, |
|
creatingConnection: false, |
|
scale: 1, |
|
offset: { x: 0, y: 0 }, |
|
panning: false, |
|
startPan: { x: 0, y: 0 }, |
|
tutorial: { |
|
active: false, |
|
currentStep: 1, |
|
highlightElement: null |
|
} |
|
}; |
|
|
|
|
|
const canvas = document.getElementById('workflow-canvas'); |
|
const nodesContainer = document.getElementById('nodes-container'); |
|
const connectionsSvg = document.getElementById('connections-svg'); |
|
const nodeConfiguration = document.getElementById('node-configuration'); |
|
const selectedNodeName = document.getElementById('selected-node-name'); |
|
const deleteNodeBtn = document.getElementById('delete-node'); |
|
const zoomInBtn = document.getElementById('zoom-in'); |
|
const zoomOutBtn = document.getElementById('zoom-out'); |
|
const zoomResetBtn = document.getElementById('zoom-reset'); |
|
const tutorialOverlay = document.getElementById('tutorial-overlay'); |
|
const startTutorialBtn = document.getElementById('start-tutorial'); |
|
const closeTutorialBtn = document.getElementById('close-tutorial'); |
|
|
|
|
|
const nodeTemplates = { |
|
webhook: { |
|
name: 'Webhook', |
|
type: 'trigger', |
|
icon: 'bolt', |
|
color: 'red', |
|
inputs: 0, |
|
outputs: 1, |
|
config: ` |
|
<div class="space-y-4"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Webhook URL</label> |
|
<div class="flex"> |
|
<input type="text" class="flex-1 border border-gray-300 rounded-l-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" value="https://your-domain.com/webhook"> |
|
<button class="bg-gray-100 border border-l-0 border-gray-300 rounded-r-md px-3 hover:bg-gray-200"> |
|
<i class="fas fa-copy"></i> |
|
</button> |
|
</div> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">HTTP Method</label> |
|
<select class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<option>GET</option> |
|
<option selected>POST</option> |
|
<option>PUT</option> |
|
<option>DELETE</option> |
|
</select> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Response Mode</label> |
|
<select class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<option>On first node execution</option> |
|
<option selected>On last node execution</option> |
|
<option>Manually in node</option> |
|
</select> |
|
</div> |
|
</div> |
|
` |
|
}, |
|
schedule: { |
|
name: 'Schedule', |
|
type: 'trigger', |
|
icon: 'clock', |
|
color: 'purple', |
|
inputs: 0, |
|
outputs: 1, |
|
config: ` |
|
<div class="space-y-4"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Frequency</label> |
|
<select class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<option>Minutes</option> |
|
<option>Hours</option> |
|
<option selected>Days</option> |
|
<option>Weeks</option> |
|
<option>Months</option> |
|
</select> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Interval</label> |
|
<input type="number" min="1" value="1" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Start Time</label> |
|
<input type="time" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
</div> |
|
</div> |
|
` |
|
}, |
|
http: { |
|
name: 'HTTP Request', |
|
type: 'action', |
|
icon: 'globe', |
|
color: 'blue', |
|
inputs: 1, |
|
outputs: 1, |
|
config: ` |
|
<div class="space-y-4"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">URL</label> |
|
<input type="text" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="https://example.com/api"> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Method</label> |
|
<select class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<option>GET</option> |
|
<option selected>POST</option> |
|
<option>PUT</option> |
|
<option>DELETE</option> |
|
</select> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Headers</label> |
|
<div class="border border-gray-300 rounded-md"> |
|
<div class="flex border-b border-gray-300"> |
|
<input type="text" placeholder="Header" class="flex-1 px-2 py-1 border-r border-gray-300 focus:outline-none"> |
|
<input type="text" placeholder="Value" class="flex-1 px-2 py-1 focus:outline-none"> |
|
</div> |
|
<button class="w-full py-1 text-sm text-blue-500 hover:bg-gray-50"> |
|
<i class="fas fa-plus mr-1"></i> Add Header |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
` |
|
}, |
|
email: { |
|
name: 'Email', |
|
type: 'action', |
|
icon: 'envelope', |
|
color: 'green', |
|
inputs: 1, |
|
outputs: 1, |
|
config: ` |
|
<div class="space-y-4"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">From</label> |
|
<input type="email" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="[email protected]"> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">To</label> |
|
<input type="email" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="[email protected]"> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Subject</label> |
|
<input type="text" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Email subject"> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Message</label> |
|
<textarea class="w-full border border-gray-300 rounded-md px-3 py-2 h-24 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Email content"></textarea> |
|
</div> |
|
</div> |
|
` |
|
}, |
|
delay: { |
|
name: 'Delay', |
|
type: 'action', |
|
icon: 'hourglass-half', |
|
color: 'yellow', |
|
inputs: 1, |
|
outputs: 1, |
|
config: ` |
|
<div class="space-y-4"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Delay Type</label> |
|
<select class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<option selected>For duration</option> |
|
<option>Until specific time</option> |
|
</select> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Duration</label> |
|
<div class="flex space-x-2"> |
|
<input type="number" min="1" value="5" class="w-20 border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<select class="flex-1 border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<option>Seconds</option> |
|
<option selected>Minutes</option> |
|
<option>Hours</option> |
|
<option>Days</option> |
|
</select> |
|
</div> |
|
</div> |
|
</div> |
|
` |
|
}, |
|
if: { |
|
name: 'IF Condition', |
|
type: 'logic', |
|
icon: 'code-branch', |
|
color: 'indigo', |
|
inputs: 1, |
|
outputs: 2, |
|
config: ` |
|
<div class="space-y-4"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Condition</label> |
|
<div class="flex items-center space-x-2"> |
|
<select class="flex-1 border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<option>Equals</option> |
|
<option>Not Equals</option> |
|
<option selected>Contains</option> |
|
<option>Greater Than</option> |
|
<option>Less Than</option> |
|
</select> |
|
</div> |
|
</div> |
|
<div class="grid grid-cols-2 gap-2"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Value 1</label> |
|
<input type="text" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Value or expression"> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Value 2</label> |
|
<input type="text" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Value or expression"> |
|
</div> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Output Labels</label> |
|
<div class="grid grid-cols-2 gap-2"> |
|
<input type="text" value="True" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<input type="text" value="False" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
</div> |
|
</div> |
|
</div> |
|
` |
|
}, |
|
switch: { |
|
name: 'Switch', |
|
type: 'logic', |
|
icon: 'random', |
|
color: 'pink', |
|
inputs: 1, |
|
outputs: 3, |
|
config: ` |
|
<div class="space-y-4"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Routing Mode</label> |
|
<select class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<option selected>Value</option> |
|
<option>Expression</option> |
|
<option>Regex</option> |
|
</select> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Cases</label> |
|
<div class="border border-gray-300 rounded-md divide-y divide-gray-300"> |
|
<div class="flex items-center p-2"> |
|
<input type="text" value="Case 1" class="flex-1 border border-gray-300 rounded-md px-2 py-1 focus:outline-none"> |
|
<button class="ml-2 text-red-500 hover:text-red-700"> |
|
<i class="fas fa-times"></i> |
|
</button> |
|
</div> |
|
<div class="flex items-center p-2"> |
|
<input type="text" value="Case 2" class="flex-1 border border-gray-300 rounded-md px-2 py-1 focus:outline-none"> |
|
<button class="ml-2 text-red-500 hover:text-red-700"> |
|
<i class="fas fa-times"></i> |
|
</button> |
|
</div> |
|
<button class="w-full py-2 text-sm text-blue-500 hover:bg-gray-50"> |
|
<i class="fas fa-plus mr-1"></i> Add Case |
|
</button> |
|
</div> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Default Case</label> |
|
<input type="text" value="Default" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
</div> |
|
</div> |
|
` |
|
} |
|
}; |
|
|
|
|
|
function init() { |
|
|
|
document.querySelectorAll('.node-item').forEach(item => { |
|
item.addEventListener('mousedown', (e) => { |
|
if (e.button !== 0) return; |
|
|
|
const nodeType = item.dataset.node; |
|
state.draggingNode = { |
|
type: nodeType, |
|
x: e.clientX, |
|
y: e.clientY |
|
}; |
|
|
|
document.addEventListener('mousemove', dragNewNode); |
|
document.addEventListener('mouseup', dropNewNode); |
|
}); |
|
}); |
|
|
|
|
|
canvas.addEventListener('mousedown', (e) => { |
|
if (e.button === 1 || (e.button === 0 && e.ctrlKey)) { |
|
e.preventDefault(); |
|
state.panning = true; |
|
state.startPan = { x: e.clientX, y: e.clientY }; |
|
canvas.style.cursor = 'grabbing'; |
|
} |
|
}); |
|
|
|
document.addEventListener('mousemove', (e) => { |
|
if (state.panning) { |
|
const dx = e.clientX - state.startPan.x; |
|
const dy = e.clientY - state.startPan.y; |
|
|
|
state.offset.x += dx; |
|
state.offset.y += dy; |
|
|
|
state.startPan = { x: e.clientX, y: e.clientY }; |
|
|
|
updateCanvasTransform(); |
|
} |
|
}); |
|
|
|
document.addEventListener('mouseup', () => { |
|
if (state.panning) { |
|
state.panning = false; |
|
canvas.style.cursor = ''; |
|
} |
|
}); |
|
|
|
|
|
zoomInBtn.addEventListener('click', () => { |
|
state.scale = Math.min(state.scale + 0.1, 2); |
|
updateCanvasTransform(); |
|
}); |
|
|
|
zoomOutBtn.addEventListener('click', () => { |
|
state.scale = Math.max(state.scale - 0.1, 0.5); |
|
updateCanvasTransform(); |
|
}); |
|
|
|
zoomResetBtn.addEventListener('click', () => { |
|
state.scale = 1; |
|
state.offset = { x: 0, y: 0 }; |
|
updateCanvasTransform(); |
|
}); |
|
|
|
|
|
deleteNodeBtn.addEventListener('click', deleteSelectedNode); |
|
|
|
|
|
document.addEventListener('contextmenu', (e) => { |
|
e.preventDefault(); |
|
}); |
|
|
|
|
|
startTutorialBtn.addEventListener('click', startTutorial); |
|
closeTutorialBtn.addEventListener('click', closeTutorial); |
|
|
|
|
|
document.querySelectorAll('.next-step').forEach(btn => { |
|
btn.addEventListener('click', () => { |
|
goToStep(state.tutorial.currentStep + 1); |
|
}); |
|
}); |
|
|
|
document.querySelectorAll('.prev-step').forEach(btn => { |
|
btn.addEventListener('click', () => { |
|
goToStep(state.tutorial.currentStep - 1); |
|
}); |
|
}); |
|
|
|
document.querySelectorAll('.finish-tutorial').forEach(btn => { |
|
btn.addEventListener('click', closeTutorial); |
|
}); |
|
|
|
|
|
if (state.nodes.length === 0) { |
|
setTimeout(() => { |
|
startTutorial(); |
|
}, 500); |
|
} |
|
} |
|
|
|
|
|
function addNode(nodeType, x, y) { |
|
const template = nodeTemplates[nodeType]; |
|
if (!template) return; |
|
|
|
const node = { |
|
id: state.nodes.length, |
|
type: nodeType, |
|
name: template.name, |
|
x: x, |
|
y: y, |
|
width: 200, |
|
height: 100, |
|
inputs: template.inputs, |
|
outputs: template.outputs, |
|
selected: false |
|
}; |
|
|
|
state.nodes.push(node); |
|
renderNode(node); |
|
|
|
|
|
if (state.tutorial.active) { |
|
if (nodeType === 'schedule' && state.tutorial.currentStep === 4) { |
|
document.getElementById('trigger-added-btn').disabled = false; |
|
} else if (nodeType === 'http' && state.tutorial.currentStep === 5) { |
|
document.getElementById('http-added-btn').disabled = false; |
|
} else if (nodeType === 'email' && state.tutorial.currentStep === 7) { |
|
document.getElementById('email-added-btn').disabled = false; |
|
} |
|
} |
|
|
|
return node; |
|
} |
|
|
|
|
|
function renderNode(node) { |
|
const template = nodeTemplates[node.type]; |
|
|
|
let nodeEl = document.getElementById(`node-${node.id}`); |
|
if (!nodeEl) { |
|
nodeEl = document.createElement('div'); |
|
nodeEl.id = `node-${node.id}`; |
|
nodeEl.className = `node absolute bg-white rounded-lg shadow-md border-2 ${node.selected ? 'border-blue-500 selected' : 'border-gray-200'}`; |
|
nodeEl.style.width = `${node.width}px`; |
|
nodeEl.style.height = `${node.height}px`; |
|
nodeEl.style.left = `${node.x}px`; |
|
nodeEl.style.top = `${node.y}px`; |
|
|
|
|
|
const header = document.createElement('div'); |
|
header.className = `flex items-center px-3 py-2 border-b border-gray-200 bg-${template.color}-50 rounded-t-lg`; |
|
|
|
const icon = document.createElement('div'); |
|
icon.className = `bg-${template.color}-100 p-1 rounded-md mr-2`; |
|
|
|
const iconEl = document.createElement('i'); |
|
iconEl.className = `fas fa-${template.icon} text-${template.color}-500`; |
|
icon.appendChild(iconEl); |
|
|
|
const title = document.createElement('h3'); |
|
title.className = 'font-medium text-sm'; |
|
title.textContent = node.name; |
|
|
|
header.appendChild(icon); |
|
header.appendChild(title); |
|
|
|
|
|
const body = document.createElement('div'); |
|
body.className = 'p-3 text-xs text-gray-500'; |
|
body.textContent = 'Double click to configure'; |
|
|
|
|
|
for (let i = 0; i < node.inputs; i++) { |
|
const port = document.createElement('div'); |
|
port.className = 'node-port input absolute top-1/2'; |
|
port.style.top = `${(i + 1) * (100 / (node.inputs + 1))}%`; |
|
port.dataset.nodeId = node.id; |
|
port.dataset.portIndex = i; |
|
port.dataset.portType = 'input'; |
|
|
|
port.addEventListener('mousedown', startConnection); |
|
port.addEventListener('mouseup', completeConnection); |
|
|
|
nodeEl.appendChild(port); |
|
} |
|
|
|
|
|
for (let i = 0; i < node.outputs; i++) { |
|
const port = document.createElement('div'); |
|
port.className = 'node-port output absolute top-1/2'; |
|
port.style.top = `${(i + 1) * (100 / (node.outputs + 1))}%`; |
|
port.dataset.nodeId = node.id; |
|
port.dataset.portIndex = i; |
|
port.dataset.portType = 'output'; |
|
|
|
port.addEventListener('mousedown', startConnection); |
|
port.addEventListener('mouseup', completeConnection); |
|
|
|
nodeEl.appendChild(port); |
|
} |
|
|
|
|
|
nodeEl.addEventListener('mousedown', (e) => { |
|
if (e.button !== 0) return; |
|
|
|
|
|
if (!e.target.classList.contains('node-port')) { |
|
selectNode(node.id); |
|
|
|
|
|
state.draggingNode = node; |
|
state.dragOffset = { |
|
x: e.clientX - node.x, |
|
y: e.clientY - node.y |
|
}; |
|
|
|
document.addEventListener('mousemove', dragNode); |
|
document.addEventListener('mouseup', dropNode); |
|
} |
|
}); |
|
|
|
nodeEl.addEventListener('dblclick', () => { |
|
selectNode(node.id); |
|
showNodeConfiguration(node); |
|
|
|
|
|
if (state.tutorial.active && state.tutorial.currentStep === 8) { |
|
document.getElementById('nodes-connected-btn').disabled = false; |
|
} |
|
}); |
|
|
|
nodeEl.appendChild(header); |
|
nodeEl.appendChild(body); |
|
|
|
nodesContainer.appendChild(nodeEl); |
|
} else { |
|
|
|
nodeEl.style.left = `${node.x}px`; |
|
nodeEl.style.top = `${node.y}px`; |
|
nodeEl.className = `node absolute bg-white rounded-lg shadow-md border-2 ${node.selected ? 'border-blue-500 selected' : 'border-gray-200'}`; |
|
} |
|
} |
|
|
|
|
|
function dragNewNode(e) { |
|
if (!state.draggingNode) return; |
|
|
|
const ghost = document.getElementById('node-ghost'); |
|
if (!ghost) { |
|
const ghostEl = document.createElement('div'); |
|
ghostEl.id = 'node-ghost'; |
|
ghostEl.className = 'node absolute bg-white rounded-lg shadow-md border-2 border-dashed border-gray-400 opacity-70 pointer-events-none'; |
|
ghostEl.style.width = '200px'; |
|
ghostEl.style.height = '100px'; |
|
document.body.appendChild(ghostEl); |
|
} |
|
|
|
document.getElementById('node-ghost').style.left = `${e.clientX - 100}px`; |
|
document.getElementById('node-ghost').style.top = `${e.clientY - 50}px`; |
|
} |
|
|
|
|
|
function dropNewNode(e) { |
|
document.removeEventListener('mousemove', dragNewNode); |
|
document.removeEventListener('mouseup', dropNewNode); |
|
|
|
const ghost = document.getElementById('node-ghost'); |
|
if (ghost) ghost.remove(); |
|
|
|
if (!state.draggingNode) return; |
|
|
|
|
|
const rect = canvas.getBoundingClientRect(); |
|
const x = e.clientX - rect.left - state.offset.x; |
|
const y = e.clientY - rect.top - state.offset.y; |
|
|
|
addNode(state.draggingNode.type, x, y); |
|
state.draggingNode = null; |
|
} |
|
|
|
|
|
function dragNode(e) { |
|
if (!state.draggingNode) return; |
|
|
|
const node = state.draggingNode; |
|
node.x = e.clientX - state.dragOffset.x; |
|
node.y = e.clientY - state.dragOffset.y; |
|
|
|
renderNode(node); |
|
renderConnections(); |
|
} |
|
|
|
|
|
function dropNode() { |
|
document.removeEventListener('mousemove', dragNode); |
|
document.removeEventListener('mouseup', dropNode); |
|
state.draggingNode = null; |
|
} |
|
|
|
|
|
function selectNode(nodeId) { |
|
|
|
state.nodes.forEach(node => { |
|
node.selected = false; |
|
}); |
|
|
|
|
|
const node = state.nodes.find(n => n.id === nodeId); |
|
if (node) { |
|
node.selected = true; |
|
state.selectedNode = node; |
|
selectedNodeName.textContent = node.name; |
|
} else { |
|
state.selectedNode = null; |
|
selectedNodeName.textContent = 'No node selected'; |
|
} |
|
|
|
|
|
state.nodes.forEach(renderNode); |
|
} |
|
|
|
|
|
function showNodeConfiguration(node) { |
|
const template = nodeTemplates[node.type]; |
|
if (!template) return; |
|
|
|
nodeConfiguration.innerHTML = template.config; |
|
} |
|
|
|
|
|
function deleteSelectedNode() { |
|
if (!state.selectedNode) return; |
|
|
|
const nodeId = state.selectedNode.id; |
|
|
|
|
|
state.connections = state.connections.filter(conn => { |
|
return conn.fromNode !== nodeId && conn.toNode !== nodeId; |
|
}); |
|
|
|
|
|
state.nodes = state.nodes.filter(node => node.id !== nodeId); |
|
|
|
|
|
const nodeEl = document.getElementById(`node-${nodeId}`); |
|
if (nodeEl) nodeEl.remove(); |
|
|
|
|
|
state.selectedNode = null; |
|
selectedNodeName.textContent = 'No node selected'; |
|
nodeConfiguration.innerHTML = ` |
|
<div class="text-center py-10 text-gray-400"> |
|
<i class="fas fa-mouse-pointer text-3xl mb-2"></i> |
|
<p>Select a node to configure</p> |
|
</div> |
|
`; |
|
|
|
|
|
renderConnections(); |
|
} |
|
|
|
|
|
function startConnection(e) { |
|
e.stopPropagation(); |
|
|
|
const port = e.target; |
|
const nodeId = parseInt(port.dataset.nodeId); |
|
const portIndex = parseInt(port.dataset.portIndex); |
|
const portType = port.dataset.portType; |
|
|
|
state.draggingPort = { |
|
nodeId: nodeId, |
|
portIndex: portIndex, |
|
portType: portType, |
|
x: e.clientX, |
|
y: e.clientY |
|
}; |
|
|
|
state.creatingConnection = true; |
|
|
|
|
|
const tempLine = document.createElement('div'); |
|
tempLine.id = 'temp-connection'; |
|
tempLine.style.position = 'absolute'; |
|
tempLine.style.pointerEvents = 'none'; |
|
tempLine.style.backgroundColor = '#94a3b8'; |
|
tempLine.style.height = '2px'; |
|
tempLine.style.transformOrigin = '0 0'; |
|
document.body.appendChild(tempLine); |
|
|
|
document.addEventListener('mousemove', drawTempConnection); |
|
document.addEventListener('mouseup', cancelConnection); |
|
} |
|
|
|
|
|
function drawTempConnection(e) { |
|
if (!state.draggingPort || !state.creatingConnection) return; |
|
|
|
const startX = state.draggingPort.x; |
|
const startY = state.draggingPort.y; |
|
const endX = e.clientX; |
|
const endY = e.clientY; |
|
|
|
|
|
const length = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2)); |
|
const angle = Math.atan2(endY - startY, endX - startX) * 180 / Math.PI; |
|
|
|
|
|
const tempLine = document.getElementById('temp-connection'); |
|
tempLine.style.left = `${startX}px`; |
|
tempLine.style.top = `${startY}px`; |
|
tempLine.style.width = `${length}px`; |
|
tempLine.style.transform = `rotate(${angle}deg)`; |
|
} |
|
|
|
|
|
function completeConnection(e) { |
|
if (!state.draggingPort || !state.creatingConnection) return; |
|
|
|
const fromPort = state.draggingPort; |
|
const toPort = e.target; |
|
|
|
|
|
if (fromPort.portType === 'input' || toPort.dataset.portType === 'output') { |
|
cancelConnection(); |
|
return; |
|
} |
|
|
|
|
|
if (fromPort.nodeId === parseInt(toPort.dataset.nodeId)) { |
|
cancelConnection(); |
|
return; |
|
} |
|
|
|
|
|
addConnection( |
|
fromPort.nodeId, |
|
fromPort.portIndex, |
|
parseInt(toPort.dataset.nodeId), |
|
parseInt(toPort.dataset.portIndex) |
|
); |
|
|
|
cancelConnection(); |
|
|
|
|
|
if (state.tutorial.active && state.tutorial.currentStep === 6) { |
|
document.getElementById('nodes-connected-btn').disabled = false; |
|
} |
|
} |
|
|
|
|
|
function cancelConnection() { |
|
document.removeEventListener('mousemove', drawTempConnection); |
|
document.removeEventListener('mouseup', cancelConnection); |
|
|
|
const tempLine = document.getElementById('temp-connection'); |
|
if (tempLine) tempLine.remove(); |
|
|
|
state.draggingPort = null; |
|
state.creatingConnection = false; |
|
} |
|
|
|
|
|
function addConnection(fromNodeId, fromPortIndex, toNodeId, toPortIndex) { |
|
|
|
const exists = state.connections.some(conn => { |
|
return conn.fromNode === fromNodeId && |
|
conn.fromPort === fromPortIndex && |
|
conn.toNode === toNodeId && |
|
conn.toPort === toPortIndex; |
|
}); |
|
|
|
if (exists) return; |
|
|
|
state.connections.push({ |
|
fromNode: fromNodeId, |
|
fromPort: fromPortIndex, |
|
toNode: toNodeId, |
|
toPort: toPortIndex |
|
}); |
|
|
|
renderConnections(); |
|
} |
|
|
|
|
|
function renderConnections() { |
|
|
|
connectionsSvg.innerHTML = ''; |
|
|
|
state.connections.forEach(conn => { |
|
const fromNode = state.nodes.find(n => n.id === conn.fromNode); |
|
const toNode = state.nodes.find(n => n.id === conn.toNode); |
|
|
|
if (!fromNode || !toNode) return; |
|
|
|
|
|
const fromPortX = fromNode.x + fromNode.width; |
|
const fromPortY = fromNode.y + (conn.fromPort + 1) * (fromNode.height / (fromNode.outputs + 1)); |
|
|
|
const toPortX = toNode.x; |
|
const toPortY = toNode.y + (conn.toPort + 1) * (toNode.height / (toNode.inputs + 1)); |
|
|
|
|
|
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); |
|
const midX = (fromPortX + toPortX) / 2; |
|
|
|
path.setAttribute('d', `M${fromPortX},${fromPortY} C${midX},${fromPortY} ${midX},${toPortY} ${toPortX},${toPortY}`); |
|
path.setAttribute('class', 'connection-path'); |
|
|
|
connectionsSvg.appendChild(path); |
|
}); |
|
} |
|
|
|
|
|
function updateCanvasTransform() { |
|
nodesContainer.style.transform = `translate(${state.offset.x}px, ${state.offset.y}px) scale(${state.scale})`; |
|
connectionsSvg.style.transform = `translate(${state.offset.x}px, ${state.offset.y}px) scale(${state.scale})`; |
|
zoomResetBtn.querySelector('span').textContent = `${Math.round(state.scale * 100)}%`; |
|
} |
|
|
|
|
|
function startTutorial() { |
|
state.tutorial.active = true; |
|
state.tutorial.currentStep = 1; |
|
tutorialOverlay.classList.remove('hidden'); |
|
goToStep(1); |
|
} |
|
|
|
function closeTutorial() { |
|
state.tutorial.active = false; |
|
tutorialOverlay.classList.add('hidden'); |
|
removeHighlight(); |
|
} |
|
|
|
function goToStep(step) { |
|
if (step < 1 || step > 9) return; |
|
|
|
state.tutorial.currentStep = step; |
|
|
|
|
|
document.querySelectorAll('.tutorial-step').forEach(el => { |
|
el.classList.add('hidden'); |
|
el.classList.remove('active'); |
|
}); |
|
|
|
const currentStepEl = document.querySelector(`.tutorial-step[data-step="${step}"]`); |
|
if (currentStepEl) { |
|
currentStepEl.classList.remove('hidden'); |
|
currentStepEl.classList.add('active'); |
|
} |
|
|
|
|
|
if (step === 4) { |
|
highlightElement('.node-item[data-node="schedule"]', 'Drag this node to the canvas'); |
|
} else if (step === 5) { |
|
highlightElement('.node-item[data-node="http"]', 'Drag this node to the canvas'); |
|
} else if (step === 6) { |
|
highlightElement('.node-port.output', 'Drag from this port to connect nodes'); |
|
} else if (step === 7) { |
|
highlightElement('.node-item[data-node="email"]', 'Drag this node to the canvas'); |
|
} else if (step === 8) { |
|
highlightElement('.node', 'Double-click a node to configure it'); |
|
} else { |
|
removeHighlight(); |
|
} |
|
} |
|
|
|
function highlightElement(selector, text) { |
|
removeHighlight(); |
|
|
|
const element = document.querySelector(selector); |
|
if (!element) return; |
|
|
|
const rect = element.getBoundingClientRect(); |
|
|
|
|
|
const highlight = document.createElement('div'); |
|
highlight.className = 'tutorial-highlight animate-pulse'; |
|
highlight.style.width = `${rect.width + 16}px`; |
|
highlight.style.height = `${rect.height + 16}px`; |
|
highlight.style.top = `${rect.top - 8 + window.scrollY}px`; |
|
highlight.style.left = `${rect.left - 8 + window.scrollX}px`; |
|
document.body.appendChild(highlight); |
|
|
|
|
|
const tooltip = document.createElement('div'); |
|
tooltip.className = 'tutorial-tooltip'; |
|
tooltip.textContent = text; |
|
|
|
|
|
tooltip.style.top = `${rect.bottom + 8 + window.scrollY}px`; |
|
tooltip.style.left = `${rect.left + window.scrollX}px`; |
|
document.body.appendChild(tooltip); |
|
|
|
state.tutorial.highlightElement = { highlight, tooltip }; |
|
} |
|
|
|
function removeHighlight() { |
|
if (state.tutorial.highlightElement) { |
|
state.tutorial.highlightElement.highlight.remove(); |
|
state.tutorial.highlightElement.tooltip.remove(); |
|
state.tutorial.highlightElement = null; |
|
} |
|
} |
|
|
|
|
|
init(); |
|
}); |
|
</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=theaimoron/my-attempt-at-automation" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
</html> |