(() => { 'use strict'; const utils = window.WorkflowModelUtils; const registry = window.WorkflowModels; if (!utils || !registry) { console.error('WorkflowModelUtils / WorkflowModels must load before BoltzGen adapter.'); return; } const MODEL_TYPE = 'BoltzGen'; const MODEL_KEY = 'boltzgen'; const STAGE_OPTIONS = { recycles: ['1', '2', '3', '4', '5', '6'], sampling: ['50', '100', '200', '300', '500', '750', '1000'], diffusion_samples: ['1', '2', '3', '5', '8', '10'] }; const BOLTZ_THREE_TO_ONE = { ALA: 'A', ARG: 'R', ASN: 'N', ASP: 'D', CYS: 'C', GLN: 'Q', GLU: 'E', GLY: 'G', HIS: 'H', ILE: 'I', LEU: 'L', LYS: 'K', MET: 'M', PHE: 'F', PRO: 'P', SER: 'S', THR: 'T', TRP: 'W', TYR: 'Y', VAL: 'V', SEC: 'U', PYL: 'O' }; const ADD_ICON = ` `; const TRASH_ICON = ` `; const LOCAL_VICI_EYE_SVG = ` `; const PRESETS = { nano: { label: 'Nanobody | 8Z8V', name: 'example_nanobody_8z8v', protocol: 'nanobody-anything', structure: { chain: 'A', url: 'https://cdn.prod.website-files.com/685009238c2e9a16d785299f/69b3244ba7914e43ffa87150_8Z8V.txt', fileName: '8Z8V.cif', directives: { include: [{ chain: 'B', residues: '' }], include_proximity: [], binding_types: [], structure_groups: [ { chain: 'B', groupId: '', visibility: '2', residues: '' }, { chain: 'B', groupId: '', visibility: '0', residues: '26..33,51..58,98..108' } ], design: [{ chain: 'B', residues: '26..33,51..58,98..108' }], secondary_structure: [], design_insertions: [ { chain: 'B', residue: '26', numResidues: '1..5', secondaryStructure: 'UNSPECIFIED' }, { chain: 'B', residue: '51', numResidues: '1..5', secondaryStructure: 'UNSPECIFIED' }, { chain: 'B', residue: '98', numResidues: '1..12', secondaryStructure: 'UNSPECIFIED' } ] } }, extraEntities: [], filters: [], constraints: [] }, pep: { label: 'Peptide | 6WJ3', name: 'example_peptide_6wj3', protocol: 'peptide-anything', structure: { chain: 'A', url: 'https://cdn.prod.website-files.com/685009238c2e9a16d785299f/69b324e5139f865f72fc87cb_6WJ3.txt', fileName: '6WJ3.cif', directives: { include: [{ chain: 'G', residues: '' }], include_proximity: [], binding_types: [{ chain: 'G', mode: 'b', residues: '190,193,194,258,259,262,263,205,214,215,216,217,218,219,220,221,222,232,236,239,278,279,280,281,282,283,284,285,286,240,245,246,249,250,253,254,256,257,261,262' }], structure_groups: [], design: [], secondary_structure: [], design_insertions: [] } }, extraEntities: [ { kind: 'protein', chain: 'P', mode: 'range', sequence: '', min: '5', max: '20', cyclic: false, secondary_structure: '', binding_types: [], __settingsTab: 'binding' } ], filters: [], constraints: [] }, anti: { label: 'Antibody | 5YOY', name: 'example_antibody_5yoy', protocol: 'antibody-anything', structure: { chain: 'A', url: 'https://cdn.prod.website-files.com/685009238c2e9a16d785299f/69b325e18f5944740950145c_5YOY.txt', fileName: '5YOY.cif', directives: { include: [ { chain: 'H', residues: '4..129' }, { chain: 'E', residues: '4..111' } ], include_proximity: [], binding_types: [], structure_groups: [ { chain: 'H', groupId: '', visibility: '2', residues: '' }, { chain: 'E', groupId: '', visibility: '2', residues: '' }, { chain: 'H', groupId: '', visibility: '0', residues: '29..35,55..60,102..118' }, { chain: 'E', groupId: '', visibility: '0', residues: '27..37,53..59,92..101' } ], design: [ { chain: 'H', residues: '29..35,55..60,102..118' }, { chain: 'E', residues: '27..37,53..59,92..101' } ], secondary_structure: [], design_insertions: [ { chain: 'H', residue: '29', numResidues: '7..9', secondaryStructure: 'UNSPECIFIED' }, { chain: 'H', residue: '55', numResidues: '5..8', secondaryStructure: 'UNSPECIFIED' }, { chain: 'H', residue: '102', numResidues: '3..21', secondaryStructure: 'UNSPECIFIED' }, { chain: 'E', residue: '27', numResidues: '10..17', secondaryStructure: 'UNSPECIFIED' }, { chain: 'E', residue: '53', numResidues: '7', secondaryStructure: 'UNSPECIFIED' }, { chain: 'E', residue: '92', numResidues: '8..12', secondaryStructure: 'UNSPECIFIED' } ] } }, extraEntities: [], filters: [], constraints: [] }, small: { label: 'Small Molecule | 4G37', name: 'example_small_molecule_4g37', protocol: 'protein-small_molecule', structure: { chain: 'A', url: 'https://cdn.prod.website-files.com/685009238c2e9a16d785299f/69b326bf8bd231738fd2a85a_4G37.txt', fileName: '4G37.pdb', directives: { include: [ { chain: 'A', residues: '' }, { chain: 'C', residues: '' } ], include_proximity: [], binding_types: [], structure_groups: [], design: [], secondary_structure: [], design_insertions: [] } }, extraEntities: [ { kind: 'protein', chain: 'B', mode: 'sequence', sequence: '1..3C1..2C1..3', min: '', max: '', cyclic: false, secondary_structure: '', binding_types: [], __settingsTab: 'binding' }, { kind: 'ligand', chain: 'D', code: 'CC(=O)NCCNC(C)=O', binding_type: 'b' } ], filters: [], constraints: [ { kind: 'bond', atom1: ['A', '105', 'SG'], atom2: ['C', '1', 'C3'] }, { kind: 'bond', atom1: ['B', '2', 'SG'], atom2: ['C', '1', 'C11'] }, { kind: 'bond', atom1: ['A', '339', 'OG'], atom2: ['D', '1', 'C1'] }, { kind: 'bond', atom1: ['B', '4', 'SG'], atom2: ['D', '1', 'C6'] } ] } }; let UID = 0; function uid(prefix = 'boltzgen') { UID += 1; return `${prefix}-${Date.now().toString(36)}-${UID}`; } function q(sel, scope) { return (scope || document).querySelector(sel); } function qa(sel, scope) { return Array.from((scope || document).querySelectorAll(sel)); } function e(value) { return utils.escapeHtml(value); } function clone(value) { return utils.deepClone(value); } function isPlainObject(v) { return Object.prototype.toString.call(v) === '[object Object]'; } function safeName(raw, fallback = '') { return utils.canonicalizeName(raw, { max: 64, fallback }); } function getViciEyeMarkup() { return String(window.VICI_EYE_SVG || LOCAL_VICI_EYE_SVG).trim(); } function viciToggleButtonHtml(label = 'Toggle Vici Lookup') { return ` `; } function removeButtonHtml(label = 'Remove') { return ` `; } function helpBubbleHtml(text, prefix = 'help') { const id = uid(prefix); return ` `; } function fieldHeadHtml(label, helpText = '') { return `
${helpText ? helpBubbleHtml(helpText, 'entity-help') : ''}
`; } function defaultStructureDirectives() { return { include: [], include_proximity: [], binding_types: [], structure_groups: [], design: [], secondary_structure: [], design_insertions: [] }; } function createBlankStructureEntity(existingChains = new Set()) { return { __uiId: uid('boltz-entity'), kind: 'structure', chain: nextAvailableEntityChain(existingChains), content: '', file_name: '', source: '', directives: defaultStructureDirectives(), __settingsTab: 'include' }; } function createBlankProteinEntity(existingChains = new Set()) { return { __uiId: uid('boltz-entity'), kind: 'protein', chain: nextAvailableEntityChain(existingChains), mode: 'sequence', sequence: '', min: '', max: '', cyclic: false, secondary_structure: '', binding_types: [], __settingsTab: 'binding' }; } function createBlankLigandEntity(existingChains = new Set()) { return { __uiId: uid('boltz-entity'), kind: 'ligand', chain: nextAvailableEntityChain(existingChains), code: '', binding_type: 'b' }; } function createBlankFilter() { return { metric: '', comparator: '>', value: '' }; } function createBlankBondConstraint() { return { kind: 'bond', atom1: ['', '', ''], atom2: ['', '', ''] }; } function createBlankLengthConstraint() { return { kind: 'length', chain: '', min: '', max: '' }; } function createDefaultData({ autoName = 'boltzgen_1' } = {}) { return { default_name: autoName, name: autoName, protocol: 'protein-anything', num_designs: '', budget: '', stages: { design: { recycling_steps: '3', sampling_steps: '200', diffusion_samples: '1' }, affinity: { recycling_steps: '3', sampling_steps: '200', diffusion_samples: '5' }, folding: { recycling_steps: '3', sampling_steps: '200', diffusion_samples: '5' }, inverse_folding: { recycling_steps: '3', sampling_steps: '200', diffusion_samples: '1' } }, filter_alpha: '', filter_biased: '', filter_rmsd: '', entities: [createBlankStructureEntity(new Set())], filters: [], constraints: [], __editorTab: 'basic', __advancedTab: 'design-stage' }; } function normalizeData(nodeData = {}) { const base = createDefaultData({ autoName: nodeData?.default_name || nodeData?.name || 'boltzgen_1' }); const next = { ...base, ...(nodeData || {}) }; next.stages = { design: { ...base.stages.design, ...(nodeData?.stages?.design || {}) }, affinity: { ...base.stages.affinity, ...(nodeData?.stages?.affinity || {}) }, folding: { ...base.stages.folding, ...(nodeData?.stages?.folding || {}) }, inverse_folding: { ...base.stages.inverse_folding, ...(nodeData?.stages?.inverse_folding || {}) } }; next.entities = Array.isArray(nodeData?.entities) && nodeData.entities.length ? nodeData.entities.map((entity) => { if (entity?.kind === 'structure') { return { __uiId: String(entity?.__uiId || uid('boltz-entity')), kind: 'structure', chain: String(entity?.chain || '').trim().toUpperCase() || 'A', content: String(entity?.content || ''), file_name: String(entity?.file_name || ''), source: String(entity?.source || ''), directives: { ...defaultStructureDirectives(), ...(entity?.directives || {}) }, __settingsTab: entity?.__settingsTab || 'include' }; } if (entity?.kind === 'protein') { return { __uiId: String(entity?.__uiId || uid('boltz-entity')), kind: 'protein', chain: String(entity?.chain || '').trim().toUpperCase() || 'A', mode: entity?.mode === 'range' ? 'range' : 'sequence', sequence: String(entity?.sequence || ''), min: String(entity?.min || ''), max: String(entity?.max || ''), cyclic: !!entity?.cyclic, secondary_structure: String(entity?.secondary_structure || ''), binding_types: Array.isArray(entity?.binding_types) ? entity.binding_types : [], __settingsTab: entity?.__settingsTab || 'binding' }; } return { __uiId: String(entity?.__uiId || uid('boltz-entity')), kind: 'ligand', chain: String(entity?.chain || '').trim().toUpperCase() || 'A', code: String(entity?.code || ''), binding_type: normalizeBindingType(entity?.binding_type, 'b') }; }) : [createBlankStructureEntity(new Set())]; next.filters = Array.isArray(nodeData?.filters) ? nodeData.filters.map((row) => ({ metric: String(row?.metric || ''), comparator: String(row?.comparator || '>'), value: String(row?.value || '') })) : []; next.constraints = Array.isArray(nodeData?.constraints) ? nodeData.constraints.map((row) => { if (row?.kind === 'length') { return { kind: 'length', chain: String(row?.chain || ''), min: String(row?.min || ''), max: String(row?.max || '') }; } return { kind: 'bond', atom1: Array.isArray(row?.atom1) ? row.atom1.map((v) => String(v || '')) : ['', '', ''], atom2: Array.isArray(row?.atom2) ? row.atom2.map((v) => String(v || '')) : ['', '', ''] }; }) : []; return next; } function nextAvailableEntityChain(existingChains = new Set()) { const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; const used = new Set(Array.from(existingChains || []).map((v) => String(v || '').toUpperCase())); for (const ch of letters) { if (!used.has(ch)) return ch; } let i = 1; while (used.has(`X${i}`)) i += 1; return `X${i}`; } function collectUsedEntityChains(entities = []) { return new Set( (Array.isArray(entities) ? entities : []) .map((entity) => String(entity?.chain || '').trim().toUpperCase()) .filter(Boolean) ); } function normalizeBindingType(value, fallback = 'u') { const raw = String(value || '').trim().toLowerCase(); if (!raw) return fallback; if (raw === 'b' || raw === 'binding') return 'b'; if (raw === 'n' || raw === 'not_binding' || raw === 'not-binding' || raw === 'not binding') return 'n'; if (raw === 'u' || raw === 'unset' || raw === 'unspecified') return 'u'; return fallback; } function isPayloadEmptyValue(value) { if (value == null) return true; if (typeof value === 'string') return !value.trim(); if (Array.isArray(value)) return value.length === 0; if (isPlainObject(value)) return Object.keys(value).length === 0; return false; } function pruneEmptyPayload(value) { if (Array.isArray(value)) { return value .map((item) => pruneEmptyPayload(item)) .filter((item) => !isPayloadEmptyValue(item)); } if (isPlainObject(value)) { const out = {}; Object.entries(value).forEach(([key, item]) => { const next = pruneEmptyPayload(item); if (!isPayloadEmptyValue(next)) out[key] = next; }); return out; } if (typeof value === 'string') return value.trim(); return value; } function canonicalResidues(value) { if (!value) return ''; return String(value) .replace(/;/g, ',') .replace(/\s+/g, '') .split(',') .map((part) => part.replace(/(\d+)-(\d+)/g, '$1..$2')) .filter(Boolean) .join(','); } function parseResidueIndexString(raw) { const out = new Set(); canonicalResidues(raw) .split(',') .map((part) => part.trim()) .filter(Boolean) .forEach((part) => { const range = part.match(/^(\d+)\s*(?:\.\.|-)\s*(\d+)$/); if (range) { const start = Math.min(Number(range[1]), Number(range[2])); const end = Math.max(Number(range[1]), Number(range[2])); for (let i = start; i <= end; i += 1) out.add(i); return; } const single = Number(part); if (Number.isFinite(single)) out.add(single); }); return Array.from(out).sort((a, b) => a - b); } function buildBindingTypeString(length, directives = [], fallback = 'u') { const size = Math.max(0, Number(length) || 0); const chars = Array(size).fill(normalizeBindingType(fallback, 'u')); directives.forEach((directive) => { const mode = normalizeBindingType(directive?.mode, fallback); Array.from(directive?.positions || []).forEach((pos) => { const idx = Number(pos) - 1; if (idx < 0 || idx >= chars.length) return; chars[idx] = mode; }); }); return chars.join(''); } function parseSingleResidueIndex(raw) { const txt = canonicalResidues(raw); if (!txt) return undefined; const parts = txt.split(',').filter(Boolean); if (parts.length !== 1) return undefined; const match = parts[0].match(/^(\d+)$/); if (!match) return undefined; return Number(match[1]); } function renderSelectOptions(values, selected) { return (Array.isArray(values) ? values : []).map((value) => { const str = String(value); return ``; }).join(''); } function detectStructureFormat(name) { return /\.(cif|mmcif)$/i.test(String(name || '')) ? 'mmcif' : 'pdb'; } function getStructureFileFormat(name = '') { const s = String(name || '').trim().toLowerCase(); if (s.endsWith('.mmcif')) return 'mmcif'; if (s.endsWith('.cif')) return 'cif'; if (s.endsWith('.ent')) return 'pdb'; if (s.endsWith('.pdb')) return 'pdb'; return detectStructureFormat(name || 'structure.pdb'); } function finalizeChainMap(chainMap) { const authNums = Array.from(chainMap.keys()).sort((a, b) => a - b); if (!authNums.length) return []; const out = []; let label = 1; for (let i = 0; i < authNums.length; i += 1) { const auth = authNums[i]; out.push({ authNum: auth, labelNum: label, aa: chainMap.get(auth), isMissing: false }); label += 1; const next = authNums[i + 1]; if (next == null) continue; const gap = next - auth - 1; if (gap > 0 && gap <= 50) { for (let g = 1; g <= gap; g += 1) { out.push({ authNum: auth + g, labelNum: label, aa: 'X', isMissing: true }); label += 1; } } } return out; } function parsePdbChains(text) { const chains = {}; const seenResidues = new Set(); String(text || '').split(/\r?\n/).forEach((line) => { if (!line.startsWith('ATOM')) return; const chainId = (line[21] || '').trim(); const authNum = parseInt(line.slice(22, 26).trim(), 10); const insCode = (line[26] || '').trim(); if (!chainId || Number.isNaN(authNum)) return; const residueKey = `${chainId}:${authNum}:${insCode}`; if (seenResidues.has(residueKey)) return; seenResidues.add(residueKey); const resName = line.slice(17, 20).trim().toUpperCase(); const aa = BOLTZ_THREE_TO_ONE[resName] || 'X'; (chains[chainId] ||= new Map()).set(authNum, aa); }); const out = {}; Object.keys(chains).forEach((chain) => { out[chain] = finalizeChainMap(chains[chain]); }); return out; } function parseMMCIFChains(text) { const rows = []; const tokenRe = /'(?:[^']*)'|"(?:[^"]*)"|\S+/g; const lines = String(text || '').split(/\r?\n/); const headers = []; let collecting = false; for (let i = 0; i < lines.length; i += 1) { const line = lines[i].trim(); if (!line) continue; if (line === 'loop_') { collecting = false; headers.length = 0; continue; } if (line.startsWith('_atom_site.')) { collecting = true; headers.push(line); continue; } if (collecting && line.startsWith('_') && !line.startsWith('_atom_site.')) { collecting = false; continue; } if (!collecting || !headers.length) continue; if (line === '#') break; const tokens = []; let chunk = line.match(tokenRe) || []; while (chunk.length && tokens.length < headers.length) { tokens.push(...chunk); if (tokens.length >= headers.length) break; if (i + 1 >= lines.length) break; chunk = (lines[++i].trim().match(tokenRe) || []); } const row = {}; headers.forEach((h, idx) => { let val = tokens[idx] ?? ''; if ((val.startsWith("'") && val.endsWith("'")) || (val.startsWith('"') && val.endsWith('"'))) { val = val.slice(1, -1); } row[h] = val; }); rows.push(row); } const chains = {}; const seenResidues = new Set(); rows.forEach((row) => { const group = String(row['_atom_site.group_PDB'] || '').trim().toUpperCase(); if (group && group !== 'ATOM') return; const chain = String( row['_atom_site.auth_asym_id'] || row['_atom_site.label_asym_id'] || '' ).trim(); const authNum = parseInt( String(row['_atom_site.auth_seq_id'] || row['_atom_site.label_seq_id'] || ''), 10 ); const insCode = String(row['_atom_site.pdbx_PDB_ins_code'] || '').trim(); if (!chain || Number.isNaN(authNum)) return; const residueKey = `${chain}:${authNum}:${insCode}`; if (seenResidues.has(residueKey)) return; seenResidues.add(residueKey); const resName = String( row['_atom_site.auth_comp_id'] || row['_atom_site.label_comp_id'] || 'UNK' ).toUpperCase(); const aa = BOLTZ_THREE_TO_ONE[resName] || 'X'; (chains[chain] ||= new Map()).set(authNum, aa); }); const out = {}; Object.keys(chains).forEach((chain) => { out[chain] = finalizeChainMap(chains[chain]); }); return out; } function parseStructureChains(text, format) { try { return format === 'mmcif' ? parseMMCIFChains(text) : parsePdbChains(text); } catch (err) { console.warn('[boltzgen] parse structure failed', err); return {}; } } function buildAuthMaps(sequences) { const authToLabel = {}; const missing = {}; Object.keys(sequences || {}).forEach((chain) => { const map = new Map(); const missingArr = []; (sequences[chain] || []).forEach((r) => { if (r.isMissing) missingArr.push(r.authNum); else map.set(Number(r.authNum), Number(r.labelNum)); }); authToLabel[chain] = map; missing[chain] = missingArr; }); return { authToLabel, missing }; } function extractPdbChainIds(text) { const ids = new Set(); String(text || '').split(/\r?\n/).forEach((line) => { if (!line.startsWith('ATOM') && !line.startsWith('HETATM')) return; const chain = (line[21] || '').trim(); if (chain) ids.add(chain); }); return Array.from(ids).sort(); } function extractMMCIFChainIds(text) { const ids = new Set(); const tokenRe = /'(?:[^']*)'|"(?:[^"]*)"|\S+/g; const lines = String(text || '').split(/\r?\n/); const headers = []; let collecting = false; for (let i = 0; i < lines.length; i += 1) { const line = lines[i].trim(); if (!line) continue; if (line === 'loop_') { collecting = false; headers.length = 0; continue; } if (line.startsWith('_atom_site.')) { collecting = true; headers.push(line); continue; } if (collecting && line.startsWith('_') && !line.startsWith('_atom_site.')) { collecting = false; continue; } if (!collecting || !headers.length) continue; if (line === '#') break; const tokens = []; let chunk = line.match(tokenRe) || []; while (chunk.length && tokens.length < headers.length) { tokens.push(...chunk); if (tokens.length >= headers.length) break; if (i + 1 >= lines.length) break; chunk = (lines[++i].trim().match(tokenRe) || []); } const row = {}; headers.forEach((h, idx) => { let val = tokens[idx] ?? ''; if ((val.startsWith("'") && val.endsWith("'")) || (val.startsWith('"') && val.endsWith('"'))) { val = val.slice(1, -1); } row[h] = val; }); const chain = String(row['_atom_site.auth_asym_id'] || row['_atom_site.label_asym_id'] || '').trim(); if (chain) ids.add(chain); } return Array.from(ids).sort(); } function extractStructureChainIds(text, format) { try { return format === 'mmcif' ? extractMMCIFChainIds(text) : extractPdbChainIds(text); } catch (err) { console.warn('[boltzgen] chain extraction failed', err); return []; } } function rangesFromNumbers(nums) { const sorted = Array.from(new Set((nums || []).map(Number).filter(Number.isFinite))).sort((a, b) => a - b); if (!sorted.length) return []; const out = []; let start = sorted[0]; let prev = sorted[0]; for (let i = 1; i < sorted.length; i += 1) { const n = sorted[i]; if (n === prev + 1) { prev = n; continue; } out.push([start, prev]); start = prev = n; } out.push([start, prev]); return out; } function formatRange(range) { return range[0] === range[1] ? String(range[0]) : `${range[0]}-${range[1]}`; } function translateResidues(state, chainId, raw) { const txt = canonicalResidues(raw); const map = state?.authToLabel?.[chainId]; if (!map || !txt) return txt; const labels = []; txt.split(/[;,]/).map((s) => s.trim()).filter(Boolean).forEach((part) => { const m = part.match(/^(\d+)\s*(?:\.\.|-)\s*(\d+)$/); if (m) { const lo = Math.min(Number(m[1]), Number(m[2])); const hi = Math.max(Number(m[1]), Number(m[2])); Array.from(map.keys()).sort((a, b) => a - b).forEach((auth) => { if (auth >= lo && auth <= hi) labels.push(map.get(auth)); }); return; } const n = Number(part); if (Number.isFinite(n) && map.has(n)) labels.push(map.get(n)); }); return rangesFromNumbers(labels) .map(([a, b]) => (a === b ? `${a}` : `${a}..${b}`)) .join(','); } function getStructurePayloadState(entity) { const format = getStructureFileFormat(entity?.file_name || ''); const sequences = parseStructureChains(entity?.content || '', format); const maps = buildAuthMaps(sequences); return { format, sequences, authToLabel: maps.authToLabel, missingAuthNums: maps.missing }; } function collectStructureBindingTypeStrings(entity, payloadState) { const directives = Array.isArray(entity?.directives?.binding_types) ? entity.directives.binding_types : []; const perChain = new Map(); directives.forEach((directive) => { const raw = String(directive?.residues || '').trim(); const chain = String(directive?.chain || '').trim(); if (!raw || !chain) return; const translated = translateResidues(payloadState, chain, raw); const positions = parseResidueIndexString(translated); if (!positions.length) return; if (!perChain.has(chain)) perChain.set(chain, []); perChain.get(chain).push({ mode: normalizeBindingType(directive?.mode, 'u'), positions }); }); return Array.from(perChain.entries()) .map(([chain, rows]) => { const seqLength = Array.isArray(payloadState?.sequences?.[chain]) ? payloadState.sequences[chain].length : 0; if (!seqLength) return null; return { chain: { id: chain, binding_types: buildBindingTypeString(seqLength, rows, 'u') } }; }) .filter(Boolean); } function collectProteinBindingTypeStringFromEntity(entity, { strict = false } = {}) { const rows = Array.isArray(entity?.binding_types) ? entity.binding_types : []; if (!rows.length) return undefined; const mode = String(entity?.mode || 'sequence').trim(); let seqLength = 0; if (mode === 'sequence') { const seq = String(entity?.sequence || '').replace(/\s+/g, '').trim(); if (!seq) { if (strict) { throw new Error(`Protein ${entity?.chain || '?'}: provide a sequence before using binding types.`); } return undefined; } seqLength = seq.length; } else { const min = parseInt(String(entity?.min || ''), 10); const max = parseInt(String(entity?.max || ''), 10); if (Number.isFinite(min) && Number.isFinite(max) && min > 0 && min === max) { seqLength = min; } else { if (strict) { throw new Error( `Protein ${entity?.chain || '?'}: binding types need a fixed sequence length. Use Sequence mode, or set Min length and Max length to the same value.` ); } return undefined; } } const directives = []; rows.forEach((row) => { const raw = String(row?.residues || '').trim(); if (!raw) return; directives.push({ mode: normalizeBindingType(row?.mode, 'u'), positions: parseResidueIndexString(raw) }); }); if (!directives.length) return undefined; return buildBindingTypeString(seqLength, directives, 'u'); } function isCcdLikeLigand(raw) { const normalized = String(raw || '').replace(/\s+/g, ''); return /^[A-Za-z0-9]{2,6}$/.test(normalized) && !/[a-z]/.test(normalized); } function parseLooseScalar(raw) { const value = String(raw ?? '').trim(); if (!value) return undefined; const lower = value.toLowerCase(); if (lower === 'true') return true; if (lower === 'false') return false; const num = Number(value); if (Number.isFinite(num)) return num; return value; } function renderStageSelect(fieldKey, selected) { const [, group] = String(fieldKey).split('.'); const sourceKey = group === 'recycling_steps' ? 'recycles' : group === 'sampling_steps' ? 'sampling' : 'diffusion_samples'; return ` `; } function structureAddLabelForKey(key) { return ({ include: 'Add include', include_proximity: 'Add proximity', binding_types: 'Add binding', structure_groups: 'Add group', design: 'Add region', secondary_structure: 'Add rule', design_insertions: 'Add insertion' })[String(key || '').trim()] || 'Add row'; } function structureDirectiveChainFieldHtml(value = '') { return `
${fieldHeadHtml('Chain ID', 'Leave blank and select residues in the sequence viewer. The chain will auto-fill from your highlight.')}
`; } function entityPreviewGridHtml(viewerId, seqId) { return ` `; } function animateIn(el, opts = {}) { if (!el) return; const duration = Number(opts.duration || 260); const easing = opts.easing || 'cubic-bezier(0.22, 1, 0.36, 1)'; const y = Number(opts.y || 10); el.style.removeProperty('display'); if (getComputedStyle(el).display === 'none') { el.style.display = 'block'; } el.style.overflow = 'hidden'; el.style.maxHeight = '0px'; el.style.opacity = '0'; el.style.transform = `translateY(${y}px)`; el.style.transition = 'none'; void el.offsetHeight; const targetHeight = Math.max(el.scrollHeight, 1); requestAnimationFrame(() => { el.style.transition = [ `max-height ${duration}ms ${easing}`, `opacity ${duration}ms ease`, `transform ${duration}ms ${easing}` ].join(', '); el.style.maxHeight = `${targetHeight}px`; el.style.opacity = '1'; el.style.transform = 'translateY(0)'; }); const cleanup = (ev) => { if (ev && ev.target !== el) return; el.removeEventListener('transitionend', cleanup); el.style.removeProperty('overflow'); el.style.removeProperty('maxHeight'); el.style.removeProperty('opacity'); el.style.removeProperty('transform'); el.style.removeProperty('transition'); }; el.addEventListener('transitionend', cleanup); } function animateOut(el, done, opts = {}) { if (!el) { if (typeof done === 'function') done(); return; } const duration = Number(opts.duration || 220); const easing = opts.easing || 'cubic-bezier(0.22, 1, 0.36, 1)'; const y = Number(opts.y || -8); el.style.overflow = 'hidden'; el.style.maxHeight = `${Math.max(el.scrollHeight, 1)}px`; el.style.opacity = '1'; el.style.transform = 'translateY(0)'; el.style.transition = 'none'; void el.offsetHeight; requestAnimationFrame(() => { el.style.transition = [ `max-height ${duration}ms ${easing}`, `opacity ${duration}ms ease`, `transform ${duration}ms ${easing}` ].join(', '); el.style.maxHeight = '0px'; el.style.opacity = '0'; el.style.transform = `translateY(${y}px)`; }); const cleanup = (ev) => { if (ev && ev.target !== el) return; el.removeEventListener('transitionend', cleanup); el.remove(); if (typeof done === 'function') done(); }; el.addEventListener('transitionend', cleanup); } function animateAutoHeight(container, mutator, opts = {}) { if (!container) { if (typeof mutator === 'function') mutator(); return; } const duration = Number(opts.duration || 260); const easing = opts.easing || 'cubic-bezier(0.22, 1, 0.36, 1)'; const startHeight = Math.max(container.getBoundingClientRect().height, 1); container.style.height = `${startHeight}px`; container.style.maxHeight = 'none'; container.style.overflow = 'hidden'; container.style.transition = 'none'; if (typeof mutator === 'function') mutator(); requestAnimationFrame(() => { const endHeight = Math.max(container.scrollHeight, 1); container.style.transition = `height ${duration}ms ${easing}`; container.style.height = `${endHeight}px`; let cleaned = false; const cleanup = () => { if (cleaned) return; cleaned = true; container.removeEventListener('transitionend', onEnd); container.style.removeProperty('height'); container.style.removeProperty('maxHeight'); container.style.removeProperty('overflow'); container.style.removeProperty('transition'); }; const onEnd = (ev) => { if (ev && ev.target !== container) return; cleanup(); }; container.addEventListener('transitionend', onEnd); setTimeout(cleanup, duration + 80); }); } function showStructurePreviewGrid(state) { state.previewGrid = q('[data-preview-grid]', state.node) || state.previewGrid; const grid = state.previewGrid; if (!grid || !grid.classList.contains('is-hidden')) return; animateAutoHeight(state.node, () => { grid.classList.remove('is-hidden'); }, { duration: 280 }); } function hideStructurePreviewGrid(state) { state.previewGrid = q('[data-preview-grid]', state.node) || state.previewGrid; const grid = state.previewGrid; if (!grid || grid.classList.contains('is-hidden')) return; animateAutoHeight(state.node, () => { grid.classList.add('is-hidden'); }, { duration: 240 }); } async function ensureMolstar() { if (window.molstar?.Viewer) return window.molstar; return new Promise((resolve, reject) => { const existing = document.querySelector('script[src*="molstar.js"]'); if (!existing) { reject(new Error('Mol* script tag not found')); return; } if (window.molstar?.Viewer) { resolve(window.molstar); return; } existing.addEventListener('load', () => resolve(window.molstar), { once: true }); existing.addEventListener('error', () => reject(new Error('Mol* failed to load')), { once: true }); }); } const VIEWERS = Object.create(null); async function renderMolstar(state, text, format) { state.viewerHost = q('[data-preview-host]', state.node) || state.viewerHost; if (!state.viewerHost) return; try { const mol = await ensureMolstar(); const ViewerCtor = mol?.Viewer || mol?.default?.Viewer || window.molstar?.Viewer; if (!ViewerCtor) return; let viewer = VIEWERS[state.viewerId]; if (!viewer) { viewer = await ViewerCtor.create(state.viewerId, { layoutIsExpanded: false, layoutShowControls: false, layoutShowLeftPanel: false, layoutShowRemoteState: false, viewportShowExpand: false, showControls: false, backgroundColor: 0x0b1420 }); VIEWERS[state.viewerId] = viewer; } try { viewer?.plugin?.clear?.(); } catch {} if (state.objectUrl) { try { URL.revokeObjectURL(state.objectUrl); } catch {} state.objectUrl = null; } state.objectUrl = URL.createObjectURL(new Blob([text], { type: 'text/plain' })); await viewer.loadStructureFromUrl( state.objectUrl, format, false, { label: state.fileName || 'Structure preview' } ); if (state.previewEmpty) state.previewEmpty.style.display = 'none'; } catch (err) { console.warn('[boltzgen] Mol* render failed', err); } } function clearMolstar(state) { const viewer = VIEWERS[state.viewerId]; try { viewer?.plugin?.clear?.(); } catch {} if (state.objectUrl) { try { URL.revokeObjectURL(state.objectUrl); } catch {} state.objectUrl = null; } if (state.previewEmpty) state.previewEmpty.style.display = ''; } function ensureSelectionState(state) { if (!state.selectionMap) state.selectionMap = new Map(); if (!state.colorCursor) state.colorCursor = 1; } function getSelectionColorForInput(state, input) { ensureSelectionState(state); if (!input.dataset.seqColor) { input.dataset.seqColor = String(state.colorCursor); state.colorCursor = state.colorCursor >= 6 ? 1 : state.colorCursor + 1; } return input.dataset.seqColor; } function parseAuthSelectionText(raw) { const nums = new Set(); String(raw || '') .split(',') .map((part) => part.trim()) .filter(Boolean) .forEach((part) => { const range = part.match(/^(\d+)\s*(?:\.\.|-)\s*(\d+)$/); if (range) { const start = Math.min(Number(range[1]), Number(range[2])); const end = Math.max(Number(range[1]), Number(range[2])); for (let n = start; n <= end; n += 1) nums.add(n); return; } const single = Number(part); if (Number.isFinite(single)) nums.add(single); }); return Array.from(nums).sort((a, b) => a - b); } function renderSelectionHighlights(state) { if (!state?.sequenceHost) return; ensureSelectionState(state); qa('.seq-res', state.sequenceHost).forEach((el) => { el.classList.remove('selected', 'hl-1', 'hl-2', 'hl-3', 'hl-4', 'hl-5', 'hl-6'); }); Array.from(state.selectionMap.entries()).forEach(([input, selection]) => { if (!input?.isConnected) { state.selectionMap.delete(input); return; } const panel = input.closest('[data-settings-panel]'); if (panel && !panel.classList.contains('is-active')) return; const color = selection.color || getSelectionColorForInput(state, input); Array.from(selection.authNums || []).forEach((auth) => { qa(`.seq-res[data-chain="${selection.chain}"][data-auth="${auth}"]`, state.sequenceHost).forEach((el) => { el.classList.add('selected', `hl-${color}`); }); }); }); } function syncInputSelectionFromValue(state, input) { if (!state || !input) return; ensureSelectionState(state); const chain = String(input.dataset.seqChain || state.defaultSeqChain || '').trim(); const nums = parseAuthSelectionText(input.value); if (!chain || !nums.length) { state.selectionMap.delete(input); renderSelectionHighlights(state); return; } state.selectionMap.set(input, { chain, authNums: new Set(nums), color: getSelectionColorForInput(state, input) }); renderSelectionHighlights(state); } function clearSequenceSelection(state, input = null) { if (!state?.sequenceHost) return; ensureSelectionState(state); if (input) state.selectionMap.delete(input); else state.selectionMap.clear(); renderSelectionHighlights(state); } function setSeqTargetFocus(state, input) { qa('.seq-focus', state.node).forEach((el) => el.classList.remove('seq-focus')); if (!input) { state.activeInput = null; return; } input.classList.add('seq-focus'); getSelectionColorForInput(state, input); state.activeInput = { input }; syncInputSelectionFromValue(state, input); } function updateActiveSequenceSelection(state, target, mode = 'toggle') { const input = state?.activeInput?.input; if (!input || !target) return; ensureSelectionState(state); const chain = String(target.dataset.chain || '').trim(); const auth = Number(target.dataset.auth); if (!chain || !Number.isFinite(auth)) return; let record = state.selectionMap.get(input); if (!record) { record = { chain, authNums: new Set(), color: getSelectionColorForInput(state, input) }; } if (record.chain !== chain) { record.chain = chain; record.authNums = new Set(); } if (mode === 'add') { record.authNums.add(auth); } else if (mode === 'remove') { record.authNums.delete(auth); } else { if (record.authNums.has(auth)) record.authNums.delete(auth); else record.authNums.add(auth); } const row = input.closest('.entity-subrow'); const chainField = row ? q('.structure-row-chain', row) : null; input.dataset.seqChain = chain; if (chainField) chainField.value = chain; input.value = rangesFromNumbers(Array.from(record.authNums)) .map(formatRange) .join(','); if (record.authNums.size) state.selectionMap.set(input, record); else state.selectionMap.delete(input); input.dispatchEvent(new Event('input', { bubbles: true })); renderSelectionHighlights(state); } function renderSequenceViewer(state, sequences) { state.sequences = sequences; const host = state.sequenceHost; if (!host) return; host.innerHTML = ''; const chainKeys = Object.keys(sequences || {}); if (state.sequenceEmpty) { state.sequenceEmpty.style.display = chainKeys.length ? 'none' : ''; } const viewer = document.createElement('div'); viewer.className = 'sequence-viewer'; chainKeys.forEach((chain) => { const chainWrap = document.createElement('div'); chainWrap.className = 'sequence-chain'; const label = document.createElement('div'); label.className = 'sequence-chain__label'; label.textContent = `Chain ${chain}`; const grid = document.createElement('div'); grid.className = 'sequence-highlightable'; grid.dataset.chain = chain; (sequences[chain] || []).forEach((res) => { const span = document.createElement('span'); span.className = `seq-res${res.isMissing ? ' is-missing' : ''}`; span.dataset.chain = chain; span.dataset.auth = String(res.authNum); span.dataset.label = String(res.labelNum); span.dataset.pos = String(res.authNum); span.textContent = res.aa || 'X'; grid.appendChild(span); }); chainWrap.append(label, grid); viewer.appendChild(chainWrap); }); host.appendChild(viewer); state.isDragging = false; state.dragMode = ''; state.dragChain = ''; state.dragSeen = new Set(); host.onmousedown = (ev) => { const target = ev.target.closest('.seq-res'); if (!target || !state?.activeInput?.input) return; const activeRecord = state.selectionMap.get(state.activeInput.input); const auth = Number(target.dataset.auth); const alreadySelected = !!( activeRecord && activeRecord.chain === String(target.dataset.chain || '').trim() && activeRecord.authNums?.has(auth) ); state.isDragging = true; state.dragMode = alreadySelected ? 'remove' : 'add'; state.dragChain = String(target.dataset.chain || '').trim(); state.dragSeen = new Set(); updateActiveSequenceSelection(state, target, state.dragMode); state.dragSeen.add(`${state.dragChain}:${auth}`); ev.preventDefault(); }; host.onmouseover = (ev) => { const target = ev.target.closest('.seq-res'); if (!target || !state.isDragging) return; if (String(target.dataset.chain || '').trim() !== state.dragChain) return; const key = `${target.dataset.chain}:${target.dataset.auth}`; if (state.dragSeen.has(key)) return; state.dragSeen.add(key); updateActiveSequenceSelection(state, target, state.dragMode); }; host.onclick = (ev) => { if (ev.target.closest('.seq-res')) ev.preventDefault(); }; if (!state.docMouseUpBound) { state.docMouseUpBound = true; document.addEventListener('mouseup', () => { state.isDragging = false; state.dragMode = ''; state.dragChain = ''; state.dragSeen = new Set(); }); } renderSelectionHighlights(state); } async function loadStructureIntoState(state, text, name, source) { const format = detectStructureFormat(name); state.text = text; state.fileName = name; state.source = source; state.format = format; state.structureChainIds = extractStructureChainIds(text, format); state.selectionMap = new Map(); state.colorCursor = 1; showStructurePreviewGrid(state); await renderMolstar(state, text, format); state.sequenceHost = q('[data-sequence-host]', state.node) || state.sequenceHost; const sequences = parseStructureChains(text, format); renderSequenceViewer(state, sequences); const maps = buildAuthMaps(sequences); state.authToLabel = maps.authToLabel; state.missingAuthNums = maps.missing; state.defaultSeqChain = Object.keys(sequences || {})[0] || String(state.node.dataset.entityChain || 'A'); qa('.seq-target-input', state.node).forEach((input) => { syncInputSelectionFromValue(state, input); }); const meta = q('.drop-zone__meta', state.node); const zone = q('.drop-zone', state.node); if (meta) { meta.innerHTML = utils.defaultMetaHtml({ label: name || 'Loaded file', sizeBytes: text.length, content: text }); } if (zone) zone.classList.add('is-filled'); } function clearStructureState(state) { state.fileInput.value = ''; state.text = ''; state.fileName = ''; state.source = ''; state.format = ''; state.defaultSeqChain = ''; state.selectionMap = new Map(); state.colorCursor = 1; state.activeInput = null; clearMolstar(state); hideStructurePreviewGrid(state); state.sequenceHost = q('[data-sequence-host]', state.node) || state.sequenceHost; if (state.sequenceHost) state.sequenceHost.innerHTML = ''; if (state.sequenceEmpty) state.sequenceEmpty.style.display = ''; clearSequenceSelection(state); const meta = q('.drop-zone__meta', state.node); const zone = q('.drop-zone', state.node); if (meta) meta.textContent = 'Drop file here or click to upload a PDB or CIF/mmCIF file'; if (zone) zone.classList.remove('is-filled'); } function renderStructureDirectiveRow(kind, row = {}, nodeId, entityIndex, rowIndex) { const chain = String(row?.chain || ''); const residues = String(row?.residues || ''); const groupId = String(row?.groupId || ''); const visibility = String(row?.visibility || '1'); const mode = normalizeBindingType(row?.mode, 'u'); const residue = String(row?.residue || ''); const numResidues = String(row?.numResidues || ''); const secondaryStructure = String(row?.secondaryStructure || 'UNSPECIFIED'); const radius = String(row?.radius || ''); const loop = String(row?.loop || ''); const helix = String(row?.helix || ''); const sheet = String(row?.sheet || ''); const remove = ` `; if (kind === 'include') { return `
${structureDirectiveChainFieldHtml(chain)}
${fieldHeadHtml('Residues', 'Residues or ranges to include.')}
${remove}
`; } if (kind === 'include_proximity') { return `
${structureDirectiveChainFieldHtml(chain)}
${fieldHeadHtml('Residues', 'Residues used as the proximity anchor.')}
${fieldHeadHtml('Radius', 'Distance cutoff for proximity include.')}
${remove}
`; } if (kind === 'binding_types') { return `
${structureDirectiveChainFieldHtml(chain)}
${fieldHeadHtml('Mode', 'Pick the code to assign to these residues.')}
${fieldHeadHtml('Residues', 'Residues or ranges to assign this binding code to.')}
${remove}
`; } if (kind === 'structure_groups') { return `
${structureDirectiveChainFieldHtml(chain)}
${fieldHeadHtml('Group ID', 'Custom group identifier.')}
${fieldHeadHtml('Visibility', 'Visibility flag for this group.')}
${fieldHeadHtml('Residues', 'Residues or ranges belonging to this group.')}
${remove}
`; } if (kind === 'design') { return `
${structureDirectiveChainFieldHtml(chain)}
${fieldHeadHtml('Residues', 'Residues or ranges that are designable.')}
${remove}
`; } if (kind === 'secondary_structure') { return `
${structureDirectiveChainFieldHtml(chain)}
${fieldHeadHtml('Loop', 'Loop residues or ranges.')}
${fieldHeadHtml('Helix', 'Helix residues or ranges.')}
${fieldHeadHtml('Sheet', 'Sheet residues or ranges.')}
${remove}
`; } return `
${structureDirectiveChainFieldHtml(chain)}
${fieldHeadHtml('After residue', 'Residue after which insertion is added.')}
${fieldHeadHtml('Num residues', 'How many residues to insert.')}
${fieldHeadHtml('Secondary structure', 'Preferred secondary structure for the insertion.')}
${remove}
`; } function renderStructureSettings(entity, entityIndex) { const directives = entity?.directives || defaultStructureDirectives(); const activeKey = String(entity?.__settingsTab || 'include'); const sections = [ ['include', 'Include residues'], ['include_proximity', 'Include proximity'], ['binding_types', 'Binding types'], ['structure_groups', 'Structure groups'], ['design', 'Design regions'], ['secondary_structure', 'Secondary structure'], ['design_insertions', 'Design insertions'] ]; return `
${sections.map(([key, label]) => ` `).join('')}
${sections.map(([key]) => `
${(Array.isArray(directives[key]) ? directives[key] : []).map((row, rowIndex) => renderStructureDirectiveRow(key, row, '', entityIndex, rowIndex) ).join('')}
`).join('')}
`; } function renderProteinBindingRow(row = {}, entityIndex, rowIndex) { const mode = normalizeBindingType(row?.mode, 'u'); const residues = String(row?.residues || ''); return `
${fieldHeadHtml('Mode', 'Pick the code to assign to these residues.')}
${fieldHeadHtml('Residues or ranges', 'Residues or ranges to assign this binding code to.')}
`; } function renderStructureEntity(entity, entityIndex, nodeId) { const uiId = String(entity.__uiId || `entity-${entityIndex}`); const viewerId = `boltzgen-viewer-${nodeId}-${uiId}`; const seqId = `boltzgen-seq-${nodeId}-${uiId}`; return `
${fieldHeadHtml('Structure', 'Required target structure input for BoltzGen.')}
${fieldHeadHtml('Chain', 'Entity chain label.')}
${viciToggleButtonHtml('Toggle Vici Lookup')}
${fieldHeadHtml('Structure file', 'Upload or drop a PDB or CIF/mmCIF file.')}
Structure file
${(entity.content && entity.file_name) ? utils.defaultMetaHtml({ label: entity.file_name, sizeBytes: entity.content.length, content: entity.content }) : 'Drop file here or click to upload a PDB or CIF/mmCIF file'}
${entityPreviewGridHtml(viewerId, seqId)} ${renderStructureSettings(entity, entityIndex)}
`; } function renderProteinEntity(entity, entityIndex) { const activeTab = String(entity?.__settingsTab || 'binding'); return `
${fieldHeadHtml('Protein', 'Optional protein binder or designed protein input.')}
${fieldHeadHtml('Chain', 'Entity chain label.')}
${fieldHeadHtml('Specification', 'Choose sequence or length range.')}
${fieldHeadHtml('Cyclic', 'Choose whether the protein should be cyclic.')}
${viciToggleButtonHtml('Toggle Vici Lookup')}
${fieldHeadHtml('Sequence', 'Protein sequence input.')}
${fieldHeadHtml('Min length', 'Minimum allowed protein length.')}
${fieldHeadHtml('Max length', 'Maximum allowed protein length.')}
${(Array.isArray(entity.binding_types) ? entity.binding_types : []).map((row, rowIndex) => renderProteinBindingRow(row, entityIndex, rowIndex) ).join('')}
${fieldHeadHtml('Secondary structure', 'Optional secondary structure guide.')}
`; } function renderLigandEntity(entity, entityIndex) { return `
${fieldHeadHtml('Ligand', 'Optional ligand input as CCD code or SMILES.')}
${fieldHeadHtml('Chain', 'Entity chain label.')}
${fieldHeadHtml('Binding', 'Set whether the ligand should bind or not bind.')}
${viciToggleButtonHtml('Toggle Vici Lookup')}
${fieldHeadHtml('CCD code or SMILES', 'Provide a CCD code or a full SMILES string.')}
`; } function renderEntityBlock(entity, entityIndex, nodeId) { if (entity.kind === 'structure') return renderStructureEntity(entity, entityIndex, nodeId); if (entity.kind === 'protein') return renderProteinEntity(entity, entityIndex); return renderLigandEntity(entity, entityIndex); } function renderFilterRow(row, index) { return `

Additional filter

${fieldHeadHtml('Metric', 'Metric name to filter on.')}
${fieldHeadHtml('Comparator', 'Comparison operator for the metric value.')}
${fieldHeadHtml('Value', 'Threshold value for the filter.')}
`; } function renderConstraintRow(row, index, chainOptions) { const optionsHtml = `` + chainOptions.map((id) => ``).join(''); if (row.kind === 'length') { return `

Length constraint

${fieldHeadHtml('Chain', 'Chain to constrain by length.')}
${fieldHeadHtml('Min length', 'Minimum allowed length.')}
${fieldHeadHtml('Max length', 'Maximum allowed length.')}
`; } return `

Bond constraint

${fieldHeadHtml('Chain A', 'First chain in the bond constraint.')}
${fieldHeadHtml('Residue A', 'First residue in the bond constraint.')}
${fieldHeadHtml('Atom A', 'First atom name in the bond constraint.')}
${fieldHeadHtml('Chain B', 'Second chain in the bond constraint.')}
${fieldHeadHtml('Residue B', 'Second residue in the bond constraint.')}
${fieldHeadHtml('Atom B', 'Second atom name in the bond constraint.')}
`; } function renderBody({ nodeId, nodeData }) { const data = normalizeData(nodeData); const activeTab = data.__editorTab === 'advanced' ? 'advanced' : 'basic'; const advTab = data.__advancedTab || 'design-stage'; const chainOptions = data.entities.map((entity) => String(entity.chain || '').trim()).filter(Boolean); return `
${helpBubbleHtml('Short label for this run. It will be canonicalized into a safe ID on execute.', 'help-boltzgen-name')}
Type anything; we’ll convert it to a safe ID when you run.
${helpBubbleHtml('Pick a preset protocol matching your modality.', 'help-boltzgen-protocol')}
${helpBubbleHtml('Total number of candidate designs to generate.', 'help-boltzgen-numdesigns')}
${helpBubbleHtml('How many designs to keep after filtering.', 'help-boltzgen-budget')}
${[ ['design-stage', 'Design stage'], ['affinity-stage', 'Affinity stage'], ['fold-stage', 'Fold stage'], ['inverse-stage', 'Inverse fold stage'], ['filtering', 'Filtering'], ['constraints', 'Constraints'] ].map(([key, label]) => ` `).join('')}
${fieldHeadHtml('Recycling steps', 'Number of recycle passes used during the design stage.')}
${renderStageSelect('stages.design.recycling_steps', data.stages.design.recycling_steps)}
${fieldHeadHtml('Sampling steps', 'Number of sampling steps used during the design stage.')}
${renderStageSelect('stages.design.sampling_steps', data.stages.design.sampling_steps)}
${fieldHeadHtml('Diffusion samples', 'How many diffusion samples to generate in the design stage.')}
${renderStageSelect('stages.design.diffusion_samples', data.stages.design.diffusion_samples)}
${fieldHeadHtml('Recycling steps', 'Number of recycle passes used during the affinity stage.')}
${renderStageSelect('stages.affinity.recycling_steps', data.stages.affinity.recycling_steps)}
${fieldHeadHtml('Sampling steps', 'Number of sampling steps used during the affinity stage.')}
${renderStageSelect('stages.affinity.sampling_steps', data.stages.affinity.sampling_steps)}
${fieldHeadHtml('Diffusion samples', 'How many diffusion samples to generate in the affinity stage.')}
${renderStageSelect('stages.affinity.diffusion_samples', data.stages.affinity.diffusion_samples)}
${fieldHeadHtml('Recycling steps', 'Number of recycle passes used during the fold stage.')}
${renderStageSelect('stages.folding.recycling_steps', data.stages.folding.recycling_steps)}
${fieldHeadHtml('Sampling steps', 'Number of sampling steps used during the fold stage.')}
${renderStageSelect('stages.folding.sampling_steps', data.stages.folding.sampling_steps)}
${fieldHeadHtml('Diffusion samples', 'How many diffusion samples to generate in the fold stage.')}
${renderStageSelect('stages.folding.diffusion_samples', data.stages.folding.diffusion_samples)}
${fieldHeadHtml('Recycling steps', 'Number of recycle passes used during the inverse fold stage.')}
${renderStageSelect('stages.inverse_folding.recycling_steps', data.stages.inverse_folding.recycling_steps)}
${fieldHeadHtml('Sampling steps', 'Number of sampling steps used during the inverse fold stage.')}
${renderStageSelect('stages.inverse_folding.sampling_steps', data.stages.inverse_folding.sampling_steps)}
${fieldHeadHtml('Diffusion samples', 'How many diffusion samples to generate in the inverse fold stage.')}
${renderStageSelect('stages.inverse_folding.diffusion_samples', data.stages.inverse_folding.diffusion_samples)}
${fieldHeadHtml('Alpha', 'Controls filtering aggressiveness in BoltzGen ranking.')}
${fieldHeadHtml('Filter biased', 'Matches the CLI biased filtering switch.')}
${fieldHeadHtml('Refolding RMSD threshold', 'Optional numeric threshold. Leave blank to disable.')}
${data.filters.map((row, index) => renderFilterRow(row, index)).join('')}
${data.constraints.map((row, index) => renderConstraintRow(row, index, chainOptions)).join('')}
${helpBubbleHtml('Add at least one entity. Typical setup is a target structure plus designed protein or ligand.', 'help-boltzgen-entities')}
${data.entities.map((entity, entityIndex) => renderEntityBlock(entity, entityIndex, nodeId)).join('')}
`; } function setByPath(obj, path, value) { const parts = String(path || '').split('.'); if (!parts.length) return obj; const next = clone(obj); let cursor = next; for (let i = 0; i < parts.length - 1; i += 1) { const key = /^\d+$/.test(parts[i]) ? Number(parts[i]) : parts[i]; const nextKey = parts[i + 1]; if (cursor[key] == null) { cursor[key] = /^\d+$/.test(nextKey) ? [] : {}; } cursor = cursor[key]; } const last = /^\d+$/.test(parts[parts.length - 1]) ? Number(parts[parts.length - 1]) : parts[parts.length - 1]; cursor[last] = value; return next; } function toggleLookup(btn, mount) { if (!btn) return; if (typeof window.toggleViciLookup === 'function') { window.toggleViciLookup(btn); return; } const block = btn.closest('.molecule-block'); 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); } } } async function fetchPresetText(url) { const resp = await fetch(url); if (!resp.ok) throw new Error(`Could not fetch preset structure: ${resp.status}`); return await resp.text(); } async function applyPresetToData(data, presetKey) { const preset = PRESETS[presetKey]; if (!preset) throw new Error('Unknown preset.'); const next = createDefaultData({ autoName: data?.default_name || data?.name || 'boltzgen_1' }); next.name = preset.name; next.protocol = preset.protocol; next.num_designs = '10'; next.budget = '2'; next.filters = clone(preset.filters || []); next.constraints = clone(preset.constraints || []); next.entities = []; const structureText = await fetchPresetText(preset.structure.url); next.entities.push({ kind: 'structure', chain: preset.structure.chain || 'A', content: structureText, file_name: preset.structure.fileName, source: 'preset', directives: clone(preset.structure.directives || defaultStructureDirectives()), __settingsTab: 'include' }); (preset.extraEntities || []).forEach((entity) => { if (entity.kind === 'protein') { next.entities.push({ kind: 'protein', chain: entity.chain || nextAvailableEntityChain(collectUsedEntityChains(next.entities)), mode: entity.mode || 'sequence', sequence: entity.sequence || '', min: entity.min || '', max: entity.max || '', cyclic: !!entity.cyclic, secondary_structure: entity.secondary_structure || '', binding_types: clone(entity.binding_types || []), __settingsTab: entity.__settingsTab || 'binding' }); } else if (entity.kind === 'ligand') { next.entities.push({ kind: 'ligand', chain: entity.chain || nextAvailableEntityChain(collectUsedEntityChains(next.entities)), code: entity.code || '', binding_type: normalizeBindingType(entity.binding_type, 'b') }); } }); return next; } function gatherStructureDirectivesForPayload(entity) { const payloadState = getStructurePayloadState(entity); const fileObj = { path: String(entity.file_name || '').trim(), file_content: String(entity.content || ''), include: [], include_proximity: [], binding_types: [], structure_groups: [], design: [], secondary_structure: [], design_insertions: [] }; fileObj.include = (entity.directives.include || []).map((row) => { const chain = String(row.chain || '').trim(); const raw = String(row.residues || '').trim(); if (!chain) return null; if (!raw) return { chain: { id: chain } }; const res = translateResidues(payloadState, chain, raw); if (!res) return null; return { chain: { id: chain, res_index: res } }; }).filter(Boolean); fileObj.include_proximity = (entity.directives.include_proximity || []).map((row) => { const chain = String(row.chain || '').trim(); const raw = String(row.residues || '').trim(); if (!chain || !raw) return null; const res = translateResidues(payloadState, chain, raw); const out = { chain: { id: chain, res_index: res } }; if (String(row.radius || '').trim()) out.radius = Number(row.radius); return out; }).filter(Boolean); fileObj.binding_types = collectStructureBindingTypeStrings(entity, payloadState); fileObj.structure_groups = (entity.directives.structure_groups || []).map((row) => { const chain = String(row.chain || '').trim(); if (!chain) return null; const group = { id: chain }; if (String(row.groupId || '').trim()) group.group_id = String(row.groupId).trim(); if (String(row.visibility || '').trim()) group.visibility = Number(row.visibility); if (String(row.residues || '').trim()) { const res = translateResidues(payloadState, chain, row.residues); if (res) group.res_index = res; } return { group }; }).filter(Boolean); fileObj.design = (entity.directives.design || []).map((row) => { const chain = String(row.chain || '').trim(); const raw = String(row.residues || '').trim(); if (!chain || !raw) return null; const res = translateResidues(payloadState, chain, raw); if (!res) return null; return { chain: { id: chain, res_index: res } }; }).filter(Boolean); fileObj.secondary_structure = (entity.directives.secondary_structure || []).map((row) => { const chain = String(row.chain || '').trim(); if (!chain) return null; const chainObj = { id: chain }; const loop = translateResidues(payloadState, chain, String(row.loop || '').trim()); const helix = translateResidues(payloadState, chain, String(row.helix || '').trim()); const sheet = translateResidues(payloadState, chain, String(row.sheet || '').trim()); if (loop) chainObj.loop = loop; if (helix) chainObj.helix = helix; if (sheet) chainObj.sheet = sheet; if (!chainObj.loop && !chainObj.helix && !chainObj.sheet) return null; return { chain: chainObj }; }).filter(Boolean); fileObj.design_insertions = (entity.directives.design_insertions || []).map((row) => { const chain = String(row.chain || '').trim(); const rawResidue = String(row.residue || '').trim(); const numResidues = String(row.numResidues || '').trim(); const ss = String(row.secondaryStructure || '').trim(); if (!chain || !rawResidue || !numResidues) return null; const translatedResidue = translateResidues(payloadState, chain, rawResidue); const resIndex = parseSingleResidueIndex(translatedResidue); if (resIndex == null) return null; const insertion = { id: chain, res_index: resIndex, num_residues: numResidues }; if (ss) insertion.secondary_structure = ss; return { insertion }; }).filter(Boolean); return pruneEmptyPayload(fileObj); } function collectUploadsAndStripEntityFileContent(entities = []) { const uploads = []; const cleanedEntities = clone(entities); const seen = new Set(); cleanedEntities.forEach((entity) => { const file = entity?.file; if (!file) return; const filePath = String(file.path || '').trim(); const fileContent = String(file.file_content || ''); if (filePath && fileContent) { const dedupeKey = `${filePath}::${fileContent.length}`; if (!seen.has(dedupeKey)) { uploads.push({ file_name: filePath, file_content: fileContent }); seen.add(dedupeKey); } } delete file.file_content; const prunedFile = pruneEmptyPayload(file); if (isPayloadEmptyValue(prunedFile)) delete entity.file; else entity.file = prunedFile; }); return { uploads, entities: cleanedEntities .map((entity) => pruneEmptyPayload(entity)) .filter((entity) => !isPayloadEmptyValue(entity)) }; } function buildBoltzgenJobFromData(nodeData, { strict = true } = {}) { const data = normalizeData(nodeData); const name = safeName(data.name || data.default_name || 'boltzgen_1', ''); if (!name) throw new Error('BoltzGen node name is required.'); const numDesigns = parseInt(String(data.num_designs || ''), 10); const budget = parseInt(String(data.budget || ''), 10); if (strict && (!Number.isFinite(numDesigns) || numDesigns < 1)) { throw new Error('Num designs is required.'); } if (strict && (!Number.isFinite(budget) || budget < 1)) { throw new Error('Budget is required.'); } const entitiesOut = []; data.entities.forEach((entity) => { if (entity.kind === 'structure') { if (strict && !String(entity.content || '').trim()) { throw new Error(`Structure ${entity.chain}: upload a structure file or load one from Vici Lookup.`); } entitiesOut.push({ file: gatherStructureDirectivesForPayload(entity) }); return; } if (entity.kind === 'protein') { const protein = { id: String(entity.chain || '').trim() }; if (entity.mode === 'sequence') { const seq = String(entity.sequence || '').trim(); if (strict && !seq) throw new Error(`Protein ${entity.chain}: provide a sequence.`); protein.sequence = seq; } else { const min = parseInt(String(entity.min || ''), 10); const max = parseInt(String(entity.max || ''), 10); if (strict && (!Number.isFinite(min) || !Number.isFinite(max) || min <= 0 || max < min)) { throw new Error(`Protein ${entity.chain}: provide a valid min and max length.`); } protein.sequence = `${String(entity.min || '').trim()}..${String(entity.max || '').trim()}`; } if (String(entity.secondary_structure || '').trim()) { protein.secondary_structure = String(entity.secondary_structure).trim(); } protein.cyclic = !!entity.cyclic; const bindingTypes = collectProteinBindingTypeStringFromEntity(entity, { strict }); if (bindingTypes !== undefined) protein.binding_types = bindingTypes; entitiesOut.push({ protein }); return; } const raw = String(entity.code || '').trim(); if (strict && !raw) throw new Error(`Ligand ${entity.chain}: provide a CCD code or SMILES string.`); const ligand = { id: String(entity.chain || '').trim(), binding_types: normalizeBindingType(entity.binding_type, 'u') }; if (raw) { if (isCcdLikeLigand(raw)) ligand.ccd = raw.replace(/\s+/g, '').toUpperCase(); else ligand.smiles = raw; } entitiesOut.push({ ligand }); }); if (strict && !entitiesOut.length) { throw new Error('Add at least one entity.'); } const normalized = collectUploadsAndStripEntityFileContent(entitiesOut); const stages = { design: { recycling_steps: Number(data.stages.design.recycling_steps), sampling_steps: Number(data.stages.design.sampling_steps), diffusion_samples: Number(data.stages.design.diffusion_samples) }, inverse_folding: { recycling_steps: Number(data.stages.inverse_folding.recycling_steps), sampling_steps: Number(data.stages.inverse_folding.sampling_steps), diffusion_samples: Number(data.stages.inverse_folding.diffusion_samples) }, folding: { recycling_steps: Number(data.stages.folding.recycling_steps), sampling_steps: Number(data.stages.folding.sampling_steps), diffusion_samples: Number(data.stages.folding.diffusion_samples) }, affinity: { recycling_steps: Number(data.stages.affinity.recycling_steps), sampling_steps: Number(data.stages.affinity.sampling_steps), diffusion_samples: Number(data.stages.affinity.diffusion_samples) } }; const additionalFilters = (data.filters || []).map((row) => { const metric = String(row.metric || '').trim(); const comparator = String(row.comparator || '').trim(); const value = parseLooseScalar(row.value); if (!metric || !comparator || value === undefined) return null; return `${metric}${comparator}${String(value)}`; }).filter(Boolean); const constraints = (data.constraints || []).map((row) => { if (row.kind === 'length') { const out = { total_len: {} }; if (String(row.chain || '').trim()) out.total_len.id = String(row.chain).trim(); if (String(row.min || '').trim()) out.total_len.min = Number(row.min); if (String(row.max || '').trim()) out.total_len.max = Number(row.max); return Object.keys(out.total_len).length ? out : null; } const a1 = Array.isArray(row.atom1) ? row.atom1 : ['', '', '']; const a2 = Array.isArray(row.atom2) ? row.atom2 : ['', '', '']; if (!String(a1[0] || '').trim() || !String(a1[1] || '').trim() || !String(a1[2] || '').trim()) return null; if (!String(a2[0] || '').trim() || !String(a2[1] || '').trim() || !String(a2[2] || '').trim()) return null; return { bond: { atom1: [String(a1[0]), Number.isFinite(Number(a1[1])) ? Number(a1[1]) : String(a1[1]), String(a1[2])], atom2: [String(a2[0]), Number.isFinite(Number(a2[1])) ? Number(a2[1]) : String(a2[1]), String(a2[2])] } }; }).filter(Boolean); const job = { name, protocol: String(data.protocol || 'protein-anything'), num_designs: Number.isFinite(numDesigns) && numDesigns > 0 ? numDesigns : 5, budget: Number.isFinite(budget) && budget > 0 ? budget : 2, entities: normalized.entities.length ? normalized.entities : [], stages }; if (normalized.uploads.length) job.uploads = normalized.uploads; if (additionalFilters.length) job.additional_filters = additionalFilters; if (constraints.length) job.constraints = constraints; const alpha = parseLooseScalar(data.filter_alpha); if (typeof alpha === 'number') job.alpha = alpha; const filterBiased = String(data.filter_biased || '').trim(); if (filterBiased === 'true') job.filter_biased = true; if (filterBiased === 'false') job.filter_biased = false; const refolding = parseLooseScalar(data.filter_rmsd); if (typeof refolding === 'number') job.refolding_rmsd_threshold = refolding; return job; } function bind({ nodeId, node, mount, editor, patchNodeData, replaceNodeData, saveHistory }) { const listeners = []; const controllers = []; const structureStates = new Map(); function animateNewest(selector, opts = {}) { requestAnimationFrame(() => { const items = qa(selector, mount); const el = items[items.length - 1]; if (!el) return; animateIn(el, { duration: Number(opts.duration || 260), y: Number(opts.y || 10), easing: opts.easing || 'cubic-bezier(0.22, 1, 0.36, 1)' }); }); } 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; return normalizeData(liveNode?.data || {}); } 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: doSaveHistory = false, rerenderEditor = false } = {}) { const nextPatch = rerenderEditor ? { ...(patchObj || {}), __editorTab: getCurrentEditorTab() } : patchObj; patchNodeData(nodeId, nextPatch, { saveHistory: doSaveHistory, rerender: rerenderEditor }); } function switchAdvancedTabUI(tab) { qa('[data-advanced-tab]', mount).forEach((btn) => { const active = btn.getAttribute('data-advanced-tab') === tab; btn.classList.toggle('is-active', active); btn.setAttribute('aria-selected', active ? 'true' : 'false'); }); qa('[data-adv-panel]', mount).forEach((panel) => { panel.classList.toggle('is-active', panel.getAttribute('data-adv-panel') === tab); }); qa('[data-adv-actions]', mount).forEach((actions) => { actions.classList.toggle('is-active', actions.getAttribute('data-adv-actions') === tab); }); } function switchStructureTabUI(entityIndex, tab) { const block = q(`.molecule-block[data-entity-index="${entityIndex}"]`, mount); if (!block) return; qa('[data-structure-tab]', block).forEach((btn) => { btn.classList.toggle('is-active', btn.getAttribute('data-structure-tab') === tab); }); qa('[data-settings-panel]', block).forEach((panel) => { panel.classList.toggle('is-active', panel.getAttribute('data-settings-panel') === tab); }); const addBtn = q('[data-add-structure-row]', block); if (addBtn) { const label = addBtn.querySelector('.btn__text'); if (label) label.textContent = structureAddLabelForKey(tab); } const state = structureStates.get(entityIndex); if (state) renderSelectionHighlights(state); } function switchProteinTabUI(entityIndex, tab) { const block = q(`.molecule-block[data-entity-index="${entityIndex}"]`, mount); if (!block) return; qa('[data-protein-tab]', block).forEach((btn) => { btn.classList.toggle('is-active', btn.getAttribute('data-protein-tab') === tab); }); qa('[data-protein-panel]', block).forEach((panel) => { panel.classList.toggle('is-active', panel.getAttribute('data-protein-panel') === tab); }); const addBtn = q('.protein-binding-add', block); if (addBtn) { addBtn.style.display = tab === 'binding' ? '' : 'none'; } } function switchProteinModeUI(block, mode) { if (!block) return; const seqWrap = q('.protein-seq-wrap', block); const rangeRow = q('.protein-range-row', block); if (seqWrap) seqWrap.style.display = mode === 'sequence' ? '' : 'none'; if (rangeRow) rangeRow.style.display = mode === 'range' ? '' : 'none'; } function replace(nextData, { saveHistory: doSaveHistory = false, rerender = true } = {}) { replaceNodeData( nodeId, { ...(nextData || {}), __editorTab: getCurrentEditorTab() }, { saveHistory: doSaveHistory, rerender } ); } function updateByFieldPath(path, rawValue, { save = false, rerender = false } = {}) { const data = getNodeData(); let value = rawValue; if (path.endsWith('.cyclic')) value = String(rawValue) === 'true'; if (path === 'filter_biased') value = String(rawValue || ''); const next = setByPath(data, path, value); replace(next, { saveHistory: save, rerender }); } function bindSimpleFields() { on(mount, 'input', (ev) => { const el = ev.target.closest('[data-field-key]'); if (!el || !mount.contains(el)) return; const path = el.getAttribute('data-field-key'); if (!path) return; if (el.tagName === 'SELECT') return; updateByFieldPath(path, el.value, { save: false, rerender: false }); if (/^entities\.\d+\.mode$/.test(path)) { switchProteinModeUI(el.closest('.molecule-block'), el.value); } }); on(mount, 'focusout', (ev) => { const el = ev.target.closest('[data-field-key]'); if (!el || !mount.contains(el)) return; const path = el.getAttribute('data-field-key'); if (!path) return; if (el.tagName === 'SELECT') return; updateByFieldPath(path, el.value, { save: true, rerender: false }); if (path === 'name') { const safe = safeName(el.value || '', ''); if (safe !== el.value) { el.value = safe; updateByFieldPath(path, safe, { save: true, rerender: false }); } } }); on(mount, 'change', (ev) => { const el = ev.target.closest('[data-field-key]'); if (!el || !mount.contains(el)) return; const path = el.getAttribute('data-field-key'); if (!path) return; updateByFieldPath(path, el.value, { save: true, rerender: false }); if (/^entities\.\d+\.mode$/.test(path)) { switchProteinModeUI(el.closest('.molecule-block'), el.value); } }); } function bindAddRemoveEntityButtons() { on(q('[data-action="add-structure"]', mount), 'click', (e) => { e.preventDefault(); const data = getNodeData(); data.entities.push(createBlankStructureEntity(collectUsedEntityChains(data.entities))); replace(data, { saveHistory: true }); animateNewest('[data-entity-list] > .molecule-block', { soft: true, duration: 220, y: 8 }); }); on(q('[data-action="add-protein"]', mount), 'click', (e) => { e.preventDefault(); const data = getNodeData(); data.entities.push(createBlankProteinEntity(collectUsedEntityChains(data.entities))); replace(data, { saveHistory: true }); animateNewest('[data-entity-list] > .molecule-block', { soft: true, duration: 220, y: 8 }); }); on(q('[data-action="add-ligand"]', mount), 'click', (e) => { e.preventDefault(); const data = getNodeData(); data.entities.push(createBlankLigandEntity(collectUsedEntityChains(data.entities))); replace(data, { saveHistory: true }); animateNewest('[data-entity-list] > .molecule-block', { soft: true, duration: 220, y: 8 }); }); qa('.entity-remove', mount).forEach((btn) => { on(btn, 'click', (e) => { e.preventDefault(); const index = Number(btn.getAttribute('data-entity-index')); const block = btn.closest('.molecule-block'); animateOut(block, () => { const data = getNodeData(); if (data.entities.length <= 1) { data.entities = [createBlankStructureEntity(new Set())]; } else { data.entities.splice(index, 1); } const used = new Set(); data.entities = data.entities.map((entity) => { const nextChain = nextAvailableEntityChain(used); used.add(nextChain); return { ...entity, chain: nextChain }; }); replace(data, { saveHistory: true }); }); }); }); } function bindAdvancedTabs() { qa('[data-advanced-tab]', mount).forEach((btn) => { on(btn, 'click', (e) => { e.preventDefault(); const tab = btn.getAttribute('data-advanced-tab') || 'design-stage'; switchAdvancedTabUI(tab); patch({ __advancedTab: tab }, { saveHistory: false, rerenderEditor: false }); }); }); } function bindFilterAndConstraintButtons() { on(q('[data-action="add-filter"]', mount), 'click', (e) => { e.preventDefault(); const data = getNodeData(); data.filters.push(createBlankFilter()); replace(data, { saveHistory: true }); animateNewest('[data-filter-list] > .form-card'); }); on(q('[data-action="add-bond"]', mount), 'click', (e) => { e.preventDefault(); const data = getNodeData(); data.constraints.push(createBlankBondConstraint()); replace(data, { saveHistory: true }); animateNewest('[data-constraint-list] > .form-card'); }); on(q('[data-action="add-length"]', mount), 'click', (e) => { e.preventDefault(); const data = getNodeData(); data.constraints.push(createBlankLengthConstraint()); replace(data, { saveHistory: true }); animateNewest('[data-constraint-list] > .form-card'); }); qa('.filter-remove', mount).forEach((btn) => { on(btn, 'click', (e) => { e.preventDefault(); const index = Number(btn.getAttribute('data-filter-index')); const card = btn.closest('.form-card'); animateOut(card, () => { const data = getNodeData(); data.filters.splice(index, 1); replace(data, { saveHistory: true }); }); }); }); qa('.constraint-remove', mount).forEach((btn) => { on(btn, 'click', (e) => { e.preventDefault(); const index = Number(btn.getAttribute('data-constraint-index')); const card = btn.closest('.form-card'); animateOut(card, () => { const data = getNodeData(); data.constraints.splice(index, 1); replace(data, { saveHistory: true }); }); }); }); } function bindStructureTabs() { function refreshStructureDirectiveList(entityIndex, key, data, { animate = false } = {}) { const block = q(`.molecule-block[data-entity-index="${entityIndex}"]`, mount); if (!block) return; const panel = q(`[data-settings-panel="${key}"]`, block); const list = q('.entity-subrows', panel); if (!list) return; const rows = Array.isArray(data?.entities?.[entityIndex]?.directives?.[key]) ? data.entities[entityIndex].directives[key] : []; list.innerHTML = rows.map((row, rowIndex) => renderStructureDirectiveRow(key, row, '', entityIndex, rowIndex) ).join(''); if (animate) { animateNewest(`.molecule-block[data-entity-index="${entityIndex}"] [data-settings-panel="${key}"] .entity-subrows > .entity-subrow`); } const state = structureStates.get(entityIndex); if (state) renderSelectionHighlights(state); } on(mount, 'click', (ev) => { const btn = ev.target.closest('[data-structure-tab]'); if (!btn || !mount.contains(btn)) return; ev.preventDefault(); const entityIndex = Number(btn.getAttribute('data-entity-index')); const tab = btn.getAttribute('data-structure-tab') || 'include'; const data = getNodeData(); if (!data.entities[entityIndex]) return; data.entities[entityIndex].__settingsTab = tab; switchStructureTabUI(entityIndex, tab); replace(data, { saveHistory: false, rerender: false }); }); on(mount, 'click', (ev) => { const btn = ev.target.closest('[data-add-structure-row]'); if (!btn || !mount.contains(btn)) return; ev.preventDefault(); const entityIndex = Number(btn.getAttribute('data-add-structure-row')); const data = getNodeData(); const entity = data.entities[entityIndex]; if (!entity || entity.kind !== 'structure') return; const key = entity.__settingsTab || 'include'; const target = entity.directives[key] || (entity.directives[key] = []); if (key === 'include') target.push({ chain: '', residues: '' }); else if (key === 'include_proximity') target.push({ chain: '', residues: '', radius: '' }); else if (key === 'binding_types') target.push({ chain: '', mode: 'u', residues: '' }); else if (key === 'structure_groups') target.push({ chain: '', groupId: '', visibility: '1', residues: '' }); else if (key === 'design') target.push({ chain: '', residues: '' }); else if (key === 'secondary_structure') target.push({ chain: '', loop: '', helix: '', sheet: '' }); else target.push({ chain: '', residue: '', numResidues: '', secondaryStructure: 'UNSPECIFIED' }); replace(data, { saveHistory: true, rerender: false }); refreshStructureDirectiveList(entityIndex, key, data, { animate: true }); }); on(mount, 'click', (ev) => { const btn = ev.target.closest('.structure-row-remove'); if (!btn || !mount.contains(btn)) return; ev.preventDefault(); const entityIndex = Number(btn.getAttribute('data-entity-index')); const rowIndex = Number(btn.getAttribute('data-row-index')); const rowKind = btn.getAttribute('data-row-kind'); const row = btn.closest('.entity-subrow'); animateOut(row, () => { const data = getNodeData(); const target = data.entities?.[entityIndex]?.directives?.[rowKind]; if (!Array.isArray(target)) return; target.splice(rowIndex, 1); replace(data, { saveHistory: true, rerender: false }); refreshStructureDirectiveList(entityIndex, rowKind, data); }); }); on(mount, 'input', (ev) => { const input = ev.target.closest('.structure-row-chain'); if (!input || !mount.contains(input)) return; const row = input.closest('.entity-subrow'); const block = input.closest('.molecule-block'); const entityIndex = Number(block?.getAttribute('data-entity-index')); const panel = row?.closest('[data-settings-panel]'); const rowIndex = qa('.entity-subrow', panel).indexOf(row); const key = panel?.getAttribute('data-settings-panel'); const data = getNodeData(); if (!data.entities?.[entityIndex]?.directives?.[key]?.[rowIndex]) return; data.entities[entityIndex].directives[key][rowIndex].chain = String(input.value || '').trim().toUpperCase(); replace(data, { saveHistory: false, rerender: false }); }); on(mount, 'change', (ev) => { const input = ev.target.closest('.structure-row-chain'); if (!input || !mount.contains(input)) return; const row = input.closest('.entity-subrow'); const block = input.closest('.molecule-block'); const entityIndex = Number(block?.getAttribute('data-entity-index')); const panel = row?.closest('[data-settings-panel]'); const rowIndex = qa('.entity-subrow', panel).indexOf(row); const key = panel?.getAttribute('data-settings-panel'); const data = getNodeData(); if (!data.entities?.[entityIndex]?.directives?.[key]?.[rowIndex]) return; data.entities[entityIndex].directives[key][rowIndex].chain = String(input.value || '').trim().toUpperCase(); replace(data, { saveHistory: true, rerender: false }); }); } function bindProteinTabs() { function refreshProteinBindingList(entityIndex, data, { animate = false } = {}) { const block = q(`.molecule-block[data-entity-index="${entityIndex}"]`, mount); if (!block) return; const list = q('.protein-binding-list', block); if (!list) return; const rows = Array.isArray(data?.entities?.[entityIndex]?.binding_types) ? data.entities[entityIndex].binding_types : []; list.innerHTML = rows.map((row, rowIndex) => renderProteinBindingRow(row, entityIndex, rowIndex) ).join(''); if (animate) { animateNewest(`.molecule-block[data-entity-index="${entityIndex}"] .protein-binding-list > .entity-subrow`); } } on(mount, 'click', (ev) => { const btn = ev.target.closest('[data-protein-tab]'); if (!btn || !mount.contains(btn)) return; ev.preventDefault(); const entityIndex = Number(btn.getAttribute('data-entity-index')); const tab = btn.getAttribute('data-protein-tab') || 'binding'; const data = getNodeData(); if (!data.entities[entityIndex] || data.entities[entityIndex].kind !== 'protein') return; data.entities[entityIndex].__settingsTab = tab; switchProteinTabUI(entityIndex, tab); replace(data, { saveHistory: false, rerender: false }); }); on(mount, 'click', (ev) => { const btn = ev.target.closest('.protein-binding-add'); if (!btn || !mount.contains(btn)) return; ev.preventDefault(); const entityIndex = Number(btn.getAttribute('data-entity-index')); const data = getNodeData(); const entity = data.entities[entityIndex]; if (!entity || entity.kind !== 'protein') return; entity.binding_types.push({ mode: 'u', residues: '' }); replace(data, { saveHistory: true, rerender: false }); refreshProteinBindingList(entityIndex, data, { animate: true }); }); on(mount, 'click', (ev) => { const btn = ev.target.closest('.protein-binding-remove'); if (!btn || !mount.contains(btn)) return; ev.preventDefault(); const entityIndex = Number(btn.getAttribute('data-entity-index')); const rowIndex = Number(btn.getAttribute('data-row-index')); const row = btn.closest('.entity-subrow'); animateOut(row, () => { const data = getNodeData(); const entity = data.entities[entityIndex]; if (!entity || entity.kind !== 'protein') return; entity.binding_types.splice(rowIndex, 1); replace(data, { saveHistory: true, rerender: false }); refreshProteinBindingList(entityIndex, data); }); }); } function bindLookupButtons() { qa('.entity-vici-btn', mount).forEach((btn) => { on(btn, 'click', (e) => { e.preventDefault(); toggleLookup(btn, mount); }); }); try { window.ViciLookup?.init?.(mount); } catch (err) { console.error(err); } } function bindStructureDropzones() { qa('.molecule-block[data-entity-kind="structure"]', mount).forEach((block) => { const entityIndex = Number(block.getAttribute('data-entity-index')); const fileInput = q('.entity-file', block); const contentField = q('.lookup-target-content', block); const nameField = q('.lookup-target-name', block); const sourceField = q('.lookup-target-source', block); const previewHost = q('[data-preview-host]', block); const sequenceHost = q('[data-sequence-host]', block); const previewEmpty = q('[data-preview-empty]', block); const sequenceEmpty = q('[data-sequence-empty]', block); const state = { node: block, entityIndex, fileInput, contentField, nameField, sourceField, previewHost, sequenceHost, previewEmpty, sequenceEmpty, previewGrid: q('[data-preview-grid]', block), viewerId: previewHost?.id, text: String(contentField?.value || ''), fileName: String(nameField?.value || ''), source: String(sourceField?.value || ''), selectionMap: new Map(), colorCursor: 1 }; structureStates.set(entityIndex, state); const controller = utils.createDropZoneController({ scope: block, dropZone: '.drop-zone', fileInput: '.entity-file', metaEl: '.drop-zone__meta', removeBtn: null, contentField: '.lookup-target-content', filenameField: '.lookup-target-name', sourceField: '.lookup-target-source', emptyMetaText: 'Drop file here or click to upload a PDB or CIF/mmCIF file', readFile: (file) => utils.readTextFile(file), beforeRead: ({ file }) => { const name = String(file?.name || '').toLowerCase(); if (!/\.(pdb|cif|mmcif|ent)$/i.test(name)) { window.showToast?.('error', 'Structure files must be .pdb, .cif, .mmcif, or .ent'); return false; } return true; }, onSet: async ({ text, meta }) => { const data = getNodeData(); const entity = data.entities[entityIndex]; if (!entity || entity.kind !== 'structure') return; entity.content = String(text || ''); entity.file_name = String(meta.name || ''); entity.source = String(meta.source || 'upload'); patch({ entities: data.entities }, { saveHistory: true, rerenderEditor: false }); state.text = entity.content; state.fileName = entity.file_name; state.source = entity.source; await loadStructureIntoState(state, entity.content, entity.file_name, entity.source); } }); controller?.bind?.(); controllers.push(controller); const initialContent = String(contentField?.value || '').trim(); const initialName = String(nameField?.value || '').trim(); const initialSource = String(sourceField?.value || '').trim(); if (initialContent && initialName) { loadStructureIntoState(state, initialContent, initialName, initialSource || 'upload').catch((err) => { console.warn('[boltzgen] initial structure load failed', err); }); } const syncLookupState = async (save = false) => { const text = String(contentField?.value || ''); const fileName = String(nameField?.value || '').trim() || 'structure.pdb'; const source = String(sourceField?.value || '').trim() || 'vici_lookup'; const data = getNodeData(); const entity = data.entities[entityIndex]; if (!entity || entity.kind !== 'structure') return; entity.content = text; entity.file_name = text.trim() ? fileName : ''; entity.source = text.trim() ? source : ''; patch({ entities: data.entities }, { saveHistory: save, rerenderEditor: false }); if (!text.trim()) { clearStructureState(state); return; } await loadStructureIntoState(state, text, entity.file_name, entity.source); }; on(contentField, 'input', () => { syncLookupState(false).catch(console.error); }); on(contentField, 'change', () => { syncLookupState(true).catch(console.error); }); on(block, 'focusin', (ev) => { const input = ev.target.closest('.seq-target-input'); if (!input) return; const row = input.closest('.entity-subrow'); const chainField = row ? q('.structure-row-chain', row) : null; const typedChain = String(chainField?.value || '').trim(); if (typedChain) input.dataset.seqChain = typedChain; else delete input.dataset.seqChain; setSeqTargetFocus(state, input); }); on(block, 'input', (ev) => { const chainField = ev.target.closest('.structure-row-chain'); if (chainField) { const row = chainField.closest('.entity-subrow'); const nextChain = String(chainField.value || '').trim(); qa('.seq-target-input', row).forEach((input) => { if (nextChain) input.dataset.seqChain = nextChain; else delete input.dataset.seqChain; syncInputSelectionFromValue(state, input); }); return; } const input = ev.target.closest('.seq-target-input'); if (!input) return; const row = input.closest('.entity-subrow'); const chainFieldEl = row ? q('.structure-row-chain', row) : null; const typedChain = String(chainFieldEl?.value || '').trim(); if (typedChain) input.dataset.seqChain = typedChain; else delete input.dataset.seqChain; syncInputSelectionFromValue(state, input); }); }); } function bindPresetAwareLookupMirrors() { qa('.molecule-block[data-entity-kind="protein"]', mount).forEach((block) => { const entityIndex = Number(block.getAttribute('data-entity-index')); const seqField = q('.protein-sequence', block); const contentField = q('.lookup-target-content', block); if (!contentField || !seqField) return; on(contentField, 'input', () => { const next = String(contentField.value || '').trim(); if (!next) return; const data = getNodeData(); const entity = data.entities[entityIndex]; if (!entity || entity.kind !== 'protein') return; entity.mode = 'sequence'; entity.sequence = next.replace(/\s+/g, ''); replace(data, { saveHistory: true }); }); }); qa('.molecule-block[data-entity-kind="ligand"]', mount).forEach((block) => { const entityIndex = Number(block.getAttribute('data-entity-index')); const codeField = q('.ligand-code', block); const contentField = q('.lookup-target-content', block); if (!contentField || !codeField) return; on(contentField, 'input', () => { const next = String(contentField.value || '').trim(); if (!next) return; const data = getNodeData(); const entity = data.entities[entityIndex]; if (!entity || entity.kind !== 'ligand') return; entity.code = next; replace(data, { saveHistory: true }); }); }); } bindSimpleFields(); bindAddRemoveEntityButtons(); bindAdvancedTabs(); bindFilterAndConstraintButtons(); bindStructureTabs(); bindProteinTabs(); bindLookupButtons(); bindStructureDropzones(); bindPresetAwareLookupMirrors(); return () => { controllers.forEach((controller) => controller?.destroy?.()); structureStates.forEach((state) => { try { clearMolstar(state); } catch {} }); listeners.splice(0).forEach((off) => { try { off(); } catch (err) { console.error(err); } }); }; } async function applyPreset({ nodeId, node, replaceNodeData, presetKey }) { const current = normalizeData(node?.data || {}); const next = await applyPresetToData(current, presetKey); replaceNodeData(nodeId, next, { saveHistory: true, rerender: true }); window.showToast?.('success', `Loaded BoltzGen preset: ${PRESETS[presetKey]?.label || presetKey}.`); } function validate({ nodeData }) { const errors = []; try { buildBoltzgenJobFromData(nodeData, { strict: true }); } catch (err) { errors.push(err?.message || 'BoltzGen validation failed.'); } return errors; } function buildExecutionNode({ node, nodeData }) { const job = buildBoltzgenJobFromData(nodeData, { strict: true }); return { node_id: String(node.id), node_type: MODEL_TYPE, node_name: job.name, payload: { [MODEL_KEY]: job } }; } registry.register(MODEL_TYPE, { type: MODEL_TYPE, title: MODEL_TYPE, tutorialUrl: '/blog', tabs: ['basic', 'advanced'], presets: [ { key: 'nano', label: 'Nanobody | 8Z8V' }, { key: 'pep', label: 'Peptide | 6WJ3' }, { key: 'anti', label: 'Antibody | 5YOY' }, { key: 'small', label: 'Small Molecule | 4G37' } ], createDefaultData, renderBody, bind, applyPreset, validate, buildExecutionNode, resetData: ({ node }) => createDefaultData({ autoName: node?.data?.default_name || node?.data?.name || 'boltzgen_1' }) }); })();