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