(() => { 'use strict'; const CATEGORY_COLORS = { 'Chai-1': 'model', 'AntiBMPNN': 'model', 'Boltz-2': 'model', 'BoltzGen': 'model', 'OpenFold-3': 'model', 'RosettaFold-3': 'model', 'DockQ': 'eval', 'ParaSurf': 'eval', 'HuMatch': 'eval', 'OpenMM': 'eval', 'PoseBusters': 'eval', 'Convert': 'tool' }; const PORT_CONFIG = { 'Chai-1': { inputs: 0, outputs: 1 }, 'AntiBMPNN': { inputs: 1, outputs: 0 }, 'Boltz-2': { inputs: 0, outputs: 1 }, 'BoltzGen': { inputs: 0, outputs: 1 }, 'OpenFold-3': { inputs: 0, outputs: 1 }, 'RosettaFold-3': { inputs: 0, outputs: 1 }, 'DockQ': { inputs: 1, outputs: 0 }, 'ParaSurf': { inputs: 1, outputs: 0 }, 'HuMatch': { inputs: 0, outputs: 1 }, 'OpenMM': { inputs: 1, outputs: 0 }, 'PoseBusters': { inputs: 1, outputs: 0 }, 'Convert': { inputs: 1, outputs: 1 } }; const TOOL_ICONS = { Convert: 'fas fa-sync-alt' }; const MAX_NODES = 32; const TAP_MOVE_THRESHOLD = 6; const utils = window.WorkflowModelUtils || {}; const modelRegistry = window.WorkflowModels || { get: () => null, has: () => false }; const MODEL_WORKFLOW_ENDPOINT = window.MODEL_WORKFLOW_ENDPOINT || 'https://ayambabu23--workflow-execute-workflow.modal.run/'; let editor = null; let nodeIndex = 0; let historyStack = []; let redoStack = []; let historyPaused = false; let activeEditorCleanup = null; const nodePointer = { active: false, moved: false, nodeId: null, startX: 0, startY: 0 }; const $ = (sel, root = document) => root.querySelector(sel); const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); const isMobile = () => window.matchMedia('(max-width: 768px)').matches; function escapeHtml(value) { return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function getModelSpec(type) { return modelRegistry?.get?.(type) || null; } function getPanel() { return document.getElementById('workflow-sidebar'); } function getHandle() { return document.querySelector('.workflow-handle'); } function getCanvas() { return document.getElementById('drawflow'); } function getEditorMount() { return document.getElementById('node-editor'); } function getEditorHeaderMount() { return document.getElementById('node-editor-header'); } function getHero() { return document.getElementById('hero-member'); } function getWorkflowNameInput() { return document.getElementById('workflow-name'); } function getToolbarButtons() { return { undo: document.getElementById('undo-button'), redo: document.getElementById('redo-button'), reset: document.getElementById('reset-button'), execute: document.getElementById('execute-button') }; } function refreshSelectedNodeEditor() { const panel = getPanel(); if (!panel?.classList.contains('is-editor-open')) return; const selectedId = getSelectedNodeId(); if (!selectedId) return; renderNodeEditor(selectedId); } function cleanupActiveEditorBindings() { if (typeof activeEditorCleanup === 'function') { try { activeEditorCleanup(); } catch (err) { console.error(err); } } activeEditorCleanup = null; } function clearEditorUi() { cleanupActiveEditorBindings(); const headerMount = getEditorHeaderMount(); const mount = getEditorMount(); if (headerMount) headerMount.innerHTML = ''; if (mount) mount.innerHTML = ''; requestAnimationFrame(syncSidebarScrollPadding); } function syncSidebarScrollPadding() { const targets = [ document.querySelector('.workflow-panel__scroll'), document.querySelector('.workflow-editor-scroll') ]; targets.forEach((el) => { if (!el) return; const needsScroll = el.scrollHeight > el.clientHeight + 1; el.classList.toggle('has-scroll-padding', needsScroll); }); } function hideHeroWelcome() { getHero()?.classList.add('hidden'); } function showHeroWelcome() { getHero()?.classList.remove('hidden'); } function maybeShowHeroWelcome() { if (getTotalNodeCount() === 0) showHeroWelcome(); } function extractNodeIdFromElement(nodeEl) { if (!nodeEl) return null; const raw = nodeEl.id || nodeEl.dataset.id || nodeEl.dataset.nodeId || ''; const match = String(raw).match(/(\d+)/); return match ? match[1] : null; } function getSelectedNodeId() { if (editor?.node_selected != null && editor.node_selected !== '') { const match = String(editor.node_selected).match(/(\d+)/); if (match) return match[1]; } const selectedEl = document.querySelector('#drawflow .drawflow-node.selected'); return extractNodeIdFromElement(selectedEl); } function clearDomNodeSelection() { $$('#drawflow .drawflow-node.selected').forEach((node) => { node.classList.remove('selected'); }); } function clearSelectedNode() { clearDomNodeSelection(); if (editor) { editor.node_selected = null; editor.drag_node = false; editor.select_node = false; editor.editor_selected = false; } } function getPanelWidth() { const panel = getPanel(); if (!panel) return 0; const rect = panel.getBoundingClientRect(); if (rect.width > 0) return rect.width; const computed = parseFloat(getComputedStyle(panel).width); return Number.isFinite(computed) ? computed : 0; } function setSidebarOpen(forceOpen) { const panel = getPanel(); const handle = getHandle(); if (!panel || !handle) return; const nextOpen = typeof forceOpen === 'boolean' ? forceOpen : !panel.classList.contains('open'); panel.classList.toggle('open', nextOpen); handle.classList.toggle('open', nextOpen); handle.setAttribute('aria-expanded', String(nextOpen)); } function setSidebarMode(mode = 'library') { const panel = getPanel(); if (!panel) return; panel.classList.toggle('is-editor-open', mode === 'editor'); } function collapseSidebar({ clearEditor = true, clearSelection = true } = {}) { if (clearEditor) clearEditorUi(); if (clearSelection) clearSelectedNode(); setSidebarOpen(false); requestAnimationFrame(() => { requestAnimationFrame(() => { const currentPanel = getPanel(); if (currentPanel && !currentPanel.classList.contains('open')) { currentPanel.classList.remove('instant-editor-open'); setSidebarMode('library'); } }); }); if (getTotalNodeCount() === 0) showHeroWelcome(); else hideHeroWelcome(); syncUiState(); } function openLibraryPanel() { clearEditorUi(); clearSelectedNode(); setSidebarMode('library'); setSidebarOpen(true); hideHeroWelcome(); syncUiState(); requestAnimationFrame(syncSidebarScrollPadding); } function toggleSidebar(forceOpen) { const panel = getPanel(); if (!panel) return; if (typeof forceOpen === 'boolean') { if (forceOpen) openLibraryPanel(); else collapseSidebar(); return; } if (panel.classList.contains('open')) collapseSidebar(); else openLibraryPanel(); } function getNodeMap() { if (!editor?.export) return {}; try { const exp = editor.export(); return exp?.drawflow?.[editor.module || 'Home']?.data || {}; } catch { return {}; } } function getNodeById(nodeId) { const numericId = Number(String(nodeId).match(/(\d+)/)?.[1] || nodeId); return editor?.getNodeFromId?.(numericId) || editor?.getNodeFromId?.(String(nodeId)) || null; } function getTotalNodeCount() { return Object.keys(getNodeMap()).length; } function slugifyNodeLabel(type = 'node') { return String(type || 'node') .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') || 'node'; } function countNodesOfType(type) { return Object.values(getNodeMap()).filter((node) => node?.name === type).length; } function generateAutoNodeName(type) { const slug = slugifyNodeLabel(type).replace(/-/g, '_'); const count = countNodesOfType(type) + 1; return `${slug}_${count}`; } function applyNodeData(nodeId, nextData, { saveHistory = false, rerender = false } = {}) { const numericId = Number(String(nodeId).match(/(\d+)/)?.[1] || nodeId); if (typeof editor.updateNodeDataFromId === 'function') { editor.updateNodeDataFromId(numericId, nextData); } else { const node = getNodeById(nodeId); if (node) node.data = nextData; } if (saveHistory) saveHistorySnapshot(); if (rerender && String(getSelectedNodeId()) === String(nodeId)) { renderNodeEditor(nodeId); } } function patchNodeData(nodeId, patch, opts = {}) { const node = getNodeById(nodeId); if (!node) return; applyNodeData(nodeId, { ...(node.data || {}), ...(patch || {}) }, opts); } function setEditorTabState(nodeId, nextTab, { persist = true } = {}) { const isBasic = nextTab === 'basic'; document.querySelectorAll(`.workflow-editor-shell[data-node-id="${nodeId}"]`).forEach((shell) => { shell.classList.toggle('is-tab-basic', isBasic); shell.classList.toggle('is-tab-advanced', !isBasic); }); document.querySelectorAll(`[data-editor-tab][data-node-id="${nodeId}"]`).forEach((btn) => { const active = btn.getAttribute('data-editor-tab') === nextTab; btn.classList.toggle('is-active', active); btn.setAttribute('aria-selected', String(active)); }); if (persist) { const node = getNodeById(nodeId); if (node) { const current = String(node.data?.__editorTab || 'basic'); if (current !== nextTab) { applyNodeData( nodeId, { ...(node.data || {}), __editorTab: nextTab }, { saveHistory: false, rerender: false } ); } } } requestAnimationFrame(syncSidebarScrollPadding); } function buildEditorHeader({ nodeId, node, spec }) { const activeTab = node?.data?.__editorTab === 'advanced' ? 'advanced' : 'basic'; const safeTitle = escapeHtml(node.name || 'Node'); const tutorialUrl = spec?.tutorialUrl || '/blog'; const tabs = Array.isArray(spec?.tabs) && spec.tabs.length ? spec.tabs : ['basic', 'advanced']; const presets = Array.isArray(spec?.presets) ? spec.presets : []; const tabsHtml = tabs.map((tab, index) => { const label = tab === 'api' ? 'API' : tab.charAt(0).toUpperCase() + tab.slice(1); const extraIcon = tab === 'api' ? ` ` : ''; return ` `; }).join(''); const presetsHtml = presets.length ? `
${presets.map((preset) => ` `).join('')}
` : ''; return `

${safeTitle}

Not sure what this is? Tutorial
${tabsHtml}
${presetsHtml}
`; } function renderFallbackEditor(nodeId) { const node = getNodeById(nodeId); const headerMount = getEditorHeaderMount(); const mount = getEditorMount(); if (!node || !headerMount || !mount) return; headerMount.innerHTML = buildEditorHeader({ nodeId, node, spec: { tutorialUrl: '/blog', tabs: ['basic', 'advanced'], presets: [] } }); mount.innerHTML = `

Node name

This model does not have a workflow editor module yet.

Advanced settings

Advanced editor UI for this model will go here next.
`; setEditorTabState(nodeId, 'basic'); requestAnimationFrame(syncSidebarScrollPadding); } function renderNodeEditor(nodeId) { const node = getNodeById(nodeId); const headerMount = getEditorHeaderMount(); const mount = getEditorMount(); if (!node || !headerMount || !mount) return; cleanupActiveEditorBindings(); const spec = getModelSpec(node.name); if (!spec || typeof spec.renderBody !== 'function') { renderFallbackEditor(nodeId); return; } headerMount.innerHTML = buildEditorHeader({ nodeId, node, spec }); mount.innerHTML = spec.renderBody({ nodeId, node, nodeData: node.data || {}, editor }); activeEditorCleanup = spec.bind?.({ nodeId, node, mount, headerMount, editor, patchNodeData, applyNodeData, replaceNodeData: applyNodeData, saveHistory: saveHistorySnapshot, rerender: () => renderNodeEditor(nodeId) }) || null; const initialTab = node.data?.__editorTab === 'advanced' ? 'advanced' : 'basic'; setEditorTabState(nodeId, initialTab, { persist: false }); requestAnimationFrame(syncSidebarScrollPadding); } function openNodeEditor(nodeId) { const panel = getPanel(); const wasClosed = panel && !panel.classList.contains('open'); renderNodeEditor(nodeId); if (wasClosed && panel) { panel.classList.add('instant-editor-open'); } setSidebarMode('editor'); setSidebarOpen(true); hideHeroWelcome(); syncUiState(); requestAnimationFrame(syncSidebarScrollPadding); if (wasClosed && panel) { requestAnimationFrame(() => { requestAnimationFrame(() => { panel.classList.remove('instant-editor-open'); }); }); } } function getCanvasDropPoint(e) { const pc = editor?.precanvas || getCanvas(); const rect = pc.getBoundingClientRect(); const zoom = Number(editor?.zoom_value || editor?.zoom || 1); const cx = Number(editor?.canvas_x || 0); const cy = Number(editor?.canvas_y || 0); return { x: (e.clientX - rect.left) / zoom - cx, y: (e.clientY - rect.top) / zoom - cy }; } function buildNodeTemplate(type, category) { const isTool = category === 'tool'; const iconClass = TOOL_ICONS[type]; const hasIcon = Boolean(iconClass); if (isTool && hasIcon) { return `
${escapeHtml(type)}
`; } return `
${escapeHtml(type)}
`; } function addNode(type, at) { if (!editor) return; if (getTotalNodeCount() >= MAX_NODES) { window.showToast?.('error', `Node limit reached (${MAX_NODES}).`); return; } hideHeroWelcome(); const category = CATEGORY_COLORS[type]; const cssClass = category ? `df-${category}` : ''; const config = PORT_CONFIG[type] || { inputs: 1, outputs: 1 }; const autoName = generateAutoNodeName(type); const spec = getModelSpec(type); const defaultData = spec?.createDefaultData?.({ autoName, type }) || { name: autoName, default_name: autoName }; const template = buildNodeTemplate(type, category); let x; let y; if (at && Number.isFinite(at.x) && Number.isFinite(at.y)) { x = at.x; y = at.y; } else { const canvas = getCanvas(); const rect = canvas?.getBoundingClientRect?.() || { width: window.innerWidth, height: window.innerHeight }; const sidebarOffset = (!isMobile() && getPanel()?.classList.contains('open')) ? getPanelWidth() : 0; const baseX = isMobile() ? 80 : Math.max(420, sidebarOffset + 120); const usableWidth = Math.max(240, rect.width - baseX - 180); x = baseX + ((nodeIndex * 90) % usableWidth); y = 140 + ((nodeIndex * 55) % Math.max(260, rect.height - 260)); } editor.addNode( type, config.inputs, config.outputs, x, y, cssClass, defaultData, template ); nodeIndex += 1; syncUiState(); } function saveHistorySnapshot() { if (!editor || historyPaused) return; try { const snap = JSON.parse(JSON.stringify(editor.export())); historyStack.push(snap); if (historyStack.length > 80) historyStack.shift(); redoStack = []; syncUiState(); } catch (err) { console.warn('[workflow] history snapshot failed', err); } } function restoreHistorySnapshot(snapshot) { if (!editor || !snapshot) return; historyPaused = true; try { editor.clear(); editor.import(JSON.parse(JSON.stringify(snapshot))); clearSelectedNode(); clearEditorUi(); setSidebarMode('library'); nodeIndex = getTotalNodeCount(); } finally { historyPaused = false; syncUiState(); maybeShowHeroWelcome(); } } function doUndo() { if (historyStack.length <= 1) return; const current = historyStack.pop(); redoStack.push(current); const previous = historyStack[historyStack.length - 1]; restoreHistorySnapshot(previous); } function doRedo() { if (!redoStack.length) return; const next = redoStack.pop(); historyStack.push(JSON.parse(JSON.stringify(next))); restoreHistorySnapshot(next); } function tryRemoveNode(selectedId) { const normalized = String(selectedId).match(/(\d+)/)?.[1] || String(selectedId); const numericId = Number(normalized); const candidates = [numericId, normalized, `node-${normalized}`]; for (const candidate of candidates) { try { if (typeof editor.removeNodeId === 'function') { editor.removeNodeId(candidate); return true; } } catch {} try { if (typeof editor.removeNodeFromId === 'function') { editor.removeNodeFromId(candidate); return true; } } catch {} try { if (typeof editor.removeNode === 'function') { editor.removeNode(candidate); return true; } } catch {} } return false; } function deleteSelectedNode() { if (!editor) return; const selectedId = getSelectedNodeId(); if (!selectedId) { window.showToast?.('info', 'Select a node first.'); return; } const removed = tryRemoveNode(selectedId); if (!removed) return; collapseSidebar(); maybeShowHeroWelcome(); } function clearSelectedNodeFields() { const selectedId = getSelectedNodeId(); if (!selectedId) return; const node = getNodeById(selectedId); if (!node) return; const spec = getModelSpec(node.name); if (spec?.resetData) { const nextData = spec.resetData({ node, autoName: node.data?.default_name || generateAutoNodeName(node.name) }); applyNodeData(selectedId, nextData, { saveHistory: true, rerender: true }); window.showToast?.('success', 'Node reset.'); return; } applyNodeData(selectedId, {}, { saveHistory: true, rerender: true }); window.showToast?.('success', 'Node cleared.'); } function clearCanvas() { if (!editor) return; historyPaused = true; try { editor.clear(); clearSelectedNode(); clearEditorUi(); nodeIndex = 0; setSidebarOpen(false); requestAnimationFrame(() => { requestAnimationFrame(() => { const panel = getPanel(); if (panel && !panel.classList.contains('open')) { panel.classList.remove('instant-editor-open'); setSidebarMode('library'); } }); }); } finally { historyPaused = false; } historyStack = []; redoStack = []; saveHistorySnapshot(); showHeroWelcome(); syncUiState(); } function getWorkflowEdges() { const data = getNodeMap(); const edges = []; Object.values(data).forEach((node) => { const outputs = node?.outputs || {}; Object.entries(outputs).forEach(([outputKey, outputData]) => { const connections = Array.isArray(outputData?.connections) ? outputData.connections : []; connections.forEach((connection) => { edges.push({ from_node_id: String(node.id), to_node_id: String(connection.node), from_port: outputKey, to_port: connection.output }); }); }); }); return edges; } function getExecutionNodeOrder() { return Object.values(getNodeMap()) .sort((a, b) => Number(a.id) - Number(b.id)); } function buildWorkflowSubmission() { const workflowNameInput = getWorkflowNameInput(); const workflowNameRaw = workflowNameInput?.value || ''; const workflowName = (utils.canonicalizeName || ((v) => v))(workflowNameRaw, { max: 80, fallback: 'my_workflow' }); if (workflowNameInput && workflowName && workflowName !== workflowNameRaw) { workflowNameInput.value = workflowName; } const nodes = getExecutionNodeOrder(); const edges = getWorkflowEdges(); const errors = []; const builtNodes = []; nodes.forEach((node) => { const spec = getModelSpec(node.name); if (!spec) { errors.push(`${node.name}: no workflow adapter is registered yet.`); return; } if (typeof spec.validate === 'function') { const modelErrors = spec.validate({ node, nodeData: node.data || {}, editor, edges }) || []; modelErrors.forEach((msg) => { errors.push(`[${node.name} | ${node.data?.name || node.id}] ${msg}`); }); } if (!errors.length && typeof spec.buildExecutionNode === 'function') { try { builtNodes.push( spec.buildExecutionNode({ workflowName, node, nodeData: node.data || {}, editor, edges }) ); } catch (err) { errors.push(`[${node.name} | ${node.data?.name || node.id}] ${err?.message || 'Build failed.'}`); } } }); if (errors.length) { return { errors }; } return { workflow_name: workflowName, nodes: builtNodes, edges }; } async function getMemberId() { const start = Date.now(); while (!window.$memberstackDom && Date.now() - start < 2000) { await new Promise((resolve) => setTimeout(resolve, 50)); } const ms = window.$memberstackDom; if (!ms?.getCurrentMember) return null; try { const res = await ms.getCurrentMember(); return res?.data?.id || null; } catch { return null; } } function getExecutionContextForMember(memberId) { const out = { member_id: memberId }; try { const ctxPayload = window.ViciContext?.payloadFor?.(memberId); if (ctxPayload?.team_id) out.team_id = ctxPayload.team_id; } catch (err) { console.warn('Could not read ViciContext payload', err); } return out; } async function handleExecuteClick() { const executeBtn = document.getElementById('execute-button'); const built = buildWorkflowSubmission(); if (built?.errors?.length) { console.warn('[workflow] validation errors', built.errors); window.showToast?.('error', built.errors[0]); return; } const memberId = await getMemberId(); if (!memberId) { window.showToast?.('error', 'Please sign in and refresh this page.'); return; } const body = { ...built, ...getExecutionContextForMember(memberId) }; if (executeBtn) { executeBtn.disabled = true; executeBtn.setAttribute('aria-busy', 'true'); } UX?.overlay?.show?.('Submitting'); UX?.progress?.start?.(); UX?.progress?.set?.(8); try { if (window.ViciExec?.post) { await window.ViciExec.post( MODEL_WORKFLOW_ENDPOINT, memberId, body, { headers: {}, range: [75, 98] } ); } else { await UX.postJSONWithProgress( MODEL_WORKFLOW_ENDPOINT, body, { headers: {}, range: [75, 98] } ); } try { Promise.resolve(window.ViciSidebar?.refresh?.()).catch(() => {}); } catch {} UX?.progress?.finishOk?.(); UX?.overlay?.show?.('Submitted'); document.querySelector('#page-overlay .label')?.removeAttribute('data-ellipsis'); window.showToast?.('success', 'Workflow submitted successfully.'); setTimeout(() => { UX?.overlay?.hide?.(); window.location.assign('/dashboard'); }, 650); } catch (err) { console.error(err); UX?.progress?.finishFail?.(); UX?.overlay?.show?.('Submission failed'); document.querySelector('#page-overlay .label')?.removeAttribute('data-ellipsis'); window.showToast?.('error', err?.message || 'Workflow submission failed. Please try again.'); setTimeout(() => { UX?.overlay?.hide?.(); }, 320); } finally { if (executeBtn) { executeBtn.disabled = false; executeBtn.removeAttribute('aria-busy'); } } } function syncUiState() { const { undo, redo, reset, execute } = getToolbarButtons(); const hasNodes = getTotalNodeCount() > 0; if (undo) undo.disabled = historyStack.length <= 1; if (redo) redo.disabled = redoStack.length === 0; if (reset) { reset.disabled = !hasNodes; reset.classList.toggle('show', hasNodes); } if (execute) { execute.disabled = !hasNodes; execute.classList.toggle('show', hasNodes); } } function bindToolbar() { const { undo, redo, reset, execute } = getToolbarButtons(); undo?.addEventListener('click', doUndo); redo?.addEventListener('click', doRedo); reset?.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); clearCanvas(); }); execute?.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); handleExecuteClick(); }); } function bindEditorActions() { document.addEventListener('click', (e) => { const clearBtn = e.target.closest('.workflow-editor-clear'); if (clearBtn) { e.preventDefault(); clearSelectedNodeFields(); return; } const deleteBtn = e.target.closest('.workflow-editor-delete'); if (deleteBtn) { e.preventDefault(); deleteSelectedNode(); } }); } function bindEditorTabsAndPresets() { document.addEventListener('click', (e) => { const tabBtn = e.target.closest('[data-editor-tab]'); if (tabBtn) { const nodeId = tabBtn.getAttribute('data-node-id'); const nextTab = tabBtn.getAttribute('data-editor-tab'); if (!nodeId || !nextTab) return; setEditorTabState(nodeId, nextTab); return; } const presetBtn = e.target.closest('[data-model-preset]'); if (presetBtn) { e.preventDefault(); const nodeId = presetBtn.getAttribute('data-node-id'); const presetKey = presetBtn.getAttribute('data-model-preset'); const node = getNodeById(nodeId); const spec = node ? getModelSpec(node.name) : null; if (!node || !spec?.applyPreset) { window.showToast?.('info', 'This preset is not implemented yet.'); return; } Promise.resolve( spec.applyPreset({ nodeId, node, editor, patchNodeData, applyNodeData, replaceNodeData: applyNodeData, saveHistory: saveHistorySnapshot, rerender: () => renderNodeEditor(nodeId), presetKey }) ).catch((err) => { console.error(err); window.showToast?.('error', err?.message || 'Failed to apply preset.'); }); } }); } function bindCanvasClickBehavior() { const canvas = getCanvas(); if (!canvas) return; canvas.addEventListener('click', (e) => { const clickedNode = e.target.closest('.drawflow-node'); const clickedConnection = e.target.closest('.connection'); const clickedHandle = e.target.closest('.workflow-handle'); const clickedBrand = e.target.closest('.workflow-canvas-brand'); const clickedWorkspaceTabs = e.target.closest('.workflow-top-tabs, .ctx-tabs, .workflow-tab, .side-tab'); const clickedExecute = e.target.closest('#execute-button'); const clickedReset = e.target.closest('#reset-button'); if (clickedHandle || clickedBrand || clickedWorkspaceTabs || clickedExecute || clickedReset) return; if (clickedNode || clickedConnection) return; toggleSidebar(); }); } function bindNodePointerBehavior() { const canvas = getCanvas(); if (!canvas) return; const resetNodePointer = () => { nodePointer.active = false; nodePointer.moved = false; nodePointer.nodeId = null; nodePointer.startX = 0; nodePointer.startY = 0; }; canvas.addEventListener('pointerdown', (e) => { const nodeEl = e.target.closest('.drawflow-node'); if (!nodeEl) { resetNodePointer(); return; } nodePointer.active = true; nodePointer.moved = false; nodePointer.nodeId = extractNodeIdFromElement(nodeEl); nodePointer.startX = e.clientX; nodePointer.startY = e.clientY; }); canvas.addEventListener('pointermove', (e) => { if (!nodePointer.active) return; const dx = Math.abs(e.clientX - nodePointer.startX); const dy = Math.abs(e.clientY - nodePointer.startY); if (dx > TAP_MOVE_THRESHOLD || dy > TAP_MOVE_THRESHOLD) { nodePointer.moved = true; } }); const finishPointer = () => { if (!nodePointer.active) return; const clickedNodeId = nodePointer.nodeId; const moved = nodePointer.moved; resetNodePointer(); if (!clickedNodeId || moved) { syncUiState(); return; } requestAnimationFrame(() => { const selectedId = getSelectedNodeId(); if (selectedId && String(selectedId) === String(clickedNodeId)) { openNodeEditor(selectedId); } syncUiState(); }); }; canvas.addEventListener('pointerup', finishPointer); canvas.addEventListener('pointercancel', () => { resetNodePointer(); syncUiState(); }); } function enableSidebarDragDrop() { $$('.node-button').forEach((btn) => { let type = btn.dataset.nodeType; if (!type) { const match = String(btn.getAttribute('onclick') || '').match(/addNode\(['"]([^'"]+)['"]\)/); type = match ? match[1] : btn.textContent.trim(); btn.dataset.nodeType = type; } if (!btn.dataset.dragBound) { btn.addEventListener('dragstart', (e) => { e.dataTransfer.setData('application/x-workflow-node', btn.dataset.nodeType || ''); e.dataTransfer.setData('text/plain', btn.dataset.nodeType || ''); e.dataTransfer.effectAllowed = 'copy'; }); btn.dataset.dragBound = '1'; } if (!isMobile()) btn.setAttribute('draggable', 'true'); else btn.removeAttribute('draggable'); }); } function allowDrop(e) { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; } function drop(e) { e.preventDefault(); const type = e.dataTransfer?.getData('application/x-workflow-node') || e.dataTransfer?.getData('text/plain'); if (!type) return; addNode(type, getCanvasDropPoint(e)); } function bindHandle() { const handle = getHandle(); if (!handle) return; handle.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const panel = getPanel(); if (panel?.classList.contains('open')) collapseSidebar(); else openLibraryPanel(); }); } function bindWorkflowNameField() { const input = getWorkflowNameInput(); if (!input) return; input.value = ''; input.addEventListener('blur', () => { const safe = (utils.canonicalizeName || ((v) => v))(input.value || '', { max: 80, fallback: '' }); if (safe && safe !== input.value) input.value = safe; }); } function initDrawflow() { const canvas = getCanvas(); if (!canvas) { console.error('[workflow] #drawflow not found'); return; } editor = new Drawflow(canvas); editor.reroute = true; editor.start(); editor.editor_mode = 'edit'; editor.on('nodeCreated', () => { hideHeroWelcome(); saveHistorySnapshot(); syncUiState(); }); editor.on('nodeRemoved', () => { collapseSidebar(); saveHistorySnapshot(); syncUiState(); maybeShowHeroWelcome(); }); editor.on('connectionCreated', () => { saveHistorySnapshot(); syncUiState(); requestAnimationFrame(refreshSelectedNodeEditor); }); editor.on('connectionRemoved', () => { saveHistorySnapshot(); syncUiState(); requestAnimationFrame(refreshSelectedNodeEditor); }); editor.on('nodeSelected', () => { syncUiState(); }); editor.on('nodeUnselected', () => { syncUiState(); }); saveHistorySnapshot(); syncUiState(); } function init() { bindWorkflowNameField(); bindHandle(); bindToolbar(); bindEditorActions(); bindEditorTabsAndPresets(); bindCanvasClickBehavior(); bindNodePointerBehavior(); enableSidebarDragDrop(); initDrawflow(); setSidebarMode('library'); setSidebarOpen(false); syncUiState(); maybeShowHeroWelcome(); requestAnimationFrame(syncSidebarScrollPadding); } window.addNode = addNode; window.allowDrop = allowDrop; window.drop = drop; window.toggleSidebar = toggleSidebar; window.toggleMobileSidebar = toggleSidebar; window.workflowEditor = () => editor; window.buildWorkflowSubmission = buildWorkflowSubmission; document.addEventListener('DOMContentLoaded', init); window.addEventListener('resize', () => { enableSidebarDragDrop(); syncSidebarScrollPadding(); }); })();