Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Insur.MCP | Agent Workflow Canvas</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> | |
/* Custom styles for canvas elements */ | |
.canvas-area { | |
background-color: #f8fafc; | |
border: 2px dashed #cbd5e1; | |
border-radius: 0.5rem; | |
min-height: 500px; | |
position: relative; | |
overflow: hidden; | |
} | |
.draggable-item { | |
cursor: grab; | |
user-select: none; | |
transition: all 0.2s ease; | |
} | |
.draggable-item:active { | |
cursor: grabbing; | |
} | |
.canvas-node { | |
position: absolute; | |
background: white; | |
border-radius: 0.5rem; | |
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
min-width: 180px; | |
z-index: 10; | |
} | |
.canvas-node:hover { | |
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
} | |
.node-header { | |
padding: 0.5rem 1rem; | |
border-bottom: 1px solid #e2e8f0; | |
font-weight: 600; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
.node-content { | |
padding: 1rem; | |
} | |
.node-connector { | |
width: 12px; | |
height: 12px; | |
border-radius: 50%; | |
background: #94a3b8; | |
position: absolute; | |
cursor: pointer; | |
} | |
.node-connector.input { | |
left: -6px; | |
top: 50%; | |
transform: translateY(-50%); | |
} | |
.node-connector.output { | |
right: -6px; | |
top: 50%; | |
transform: translateY(-50%); | |
} | |
.connection-line { | |
position: absolute; | |
height: 2px; | |
background: #64748b; | |
z-index: 5; | |
pointer-events: none; | |
} | |
.tab-content { | |
display: none; | |
} | |
.tab-content.active { | |
display: block; | |
} | |
.tab-button.active { | |
background-color: #e2e8f0; | |
font-weight: 600; | |
} | |
/* Animation for empty canvas */ | |
@keyframes pulse { | |
0%, 100% { | |
opacity: 0.5; | |
} | |
50% { | |
opacity: 0.2; | |
} | |
} | |
.empty-canvas { | |
animation: pulse 2s infinite; | |
} | |
</style> | |
</head> | |
<body class="font-sans antialiased text-gray-800 bg-white"> | |
<!-- Navigation --> | |
<nav class="bg-white shadow-sm sticky top-0 z-50"> | |
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |
<div class="flex justify-between h-16"> | |
<div class="flex items-center"> | |
<div class="flex-shrink-0 flex items-center"> | |
<a href="index.html" class="text-blue-600 font-bold text-xl">Insur.MCP</a> | |
</div> | |
</div> | |
<div class="hidden md:flex items-center space-x-8"> | |
<a href="index.html" class="text-gray-500 hover:text-blue-600 font-medium px-1">Home</a> | |
<a href="index.html#features" class="text-gray-500 hover:text-blue-600 font-medium px-1">Features</a> | |
<a href="index.html#about" class="text-gray-500 hover:text-blue-600 font-medium px-1">About</a> | |
<a href="#" class="text-blue-600 font-medium border-b-2 border-blue-600 px-1">Canvas</a> | |
<button class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md font-medium transition duration-300"> | |
Contact Us | |
</button> | |
</div> | |
<div class="md:hidden flex items-center"> | |
<button id="mobile-menu-button" class="text-gray-500 hover:text-blue-600 focus:outline-none"> | |
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path> | |
</svg> | |
</button> | |
</div> | |
</div> | |
</div> | |
<div id="mobile-menu" class="mobile-menu md:hidden bg-white"> | |
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3"> | |
<a href="index.html" class="block px-3 py-2 rounded-md text-base font-medium text-gray-500 hover:text-blue-600 hover:bg-blue-50">Home</a> | |
<a href="index.html#features" class="block px-3 py-2 rounded-md text-base font-medium text-gray-500 hover:text-blue-600 hover:bg-blue-50">Features</a> | |
<a href="index.html#about" class="block px-3 py-2 rounded-md text-base font-medium text-gray-500 hover:text-blue-600 hover:bg-blue-50">About</a> | |
<a href="#" class="block px-3 py-2 rounded-md text-base font-medium text-blue-600 bg-blue-50">Canvas</a> | |
<button class="w-full text-left block px-3 py-2 rounded-md text-base font-medium text-white bg-blue-600 hover:bg-blue-700"> | |
Contact Us | |
</button> | |
</div> | |
</div> | |
</nav> | |
<!-- Canvas Subpage Content --> | |
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> | |
<div class="flex justify-between items-center mb-8"> | |
<div> | |
<h1 class="text-3xl font-bold text-gray-900">Agent Workflow Canvas</h1> | |
<p class="text-gray-600 mt-2">Drag and drop components to create your AI agent workflow</p> | |
</div> | |
<div class="flex space-x-3"> | |
<button id="clear-canvas" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"> | |
Clear Canvas | |
</button> | |
<button id="save-workflow" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"> | |
Save Workflow | |
</button> | |
</div> | |
</div> | |
<div class="flex flex-col lg:flex-row gap-6"> | |
<!-- Components Panel --> | |
<div class="lg:w-1/4 bg-gray-50 p-4 rounded-lg"> | |
<div class="flex border-b border-gray-200 mb-4"> | |
<button class="tab-button active px-4 py-2 rounded-t-lg" data-tab="agents">Agents</button> | |
<button class="tab-button px-4 py-2 rounded-t-lg" data-tab="tasks">Tasks</button> | |
<button class="tab-button px-4 py-2 rounded-t-lg" data-tab="tools">Tools</button> | |
<button class="tab-button px-4 py-2 rounded-t-lg" data-tab="data">Data</button> | |
</div> | |
<!-- Agents Tab --> | |
<div id="agents" class="tab-content active"> | |
<div class="space-y-3"> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="agent" data-subtype="underwriting_agent"> | |
<i class="fas fa-user-tie text-purple-500 mr-2"></i> Underwriting Agent | |
</div> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="agent" data-subtype="sales_agent"> | |
<i class="fas fa-handshake text-purple-500 mr-2"></i> Sales Agent | |
</div> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="agent" data-subtype="fraud_agent"> | |
<i class="fas fa-search-dollar text-purple-500 mr-2"></i> Fraud Agent | |
</div> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="agent" data-subtype="claims_agent"> | |
<i class="fas fa-file-invoice-dollar text-purple-500 mr-2"></i> Claims Agent | |
</div> | |
</div> | |
</div> | |
<!-- Tasks Tab --> | |
<div id="tasks" class="tab-content"> | |
<div class="space-y-3"> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="task" data-subtype="risk_assessment"> | |
<i class="fas fa-shield-alt text-blue-500 mr-2"></i> Risk Assessment | |
</div> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="task" data-subtype="fraud_detection"> | |
<i class="fas fa-search-dollar text-blue-500 mr-2"></i> Fraud Detection | |
</div> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="task" data-subtype="sales_prospective"> | |
<i class="fas fa-bullseye text-blue-500 mr-2"></i> Sales Prospective | |
</div> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="task" data-subtype="appraisal"> | |
<i class="fas fa-clipboard-check text-blue-500 mr-2"></i> Appraisal | |
</div> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="task" data-subtype="estimation"> | |
<i class="fas fa-calculator text-blue-500 mr-2"></i> Estimation | |
</div> | |
</div> | |
</div> | |
<!-- Tools Tab --> | |
<div id="tools" class="tab-content"> | |
<div class="space-y-3"> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="tool" data-subtype="web_search"> | |
<i class="fas fa-globe text-green-500 mr-2"></i> Web Search | |
</div> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="tool" data-subtype="data_set"> | |
<i class="fas fa-database text-green-500 mr-2"></i> Data Set | |
</div> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="tool" data-subtype="analysis"> | |
<i class="fas fa-chart-line text-green-500 mr-2"></i> Analysis | |
</div> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="tool" data-subtype="delegation"> | |
<i class="fas fa-users text-green-500 mr-2"></i> Delegation | |
</div> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="tool" data-subtype="think"> | |
<i class="fas fa-brain text-green-500 mr-2"></i> Think | |
</div> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="tool" data-subtype="output_delivery"> | |
<i class="fas fa-paper-plane text-green-500 mr-2"></i> Output Delivery | |
</div> | |
</div> | |
</div> | |
<!-- Data Tab --> | |
<div id="data" class="tab-content"> | |
<div class="space-y-3"> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="data" data-subtype="customer_synt"> | |
<i class="fas fa-users text-orange-500 mr-2"></i> Customer Synthetic | |
</div> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="data" data-subtype="claim_synt"> | |
<i class="fas fa-file-invoice text-orange-500 mr-2"></i> Claim Synthetic | |
</div> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="data" data-subtype="process_synt"> | |
<i class="fas fa-project-diagram text-orange-500 mr-2"></i> Process Synthetic | |
</div> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="data" data-subtype="dataset"> | |
<i class="fas fa-table text-orange-500 mr-2"></i> Dataset | |
</div> | |
<div class="draggable-item bg-white p-3 rounded border border-gray-200 cursor-move" draggable="true" data-type="data" data-subtype="sales"> | |
<i class="fas fa-chart-bar text-orange-500 mr-2"></i> Sales Data | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Canvas Area --> | |
<div class="lg:w-3/4"> | |
<div id="workflow-canvas" class="canvas-area p-6"> | |
<div class="empty-canvas text-center py-20 text-gray-400"> | |
<i class="fas fa-arrows-alt text-4xl mb-4"></i> | |
<p class="text-xl">Drag components here to build your workflow</p> | |
<p class="text-sm mt-2">Connect nodes by dragging from output to input connectors</p> | |
</div> | |
</div> | |
<div class="mt-4 flex justify-between items-center"> | |
<div class="text-sm text-gray-500"> | |
<span id="node-count">0</span> nodes on canvas | |
</div> | |
<div class="flex space-x-3"> | |
<button id="zoom-in" class="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded"> | |
<i class="fas fa-search-plus"></i> | |
</button> | |
<button id="zoom-out" class="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded"> | |
<i class="fas fa-search-minus"></i> | |
</button> | |
<button id="center-canvas" class="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded"> | |
<i class="fas fa-expand"></i> | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
// Mobile menu toggle | |
const mobileMenuButton = document.getElementById('mobile-menu-button'); | |
const mobileMenu = document.getElementById('mobile-menu'); | |
mobileMenuButton.addEventListener('click', () => { | |
mobileMenu.classList.toggle('open'); | |
}); | |
// Tab functionality | |
const tabButtons = document.querySelectorAll('.tab-button'); | |
const tabContents = document.querySelectorAll('.tab-content'); | |
tabButtons.forEach(button => { | |
button.addEventListener('click', () => { | |
// Remove active class from all buttons and contents | |
tabButtons.forEach(btn => btn.classList.remove('active')); | |
tabContents.forEach(content => content.classList.remove('active')); | |
// Add active class to clicked button and corresponding content | |
button.classList.add('active'); | |
const tabId = button.getAttribute('data-tab'); | |
document.getElementById(tabId).classList.add('active'); | |
}); | |
}); | |
// Canvas functionality | |
const canvas = document.getElementById('workflow-canvas'); | |
const draggableItems = document.querySelectorAll('.draggable-item'); | |
const clearCanvasBtn = document.getElementById('clear-canvas'); | |
const saveWorkflowBtn = document.getElementById('save-workflow'); | |
const nodeCountElement = document.getElementById('node-count'); | |
const emptyCanvasElement = document.querySelector('.empty-canvas'); | |
let nodes = []; | |
let connections = []; | |
let isDragging = false; | |
let startConnector = null; | |
let tempLine = null; | |
let canvasScale = 1; | |
// Update node count | |
function updateNodeCount() { | |
const count = document.querySelectorAll('.canvas-node').length; | |
nodeCountElement.textContent = count; | |
if (count > 0) { | |
emptyCanvasElement.style.display = 'none'; | |
} else { | |
emptyCanvasElement.style.display = 'block'; | |
} | |
} | |
// Create a new node on canvas | |
function createNode(type, subtype, x, y) { | |
const nodeId = 'node-' + Date.now(); | |
const node = document.createElement('div'); | |
node.className = 'canvas-node'; | |
node.id = nodeId; | |
node.style.left = `${x}px`; | |
node.style.top = `${y}px`; | |
// Set node color based on type | |
let icon, color, title; | |
switch(type) { | |
case 'agent': | |
icon = 'robot'; | |
color = 'purple'; | |
title = 'Agent: ' + subtype.replace('_', ' '); | |
break; | |
case 'task': | |
icon = 'tasks'; | |
color = 'blue'; | |
title = 'Task: ' + subtype.replace('_', ' '); | |
break; | |
case 'tool': | |
icon = 'tools'; | |
color = 'green'; | |
title = 'Tool: ' + subtype.replace('_', ' '); | |
break; | |
case 'data': | |
icon = 'database'; | |
color = 'orange'; | |
title = 'Data: ' + subtype.replace('_', ' '); | |
break; | |
} | |
// Create node HTML | |
node.innerHTML = ` | |
<div class="node-header bg-${color}-50 text-${color}-700"> | |
<div> | |
<i class="fas fa-${icon} mr-2"></i> | |
${title} | |
</div> | |
<button class="node-delete text-gray-400 hover:text-red-500"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<div class="node-content"> | |
<select class="w-full p-2 border border-gray-300 rounded text-sm"> | |
${getOptionsForType(type)} | |
</select> | |
<div class="mt-2 text-xs text-gray-500"> | |
<i class="fas fa-info-circle mr-1"></i> | |
${getDescriptionForSubtype(type, subtype)} | |
</div> | |
</div> | |
<div class="node-connector input" data-node="${nodeId}" data-type="input"></div> | |
<div class="node-connector output" data-node="${nodeId}" data-type="output"></div> | |
`; | |
// Add node to canvas | |
canvas.appendChild(node); | |
// Make node draggable | |
makeNodeDraggable(node); | |
// Add delete functionality | |
const deleteBtn = node.querySelector('.node-delete'); | |
deleteBtn.addEventListener('click', () => { | |
// Remove any connections to this node | |
connections = connections.filter(conn => { | |
return conn.fromNode !== nodeId && conn.toNode !== nodeId; | |
}); | |
// Remove connection lines from DOM | |
document.querySelectorAll('.connection-line').forEach(line => { | |
if (line.dataset.fromNode === nodeId || line.dataset.toNode === nodeId) { | |
line.remove(); | |
} | |
}); | |
// Remove the node | |
node.remove(); | |
updateNodeCount(); | |
}); | |
// Store node in array | |
nodes.push({ | |
id: nodeId, | |
type: type, | |
subtype: subtype, | |
element: node | |
}); | |
updateNodeCount(); | |
return node; | |
} | |
// Get options for select dropdown based on node type | |
function getOptionsForType(type) { | |
let options = ''; | |
switch(type) { | |
case 'agent': | |
options = ` | |
<option value="underwriting_agent">Underwriting Agent</option> | |
<option value="sales_agent">Sales Agent</option> | |
<option value="fraud_agent">Fraud Agent</option> | |
<option value="claims_agent">Claims Agent</option> | |
`; | |
break; | |
case 'task': | |
options = ` | |
<option value="risk_assessment">Risk Assessment</option> | |
<option value="fraud_detection">Fraud Detection</option> | |
<option value="sales_prospective">Sales Prospective</option> | |
<option value="appraisal">Appraisal</option> | |
<option value="estimation">Estimation</option> | |
`; | |
break; | |
case 'tool': | |
options = ` | |
<option value="web_search">Web Search</option> | |
<option value="data_set">Data Set</option> | |
<option value="analysis">Analysis</option> | |
<option value="delegation">Delegation</option> | |
<option value="think">Think</option> | |
<option value="output_delivery">Output Delivery</option> | |
`; | |
break; | |
case 'data': | |
options = ` | |
<option value="customer_synt">Customer Synthetic</option> | |
<option value="claim_synt">Claim Synthetic</option> | |
<option value="process_synt">Process Synthetic</option> | |
<option value="dataset">Dataset</option> | |
<option value="sales">Sales Data</option> | |
`; | |
break; | |
} | |
return options; | |
} | |
// Get description for subtype | |
function getDescriptionForSubtype(type, subtype) { | |
const descriptions = { | |
agent: { | |
underwriting_agent: "Evaluates and assesses risks for insurance policies", | |
sales_agent: "Handles customer acquisition and policy sales", | |
fraud_agent: "Detects and investigates potential fraudulent claims", | |
claims_agent: "Manages and processes insurance claims" | |
}, | |
task: { | |
risk_assessment: "Analyzes and evaluates potential risks", | |
fraud_detection: "Identifies suspicious patterns in claims", | |
sales_prospective: "Finds and qualifies potential customers", | |
appraisal: "Evaluates property or damage for claims", | |
estimation: "Calculates claim amounts or policy costs" | |
}, | |
tool: { | |
web_search: "Searches the web for relevant information", | |
data_set: "Accesses structured data for analysis", | |
analysis: "Performs data analysis and interpretation", | |
delegation: "Assigns tasks to other agents", | |
think: "Processes information and makes decisions", | |
output_delivery: "Sends results to the next step" | |
}, | |
data: { | |
customer_synt: "Synthetic customer profile data", | |
claim_synt: "Synthetic insurance claim data", | |
process_synt: "Synthetic process workflow data", | |
dataset: "General structured dataset", | |
sales: "Historical sales performance data" | |
} | |
}; | |
return descriptions[type]?.[subtype] || "No description available"; | |
} | |
// Make node draggable | |
function makeNodeDraggable(node) { | |
const header = node.querySelector('.node-header'); | |
header.addEventListener('mousedown', (e) => { | |
if (e.target.classList.contains('node-delete') || e.target.closest('.node-delete')) { | |
return; // Don't drag if clicking delete button | |
} | |
const startX = e.clientX; | |
const startY = e.clientY; | |
const startLeft = parseInt(node.style.left); | |
const startTop = parseInt(node.style.top); | |
function moveNode(e) { | |
const dx = e.clientX - startX; | |
const dy = e.clientY - startY; | |
node.style.left = `${startLeft + dx}px`; | |
node.style.top = `${startTop + dy}px`; | |
// Update connection lines | |
updateConnectionLines(); | |
} | |
function stopDrag() { | |
document.removeEventListener('mousemove', moveNode); | |
document.removeEventListener('mouseup', stopDrag); | |
} | |
document.addEventListener('mousemove', moveNode); | |
document.addEventListener('mouseup', stopDrag); | |
}); | |
} | |
// Create a connection between two nodes | |
function createConnection(fromNodeId, toNodeId) { | |
// Check if connection already exists | |
const existingConnection = connections.find(conn => | |
conn.fromNode === fromNodeId && conn.toNode === toNodeId | |
); | |
if (existingConnection) return; | |
// Add to connections array | |
connections.push({ | |
fromNode: fromNodeId, | |
toNode: toNodeId | |
}); | |
// Create connection line | |
updateConnectionLines(); | |
} | |
// Update all connection lines | |
function updateConnectionLines() { | |
// Remove all existing connection lines | |
document.querySelectorAll('.connection-line').forEach(line => line.remove()); | |
// Create new connection lines | |
connections.forEach(conn => { | |
const fromNode = document.getElementById(conn.fromNode); | |
const toNode = document.getElementById(conn.toNode); | |
if (fromNode && toNode) { | |
const fromConnector = fromNode.querySelector('.node-connector.output'); | |
const toConnector = toNode.querySelector('.node-connector.input'); | |
const fromRect = fromConnector.getBoundingClientRect(); | |
const toRect = toConnector.getBoundingClientRect(); | |
const canvasRect = canvas.getBoundingClientRect(); | |
const fromX = fromRect.left + fromRect.width / 2 - canvasRect.left; | |
const fromY = fromRect.top + fromRect.height / 2 - canvasRect.top; | |
const toX = toRect.left + toRect.width / 2 - canvasRect.left; | |
const toY = toRect.top + toRect.height / 2 - canvasRect.top; | |
const length = Math.sqrt(Math.pow(toX - fromX, 2) + Math.pow(toY - fromY, 2)); | |
const angle = Math.atan2(toY - fromY, toX - fromX) * 180 / Math.PI; | |
const line = document.createElement('div'); | |
line.className = 'connection-line'; | |
line.style.width = `${length}px`; | |
line.style.left = `${fromX}px`; | |
line.style.top = `${fromY}px`; | |
line.style.transform = `rotate(${angle}deg)`; | |
line.style.transformOrigin = '0 0'; | |
line.dataset.fromNode = conn.fromNode; | |
line.dataset.toNode = conn.toNode; | |
canvas.appendChild(line); | |
} | |
}); | |
} | |
// Handle drag and drop from components panel to canvas | |
draggableItems.forEach(item => { | |
item.addEventListener('dragstart', (e) => { | |
e.dataTransfer.setData('type', e.target.dataset.type); | |
e.dataTransfer.setData('subtype', e.target.dataset.subtype); | |
e.dataTransfer.effectAllowed = 'copy'; | |
}); | |
}); | |
canvas.addEventListener('dragover', (e) => { | |
e.preventDefault(); | |
e.dataTransfer.dropEffect = 'copy'; | |
}); | |
canvas.addEventListener('drop', (e) => { | |
e.preventDefault(); | |
const type = e.dataTransfer.getData('type'); | |
const subtype = e.dataTransfer.getData('subtype'); | |
if (!type || !subtype) return; | |
const rect = canvas.getBoundingClientRect(); | |
const x = e.clientX - rect.left - 90; // Center the node on cursor | |
const y = e.clientY - rect.top - 40; | |
createNode(type, subtype, x, y); | |
}); | |
// Handle connector dragging for creating connections | |
canvas.addEventListener('mousedown', (e) => { | |
const connector = e.target.closest('.node-connector'); | |
if (!connector) return; | |
e.preventDefault(); | |
e.stopPropagation(); | |
startConnector = { | |
node: connector.dataset.node, | |
type: connector.dataset.type, | |
x: e.clientX, | |
y: e.clientY | |
}; | |
// Create temporary line | |
tempLine = document.createElement('div'); | |
tempLine.className = 'connection-line'; | |
tempLine.style.backgroundColor = '#6366f1'; | |
canvas.appendChild(tempLine); | |
isDragging = true; | |
}); | |
document.addEventListener('mousemove', (e) => { | |
if (!isDragging || !startConnector) return; | |
const fromNode = document.getElementById(startConnector.node); | |
if (!fromNode) return; | |
const fromConnector = fromNode.querySelector(`.node-connector.${startConnector.type}`); | |
const fromRect = fromConnector.getBoundingClientRect(); | |
const canvasRect = canvas.getBoundingClientRect(); | |
const fromX = fromRect.left + fromRect.width / 2 - canvasRect.left; | |
const fromY = fromRect.top + fromRect.height / 2 - canvasRect.top; | |
const toX = e.clientX - canvasRect.left; | |
const toY = e.clientY - canvasRect.top; | |
const length = Math.sqrt(Math.pow(toX - fromX, 2) + Math.pow(toY - fromY, 2)); | |
const angle = Math.atan2(toY - fromY, toX - fromX) * 180 / Math.PI; | |
tempLine.style.width = `${length}px`; | |
tempLine.style.left = `${fromX}px`; | |
tempLine.style.top = `${fromY}px`; | |
tempLine.style.transform = `rotate(${angle}deg)`; | |
}); | |
document.addEventListener('mouseup', (e) => { | |
if (!isDragging || !startConnector) { | |
isDragging = false; | |
startConnector = null; | |
return; | |
} | |
const connector = e.target.closest('.node-connector'); | |
if (connector) { | |
const endConnector = { | |
node: connector.dataset.node, | |
type: connector.dataset.type | |
}; | |
// Only connect output to input | |
if (startConnector.type === 'output' && endConnector.type === 'input') { | |
// Don't allow self-connections | |
if (startConnector.node !== endConnector.node) { | |
createConnection(startConnector.node, endConnector.node); | |
} | |
} | |
} | |
// Clean up | |
if (tempLine) { | |
tempLine.remove(); | |
tempLine = null; | |
} | |
isDragging = false; | |
startConnector = null; | |
}); | |
// Clear canvas | |
clearCanvasBtn.addEventListener('click', () => { | |
if (confirm('Are you sure you want to clear the canvas?')) { | |
document.querySelectorAll('.canvas-node').forEach(node => node.remove()); | |
document.querySelectorAll('.connection-line').forEach(line => line.remove()); | |
nodes = []; | |
connections = []; | |
updateNodeCount(); | |
} | |
}); | |
// Save workflow | |
saveWorkflowBtn.addEventListener('click', () => { | |
const workflow = { | |
nodes: nodes.map(node => ({ | |
id: node.id, | |
type: node.type, | |
subtype: node.subtype, | |
position: { | |
x: parseInt(node.element.style.left), | |
y: parseInt(node.element.style.top) | |
} | |
})), | |
connections: connections | |
}; | |
// In a real app, you would send this to a server | |
console.log('Workflow saved:', workflow); | |
alert('Workflow saved successfully!'); | |
}); | |
// Zoom functionality | |
document.getElementById('zoom-in').addEventListener('click', () => { | |
canvasScale = Math.min(canvasScale + 0.1, 2); | |
canvas.style.transform = `scale(${canvasScale})`; | |
}); | |
document.getElementById('zoom-out').addEventListener('click', () => { | |
canvasScale = Math.max(canvasScale - 0.1, 0.5); | |
canvas.style.transform = `scale(${canvasScale})`; | |
}); | |
document.getElementById('center-canvas').addEventListener('click', () => { | |
canvasScale = 1; | |
canvas.style.transform = `scale(${canvasScale})`; | |
canvas.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
}); | |
// Initialize | |
updateNodeCount(); | |
</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=designfailure/insur-mcp" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |