(() => { 'use strict'; const utils = window.WorkflowModelUtils; const registry = window.WorkflowModels; if (!utils || !registry) { console.error('WorkflowModelUtils / WorkflowModels must load before OpenFold-3 adapter.'); return; } const AA20 = 'ACDEFGHIKLMNPQRSTVWY'; const RE_PROT_SEQ = new RegExp(`^[${AA20}]+$`); const DEFAULT_GLOBAL_MSA = 'mmseq2'; const DEFAULT_TEMPLATES = 'true'; const DEFAULT_SEED = '42'; const DEFAULT_SAMPLES = '1'; function svgUp() { return ` `; } function svgDown() { return ` `; } function svgTrash() { return ` `; } function isValidProteinSeq(seq) { return RE_PROT_SEQ.test(String(seq || '').toUpperCase()); } function isValidDNASeq(seq) { return /^[ACGT]+$/.test(String(seq || '').toUpperCase()); } function isValidRNASeq(seq) { return /^[ACGU]+$/.test(String(seq || '').toUpperCase()); } function isValidSMILES(s) { const x = String(s || '').trim(); if (!x || /\s/.test(x)) return false; return /^[A-Za-z0-9@+\-\[\]\(\)=#$%\\/\.]+$/.test(x) && /[A-Za-z]/.test(x); } function isValidGlycanCCD(s) { const x = String(s || '').trim(); if (!x) return false; if (!/^[A-Za-z0-9()\-\s]+$/.test(x)) return false; if (!/[A-Za-z]{3}/.test(x)) return false; let balance = 0; for (const ch of x) { if (ch === '(') balance += 1; else if (ch === ')') { balance -= 1; if (balance < 0) return false; } } return balance === 0; } function detectLigandSequenceType(raw) { const sequence = String(raw || '').trim(); if (!sequence) return null; const canonical = sequence.toUpperCase(); const lettersOnly = /^[A-Za-z]+$/.test(sequence); const smilesTokens = /[=#[\]@\\/0-9a-z]/.test(sequence); if (isValidSMILES(sequence) && smilesTokens && !(lettersOnly && sequence.length <= 4)) { return 'smiles'; } if (isValidGlycanCCD(canonical)) { return 'ccd_codes'; } if (isValidSMILES(sequence)) { return 'smiles'; } if (isValidGlycanCCD(sequence)) { return 'ccd_codes'; } return null; } function normalizeChainId(raw) { const id = String(raw || '').trim().toUpperCase(); if (!id) return ''; if (!/^[A-Z0-9]{1,2}$/.test(id)) return ''; return id; } function normalizeNoSpaceUpper(raw) { return String(raw || '').replace(/\s+/g, '').toUpperCase(); } function chainIdFromIndex(index) { const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; let n = Number(index); if (!Number.isInteger(n) || n < 0) return 'A'; if (n < 26) return letters[n]; let out = ''; while (n >= 0) { out = letters[n % 26] + out; n = Math.floor(n / 26) - 1; } return out; } function getMolPlaceholder(type) { if (type === 'protein') return 'Enter amino-acid sequence (1-letter; e.g. MKTIIALSYI...)'; if (type === 'dna') return 'Enter DNA sequence (A/C/G/T; e.g. ATGGCC...)'; if (type === 'rna') return 'Enter RNA sequence (A/C/G/U; e.g. AUGGCC...)'; return 'Enter ligand string; auto-detects SMILES vs CCD (e.g. CC(=O)... or ATP).'; } function getMsasForType(type) { if (type === 'protein') return ['paired', 'unpaired', 'both', 'none']; if (type === 'rna') return ['both', 'unpaired', 'none']; return ['none']; } function getDefaultMsasForType(type) { return getMsasForType(type)[0] || 'none'; } function renderSelectOptions(values, selected, labels = {}) { return (Array.isArray(values) ? values : []).map((value) => { const str = String(value); const label = labels[str] || str; return ``; }).join(''); } function cloneMolecule(m) { const type = m?.type || 'protein'; const allowedMsas = getMsasForType(type); const msas = allowedMsas.includes(String(m?.msas || '')) ? String(m.msas) : getDefaultMsasForType(type); return { chain_id: normalizeChainId(m?.chain_id) || '', type, sequence: String(m?.sequence || ''), msas }; } function createBlankMolecule() { return { chain_id: 'A', type: 'protein', sequence: '', msas: 'paired' }; } function normalizeMoleculesForEditor(molecules) { const raw = Array.isArray(molecules) && molecules.length ? molecules.map(cloneMolecule) : [createBlankMolecule()]; return raw.map((mol, index) => { const type = mol.type || 'protein'; const allowedMsas = getMsasForType(type); const msas = allowedMsas.includes(String(mol.msas || '')) ? String(mol.msas) : getDefaultMsasForType(type); return { chain_id: chainIdFromIndex(index), type, sequence: String(mol.sequence || ''), msas }; }); } function createDefaultData({ autoName = 'openfold3_1' } = {}) { return { default_name: autoName, name: autoName, msa_global: DEFAULT_GLOBAL_MSA, templates: DEFAULT_TEMPLATES, seed: DEFAULT_SEED, samples: DEFAULT_SAMPLES, molecules: normalizeMoleculesForEditor([createBlankMolecule()]) }; } function renderMoleculeBlock(mol, index, total, nodeId, uiEnter) { const e = utils.escapeHtml; const chainId = normalizeChainId(mol?.chain_id || '') || chainIdFromIndex(index); const type = mol?.type || 'protein'; const sequence = String(mol?.sequence || ''); const showMsas = type === 'protein' || type === 'rna'; const msaValues = getMsasForType(type); const msasValue = msaValues.includes(String(mol?.msas || '')) ? String(mol?.msas) : getDefaultMsasForType(type); const entering = uiEnter?.kind === 'molecule' && uiEnter.index === index; return `
`; } function renderBody({ nodeId, nodeData }) { const e = utils.escapeHtml; const data = { ...createDefaultData({ autoName: nodeData?.default_name || nodeData?.name || 'openfold3_1' }), ...(nodeData || {}) }; const activeTab = data?.__editorTab === 'advanced' ? 'advanced' : 'basic'; const uiEnter = data?._ui?.enter || null; const molecules = normalizeMoleculesForEditor(data.molecules); return `
This is the OpenFold-3 node name. The workflow name is set at the top of the workflow sidebar.
${molecules.map((mol, index) => renderMoleculeBlock(mol, index, molecules.length, nodeId, uiEnter)).join('')}
`; } function toggleLookup(btn, mount) { if (!btn) return; if (typeof window.toggleViciLookup === 'function') { window.toggleViciLookup(btn); return; } const block = btn.closest('.molecule-block') || btn.closest('.form-card'); const panel = block?.querySelector('.vici-lookup'); if (!panel) return; const isOpen = panel.style.display !== 'none' && !panel.hidden; panel.style.display = isOpen ? 'none' : 'block'; btn.classList.toggle('active', !isOpen); btn.setAttribute('aria-expanded', String(!isOpen)); if (!isOpen) { try { window.ViciLookup?.init?.(mount || block); } catch (err) { console.error(err); } } } function bind({ nodeId, node, mount, editor, patchNodeData, replaceNodeData }) { const listeners = []; function on(el, type, fn, options) { if (!el) return; el.addEventListener(type, fn, options); listeners.push(() => el.removeEventListener(type, fn, options)); } function getNodeData() { const liveNode = editor?.getNodeFromId?.(Number(String(nodeId).match(/(\d+)/)?.[1] || nodeId)) || editor?.getNodeFromId?.(String(nodeId)) || node; const base = { ...createDefaultData({ autoName: liveNode?.data?.default_name || liveNode?.data?.name || 'openfold3_1' }), ...(liveNode?.data || {}) }; return { ...base, molecules: normalizeMoleculesForEditor(base.molecules) }; } function getCurrentEditorTab() { const shell = mount.querySelector(`.workflow-editor-shell[data-node-id="${nodeId}"]`) || mount.closest('.workflow-editor-shell'); return shell?.classList.contains('is-tab-advanced') ? 'advanced' : 'basic'; } function patch(patchObj, { saveHistory = false, rerenderEditor = false } = {}) { const nextPatch = rerenderEditor ? { ...(patchObj || {}), __editorTab: getCurrentEditorTab() } : patchObj; patchNodeData(nodeId, nextPatch, { saveHistory, rerender: rerenderEditor }); } function replace(nextData, { saveHistory = false } = {}) { const payload = { ...(nextData || {}), __editorTab: getCurrentEditorTab() }; if (payload.molecules) { payload.molecules = normalizeMoleculesForEditor(payload.molecules); } replaceNodeData(nodeId, payload, { saveHistory, rerender: true }); } function clearTransientUiFlag() { const data = getNodeData(); if (data?._ui) patch({ _ui: null }); } function bindEnterAnimations() { mount.querySelectorAll('.openfold3-list-item.is-entering').forEach((el) => { el.style.overflow = 'hidden'; el.style.maxHeight = '0px'; el.style.opacity = '0'; el.style.transform = 'translateY(-6px)'; el.style.marginBottom = '0px'; el.style.transition = 'max-height .26s ease, opacity .2s ease, transform .26s ease, margin-bottom .26s ease'; requestAnimationFrame(() => { el.style.maxHeight = `${el.scrollHeight}px`; el.style.opacity = '1'; el.style.transform = 'translateY(0)'; el.style.marginBottom = '16px'; }); const done = (evt) => { if (evt.target !== el || evt.propertyName !== 'max-height') return; el.classList.remove('is-entering'); el.style.maxHeight = 'none'; el.style.opacity = ''; el.style.transform = ''; el.style.overflow = ''; el.style.transition = ''; el.style.marginBottom = '16px'; el.removeEventListener('transitionend', done); clearTransientUiFlag(); }; el.addEventListener('transitionend', done); }); } function animateRemoval(block, onDone) { if (!block) { onDone?.(); return; } const startHeight = block.scrollHeight; block.classList.remove('is-entering'); block.style.overflow = 'hidden'; block.style.maxHeight = `${startHeight}px`; block.style.opacity = '1'; block.style.transform = 'translateY(0)'; block.style.marginBottom = '16px'; block.style.transition = 'max-height .24s ease, opacity .18s ease, transform .24s ease, margin-bottom .24s ease'; block.offsetHeight; block.style.maxHeight = '0px'; block.style.opacity = '0'; block.style.transform = 'translateY(-6px)'; block.style.marginBottom = '0px'; const finish = (evt) => { if (evt.target !== block || evt.propertyName !== 'max-height') return; block.removeEventListener('transitionend', finish); onDone?.(); }; block.addEventListener('transitionend', finish); } function canonicalizeNameField() { const nameEl = mount.querySelector('[data-field-key="name"]'); if (!nameEl) return; const safe = utils.canonicalizeName(nameEl.value || '', { max: 64, fallback: '' }); if (safe !== nameEl.value) nameEl.value = safe; patch({ name: safe }, { saveHistory: true }); } function updateMoleculeAt(index, updater, { saveHistory = false, rerenderEditor = false } = {}) { const data = getNodeData(); const molecules = normalizeMoleculesForEditor(data.molecules); if (!molecules[index]) return; const updated = updater(cloneMolecule(molecules[index])) || molecules[index]; molecules[index] = { ...molecules[index], ...updated, chain_id: molecules[index].chain_id }; const normalized = normalizeMoleculesForEditor(molecules); patch({ molecules: normalized }, { saveHistory, rerenderEditor }); } function swapMoleculeContent(molecules, fromIndex, toIndex) { if (!molecules[fromIndex] || !molecules[toIndex]) return molecules; const next = molecules.map(cloneMolecule); const current = next[fromIndex]; const other = next[toIndex]; next[fromIndex] = { ...current, chain_id: current.chain_id, type: other.type, sequence: other.sequence, msas: other.msas }; next[toIndex] = { ...other, chain_id: other.chain_id, type: current.type, sequence: current.sequence, msas: current.msas }; return next; } const nameEl = mount.querySelector('[data-field-key="name"]'); on(nameEl, 'input', () => patch({ name: nameEl.value })); on(nameEl, 'change', () => patch({ name: nameEl.value }, { saveHistory: true })); on(nameEl, 'blur', canonicalizeNameField); ['msa_global', 'templates', 'seed', 'samples'].forEach((key) => { const el = mount.querySelector(`[data-field-key="${key}"]`); if (!el) return; if (key === 'seed') { on(el, 'input', () => patch({ [key]: String(el.value || '') })); on(el, 'change', () => patch({ [key]: String(el.value || '') }, { saveHistory: true })); } else { on(el, 'change', () => patch({ [key]: String(el.value || '') }, { saveHistory: true })); } }); mount.querySelectorAll('.molecule-block[data-molecule-index]').forEach((block) => { const index = Number(block.getAttribute('data-molecule-index')); const typeEl = block.querySelector('.mol-type'); const msasEl = block.querySelector('.mol-msas'); const seqEl = block.querySelector('.mol-seq'); const removeBtn = block.querySelector('.mol-remove'); const moveBtns = block.querySelectorAll('.mol-move'); const viciBtn = block.querySelector('.vici-toggle-btn'); on(typeEl, 'change', () => { updateMoleculeAt(index, (mol) => { const nextType = String(typeEl.value || 'protein'); return { ...mol, type: nextType, msas: getDefaultMsasForType(nextType) }; }, { saveHistory: true, rerenderEditor: true }); }); on(msasEl, 'change', () => { updateMoleculeAt(index, (mol) => ({ ...mol, msas: String(msasEl.value || getDefaultMsasForType(mol.type)) }), { saveHistory: true }); }); on(seqEl, 'input', () => { let value = seqEl.value; const currentType = String(typeEl?.value || 'protein'); if (currentType === 'protein' || currentType === 'dna' || currentType === 'rna') { const cleaned = normalizeNoSpaceUpper(value); if (cleaned !== value) { value = cleaned; seqEl.value = cleaned; } } updateMoleculeAt(index, (mol) => ({ ...mol, sequence: value })); }); on(seqEl, 'change', () => { updateMoleculeAt(index, (mol) => ({ ...mol, sequence: String(seqEl.value || '') }), { saveHistory: true }); }); on(removeBtn, 'click', (e) => { e.preventDefault(); e.stopPropagation(); const data = getNodeData(); const molecules = normalizeMoleculesForEditor(data.molecules); if (!molecules[index]) return; if (molecules.length === 1) { replace({ ...data, molecules: [createBlankMolecule()], _ui: null }, { saveHistory: true }); return; } animateRemoval(block, () => { const nextData = getNodeData(); const nextMolecules = normalizeMoleculesForEditor(nextData.molecules); if (!nextMolecules[index]) return; nextMolecules.splice(index, 1); replace({ ...nextData, molecules: normalizeMoleculesForEditor(nextMolecules), _ui: null }, { saveHistory: true }); }); }); moveBtns.forEach((btn) => { on(btn, 'click', (e) => { e.preventDefault(); e.stopPropagation(); const dir = btn.getAttribute('data-dir'); const data = getNodeData(); const molecules = normalizeMoleculesForEditor(data.molecules); const otherIndex = dir === 'up' ? index - 1 : index + 1; if (!molecules[index] || !molecules[otherIndex]) return; const swapped = swapMoleculeContent(molecules, index, otherIndex); replace({ ...data, molecules: swapped, _ui: null }, { saveHistory: true }); }); }); on(viciBtn, 'click', (e) => { e.preventDefault(); toggleLookup(viciBtn, mount); }); }); const addMolBtn = mount.querySelector('[data-action="add-molecule"]'); on(addMolBtn, 'click', (e) => { e.preventDefault(); e.stopPropagation(); const data = getNodeData(); const molecules = normalizeMoleculesForEditor(data.molecules); molecules.push(createBlankMolecule()); const normalized = normalizeMoleculesForEditor(molecules); replace({ ...data, molecules: normalized, _ui: { enter: { kind: 'molecule', index: normalized.length - 1 } } }, { saveHistory: true }); }); try { window.ViciLookup?.init?.(mount); } catch (err) { console.error(err); } bindEnterAnimations(); return () => { listeners.splice(0).forEach((off) => { try { off(); } catch (err) { console.error(err); } }); }; } function buildExample1(autoName) { return { ...createDefaultData({ autoName }), name: autoName, msa_global: 'mmseq2', templates: 'true', seed: '42', samples: '1', molecules: normalizeMoleculesForEditor([ { type: 'protein', msas: 'paired', sequence: 'MKKGHHHHHHGAISLISALVRAHVDSNPAMTSLDYSRFQANPDYQMSGDDTQHIQQFYDLLTGSMEIIRGWAEKIPGFADLPKADQDLLFESAFLELFVLRLAYRSNPVEGKLIFCNGVVLHRLQCVRGFGEWIDSIVEFSSNLQNMNIDISAFSCIAALAMVTERHGLKEPKRVEELQNKIVNTLKDHVTFNNGGLNRPNYLSKLLGKLPELRTLCTQGLQRIFYLKLEDLVPPPAIIDKLFLDTLPF' }, { type: 'ligand', msas: 'none', sequence: 'c1cc(c(cc1OCC(=O)NCCS)Cl)Cl' } ]) }; } function buildExample2(autoName) { return { ...createDefaultData({ autoName }), name: autoName, msa_global: 'mmseq2', templates: 'true', seed: '42', samples: '1', molecules: normalizeMoleculesForEditor([ { type: 'protein', msas: 'paired', sequence: 'LPSSEEYKVAYELLPGLSEVPDPSNIPQMHAGHIPLRSEDADEQDSSDLEYFFWKFTNNDSNGNVDRPLIIWLNGGPGCSSMDGALVESGPFRVNSDGKLYLNEGSWISKGDLLFIDQPTGTGFSVEQNKDEGKIDKNKFDEDLEDVTKHFMDFLENYFKIFPEDLTRKIILSGESYAGQYIPFFANAILNHNKFSKIDGDTYDLKALLIGNGWIDPNTQSLSYLPFAMEKKLIDESNPNFKHLTNAHENCQNLINSASTDEAAHFSYQECENILNLLLSYTRESSQKGTADCLNMYNFNLKDSYPSCGMNWPKDISFVSKFFSTPGVIDSLHLDSDKIDHWKECTNSVGTKLSNPISKPSIHLLPGLLESGIEIVLFNGDKDLICNNKGVLDTIDNLKWGGIKGFSDDAVSFDWIHKSKSTDDSEEFSGYVKYDRNLTFVSVYNASHMVPFDKSLVSRGIVDIYSNDVMIIDNNGKNVMITT' }, { type: 'ligand', msas: 'none', sequence: 'NAG' } ]) }; } async function applyPreset({ nodeId, node, replaceNodeData, presetKey }) { const autoName = node?.data?.default_name || node?.data?.name || 'openfold3_1'; const nextData = presetKey === 'example2' ? buildExample2(autoName) : buildExample1(autoName); replaceNodeData(nodeId, nextData, { saveHistory: true, rerender: true }); window.showToast?.('success', `Loaded OpenFold-3 preset: ${presetKey === 'example2' ? '1AC5' : '8CYO'}.`); } function buildOpenFoldJobFromData(nodeData, { strict = true } = {}) { const name = utils.canonicalizeName( nodeData?.name || nodeData?.default_name || 'openfold3_1', { max: 64, fallback: '' } ); if (!name) { throw new Error('OpenFold-3 node name is required.'); } if (!utils.SAFE_NAME_RE.test(name)) { throw new Error('OpenFold-3 node name must be 3-64 chars using a-z, 0-9, _ or - and start/end with letter or digit.'); } const msaChoice = String(nodeData?.msa_global || DEFAULT_GLOBAL_MSA); const templateChoice = String(nodeData?.templates || DEFAULT_TEMPLATES); const useMsa = msaChoice !== 'none'; const useTemplates = templateChoice === 'true'; let seed = parseInt(String(nodeData?.seed || DEFAULT_SEED), 10); if (!Number.isFinite(seed) || seed < 0) seed = parseInt(DEFAULT_SEED, 10); let samples = parseInt(String(nodeData?.samples || DEFAULT_SAMPLES), 10); if (!Number.isFinite(samples) || samples < 1) samples = parseInt(DEFAULT_SAMPLES, 10); const moleculesInput = normalizeMoleculesForEditor(nodeData?.molecules); const molecules = []; const chainSet = new Set(); for (const mol of moleculesInput) { const chainId = normalizeChainId(mol?.chain_id || ''); const type = String(mol?.type || 'protein'); const rawSeq = String(mol?.sequence || '').trim(); if (!chainId) { throw new Error('Each molecule must have a Chain ID.'); } if (chainSet.has(chainId)) { throw new Error(`Duplicate Chain ID: ${chainId}.`); } chainSet.add(chainId); if (!rawSeq) { if (strict) throw new Error(`Molecule ${chainId}: provide a sequence or ligand string.`); continue; } let sequenceType = 'sequence'; let sequenceValue = rawSeq; if (type === 'protein') { sequenceValue = normalizeNoSpaceUpper(rawSeq); if (strict && !isValidProteinSeq(sequenceValue)) { throw new Error(`Chain ${chainId}: protein sequence must use ACDEFGHIKLMNPQRSTVWY.`); } } else if (type === 'dna') { sequenceValue = normalizeNoSpaceUpper(rawSeq); if (strict && !isValidDNASeq(sequenceValue)) { throw new Error(`Chain ${chainId}: DNA sequence must use A/C/G/T.`); } } else if (type === 'rna') { sequenceValue = normalizeNoSpaceUpper(rawSeq); if (strict && !isValidRNASeq(sequenceValue)) { throw new Error(`Chain ${chainId}: RNA sequence must use A/C/G/U.`); } } else if (type === 'ligand') { const detected = detectLigandSequenceType(rawSeq); if (!detected) { throw new Error(`Chain ${chainId}: unable to detect whether the ligand is SMILES or CCD codes.`); } sequenceType = detected; if (sequenceType === 'ccd_codes') sequenceValue = rawSeq.toUpperCase(); } else { throw new Error(`Chain ${chainId}: unsupported molecule type "${type}".`); } const entry = { type, chain_id: chainId, sequence_type: sequenceType, sequence: sequenceValue }; if (type === 'protein' || type === 'rna') { const allowedMsas = getMsasForType(type); const msasValue = allowedMsas.includes(String(mol?.msas || '')) ? String(mol.msas) : getDefaultMsasForType(type); entry.msas = msasValue; } molecules.push(entry); } if (strict && molecules.length < 1) { throw new Error('At least one molecule sequence must be filled.'); } return { class: 'OpenFold3', name, msa: useMsa, templates: useTemplates, seed, samples, molecules }; } function validate({ nodeData }) { const errors = []; try { buildOpenFoldJobFromData(nodeData, { strict: true }); } catch (err) { errors.push(err?.message || 'OpenFold-3 validation failed.'); } return errors; } function buildExecutionNode({ node, nodeData }) { const job = buildOpenFoldJobFromData(nodeData, { strict: true }); return { node_id: String(node.id), node_type: 'OpenFold-3', node_name: job.name, payload: { openfold3: job } }; } registry.register('OpenFold-3', { type: 'OpenFold-3', title: 'OpenFold-3', tutorialUrl: '/blog', tabs: ['basic', 'advanced'], presets: [ { key: 'example1', label: '8CYO' }, { key: 'example2', label: '1AC5' } ], createDefaultData, renderBody, bind, applyPreset, validate, buildExecutionNode, resetData: ({ node }) => createDefaultData({ autoName: node?.data?.default_name || node?.data?.name || 'openfold3_1' }) }); })();