theaimoron's picture
Add 3 files
fc9f7e9 verified
<!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 -->
<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>
<!-- Main Content -->
<div class="flex flex-1 overflow-hidden">
<!-- Left Sidebar - Nodes -->
<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>
<!-- Main Workflow Area -->
<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>
<!-- Zoom Controls -->
<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>
<!-- Right Sidebar - Node Configuration -->
<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>
<!-- Tutorial Overlay -->
<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() {
// State management
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
}
};
// DOM elements
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');
// Node templates
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>
`
}
};
// Initialize the app
function init() {
// Event listeners for node creation
document.querySelectorAll('.node-item').forEach(item => {
item.addEventListener('mousedown', (e) => {
if (e.button !== 0) return; // Only left click
const nodeType = item.dataset.node;
state.draggingNode = {
type: nodeType,
x: e.clientX,
y: e.clientY
};
document.addEventListener('mousemove', dragNewNode);
document.addEventListener('mouseup', dropNewNode);
});
});
// Canvas panning
canvas.addEventListener('mousedown', (e) => {
if (e.button === 1 || (e.button === 0 && e.ctrlKey)) { // Middle click or Ctrl+Left click
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 = '';
}
});
// Zoom controls
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();
});
// Delete node
deleteNodeBtn.addEventListener('click', deleteSelectedNode);
// Prevent context menu
document.addEventListener('contextmenu', (e) => {
e.preventDefault();
});
// Tutorial controls
startTutorialBtn.addEventListener('click', startTutorial);
closeTutorialBtn.addEventListener('click', closeTutorial);
// Tutorial navigation
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);
});
// Start tutorial automatically if no nodes exist
if (state.nodes.length === 0) {
setTimeout(() => {
startTutorial();
}, 500);
}
}
// Add a new node to the canvas
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);
// Check if this was part of the tutorial
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;
}
// Render a node on the canvas
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`;
// Node header
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);
// Node body
const body = document.createElement('div');
body.className = 'p-3 text-xs text-gray-500';
body.textContent = 'Double click to configure';
// Input ports
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);
}
// Output ports
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);
}
// Node events
nodeEl.addEventListener('mousedown', (e) => {
if (e.button !== 0) return; // Only left click
// Select node
if (!e.target.classList.contains('node-port')) {
selectNode(node.id);
// Start dragging
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);
// Check if this was part of the tutorial
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 {
// Update existing node
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'}`;
}
}
// Drag a new node from the sidebar
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`;
}
// Drop a new node onto the canvas
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;
// Calculate position relative to canvas
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;
}
// Drag an existing node
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();
}
// Drop an existing node
function dropNode() {
document.removeEventListener('mousemove', dragNode);
document.removeEventListener('mouseup', dropNode);
state.draggingNode = null;
}
// Select a node
function selectNode(nodeId) {
// Deselect all nodes
state.nodes.forEach(node => {
node.selected = false;
});
// Select the clicked node
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';
}
// Re-render all nodes to update selection state
state.nodes.forEach(renderNode);
}
// Show node configuration in sidebar
function showNodeConfiguration(node) {
const template = nodeTemplates[node.type];
if (!template) return;
nodeConfiguration.innerHTML = template.config;
}
// Delete the selected node
function deleteSelectedNode() {
if (!state.selectedNode) return;
const nodeId = state.selectedNode.id;
// Remove connections to/from this node
state.connections = state.connections.filter(conn => {
return conn.fromNode !== nodeId && conn.toNode !== nodeId;
});
// Remove the node
state.nodes = state.nodes.filter(node => node.id !== nodeId);
// Remove from DOM
const nodeEl = document.getElementById(`node-${nodeId}`);
if (nodeEl) nodeEl.remove();
// Clear selection
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>
`;
// Re-render connections
renderConnections();
}
// Start creating a connection
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;
// Create a temporary connection line
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);
}
// Draw temporary connection while dragging
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;
// Calculate length and angle of the line
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;
// Update temporary line
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)`;
}
// Complete a connection between nodes
function completeConnection(e) {
if (!state.draggingPort || !state.creatingConnection) return;
const fromPort = state.draggingPort;
const toPort = e.target;
// Only connect output to input
if (fromPort.portType === 'input' || toPort.dataset.portType === 'output') {
cancelConnection();
return;
}
// Don't connect to same node
if (fromPort.nodeId === parseInt(toPort.dataset.nodeId)) {
cancelConnection();
return;
}
// Add the connection
addConnection(
fromPort.nodeId,
fromPort.portIndex,
parseInt(toPort.dataset.nodeId),
parseInt(toPort.dataset.portIndex)
);
cancelConnection();
// Check if this was part of the tutorial
if (state.tutorial.active && state.tutorial.currentStep === 6) {
document.getElementById('nodes-connected-btn').disabled = false;
}
}
// Cancel connection creation
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;
}
// Add a connection between nodes
function addConnection(fromNodeId, fromPortIndex, toNodeId, toPortIndex) {
// Check if connection already exists
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();
}
// Render all connections on the canvas
function renderConnections() {
// Clear existing connections
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;
// Calculate port positions
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));
// Create a smooth bezier curve between nodes
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);
});
}
// Update canvas transform for pan/zoom
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)}%`;
}
// Tutorial functions
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;
// Update UI
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');
}
// Handle step-specific UI
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();
// Create highlight
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);
// Create tooltip
const tooltip = document.createElement('div');
tooltip.className = 'tutorial-tooltip';
tooltip.textContent = text;
// Position tooltip below the element
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;
}
}
// Initialize the application
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>