function createNode(type, x, y) { const template = nodeTemplates[type]; if (!template) { console.warn('No template found for node type:', type); return null; } const id = 'node-' + state.nextNodeId++; const node = { id, type, x, y, fields: {} }; template.fields.forEach(field => { node.fields[field.name] = field.default; }); state.nodes.set(id, node); renderNode(node); saveToHistory(); updateStatusBar(); return node; } function renderNode(node) { const template = nodeTemplates[node.type]; const typeClass = node.type.toLowerCase().replace(/\s+/g, '-'); let fieldsHtml = ''; template.fields.forEach(field => { const value = node.fields[field.name] || ''; if (field.type === 'textarea') { fieldsHtml += `
`; } else if (field.type === 'select') { const options = field.options.map(opt => `` ).join(''); fieldsHtml += `
`; } else { fieldsHtml += `
`; } }); let portsHtml = ''; if (template.hasInput) { portsHtml += `
`; } if (template.hasOutput) { portsHtml += `
`; } if (template.hasOutputTrue) { portsHtml += `
`; } if (template.hasOutputFalse) { portsHtml += `
`; } const nodeEl = document.createElement('div'); nodeEl.className = 'node'; nodeEl.id = node.id; nodeEl.style.left = node.x + 'px'; nodeEl.style.top = node.y + 'px'; nodeEl.innerHTML = `
${getNodeIcon(node.type)} ${node.type}
${fieldsHtml}
${portsHtml} `; nodeEl.addEventListener('mousedown', (e) => { if (e.target.classList.contains('node-port')) return; selectNode(node.id); startNodeDrag(e, node); }); nodeEl.querySelectorAll('.node-field-input, .node-field-select').forEach(input => { input.addEventListener('change', (e) => { node.fields[e.target.dataset.field] = e.target.value; saveToHistory(); }); }); nodeEl.querySelectorAll('.node-port').forEach(port => { port.addEventListener('mousedown', (e) => { e.stopPropagation(); startConnection(node.id, port.dataset.port); }); port.addEventListener('mouseup', (e) => { e.stopPropagation(); endConnection(node.id, port.dataset.port); }); }); document.getElementById('canvas-inner').appendChild(nodeEl); } function getNodeIcon(type) { const icons = { 'TALK': '', 'HEAR': '', 'SET': '', 'IF': '', 'FOR': '', 'SWITCH': '', 'CALL': '', 'SEND MAIL': '', 'GET': '', 'POST': '', 'SAVE': '', 'WAIT': '', 'SET BOT MEMORY': '', 'GET BOT MEMORY': '', 'SET USER MEMORY': '', 'GET USER MEMORY': '' }; return icons[type] || ''; } function startNodeDrag(e, node) { state.isDragging = true; const nodeEl = document.getElementById(node.id); const startX = e.clientX; const startY = e.clientY; const origX = node.x; const origY = node.y; function onMove(e) { const dx = (e.clientX - startX) / state.zoom; const dy = (e.clientY - startY) / state.zoom; node.x = snapToGrid(origX + dx); node.y = snapToGrid(origY + dy); nodeEl.style.left = node.x + 'px'; nodeEl.style.top = node.y + 'px'; updateConnections(); } function onUp() { state.isDragging = false; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); saveToHistory(); } document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); } function selectNode(id) { if (state.selectedNode) { const prevEl = document.getElementById(state.selectedNode); if (prevEl) prevEl.classList.remove('selected'); } state.selectedNode = id; const nodeEl = document.getElementById(id); if (nodeEl) nodeEl.classList.add('selected'); updatePropertiesPanel(); } function deselectAll() { if (state.selectedNode) { const el = document.getElementById(state.selectedNode); if (el) el.classList.remove('selected'); } state.selectedNode = null; state.selectedConnection = null; updatePropertiesPanel(); } function deleteSelectedNode() { if (!state.selectedNode) return; const nodeEl = document.getElementById(state.selectedNode); if (nodeEl) nodeEl.remove(); state.connections = state.connections.filter( conn => conn.from !== state.selectedNode && conn.to !== state.selectedNode ); state.nodes.delete(state.selectedNode); state.selectedNode = null; updateConnections(); updatePropertiesPanel(); updateStatusBar(); saveToHistory(); } function duplicateNode() { if (!state.selectedNode) return; const node = state.nodes.get(state.selectedNode); if (!node) return; const newNode = {...node, fields: {...node.fields}}; newNode.id = 'node-' + state.nextNodeId++; newNode.x += 40; newNode.y += 40; state.nodes.set(newNode.id, newNode); renderNode(newNode); selectNode(newNode.id); saveToHistory(); hideContextMenu(); } function updateStatusBar() { document.getElementById('node-count').textContent = state.nodes.size + ' nodes'; document.getElementById('connection-count').textContent = state.connections.length + ' connections'; }