(() => {
'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 `
`;
}
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 = `
`;
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 `
`;
}
return `
`;
}
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();
});
})();