(() => {
'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 `
${getViciEyeMarkup()}
`;
}
function removeButtonHtml(label = 'Remove') {
return `
${TRASH_ICON}
${e(label)}
`;
}
function helpBubbleHtml(text, prefix = 'help') {
const id = uid(prefix);
return `
i
`;
}
function fieldHeadHtml(label, helpText = '') {
return `
${e(label)}
${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 `${e(str)} `;
}).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 `
${renderSelectOptions(STAGE_OPTIONS[sourceKey], selected)}
`;
}
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 `
${fieldHeadHtml('Structure preview', 'Mol* preview of the uploaded structure.')}
Upload or load a structure to preview it in Mol*
${fieldHeadHtml('Sequence viewer', 'Sequence extracted from the structure. Use it to pick residues.')}
Residues will appear here after the structure is loaded
`;
}
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 = `
${TRASH_ICON}
`;
if (kind === 'include') {
return `
${structureDirectiveChainFieldHtml(chain)}
${fieldHeadHtml('Residues', 'Residues or ranges to include.')}
${remove}
`;
}
if (kind === 'include_proximity') {
return `
`;
}
if (kind === 'binding_types') {
return `
${structureDirectiveChainFieldHtml(chain)}
${fieldHeadHtml('Mode', 'Pick the code to assign to these residues.')}
u · unspecified
b · binding
n · not binding
${fieldHeadHtml('Residues', 'Residues or ranges to assign this binding code to.')}
${remove}
`;
}
if (kind === 'structure_groups') {
return `
`;
}
if (kind === 'design') {
return `
${structureDirectiveChainFieldHtml(chain)}
${fieldHeadHtml('Residues', 'Residues or ranges that are designable.')}
${remove}
`;
}
if (kind === 'secondary_structure') {
return `
`;
}
return `
`;
}
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]) => `
${e(label)}
`).join('')}
${ADD_ICON}
${e(structureAddLabelForKey(activeKey))}
${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 `
`;
}
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('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.')}
Sequence
Length range
${fieldHeadHtml('Cyclic', 'Choose whether the protein should be cyclic.')}
false
true
${viciToggleButtonHtml('Toggle Vici Lookup')}
${TRASH_ICON}
Remove
${fieldHeadHtml('Sequence', 'Protein sequence input.')}
`;
}
function renderLigandEntity(entity, entityIndex) {
return `
`;
}
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 `
`;
}
function renderConstraintRow(row, index, chainOptions) {
const optionsHtml = `Select chain ` +
chainOptions.map((id) => `${e(id)} `).join('');
if (row.kind === 'length') {
return `
`;
}
return `
`;
}
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 `
${[
['design-stage', 'Design stage'],
['affinity-stage', 'Affinity stage'],
['fold-stage', 'Fold stage'],
['inverse-stage', 'Inverse fold stage'],
['filtering', 'Filtering'],
['constraints', 'Constraints']
].map(([key, label]) => `
${e(label)}
`).join('')}
${ADD_ICON}
Add filter
${ADD_ICON}
Add bond
${ADD_ICON}
Add length
${data.filters.map((row, index) => renderFilterRow(row, index)).join('')}
${data.constraints.map((row, index) => renderConstraintRow(row, index, chainOptions)).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'
})
});
})();