(() => {
'use strict';
const MODEL_WORKFLOW_ENDPOINT =
window.MODEL_WORKFLOW_ENDPOINT ||
'https://ayambabu23--workflow-execute-workflow.modal.run/';
const MODEL_API_ENDPOINT =
window.MODEL_API_ENDPOINT ||
'https://vici-bio--api-execute-workflow.modal.run/';
const MODEL_STATUS_ENDPOINT =
window.MODEL_STATUS_ENDPOINT ||
'https://vici-bio--api-check-status.modal.run/';
const root = document.getElementById('model-ui');
if (!root) return;
/* -----------------------------
Boltz-2 adapter (model-specific)
------------------------------ */
const AA20 = 'ACDEFGHIKLMNPQRSTVWY';
const RE_PROT_SEQ = new RegExp(`^[${AA20}]+$`);
const RE_RES_TOKEN = new RegExp(`^[${AA20}]\\d+$`);
const RE_ATOM_LABEL = /^[A-Z]{1,2}\d*$/;
function isPlainObject(v) {
return Object.prototype.toString.call(v) === '[object Object]';
}
function deepClone(v) {
return JSON.parse(JSON.stringify(v));
}
function stableSerialize(value) {
const sortRec = (v) => {
if (Array.isArray(v)) return v.map(sortRec);
if (!isPlainObject(v)) return v;
const out = {};
Object.keys(v).sort().forEach((k) => { out[k] = sortRec(v[k]); });
return out;
};
return JSON.stringify(sortRec(value));
}
function escapeHtml(str) {
return String(str ?? '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
function canonicalizeRunName(raw) {
let s = String(raw || '').trim();
if (!s) return '';
s = s.replace(/\s+/g, '_');
try { s = s.normalize('NFKD'); } catch {}
s = s.replace(/[^\w-]+/g, '');
s = s.replace(/_+/g, '_').toLowerCase();
s = s.replace(/^[^a-z0-9]+/, '');
s = s.replace(/[^a-z0-9]+$/, '');
return s.slice(0, 64);
}
const SAFE_NAME_RE = /^[a-z0-9](?:[a-z0-9_-]{1,62}[a-z0-9])?$/;
function isValidProteinSeq(seq) { return RE_PROT_SEQ.test(String(seq || '').toUpperCase()); }
function isValidDNASeq(seq) { return /^[ACGT]+$/.test(String(seq || '').toUpperCase()); }
function isValidRNASeq(seq) { return /^[ACGU]+$/.test(String(seq || '').toUpperCase()); }
function isValidSMILES(s) {
const x = String(s || '').trim();
if (!x || /\s/.test(x)) return false;
if (isValidCCDCode(x)) return false;
return /^[A-Za-z0-9@+\-\[\]\(\)=#$%\\\/\.]+$/.test(x) && /[A-Za-z]/.test(x);
}
function isValidGlycanCCD(s) {
const x = String(s || '');
if (!x || !/^[A-Za-z0-9()\-\s]+$/.test(x)) return false;
if (!/[A-Za-z]{3}/.test(x)) return false;
let bal = 0;
for (const ch of x) {
if (ch === '(') bal++;
else if (ch === ')') {
bal--;
if (bal < 0) return false;
}
}
return bal === 0;
}
const ELEMENTS = new Set([
'H','B','C','N','O','F','P','S','K','I',
'Cl','Br','Si','Na','Mg','Al','Ca','Fe','Zn','Cu','Mn','Co','Ni','Se'
]);
function looksLikeElementOnlySmilesShort(s) {
// Treat very short strings composed only of element symbols (e.g., "CCO", "COC") as SMILES-like
const x = String(s || '').trim();
if (!x || x.length > 6) return false;
let i = 0;
while (i < x.length) {
const ch = x[i];
if (!/[A-Z]/.test(ch)) return false;
let sym = ch;
const next = x[i + 1];
if (next && /[a-z]/.test(next)) {
sym += next;
i += 2;
} else {
i += 1;
}
if (!ELEMENTS.has(sym)) return false;
}
return true;
}
function isValidCCDCode(s) {
const x = String(s || '').trim();
// Typical PDB CCD identifiers are exactly 3 alnum chars
if (!/^[A-Za-z0-9]{3}$/.test(x)) return false;
// If it looks like a short element-only SMILES (e.g., "CCO"), do NOT treat as CCD
if (looksLikeElementOnlySmilesShort(x)) return false;
return true;
}
// Rename your glycan check mentally: it’s not "CCD", it's a glycan text notation,
// but you want to map it to "ccd" for backend purposes.
function isValidGlycanCCD(s) {
const x = String(s || '');
if (!x || !/^[A-Za-z0-9()\-\s]+$/.test(x)) return false;
if (!/[A-Za-z]{3}/.test(x)) return false;
let bal = 0;
for (const ch of x) {
if (ch === '(') bal++;
else if (ch === ')') {
bal--;
if (bal < 0) return false;
}
}
return bal === 0;
}
function detectLigandNotation(s) {
const x = String(s || '').trim();
if (isValidCCDCode(x)) return 'ccd';
if (isValidGlycanCCD(x)) return 'ccd';
if (isValidSMILES(x)) return 'smiles';
return 'unknown';
}
function isResidueToken(s) {
return RE_RES_TOKEN.test(String(s || '').trim().toUpperCase());
}
function aaIndexFromToken(tok) {
const s = String(tok || '').trim();
const m = s.match(/\d+/);
return m ? parseInt(m[0], 10) : NaN;
}
function posFromConstraintToken(tok) {
const s = String(tok || '').trim();
if (!s) return NaN;
if (s.includes('@')) return NaN;
const n = aaIndexFromToken(s);
return Number.isInteger(n) && n > 0 ? n : NaN;
}
function covalentAtomTuple(token, chainKind) {
const s = String(token || '').trim();
if (!s) return null;
if (chainKind === 'protein') {
const i = s.indexOf('@');
if (i < 1) return null;
const res = s.slice(0, i).toUpperCase();
const atom = s.slice(i + 1).toUpperCase();
const idx = aaIndexFromToken(res);
if (!Number.isInteger(idx)) return null;
if (!isResidueToken(res)) return null;
if (!RE_ATOM_LABEL.test(atom)) return null;
return [idx, atom];
}
// ligand/glycan/dna/rna side
if (!/^@[A-Za-z]{1,2}\d*$/i.test(s)) return null;
return [1, s.slice(1).toUpperCase()];
}
function base64FromUtf8(text) {
const s = String(text || '');
// UTF-8 safe base64
try {
return btoa(unescape(encodeURIComponent(s)));
} catch {
// last resort: basic btoa (works for ASCII, which PDB/CIF usually are)
return btoa(s);
}
}
function guessTemplateKindFromNameOrContent(name, content) {
const n = String(name || '').toLowerCase();
if (n.endsWith('.cif')) return 'cif';
if (n.endsWith('.pdb')) return 'pdb';
const c = String(content || '').trim();
if (!c) return 'pdb';
if (/^(data_|loop_|#\s*|_entry)/i.test(c)) return 'cif';
return 'pdb';
}
function nextAutoChainId(existingSet) {
const used = new Set(Array.from(existingSet || []).map(v => String(v || '').toUpperCase()));
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
for (const ch of letters) {
if (!used.has(ch)) return ch;
}
for (const a of letters) {
for (const b of letters) {
const id = `${a}${b}`;
if (!used.has(id)) return id;
}
}
return 'Z';
}
function svgUp() {
return `
`;
}
function svgDown() {
return `
`;
}
function slideOpen(el, { duration = 240 } = {}) {
if (!el) return;
el.style.display = 'block';
el.style.overflow = 'hidden';
el.style.maxHeight = '0px';
el.style.opacity = '0';
el.style.transition = `max-height ${duration}ms ease, opacity ${duration}ms ease`;
void el.offsetHeight;
el.classList.add('open');
el.style.maxHeight = `${Math.max(el.scrollHeight, 1)}px`;
el.style.opacity = '1';
const cleanup = () => {
el.removeEventListener('transitionend', done);
el.style.maxHeight = '';
el.style.overflow = '';
el.style.transition = '';
};
const done = (e) => {
if (e.target !== el) return;
if (e.propertyName !== 'max-height') return;
cleanup();
};
el.addEventListener('transitionend', done);
setTimeout(() => {
if (el.classList.contains('open')) cleanup();
}, duration + 80);
}
function slideClose(el, { duration = 240, after } = {}) {
if (!el) return;
el.style.overflow = 'hidden';
el.style.maxHeight = `${Math.max(el.scrollHeight, el.offsetHeight, 1)}px`;
el.style.opacity = '1';
el.style.transition = `max-height ${duration}ms ease, opacity ${duration}ms ease`;
void el.offsetHeight;
el.classList.remove('open');
el.style.maxHeight = '0px';
el.style.opacity = '0';
const cleanup = () => {
el.removeEventListener('transitionend', done);
el.style.display = 'none';
el.style.maxHeight = '';
el.style.overflow = '';
el.style.opacity = '';
el.style.transition = '';
try { after?.(); } catch {}
};
const done = (e) => {
if (e.target !== el) return;
if (e.propertyName !== 'max-height') return;
cleanup();
};
el.addEventListener('transitionend', done);
setTimeout(cleanup, duration + 80);
}
let _viciInitRaf = null;
function scheduleViciInit() {
if (_viciInitRaf) cancelAnimationFrame(_viciInitRaf);
_viciInitRaf = requestAnimationFrame(() => {
_viciInitRaf = null;
try { window.ViciLookup?.init?.(root); } catch {}
});
}
window.toggleViciLookup = function toggleViciLookup(btn) {
const block = btn?.closest('.field-group');
const panel = block?.querySelector('.vici-lookup');
if (!panel) return;
const isOpen = panel.classList.contains('open') && panel.style.display !== 'none';
if (isOpen) {
btn.classList.remove('active');
btn.setAttribute('aria-expanded', 'false');
// closing from toggle should NOT wipe the target content
try { window.ViciLookup?.clear?.(panel, { keepInput: false, keepTarget: true }); } catch {}
slideClose(panel, { duration: 240 });
return;
}
btn.classList.add('active');
btn.setAttribute('aria-expanded', 'true');
// mount widgets first, then open on next frame so scrollHeight is correct
panel.style.display = 'block';
panel.style.maxHeight = '0px';
panel.style.opacity = '0';
scheduleViciInit();
requestAnimationFrame(() => {
slideOpen(panel, { duration: 240 });
// if the widget injects asynchronously, a second pass helps
setTimeout(() => {
if (panel.style.display !== 'none' && panel.classList.contains('open')) {
panel.style.maxHeight = `${Math.max(panel.scrollHeight, 1)}px`;
}
}, 50);
});
};
function dzReadFileAsText(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(String(e.target?.result || ''));
reader.onerror = () => reject(new Error('Failed to read file.'));
reader.readAsText(file);
});
}
function dzFormatBytes(bytes) {
const n = Number(bytes);
if (!Number.isFinite(n) || n <= 0) return '';
if (n < 1024) return `${n} B`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1).replace(/\.0$/, '')} KB`;
return `${(n / (1024 * 1024)).toFixed(2).replace(/\.00$/, '')} MB`;
}
function createDropZoneController(config = {}) {
const scope = config.scope || document;
const drop = scope.querySelector(config.dropZone || '.drop-zone');
const input = scope.querySelector(config.fileInput || 'input[type="file"]');
const metaEl = scope.querySelector(config.metaEl || '.drop-zone__meta');
const titleEl = scope.querySelector(config.titleEl || '.drop-zone__title');
const removeBtn = scope.querySelector(config.removeBtn || null);
const contentField = scope.querySelector(config.contentField || '.lookup-target-content');
const nameField = scope.querySelector(config.nameField || '.lookup-target-name');
const sourceField = scope.querySelector(config.sourceField || '.lookup-target-source');
if (!drop || !input || !metaEl || !titleEl || !contentField) return null;
function render() {
const content = String(contentField.value || '');
const has = content.trim().length > 0;
drop.classList.toggle('is-filled', has);
if (!has) {
titleEl.textContent = config.emptyTitle || 'Drop file here or click to upload';
metaEl.textContent = config.emptyMeta || 'Content from upload or Vici Lookup will appear here';
if (removeBtn) removeBtn.style.display = 'none';
return;
}
const n = String(nameField?.value || '').trim();
const src = String(sourceField?.value || '').trim();
const sz = drop.dataset.fileSizeBytes ? dzFormatBytes(drop.dataset.fileSizeBytes) : '';
const bits = [];
bits.push(n || (src ? `Loaded (${src})` : 'Loaded content'));
if (sz) bits.push(sz);
bits.push(`${content.split(/\r?\n/).length} lines`);
bits.push(`${content.length.toLocaleString()} chars`);
metaEl.innerHTML = bits
.map((v, i) => i === 0 ? escapeHtml(v) : `• ${escapeHtml(v)}`)
.join(' ');
titleEl.textContent = 'Template loaded';
if (removeBtn) removeBtn.style.display = '';
}
async function setFromFile(file) {
if (!file) return;
const okExt = /\.(pdb|cif)$/i.test(file.name || '');
if (!okExt) {
showToast?.('error', 'Template must be .pdb or .cif');
return;
}
try {
const txt = await dzReadFileAsText(file);
contentField.value = txt;
if (nameField) nameField.value = file.name || '';
if (sourceField) sourceField.value = 'upload';
drop.dataset.fileSizeBytes = String(file.size || '');
contentField.dispatchEvent(new Event('input', { bubbles: true }));
contentField.dispatchEvent(new Event('change', { bubbles: true }));
render();
} catch (err) {
showToast?.('error', err?.message || 'Failed to read file.');
}
}
function clear() {
contentField.value = '';
if (nameField) nameField.value = '';
if (sourceField) sourceField.value = '';
input.value = '';
delete drop.dataset.fileSizeBytes;
contentField.dispatchEvent(new Event('input', { bubbles: true }));
contentField.dispatchEvent(new Event('change', { bubbles: true }));
render();
}
function bind() {
drop.addEventListener('click', () => input.click());
drop.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
input.click();
}
});
drop.addEventListener('dragover', (e) => { e.preventDefault(); drop.classList.add('dragover'); });
drop.addEventListener('dragleave', () => drop.classList.remove('dragover'));
drop.addEventListener('drop', (e) => {
e.preventDefault();
drop.classList.remove('dragover');
const file = e.dataTransfer?.files?.[0];
if (file) setFromFile(file);
});
input.addEventListener('change', () => {
const file = input.files?.[0];
if (file) setFromFile(file);
});
if (removeBtn) removeBtn.addEventListener('click', (e) => { e.preventDefault(); clear(); showToast?.('success', 'Template file cleared.'); });
render();
}
return { bind, render, clear, setFromFile, refs: { drop, input, contentField, nameField, sourceField } };
}
// Global state refs
const molList = document.getElementById('boltz-molecule-list');
const restList = document.getElementById('boltz-restraint-list');
const tplToggle = document.getElementById('boltz-use-templates');
const tplPanel = document.getElementById('boltz-templates-panel');
const tplList = document.getElementById('boltz-template-list');
const dropZoneControllers = [];
function getTemplateBlocks() {
return Array.from(tplList.querySelectorAll('.template-item'));
}
function clearTemplateBlock(block, { collapseLookup = true } = {}) {
if (!block) return;
// Clear template fields
const chainEl = block.querySelector('.tpl-chain');
const tmplEl = block.querySelector('.tpl-template');
if (chainEl) chainEl.value = '';
if (tmplEl) tmplEl.value = '';
// Reset force + threshold
const forceSel = block.querySelector('.tpl-force');
const thr = block.querySelector('.tpl-threshold');
if (forceSel) forceSel.value = 'false';
if (thr) {
thr.value = '';
thr.disabled = true;
}
// Clear dropzone-backed hidden fields
const contentField = block.querySelector('.lookup-target-content');
const nameField = block.querySelector('.lookup-target-name');
const sourceField = block.querySelector('.lookup-target-source');
if (contentField) {
contentField.value = '';
contentField.dispatchEvent(new Event('input', { bubbles: true }));
contentField.dispatchEvent(new Event('change', { bubbles: true }));
}
if (nameField) nameField.value = '';
if (sourceField) sourceField.value = '';
// Close + clear vici lookup (and wipe the target since we are "clearing")
const viciBtn = block.querySelector('.vici-toggle-btn');
const panel = block.querySelector('.vici-lookup');
if (viciBtn) {
viciBtn.classList.remove('active');
viciBtn.setAttribute('aria-expanded', 'false');
}
if (panel) {
try { window.ViciLookup?.clear?.(panel, { keepInput: false, keepTarget: false }); } catch {}
if (collapseLookup) {
if (panel.classList.contains('open') || panel.style.display !== 'none') {
slideClose(panel, { duration: 240 });
} else {
panel.style.display = 'none';
}
}
}
// Re-render dropzone UI if we have a controller for this block
const ctrl = dropZoneControllers.find(
c => c.refs?.contentField && block.contains(c.refs.contentField)
);
try { ctrl?.render?.(); } catch {}
scheduleViciInit();
}
function ensureAtLeastOneTemplateBlock() {
if (!tplToggle?.checked) return;
if (!tplList.querySelector('.template-item')) addTemplateBlock({});
}
function getMoleculeBlocks() {
return Array.from(molList.querySelectorAll('.molecule-block'));
}
function getBaseMoleculeBlock() {
const blocks = getMoleculeBlocks();
if (blocks.length === 0) return null;
const marked = blocks.find(b => b.dataset.base === '1');
if (marked) return marked;
// if nothing marked, mark first as base
blocks[0].dataset.base = '1';
return blocks[0];
}
function clearMoleculeBlock(block, { collapseLookup = true } = {}) {
if (!block) return;
// clear molecule fields
const ta = block.querySelector('.mol-seq');
if (ta) {
ta.value = '';
ta.dispatchEvent(new Event('input', { bubbles: true }));
ta.dispatchEvent(new Event('change', { bubbles: true }));
}
const cyc = block.querySelector('.mol-cyclic');
if (cyc) {
cyc.checked = false;
cyc.dispatchEvent(new Event('input', { bubbles: true }));
cyc.dispatchEvent(new Event('change', { bubbles: true }));
}
const typ = block.querySelector('.mol-type');
if (typ) {
typ.value = 'protein';
typ.dispatchEvent(new Event('input', { bubbles: true }));
typ.dispatchEvent(new Event('change', { bubbles: true }));
}
onMolTypeChange(block);
// clear + collapse vici lookup
const viciBtn = block.querySelector('.vici-toggle-btn');
const panel = block.querySelector('.vici-lookup');
if (viciBtn) {
viciBtn.classList.remove('active');
viciBtn.setAttribute('aria-expanded', 'false');
}
if (panel) {
// for clearing molecule, we DO want to clear the target content too
try { window.ViciLookup?.clear?.(panel, { keepInput: false, keepTarget: false }); } catch {}
if (collapseLookup) {
if (panel.classList.contains('open') || panel.style.display !== 'none') {
slideClose(panel, { duration: 240 });
} else {
panel.style.display = 'none';
}
}
}
scheduleViciInit();
}
function setMoleculesFromArray(mols, { animateAdds = false, animateRemovals = false } = {}) {
const desired = Array.isArray(mols) && mols.length ? mols : [{
chain_id: 'A', type: 'protein', sequence: '', cyclic: false
}];
// ensure base exists
let base = getBaseMoleculeBlock();
if (!base) {
base = addMolecule({
chainId: desired[0]?.chain_id || 'A',
type: 'protein',
sequence: '',
cyclic: false,
animate: false,
isBase: true
});
} else {
base.dataset.base = '1';
}
// remove extras
const blocks = getMoleculeBlocks();
blocks.forEach((b) => {
if (b === base) return;
if (animateRemovals) {
slideClose(b, { duration: 240, after: () => b.remove() });
} else {
b.remove();
}
});
// reset base first, then apply desired[0]
clearMoleculeBlock(base, { collapseLookup: true });
// update base chain id + label
const baseChain = String(desired[0]?.chain_id || 'A').trim().toUpperCase();
const chainEl = base.querySelector('.mol-chain');
if (chainEl) chainEl.value = baseChain;
updateMolLabel(base);
// apply base content
const baseType = desired[0]?.type || 'protein';
const baseSeq = desired[0]?.sequence || '';
const baseCyc = !!desired[0]?.cyclic;
const typeSel = base.querySelector('.mol-type');
if (typeSel) typeSel.value = baseType;
const seqEl = base.querySelector('.mol-seq');
if (seqEl) seqEl.value = baseSeq;
const cycEl = base.querySelector('.mol-cyclic');
if (cycEl) cycEl.checked = baseCyc;
onMolTypeChange(base);
// add extras
for (let i = 1; i < desired.length; i++) {
const m = desired[i] || {};
addMolecule({
chainId: m.chain_id || null,
type: m.type || 'protein',
sequence: m.sequence || '',
cyclic: !!m.cyclic,
animate: animateAdds,
isBase: false
});
}
updateRestraintChainDropdowns();
scheduleViciInit();
}
function getChainIds() {
return Array.from(root.querySelectorAll('.molecule-block .mol-chain'))
.map(el => String(el.value || '').trim())
.filter(Boolean);
}
function updateRestraintChainDropdowns() {
const ids = getChainIds();
root.querySelectorAll('.restraint-block select.chain1, .restraint-block select.chain2').forEach((sel) => {
const prev = sel.value;
sel.innerHTML = '';
ids.forEach((id) => {
const opt = document.createElement('option');
opt.value = id;
opt.textContent = id;
sel.appendChild(opt);
});
if (ids.includes(prev)) sel.value = prev;
else if (ids[0]) sel.value = ids[0];
});
}
function updateMolLabel(block) {
const id = String(block.querySelector('.mol-chain')?.value || '').trim();
const label = block.querySelector('.mol-label');
if (label) label.textContent = id ? `Molecule ${id}` : 'Molecule';
}
function onMolTypeChange(block) {
const sel = block.querySelector('.mol-type');
const ta = block.querySelector('.mol-seq');
const cyclicWrap = block.querySelector('.mol-cyclic-wrap');
const cyclic = block.querySelector('.mol-cyclic');
const viciPanel = block.querySelector('.vici-lookup');
const t = String(sel?.value || 'protein');
if (t === 'protein') {
ta.placeholder = 'Enter amino-acid sequence (1-letter; e.g., MKTIIALSYI...)';
if (cyclicWrap) cyclicWrap.style.display = 'inline-flex';
} else if (t === 'dna') {
ta.placeholder = 'Enter DNA sequence (A/C/G/T; e.g., ATGGCC...)';
if (cyclicWrap) cyclicWrap.style.display = 'none';
if (cyclic) cyclic.checked = false;
} else if (t === 'rna') {
ta.placeholder = 'Enter RNA sequence (A/C/G/U; e.g., AUGGCC...)';
if (cyclicWrap) cyclicWrap.style.display = 'none';
if (cyclic) cyclic.checked = false;
} else {
ta.placeholder = 'Enter SMILES or CCD glycan string (e.g., Cn1cnc2..., or NAG(4-1 NAG))';
if (cyclicWrap) cyclicWrap.style.display = 'none';
if (cyclic) cyclic.checked = false;
}
// Update vici mode
if (viciPanel) {
viciPanel.setAttribute('data-vici-mode', t === 'ligand' ? 'ligand' : t);
}
// Bio sanitize for protein/dna/rna only
const sanitizeBio = (val) => String(val || '').replace(/\s+/g, '').toUpperCase();
if (ta._bioSanitizer) {
ta.removeEventListener('input', ta._bioSanitizer);
ta._bioSanitizer = null;
}
if (t === 'protein' || t === 'dna' || t === 'rna') {
const handler = (evt) => {
const cur = evt.target.value;
const cleaned = sanitizeBio(cur);
if (cur !== cleaned) {
const selStart = evt.target.selectionStart;
const delta = cleaned.length - cur.length;
evt.target.value = cleaned;
try { evt.target.setSelectionRange(selStart + delta, selStart + delta); } catch {}
}
};
ta.addEventListener('input', handler);
ta._bioSanitizer = handler;
}
}
function addMolecule({
chainId = null,
type = 'protein',
sequence = '',
cyclic = false,
animate = true,
isBase = false
} = {}) {
const existing = new Set(getChainIds());
const id = String(chainId || nextAutoChainId(existing)).toUpperCase();
const uid = `boltz-mol-${Math.random().toString(36).slice(2, 8)}`;
const div = document.createElement('div');
div.className = 'form-card field-group molecule-block';
div.dataset.uid = uid;
// if list empty, treat as base and do not animate
if (molList.querySelectorAll('.molecule-block').length === 0) {
isBase = true;
animate = false;
}
div.dataset.base = isBase ? '1' : '0';
div.innerHTML = `
Select the molecule type. The sequence box enforces simple validation on submit.
`;
molList.appendChild(div);
// set initial values
const typeSel = div.querySelector('.mol-type');
if (typeSel) typeSel.value = type;
const cyc = div.querySelector('.mol-cyclic');
if (cyc) cyc.checked = !!cyclic;
onMolTypeChange(div);
updateMolLabel(div);
updateRestraintChainDropdowns();
// animate only for non-base molecules
if (!isBase && animate) {
div.style.display = 'none';
slideOpen(div, { duration: 240 });
}
scheduleViciInit();
return div;
}
function ensureAtLeastOneMolecule() {
if (!molList.querySelector('.molecule-block')) {
addMolecule({ chainId: 'A', type: 'protein', animate: false, isBase: true });
} else {
// ensure first is marked base
const base = getBaseMoleculeBlock();
if (base) base.dataset.base = '1';
}
}
const msaEl = document.getElementById('boltz-msa');
if (msaEl) {
msaEl.value = 'true';
msaEl.disabled = true;
}
function removeMolecule(block) {
const blocks = getMoleculeBlocks();
const base = getBaseMoleculeBlock();
const isBase = (block === base) || block?.dataset.base === '1' || blocks[0] === block;
if (isBase) {
// base never deletes, it clears everything including lookup + closes it
clearMoleculeBlock(block, { collapseLookup: true });
showToast?.('success', 'Molecule cleared.');
return;
}
// non-base molecules slide out and are removed
slideClose(block, {
duration: 240,
after: () => {
block.remove();
updateRestraintChainDropdowns();
}
});
}
function swapMoleculeContent(a, b) {
if (!a || !b) return;
const aType = a.querySelector('.mol-type');
const bType = b.querySelector('.mol-type');
const aSeq = a.querySelector('.mol-seq');
const bSeq = b.querySelector('.mol-seq');
const aCyc = a.querySelector('.mol-cyclic');
const bCyc = b.querySelector('.mol-cyclic');
// swap type
const t = aType?.value;
if (aType && bType) {
aType.value = bType.value;
bType.value = t;
}
// swap sequence text
const s = aSeq?.value;
if (aSeq && bSeq) {
aSeq.value = bSeq.value;
bSeq.value = s;
}
// swap cyclic
const c = !!aCyc?.checked;
if (aCyc && bCyc) {
aCyc.checked = !!bCyc.checked;
bCyc.checked = c;
}
// re-run type handlers so placeholders + vici modes update
onMolTypeChange(a);
onMolTypeChange(b);
// notify dirty tracking
[aType, bType, aSeq, bSeq, aCyc, bCyc].forEach((el) => {
if (!el) return;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
});
}
function moveMolecule(block, dir) {
const items = Array.from(molList.querySelectorAll('.molecule-block'));
const i = items.indexOf(block);
if (i < 0) return;
if (dir === 'up' && i > 0) swapMoleculeContent(items[i], items[i - 1]);
if (dir === 'down' && i < items.length - 1) swapMoleculeContent(items[i], items[i + 1]);
}
function onRestraintTypeChange(block) {
const type = block.querySelector('.restraint-type')?.value;
const res1Wrap = block.querySelector('.res1-wrap');
const distWrap = block.querySelector('.distance-wrap');
const res1 = block.querySelector('.res1');
const res2 = block.querySelector('.res2');
if (type === 'pocket') {
if (res1Wrap) res1Wrap.style.display = 'none';
if (distWrap) distWrap.style.display = '';
if (res1) res1.value = '';
if (res2) res2.placeholder = 'Pocket residue position (e.g., 57 or D57)';
} else if (type === 'covalent') {
if (res1Wrap) res1Wrap.style.display = '';
if (distWrap) distWrap.style.display = 'none';
if (res1) res1.placeholder = 'Token 1 (e.g., N436@N or @C1)';
if (res2) res2.placeholder = 'Token 2 (e.g., @C1 or K56@CA)';
} else {
if (res1Wrap) res1Wrap.style.display = '';
if (distWrap) distWrap.style.display = '';
if (res1) res1.placeholder = 'Residue 1 position (e.g., 120 or R84)';
if (res2) res2.placeholder = 'Residue 2 position (e.g., 45 or K45)';
}
}
function addRestraint({
type = 'contact',
chain1 = '',
res1 = '',
chain2 = '',
res2 = '',
distance = '',
force = false
} = {}) {
const uid = `boltz-rest-${Math.random().toString(36).slice(2, 8)}`;
const div = document.createElement('div');
div.className = 'form-card field-group restraint-block';
div.dataset.uid = uid;
div.innerHTML = `
`;
div.style.display = 'none';
restList.appendChild(div);
slideOpen(div, { duration: 240 });
// init values
div.querySelector('.restraint-type').value = type;
div.querySelector('.res1').value = res1;
div.querySelector('.res2').value = res2;
div.querySelector('.distance').value = distance;
div.querySelector('.restraint-force').checked = !!force;
updateRestraintChainDropdowns();
if (chain1) div.querySelector('.chain1').value = chain1;
if (chain2) div.querySelector('.chain2').value = chain2;
onRestraintTypeChange(div);
return div;
}
function addTemplateBlock({
chain_id = '',
template_id = '',
force = false,
threshold = '',
file_name = '',
content = '',
source = ''
} = {}) {
const idx = tplList.querySelectorAll('.template-item').length;
const uid = `boltz-tpl-${Math.random().toString(36).slice(2, 8)}`;
const contentKey = `template_${idx}_content`;
const item = document.createElement('div');
item.className = 'field-group template-item molecule-block molecule-block--dropzone';
item.dataset.uid = uid;
item.innerHTML = `
`;
item.style.display = 'none';
tplList.appendChild(item);
slideOpen(item, { duration: 240 });
try { window.ViciLookup?.init?.(item); } catch {}
// init force + threshold
const forceSel = item.querySelector('.tpl-force');
const thr = item.querySelector('.tpl-threshold');
forceSel.value = force ? 'true' : 'false';
thr.value = String(threshold ?? '');
thr.disabled = forceSel.value !== 'true';
if (thr.disabled) thr.value = '';
forceSel.addEventListener('change', () => {
const on = forceSel.value === 'true';
thr.disabled = !on;
if (!on) thr.value = '';
thr.dispatchEvent(new Event('input', { bubbles: true }));
thr.dispatchEvent(new Event('change', { bubbles: true }));
});
// bind drop zone
const ctrl = createDropZoneController({
scope: item,
dropZone: '.tpl-drop-zone',
fileInput: '.lookup-drop-input',
metaEl: '.drop-zone__meta',
titleEl: '.drop-zone__title',
contentField: '.lookup-target-content',
nameField: '.lookup-target-name',
sourceField: '.lookup-target-source',
removeBtn: '.tpl-clear-file',
emptyTitle: 'Drop .pdb or .cif here or click to upload',
emptyMeta: 'Template content from upload or Vici Lookup will appear here'
});
if (ctrl) {
ctrl.bind();
dropZoneControllers.push(ctrl);
// If lookup writes directly into textarea, normalize meta + show filled state
const contentField = ctrl.refs.contentField;
const nameField = ctrl.refs.nameField;
const sourceField = ctrl.refs.sourceField;
const drop = ctrl.refs.drop;
const syncLookupState = () => {
const text = String(contentField.value || '');
if (!text.trim()) {
if (nameField) nameField.value = '';
if (sourceField) sourceField.value = '';
ctrl.render();
return;
}
if (sourceField && !sourceField.value) sourceField.value = 'vici_lookup';
if (nameField && !nameField.value) nameField.value = `template.${guessTemplateKindFromNameOrContent('', text)}`;
if (!drop.dataset.fileSizeBytes) drop.dataset.fileSizeBytes = String(text.length || '');
ctrl.render();
};
contentField.addEventListener('input', syncLookupState);
contentField.addEventListener('change', syncLookupState);
syncLookupState();
}
return item;
}
function clearTemplatesUI() {
tplList.innerHTML = '';
// clear controllers
while (dropZoneControllers.length) {
dropZoneControllers.pop();
}
}
function ensureApiDefPopout() {
if (apiDefPopoutEl) return apiDefPopoutEl;
const el = document.createElement('div');
el.className = 'api-def-popout';
el.setAttribute('role', 'dialog');
el.setAttribute('aria-hidden', 'true');
el.innerHTML = `
`;
document.body.appendChild(el);
apiDefPopoutEl = el;
return el;
}
function getApiDefinition(defRef) {
return apiDynamicDefContent?.[defRef] || API_DEF_CONTENT?.[defRef] || null;
}
function positionApiDefPopout(anchorEl) {
const pop = ensureApiDefPopout();
if (!anchorEl) return;
const a = anchorEl.getBoundingClientRect();
const p = pop.getBoundingClientRect();
const gap = 10;
const margin = 12;
let left = a.left;
let top = a.bottom + gap;
if (left + p.width > window.innerWidth - margin) left = window.innerWidth - p.width - margin;
if (left < margin) left = margin;
if (top + p.height > window.innerHeight - margin) top = a.top - p.height - gap;
if (top < margin) top = margin;
pop.style.left = `${Math.round(left)}px`;
pop.style.top = `${Math.round(top)}px`;
}
function showApiDefPopoutFor(targetEl) {
const defRef = targetEl?.getAttribute('data-def-ref');
if (!defRef) return;
const def = getApiDefinition(defRef);
if (!def) return;
if (apiDefHideTimer) { clearTimeout(apiDefHideTimer); apiDefHideTimer = null; }
const pop = ensureApiDefPopout();
pop.querySelector('.api-def-popout__title').textContent = def.title || defRef;
pop.querySelector('.api-def-popout__body').innerHTML = def.html || '';
apiDefAnchorEl = targetEl;
pop.classList.add('is-visible');
pop.setAttribute('aria-hidden', 'false');
positionApiDefPopout(targetEl);
}
function hideApiDefPopout() {
const pop = ensureApiDefPopout();
pop.classList.remove('is-visible');
pop.setAttribute('aria-hidden', 'true');
apiDefAnchorEl = null;
}
function scheduleHideApiDefPopout(delay = 120) {
if (apiDefHideTimer) clearTimeout(apiDefHideTimer);
apiDefHideTimer = setTimeout(() => {
apiDefHideTimer = null;
hideApiDefPopout();
}, delay);
}
function bindApiDefinitionPopout() {
if (!apiCodeEl) return;
ensureApiDefPopout();
apiCodeEl.addEventListener('mouseover', (e) => {
const target = e.target.closest('.tok-editable[data-def-ref]');
if (!target || !apiCodeEl.contains(target)) return;
showApiDefPopoutFor(target);
});
apiCodeEl.addEventListener('mouseout', (e) => {
const from = e.target.closest('.tok-editable[data-def-ref]');
if (!from || !apiCodeEl.contains(from)) return;
const to = e.relatedTarget;
if (to && (from.contains(to) || ensureApiDefPopout().contains(to))) return;
scheduleHideApiDefPopout();
});
apiCodeEl.addEventListener('mousemove', (e) => {
const target = e.target.closest('.tok-editable[data-def-ref]');
if (!target || !apiCodeEl.contains(target)) return;
if (apiDefAnchorEl === target) positionApiDefPopout(target);
});
window.addEventListener('scroll', () => {
if (apiDefAnchorEl && apiDefPopoutEl?.classList.contains('is-visible')) {
positionApiDefPopout(apiDefAnchorEl);
}
}, true);
window.addEventListener('resize', () => {
if (apiDefAnchorEl && apiDefPopoutEl?.classList.contains('is-visible')) {
positionApiDefPopout(apiDefAnchorEl);
}
});
}
// Presets (buttons already in HTML)
window.MODEL_PRESETS = window.MODEL_PRESETS || {};
window.MODEL_PRESETS.example1 = function presetExample1() {
// wipe existing
molList.innerHTML = '';
restList.innerHTML = '';
clearTemplatesUI();
if (tplToggle) tplToggle.checked = false;
if (tplPanel) tplPanel.style.display = 'none';
// Example 1
// sequences:
// - protein id [A,B] sequence: ...
// - ligand id [C,D] ccd: SAH
// - ligand id [E,F] smiles: ...
const proteinSeq = `MVTPEGNVSLVDESLLVGVTDEDRAVRSAHQFYERLIGLWAPAVMEAAHELGVFAALAEAPADSGELARRLDCDARAMRVLLDALYAYDVIDRIHDTNGFRYLLSAEARECLLPGTLFSLVGKFMHDINVAWPAWRNLAEVVRHGARDTSGAESPNGIAQEDYESLVGGINFWAPPIVTTLSRKLRASGRSGDATASVLDVGCGTGLYSQLLLREFPRWTATGLDVERIATLANAQALRLGVEERFATRAGDFWRGGWGTGYDLVLFANIFHLQTPASAVRLMRHAAACLAPDGLVAVVDQIVDADREPKTPQDRFALLFAASMTNTGGGDAYTFQEYEEWFTAAGLQRIETLDTPMHRILLARRATEPSAVPEGQASENLYFQ`;
addMolecule({ chainId: 'A', type: 'protein', sequence: proteinSeq });
addMolecule({ chainId: 'B', type: 'protein', sequence: proteinSeq });
addMolecule({ chainId: 'C', type: 'ligand', sequence: 'SAH' });
addMolecule({ chainId: 'D', type: 'ligand', sequence: 'SAH' });
addMolecule({ chainId: 'E', type: 'ligand', sequence: `N[C@@H](Cc1ccc(O)cc1)C(=O)O` });
addMolecule({ chainId: 'F', type: 'ligand', sequence: `N[C@@H](Cc1ccc(O)cc1)C(=O)O` });
// advanced defaults
document.getElementById('boltz-msa').value = 'true';
document.getElementById('boltz-recycles').value = '3';
document.getElementById('boltz-diffusion').value = '200';
document.getElementById('boltz-samples').value = '1';
document.getElementById('boltz-affinity').value = 'false';
updateRestraintChainDropdowns();
};
window.MODEL_PRESETS.example2 = function presetExample2() {
// wipe existing
molList.innerHTML = '';
restList.innerHTML = '';
clearTemplatesUI();
if (tplToggle) tplToggle.checked = false;
if (tplPanel) tplPanel.style.display = 'none';
const prot = `MYNMRRLSLSPTFSMGFHLLVTVSLLFSHVDHVIAETEMEGEGNETGECTGSYYCKKGVILPIWEPQDPSFGDKIARATVYFVAMVYMFLGVSIIADRFMSSIEVITSQEKEITIKKPNGETTKTTVRIWNETVSNLTLMALGSSAPEILLSVIEVCGHNFTAGDLGPSTIVGSAAFNMFIIIALCVYVVPDGETRKIKHLRVFFVTAAWSIFAYTWLYIILSVISPGVVEVWEGLLTFFFFPICVVFAWVADRRLLFYKYVYKRYRAGKQRGMIIEHEGDRPSSKTEIEMDGKVVNSHVENFLDGALVLEVDERDQDDEEARREMARILKELKQKHPDKEIEQLIELANYQVLSQQQKSRAFYRIQATRLMTGAGNILKRHAADQARKAVSMHEVNTEVTENDPVSKIFFEQGTYQCLENCGTVALTIIRRGGDLTNTVFVDFRTEDGTANAGSDYEFTEGTVVFKPGDTQKEIRVGIIDDDIFEEDENFLVHLSNVKVSSEASEDGILEANHVSTLACLGSPSTATVTIFDDDHAGIFTFEEPVTHVSESIGIMEVKVLRTSGARGNVIVPYKTIEGTARGGGEDFEDTCGELEFQNDEIVKIITIRIFDREEYEKECSFSLVLEEPKWIRRGMKGGFTITDEYDDKQPLTSKEEEERRIAEMGRPILGEHTKLEVIIEESYEFKSTVDKLIKKTNLALVVGTNSWREQFIEAITVSAGEDDDDDECGEEKLPSCFDYVMHFLTVFWKVLFAFVPPTEYWNGWACFIVSILMIGLLTAFIGDLASHFGCTIGLKDSVTAVVFVALGTSVPDTFASKVAATQDQYADASIGNVTGSNAVNVFLGIGVAWSIAAIYHAANGEQFKVSPGTLAFSVTLFTIFAFINVGVLLYRRRPEIGGELGGPRTAKLLTSCLFVLLWLLYIFFSSLEAYCHIKGF`;
addMolecule({ chainId: 'A', type: 'protein', sequence: prot });
addMolecule({ chainId: 'B', type: 'ligand', sequence: 'EKY' });
addRestraint({
type: 'pocket',
chain1: 'B',
chain2: 'A',
res2: '829',
distance: '0.5',
force: false
});
// also add second contact in same pocket constraint (matches example)
// We will add as a second pocket restraint for UI clarity
addRestraint({
type: 'pocket',
chain1: 'B',
chain2: 'A',
res2: '138',
distance: '0.5',
force: false
});
// advanced defaults
document.getElementById('boltz-msa').value = 'true';
document.getElementById('boltz-recycles').value = '3';
document.getElementById('boltz-diffusion').value = '200';
document.getElementById('boltz-samples').value = '1';
document.getElementById('boltz-affinity').value = 'true';
updateRestraintChainDropdowns();
};
// Adapter consumed by the page framework
window.ModelPageAdapter = {
onInit() {
// populate selects
const rec = document.getElementById('boltz-recycles');
if (rec) {
rec.innerHTML = '';
for (let i = 1; i <= 20; i++) {
const opt = document.createElement('option');
opt.value = String(i);
opt.textContent = String(i);
if (i === 3) opt.selected = true;
rec.appendChild(opt);
}
}
const diff = document.getElementById('boltz-diffusion');
if (diff) {
diff.innerHTML = '';
for (let i = 100; i <= 2000; i += 100) {
const opt = document.createElement('option');
opt.value = String(i);
opt.textContent = String(i);
if (i === 200) opt.selected = true;
diff.appendChild(opt);
}
}
const sam = document.getElementById('boltz-samples');
if (sam) {
sam.innerHTML = '';
for (let i = 1; i <= 10; i++) {
const opt = document.createElement('option');
opt.value = String(i);
opt.textContent = String(i);
if (i === 1) opt.selected = true;
sam.appendChild(opt);
}
}
ensureAtLeastOneMolecule();
// button bindings
document.getElementById('boltz-add-molecule')?.addEventListener('click', (e) => {
e.preventDefault();
addMolecule({});
});
document.getElementById('boltz-add-restraint')?.addEventListener('click', (e) => {
e.preventDefault();
addRestraint({});
updateRestraintChainDropdowns();
});
document.getElementById('boltz-add-template')?.addEventListener('click', (e) => {
e.preventDefault();
const item = addTemplateBlock({});
try { window.ViciLookup?.init?.(item); } catch {}
});
tplToggle?.addEventListener('change', () => {
if (!tplPanel) return;
const card = root.querySelector('.boltz-templates-card');
card?.classList.toggle('is-on', !!tplToggle.checked);
if (tplToggle.checked) {
ensureAtLeastOneTemplateBlock();
slideOpen(tplPanel, { duration: 240 });
try { window.ViciLookup?.init?.(root); } catch {}
}
if (tplToggle.checked) {
if (!tplList.querySelector('.template-item')) addTemplateBlock({});
slideOpen(tplPanel, { duration: 240 });
try { window.ViciLookup?.init?.(root); } catch {}
} else {
slideClose(tplPanel, {
duration: 240,
after: () => {
clearTemplatesUI();
}
});
}
});
// molecule + restraint delegated actions
root.addEventListener('click', (e) => {
const molBlock = e.target.closest('.molecule-block');
if (molBlock) {
const mv = e.target.closest('.mol-move');
if (mv) {
moveMolecule(molBlock, mv.dataset.dir);
return;
}
const rm = e.target.closest('.mol-remove');
if (rm) {
removeMolecule(molBlock);
return;
}
const viciBtn = e.target.closest('.molecule-block .vici-toggle-btn');
if (viciBtn) {
window.toggleViciLookup(viciBtn);
return;
}
}
const restBlock = e.target.closest('.restraint-block');
if (restBlock) {
const rm = e.target.closest('.rest-remove');
if (rm) {
slideClose(restBlock, { duration: 240, after: () => restBlock.remove() });
return;
}
}
const tplBlock = e.target.closest('.template-item');
if (tplBlock) {
const viciBtn = e.target.closest('.template-item .vici-toggle-btn');
if (viciBtn) {
window.toggleViciLookup(viciBtn);
return;
}
const rm = e.target.closest('.tpl-remove');
if (rm) {
const items = getTemplateBlocks();
const isLast = items.length <= 1;
// If Templates is ON, never allow 0 blocks
if (tplToggle?.checked && isLast) {
// Clear instead of removing
const idx = dropZoneControllers.findIndex(
c => c.refs?.contentField && tplBlock.contains(c.refs.contentField)
);
// keep controller so ctrl.render/clear still works for this block
// (no splice here)
clearTemplateBlock(tplBlock, { collapseLookup: true });
showToast?.('success', 'Template cleared.');
return;
}
// Normal remove for non-last blocks (or if Templates is OFF)
const idx = dropZoneControllers.findIndex(
c => c.refs?.contentField && tplBlock.contains(c.refs.contentField)
);
if (idx >= 0) dropZoneControllers.splice(idx, 1);
slideClose(tplBlock, {
duration: 240,
after: () => {
tplBlock.remove();
// Safety: if Templates still ON and we somehow hit 0, re-add one
ensureAtLeastOneTemplateBlock();
}
});
return;
}
}
});
root.addEventListener('change', (e) => {
const restBlock = e.target.closest('.restraint-block');
if (restBlock && e.target.classList.contains('restraint-type')) {
onRestraintTypeChange(restBlock);
return;
}
});
root.addEventListener('input', (e) => {
const molBlock = e.target.closest('.molecule-block');
if (molBlock && e.target.classList.contains('mol-chain')) {
updateMolLabel(molBlock);
updateRestraintChainDropdowns();
}
});
},
captureState() {
const name = String(document.getElementById('jobname')?.value || '');
const adv = {
msa: 'true',
diffusion_steps: document.getElementById('boltz-diffusion')?.value || '200',
recycles: document.getElementById('boltz-recycles')?.value || '3',
samples: document.getElementById('boltz-samples')?.value || '1',
affinity: document.getElementById('boltz-affinity')?.value || 'false',
templates_use: !!tplToggle?.checked
};
const molecules = Array.from(molList.querySelectorAll('.molecule-block')).map((b) => ({
chain_id: String(b.querySelector('.mol-chain')?.value || '').trim(),
type: String(b.querySelector('.mol-type')?.value || 'protein'),
sequence: String(b.querySelector('.mol-seq')?.value || ''),
cyclic: !!b.querySelector('.mol-cyclic')?.checked
}));
const restraints = Array.from(restList.querySelectorAll('.restraint-block')).map((b) => ({
type: String(b.querySelector('.restraint-type')?.value || 'contact'),
chain1: String(b.querySelector('.chain1')?.value || ''),
res1: String(b.querySelector('.res1')?.value || ''),
chain2: String(b.querySelector('.chain2')?.value || ''),
res2: String(b.querySelector('.res2')?.value || ''),
distance: String(b.querySelector('.distance')?.value || ''),
force: !!b.querySelector('.restraint-force')?.checked
}));
const templates = Array.from(tplList.querySelectorAll('.template-item')).map((b) => ({
chain_id: String(b.querySelector('.tpl-chain')?.value || ''),
template_id: String(b.querySelector('.tpl-template')?.value || ''),
force: (String(b.querySelector('.tpl-force')?.value || 'false') === 'true'),
threshold: String(b.querySelector('.tpl-threshold')?.value || ''),
content: String(b.querySelector('.lookup-target-content')?.value || ''),
file_name: String(b.querySelector('.lookup-target-name')?.value || ''),
source: String(b.querySelector('.lookup-target-source')?.value || '')
}));
return {
tab: (root.classList.contains('is-tab-api') ? 'api' : root.classList.contains('is-tab-advanced') ? 'advanced' : 'basic'),
name,
adv,
molecules,
restraints,
templates
};
},
applyState(state) {
if (!state) return;
const nameEl = document.getElementById('jobname');
if (nameEl && typeof state.name === 'string') nameEl.value = state.name;
const adv = state.adv || {};
const msaEl2 = document.getElementById('boltz-msa');
if (msaEl2) {
msaEl2.value = 'true';
msaEl2.disabled = true;
}
if (document.getElementById('boltz-diffusion') && adv.diffusion_steps != null) document.getElementById('boltz-diffusion').value = String(adv.diffusion_steps);
if (document.getElementById('boltz-recycles') && adv.recycles != null) document.getElementById('boltz-recycles').value = String(adv.recycles);
if (document.getElementById('boltz-samples') && adv.samples != null) document.getElementById('boltz-samples').value = String(adv.samples);
if (document.getElementById('boltz-affinity') && adv.affinity != null) document.getElementById('boltz-affinity').value = String(adv.affinity);
molList.innerHTML = '';
restList.innerHTML = '';
(state.molecules || []).forEach((m) => {
addMolecule({
chainId: m.chain_id || null,
type: m.type || 'protein',
sequence: m.sequence || '',
cyclic: !!m.cyclic
});
});
ensureAtLeastOneMolecule();
updateRestraintChainDropdowns();
(state.restraints || []).forEach((r) => {
addRestraint({
type: r.type || 'contact',
chain1: r.chain1 || '',
res1: r.res1 || '',
chain2: r.chain2 || '',
res2: r.res2 || '',
distance: r.distance || '',
force: !!r.force
});
});
// templates
if (tplToggle) tplToggle.checked = !!adv.templates_use;
if (tplPanel) {
if (tplToggle?.checked) {
tplPanel.style.display = 'block';
} else {
tplPanel.style.display = 'none';
}
}
tplList.innerHTML = '';
while (dropZoneControllers.length) dropZoneControllers.pop();
if (tplToggle?.checked) {
const tpls = Array.isArray(state.templates) ? state.templates : [];
if (tpls.length === 0) addTemplateBlock({});
else {
tpls.forEach((t) => {
addTemplateBlock({
chain_id: t.chain_id || '',
template_id: t.template_id || '',
force: !!t.force,
threshold: t.threshold || '',
file_name: t.file_name || '',
content: t.content || '',
source: t.source || ''
});
});
}
}
},
reset({ baselineState }) {
const desired = baselineState?.molecules || [
{ chain_id: 'A', type: 'protein', sequence: '', cyclic: false }
];
// base clears, extras slide out
setMoleculesFromArray(desired, { animateAdds: false, animateRemovals: true });
// clear other stuff
restList.innerHTML = '';
clearTemplatesUI();
if (tplToggle) tplToggle.checked = false;
if (tplPanel) tplPanel.style.display = 'none';
updateRestraintChainDropdowns();
},
buildJob(opts = {}, ctx = {}) {
const requireName = opts.requireName !== false;
const validateName = opts.validate !== false;
const toast = !!opts.toast;
const forApi = !!opts.forApi;
const nameInput = document.getElementById('jobname');
const rawName = String(nameInput?.value || '').trim();
const runName = canonicalizeRunName(rawName || `my_${ctx.modelKey || 'boltz2'}_run`);
if (nameInput && rawName && runName !== rawName) {
nameInput.value = runName;
if (toast) showToast?.('success', `Name adjusted to "${runName}".`);
}
if (requireName && !runName) {
if (toast) showToast?.('error', 'Name is required.');
return { error: 'Name is required.' };
}
if (validateName && runName && !SAFE_NAME_RE.test(runName)) {
if (toast) showToast?.('error', 'Name must be 3-64 chars using a-z, 0-9, _ or - and start/end with letter or digit.');
return { error: 'Invalid name.' };
}
// advanced
const use_mmseqs2 = true;
const recycles = parseInt(document.getElementById('boltz-recycles')?.value || '3', 10);
const diffusion_steps = parseInt(document.getElementById('boltz-diffusion')?.value || '200', 10);
let samples = parseInt(document.getElementById('boltz-samples')?.value || '1', 10);
if (!Number.isInteger(samples) || samples < 1) samples = 1;
const affinityOn = document.getElementById('boltz-affinity')?.value === 'true';
const strict = !forApi;
// molecules
const molBlocks = Array.from(molList.querySelectorAll('.molecule-block'));
if (molBlocks.length < 1) {
if (toast) showToast?.('error', 'At least one molecule is required.');
return { error: 'No molecules.' };
}
const molecules = [];
const chainType = {};
const chainSet = new Set();
for (const b of molBlocks) {
const chain_id = String(b.querySelector('.mol-chain')?.value || '').trim().toUpperCase();
const type = String(b.querySelector('.mol-type')?.value || 'protein');
const sequenceRaw = String(b.querySelector('.mol-seq')?.value || '').trim();
const cyclic = !!b.querySelector('.mol-cyclic')?.checked;
if (!chain_id) {
if (strict) {
if (toast) showToast?.('error', 'Each molecule must have a Chain ID.');
return { error: 'Missing chain_id.' };
}
continue;
}
if (chainSet.has(chain_id)) {
if (strict) {
if (toast) showToast?.('error', `Duplicate Chain ID: ${chain_id}.`);
return { error: 'Duplicate chain_id.' };
}
continue;
}
chainSet.add(chain_id);
// Always store chain types so constraints parsing works
chainType[chain_id] = (type === 'ligand') ? 'ligand' : type;
// Only validate on real submit
if (strict && sequenceRaw) {
if (type === 'protein' && !isValidProteinSeq(sequenceRaw)) {
if (toast) showToast?.('error', `Chain ${chain_id}: protein sequence should contain only ACDEFGHIKLMNPQRSTVWY.`);
return { error: 'Invalid protein seq.' };
}
if (type === 'dna' && !isValidDNASeq(sequenceRaw)) {
if (toast) showToast?.('error', `Chain ${chain_id}: DNA sequence must be A/C/G/T.`);
return { error: 'Invalid DNA seq.' };
}
if (type === 'rna' && !isValidRNASeq(sequenceRaw)) {
if (toast) showToast?.('error', `Chain ${chain_id}: RNA sequence must be A/C/G/U.`);
return { error: 'Invalid RNA seq.' };
}
if (type === 'ligand' && !(isValidCCDCode(sequenceRaw) || isValidGlycanCCD(sequenceRaw) || isValidSMILES(sequenceRaw))) {
if (toast) showToast?.('error', `Chain ${chain_id}: invalid ligand/glycan input (provide SMILES or CCD glycan string).`);
return { error: 'Invalid ligand.' };
}
}
// For API preview, include even empty sequences so Sync shows them
if (forApi || sequenceRaw) {
const sequence_type = (type === 'ligand')
? detectLigandNotation(sequenceRaw)
: 'sequence';
const out = {
type: type === 'ligand' ? 'ligand' : type,
sequence_type,
chain_id,
sequence: sequenceRaw
};
if (type === 'protein' && cyclic) out.cyclic = true;
molecules.push(out);
}
}
// On real submit, require at least one filled sequence
if (strict && molecules.length < 1) {
if (toast) showToast?.('error', 'At least one molecule sequence must be filled.');
return { error: 'No sequences filled.' };
}
// restraints -> constraints
const constraints = [];
const restraintsUI = [];
const restBlocks = Array.from(restList.querySelectorAll('.restraint-block'));
for (const b of restBlocks) {
const type = String(b.querySelector('.restraint-type')?.value || 'contact');
const chain1 = String(b.querySelector('.chain1')?.value || '').trim();
const chain2 = String(b.querySelector('.chain2')?.value || '').trim();
const res1 = String(b.querySelector('.res1')?.value || '').trim();
const res2 = String(b.querySelector('.res2')?.value || '').trim();
const force = !!b.querySelector('.restraint-force')?.checked;
const distRaw = String(b.querySelector('.distance')?.value || '').trim();
const distance = distRaw ? parseFloat(distRaw) : 6.0;
if (!chain1 || !chain2) {
if (toast) showToast?.('error', 'Each restraint must choose Chain 1 and Chain 2.');
return { error: 'Missing chains in restraint.' };
}
const t1 = chainType[chain1];
const t2 = chainType[chain2];
if (type === 'contact') {
if (t1 !== 'protein' || t2 !== 'protein') {
if (toast) showToast?.('error', 'Contact restraints require protein residues on both sides.');
return { error: 'Contact requires protein.' };
}
const p1 = posFromConstraintToken(res1);
const p2 = posFromConstraintToken(res2);
if (!Number.isInteger(p1) || !Number.isInteger(p2)) {
if (toast) showToast?.('error', 'Contact: residues must include a position number (e.g., 120, R84, or A120).');
return { error: 'Bad contact residue.' };
}
restraintsUI.push({ type, chain1, res1, chain2, res2, distance, force });
constraints.push({
contact: {
token1: [chain1, p1],
token2: [chain2, p2],
max_distance: distance,
...(force ? { force: true } : {})
}
});
} else if (type === 'pocket') {
// Pocket: binder is Chain 1 (can be ligand/protein), residue lives on Chain 2 (must be protein)
if (t2 !== 'protein') {
if (toast) showToast?.('error', 'Pocket: Chain 2 must be a protein chain (Residue 2 is a protein position).');
return { error: 'Pocket requires protein on chain2.' };
}
if (res1) {
if (toast) showToast?.('error', 'Pocket: leave Token 1 (Residue 1) empty.');
return { error: 'Pocket token1 must be empty.' };
}
const p2 = posFromConstraintToken(res2);
if (!Number.isInteger(p2)) {
if (toast) showToast?.('error', 'Pocket: Token 2 must include a position number (e.g., 57, D57, or A57).');
return { error: 'Bad pocket residue.' };
}
restraintsUI.push({ type, chain1, chain2, res2, distance, force });
constraints.push({
pocket: {
binder: chain1,
contacts: [[chain2, p2]],
max_distance: distance,
...(force ? { force: true } : {})
}
});
} else if (type === 'covalent') {
const a1 = covalentAtomTuple(res1, t1);
const a2 = covalentAtomTuple(res2, t2);
if (!a1 || !a2) {
if (toast) showToast?.('error', 'Covalent: protein side must be R84@N/C etc; non-protein side must be @C/@C1/@N2 etc.');
return { error: 'Bad covalent tokens.' };
}
restraintsUI.push({ type, chain1, res1, chain2, res2 });
constraints.push({ bond: { atom1: [chain1, a1[0], a1[1]], atom2: [chain2, a2[0], a2[1]] } });
} else {
if (toast) showToast?.('error', 'Unknown restraint type.');
return { error: 'Unknown restraint.' };
}
}
// templates
const templates_use = !!tplToggle?.checked;
const templates = [];
if (templates_use) {
const items = Array.from(tplList.querySelectorAll('.template-item'));
if (!forApi && items.length === 0) {
if (toast) showToast?.('error', 'Add at least one template or turn off Templates.');
return { error: 'No templates.' };
}
for (const item of items) {
const content = String(item.querySelector('.lookup-target-content')?.value || '');
const fileNameRaw = String(item.querySelector('.lookup-target-name')?.value || '').trim();
const forceOn = String(item.querySelector('.tpl-force')?.value || 'false') === 'true';
const thrRaw = String(item.querySelector('.tpl-threshold')?.value || '').trim();
const threshold = forceOn ? parseFloat(thrRaw) : undefined;
if (!forApi && !content.trim()) {
if (toast) showToast?.('error', 'Each template block needs a .pdb/.cif file or Vici Lookup content.');
return { error: 'Missing template content.' };
}
if (forceOn && !forApi && !Number.isFinite(threshold)) {
if (toast) showToast?.('error', 'When Force potentials is True, set a numeric Threshold (Å).');
return { error: 'Missing threshold.' };
}
const kind = guessTemplateKindFromNameOrContent(fileNameRaw, content);
const filename = fileNameRaw || `template.${kind}`;
const chain_ids = String(item.querySelector('.tpl-chain')?.value || '')
.split(',')
.map(s => s.trim())
.filter(Boolean);
const template_ids = String(item.querySelector('.tpl-template')?.value || '')
.split(',')
.map(s => s.trim())
.filter(Boolean);
const entry = {
filename,
kind,
// include content only on Sync in API, or always for real submission
content_b64: (forApi ? (opts.includeApiDropzoneContent ? base64FromUtf8(content) : '') : base64FromUtf8(content))
};
if (chain_ids.length) entry.chain_id = chain_ids;
if (template_ids.length) entry.template_id = template_ids;
if (forceOn) {
entry.force = true;
if (!forApi) entry.threshold = threshold;
else if (Number.isFinite(threshold)) entry.threshold = threshold;
}
templates.push(entry);
}
}
// affinity binder = first ligand chain
const ligandChains = molecules.filter(m => m.type === 'ligand').map(m => m.chain_id);
let affinityBinder = null;
if (affinityOn) {
if (ligandChains.length === 0) {
if (toast) showToast?.('error', 'Affinity requires at least one Ligand/Glycan chain.');
return { error: 'No ligand for affinity.' };
}
affinityBinder = ligandChains[0];
if (toast && ligandChains.length > 1) {
showToast?.('info', `Multiple ligand chains detected (${ligandChains.join(', ')}). Affinity will be computed for ${affinityBinder}.`);
}
}
const job = {
name: runName || `my_${ctx.modelKey || 'boltz2'}_run`,
msa: use_mmseqs2,
recycles,
diffusion_steps,
samples,
molecules,
restraints: restraintsUI,
constraints,
templates_use,
templates,
properties: (affinityOn && affinityBinder) ? [{ affinity: { binder: affinityBinder } }] : [],
affinity: affinityOn,
affinity_binder: affinityBinder
};
const payload = {
workflow_name: job.name,
boltz2: job
};
return { job, payload };
},
getDefaultApiPayload({ modelKey }) {
const name = `my_${modelKey}_run`;
return {
workflow_name: name,
boltz2: {
name,
msa: true,
recycles: 3,
diffusion_steps: 200,
samples: 1,
molecules: [
{ type: 'protein', sequence_type: 'sequence', chain_id: 'A', sequence: 'MKTIIALSYIFCLVFAD' },
{ type: 'ligand', sequence_type: 'ccd', chain_id: 'B', sequence: 'NAG(4-1 NAG)' }
],
restraints: [],
constraints: [],
templates_use: false,
templates: [],
properties: [],
affinity: false,
affinity_binder: null
}
};
}
};
/* -----------------------------
Framework (mostly your template)
------------------------------ */
const adapter = window.ModelPageAdapter || {};
const tabs = Array.from(root.querySelectorAll('.model-tab[data-tab]'));
const resetBtn = root.querySelector('.model-reset-btn');
const actionsWrap = root.querySelector('.model-actions');
const presetBtns = Array.from(root.querySelectorAll('.model-preset-btn[data-example]'));
const apiCodeEl = document.getElementById('api-code-block');
const apiLangTabs = Array.from(root.querySelectorAll('.api-lang-tab[data-lang]'));
const apiActionBtns = Array.from(root.querySelectorAll('.api-action-btn'));
const executeBtnMembers = root.querySelector('.model-actions [data-ms-content="members"]');
const executeBtnGuest = root.querySelector('.model-actions [data-ms-content="!members"]');
const modelSlug = String(root.dataset.model || 'boltz2').trim() || 'boltz2';
const modelKey = modelSlug
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '') || 'model';
const API_LANGS = ['python', 'curl', 'javascript'];
let currentTab = inferInitialTab();
let currentApiLang = 'python';
let currentApiSnippet = { text: '', html: '' };
let baselineState = null;
let defaultApiJob = null;
let isRenderingApiSnippet = false;
// For boltz2: include template content into API snippet only after Sync
let apiManualIncludeDropzoneContent = false;
const API_DEF_CONTENT = window.VICI_API_DEF_CONTENT || {
'token-id': {
title: 'Token-ID',
html: `
Your Vici Token ID. Send it as the Token-ID header.
Generate it in your
Account
.
`
},
'token-secret': {
title: 'Token-Secret',
html: `
Your Vici Token Secret. Send it as the Token-Secret header.
You only see this once when you generate it.
Generate it in your
Account
.
`
},
'workflow-name': {
title: `workflow_name / ${modelKey}.name`,
html: `A friendly run name shown in your Dashboard. The outer workflow_name and inner ${escapeHtml(modelKey)}.name should match.`
}
};
let apiDefPopoutEl = null;
let apiDefAnchorEl = null;
let apiDefHideTimer = null;
let apiDynamicDefContent = {};
function tabFromHash(hash = window.location.hash) {
const h = String(hash || '').trim().toLowerCase();
if (h === '#basic') return 'basic';
if (h === '#advanced') return 'advanced';
if (h === '#api') return 'api';
return null;
}
function syncHashToTab(tab, { replace = true } = {}) {
const nextHash = `#${tab}`;
if (String(window.location.hash || '').toLowerCase() === nextHash) return;
try {
const url = new URL(window.location.href);
url.hash = nextHash;
if (replace && window.history?.replaceState) {
window.history.replaceState(null, '', url.toString());
} else if (!replace && window.history?.pushState) {
window.history.pushState(null, '', url.toString());
} else {
window.location.hash = nextHash;
}
} catch {
window.location.hash = nextHash;
}
}
function bindHashRouting() {
window.addEventListener('hashchange', () => {
const next = tabFromHash();
if (!next || next === currentTab) return;
setTab(next, { silent: true, syncHash: false });
});
}
function inferInitialTab() {
const fromHash = tabFromHash();
if (fromHash) return fromHash;
if (root.classList.contains('is-tab-api')) return 'api';
if (root.classList.contains('is-tab-advanced')) return 'advanced';
return 'basic';
}
function pulseBtn(btn, cls) {
if (!btn) return;
btn.classList.remove(cls);
void btn.offsetWidth;
btn.classList.add(cls);
const onEnd = () => {
btn.classList.remove(cls);
btn.removeEventListener('animationend', onEnd);
};
btn.addEventListener('animationend', onEnd);
}
function copyTextRobust(text) {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text);
}
return new Promise((resolve, reject) => {
try {
const ta = document.createElement('textarea');
ta.value = text;
ta.setAttribute('readonly', '');
ta.style.position = 'fixed';
ta.style.left = '-9999px';
ta.style.top = '-9999px';
document.body.appendChild(ta);
ta.select();
ta.setSelectionRange(0, ta.value.length);
const ok = document.execCommand('copy');
ta.remove();
ok ? resolve() : reject(new Error('copy failed'));
} catch (err) {
reject(err);
}
});
}
function stripExecutionContextForApi(value) {
const blocked = new Set(['member_id', 'msid', 'user_id', 'team_id']);
if (Array.isArray(value)) return value.map(stripExecutionContextForApi);
if (!isPlainObject(value)) return value;
const out = {};
Object.entries(value).forEach(([k, v]) => {
if (blocked.has(k)) return;
out[k] = stripExecutionContextForApi(v);
});
return out;
}
function toDefRefSafe(path) {
return String(path).replace(/[^a-zA-Z0-9._:-]+/g, '_').slice(0, 180);
}
function valueTypeLabel(v) {
if (Array.isArray(v)) return 'array';
if (v === null) return 'null';
return typeof v;
}
function buildGenericPayloadDef(path, value) {
const pathLabel = String(path || 'payload');
const type = valueTypeLabel(value);
let typeHint = `Expected type: ${escapeHtml(type)}.`;
if (type === 'string') typeHint = 'Expected type: string. Replace with the value for your run.';
if (type === 'number') typeHint = 'Expected type: number. Use an integer or decimal that your model supports.';
if (type === 'boolean') typeHint = 'Expected type: boolean (true or false).';
let extra = 'Replace this example value with a valid value for your model.';
const pathLower = pathLabel.toLowerCase();
if (pathLower.endsWith('.name')) {
extra = 'Run/job name for this model block. Keep this aligned with workflow_name.';
}
return {
title: pathLabel,
html: `
${escapeHtml(pathLabel)}
${typeHint}
${extra}
`
};
}
function stringifyPayloadWithMarkers(payloadObj) {
const markers = [];
const dynamicDefs = {};
const mark = (value, kind = 'string', defRef = '') => {
const token = `__MARK_${markers.length}__`;
markers.push({ token, value, kind, defRef });
return token;
};
const payload = deepClone(payloadObj);
function walk(node, pathParts = []) {
if (Array.isArray(node)) {
for (let i = 0; i < node.length; i++) {
const v = node[i];
const childPath = [...pathParts, `[${i}]`];
if (v && typeof v === 'object') {
walk(v, childPath);
continue;
}
const pathStr = childPath.join('.');
const defRef = toDefRefSafe(`payload:${pathStr}`);
dynamicDefs[defRef] = buildGenericPayloadDef(pathStr, v);
let kind = 'string';
if (typeof v === 'number') kind = 'number';
else if (typeof v === 'boolean') kind = 'boolean';
else if (v === null) kind = 'null';
node[i] = mark(v, kind, defRef);
}
return;
}
if (!node || typeof node !== 'object') return;
Object.keys(node).forEach((key) => {
const v = node[key];
const childPath = [...pathParts, key];
if (v && typeof v === 'object') {
walk(v, childPath);
return;
}
const pathStr = childPath.join('.');
const isWorkflowName = pathStr === 'workflow_name';
const isInnerModelName = pathStr === `${modelKey}.name`;
let defRef = 'workflow-name';
if (!isWorkflowName && !isInnerModelName) {
defRef = toDefRefSafe(`payload:${pathStr}`);
dynamicDefs[defRef] = buildGenericPayloadDef(pathStr, v);
}
let kind = 'string';
if (typeof v === 'number') kind = 'number';
else if (typeof v === 'boolean') kind = 'boolean';
else if (v === null) kind = 'null';
node[key] = mark(v, kind, defRef);
});
}
walk(payload, []);
const jsonText = JSON.stringify(payload, null, 2);
let text = jsonText;
let html = escapeHtml(jsonText);
markers.forEach((m) => {
const quotedToken = `"${m.token}"`;
const quotedTokenHtml = `"${m.token}"`;
const jsonEscaped = JSON.stringify(String(m.value));
let textVal = jsonEscaped;
let htmlVal = `${escapeHtml(jsonEscaped)}`;
if (m.kind === 'number') {
textVal = String(m.value);
htmlVal = `${escapeHtml(String(m.value))}`;
} else if (m.kind === 'boolean') {
textVal = m.value ? 'true' : 'false';
htmlVal = `${m.value ? 'true' : 'false'}`;
} else if (m.kind === 'null') {
textVal = 'null';
htmlVal = `null`;
}
text = text.split(quotedToken).join(textVal);
html = html.split(quotedTokenHtml).join(htmlVal);
});
return { text, html, defs: dynamicDefs };
}
function getApiTemplate(lang, payloadText, payloadHtml) {
const HEREDOC_TAG = '__VICI_PAYLOAD_JSON__';
if (lang === 'python') {
return {
text: [
'# POST a model job (Python)',
'# Set TOKEN_ID and TOKEN_SECRET to your values.',
'import json',
'import requests',
'',
`API_URL = "${MODEL_API_ENDPOINT}"`,
'TOKEN_ID = ""',
'TOKEN_SECRET = ""',
'',
'payload = json.loads(r"""',
payloadText,
'""")',
'',
'resp = requests.post(',
' API_URL,',
' headers={',
' "Content-Type": "application/json",',
' "Token-ID": TOKEN_ID,',
' "Token-Secret": TOKEN_SECRET,',
' },',
' json=payload',
')',
'',
'resp.raise_for_status()',
'print(resp.json())'
].join('\n'),
html: [
'',
'',
'import json',
'import requests',
'',
`API_URL = "${escapeHtml(MODEL_API_ENDPOINT)}"`,
`TOKEN_ID = "<TOKEN_ID>"`,
`TOKEN_SECRET = "<TOKEN_SECRET>"`,
'',
'payload = json.loads(r"""',
payloadHtml,
'""")',
'',
'resp = requests.post(',
' API_URL,',
' headers={',
' "Content-Type": "application/json",',
' "Token-ID": TOKEN_ID,',
' "Token-Secret": TOKEN_SECRET,',
' },',
' json=payload',
')',
'',
'resp.raise_for_status()',
'print(resp.json());'
].join('\n')
};
}
if (lang === 'curl') {
return {
text: [
'# POST a model job (cURL)',
'# Set TOKEN_ID and TOKEN_SECRET to your values.',
'',
`curl -X POST "${MODEL_API_ENDPOINT}" \\`,
' -H "Content-Type: application/json" \\',
' -H "Token-ID: " \\',
' -H "Token-Secret: " \\',
` --data-binary @- <<'${HEREDOC_TAG}'`,
payloadText,
HEREDOC_TAG
].join('\n'),
html: [
'',
'',
'',
`curl -X POST "${escapeHtml(MODEL_API_ENDPOINT)}" \\`,
' -H "Content-Type: application/json" \\',
' -H "Token-ID: <TOKEN_ID>" \\',
' -H "Token-Secret: <TOKEN_SECRET>" \\',
` --data-binary @- <<'${escapeHtml(HEREDOC_TAG)}'`,
payloadHtml,
`${escapeHtml(HEREDOC_TAG)}`
].join('\n')
};
}
return {
text: [
'// POST a model job (JavaScript)',
'// Set TOKEN_ID and TOKEN_SECRET to your values.',
'',
'(async () => {',
` const API_URL = "${MODEL_API_ENDPOINT}";`,
' const TOKEN_ID = "";',
' const TOKEN_SECRET = "";',
'',
` const payload = ${payloadText};`,
'',
' const resp = await fetch(API_URL, {',
' method: "POST",',
' headers: {',
' "Content-Type": "application/json",',
' "Token-ID": TOKEN_ID,',
' "Token-Secret": TOKEN_SECRET,',
' },',
' body: JSON.stringify(payload),',
' });',
'',
' if (!resp.ok) throw new Error(`Request failed: ${resp.status}`);',
' console.log(await resp.json());',
'})().catch((err) => {',
' console.error(err);',
'});'
].join('\n'),
html: [
'',
'',
'',
'(async () => {',
` const API_URL = "${escapeHtml(MODEL_API_ENDPOINT)}";`,
` const TOKEN_ID = "<TOKEN_ID>";`,
` const TOKEN_SECRET = "<TOKEN_SECRET>";`,
'',
` const payload = ${payloadHtml};`,
'',
' const resp = await fetch(API_URL, {',
' method: "POST",',
' headers: {',
' "Content-Type": "application/json",',
' "Token-ID": TOKEN_ID,',
' "Token-Secret": TOKEN_SECRET,',
' },',
' body: JSON.stringify(payload),',
' });',
'',
' if (!resp.ok) throw new Error(`Request failed: ${resp.status}`);',
' console.log(await resp.json());',
'})().catch((err) => {',
' console.error(err);',
'});'
].join('\n')
};
}
function renderApiSnippet({ forceDefault = false, toast = false } = {}) {
if (!apiCodeEl) return;
if (isRenderingApiSnippet) return;
isRenderingApiSnippet = true;
try {
let payloadSource = null;
apiDynamicDefContent = {};
if (!forceDefault) {
const built = buildJob({
requireName: false,
validate: false,
toast: false,
forApi: true,
includeApiDropzoneContent: apiManualIncludeDropzoneContent
});
if (built && !built.error && built.payload) {
payloadSource = built.payload;
}
}
if (!payloadSource) {
payloadSource = deepClone(defaultApiJob || adapter.getDefaultApiPayload?.({ root, modelSlug, modelKey }) || {
workflow_name: `my_${modelKey}_run`,
[modelKey]: { name: `my_${modelKey}_run` }
});
}
payloadSource = stripExecutionContextForApi(payloadSource);
const payloadBlock = stringifyPayloadWithMarkers(payloadSource);
apiDynamicDefContent = payloadBlock.defs || {};
const snippet = getApiTemplate(
currentApiLang,
payloadBlock.text,
payloadBlock.html
);
currentApiSnippet = snippet;
apiCodeEl.innerHTML = snippet.html;
if (toast) {
showToast?.(
'success',
forceDefault ? 'Reset API snippet to defaults.' : 'Synced API snippet from form.'
);
}
} finally {
isRenderingApiSnippet = false;
}
}
function captureState() {
if (typeof adapter.captureState === 'function') {
try { return adapter.captureState({ root, modelSlug, modelKey }); }
catch (err) { console.error(err); }
}
return { tab: currentTab };
}
function applyState(state) {
if (!state) return;
if (typeof adapter.applyState === 'function') {
try { adapter.applyState(state, { root, modelSlug, modelKey }); }
catch (err) { console.error(err); }
}
}
function isDirty() {
if (!baselineState) return false;
const current = captureState() || {};
const base = baselineState || {};
const { tab: _t1, ...a } = current;
const { tab: _t2, ...b } = base;
return stableSerialize(a) !== stableSerialize(b);
}
function updateActionVisibility() {
const dirty = isDirty();
const hideForApi = currentTab === 'api';
resetBtn?.classList.toggle('is-visible', dirty && !hideForApi);
actionsWrap?.classList.toggle('is-visible', dirty && !hideForApi);
}
function setTab(tab, { silent = false, syncHash = true, replaceHash = false } = {}) {
if (!['basic', 'advanced', 'api'].includes(tab)) return;
currentTab = tab;
root.classList.remove('is-tab-basic', 'is-tab-advanced', 'is-tab-api');
root.classList.add(`is-tab-${tab}`);
tabs.forEach(btn => {
const active = btn.dataset.tab === tab;
btn.classList.toggle('is-active', active);
btn.setAttribute('aria-selected', active ? 'true' : 'false');
btn.setAttribute('tabindex', active ? '0' : '-1');
});
if (syncHash) {
syncHashToTab(tab, { replace: replaceHash || silent });
}
if (tab === 'api') renderApiSnippet();
updateActionVisibility();
}
function initTabs() {
tabs.forEach(btn => {
btn.addEventListener('click', () => setTab(btn.dataset.tab));
});
setTab(currentTab || 'basic', { silent: true, syncHash: true, replaceHash: true });
}
function buildJob(opts = {}) {
if (typeof adapter.buildJob === 'function') {
try { return adapter.buildJob(opts, { root, modelSlug, modelKey }); }
catch (err) { return { error: err?.message || 'Failed to build job.' }; }
}
return { error: 'No adapter.' };
}
function initDefaultApiJob() {
if (typeof adapter.getDefaultApiPayload === 'function') {
try {
defaultApiJob = adapter.getDefaultApiPayload({ root, modelSlug, modelKey });
if (defaultApiJob) defaultApiJob = stripExecutionContextForApi(defaultApiJob);
} catch (err) {
console.error(err);
}
}
if (!defaultApiJob) {
defaultApiJob = {
workflow_name: `my_${modelKey}_run`,
boltz2: { name: `my_${modelKey}_run` }
};
}
}
function bindDirtyTracking() {
root.addEventListener('input', (e) => {
if (e.target.closest('[data-panel="api"]')) return;
apiManualIncludeDropzoneContent = false;
updateActionVisibility();
});
root.addEventListener('change', (e) => {
if (e.target.closest('[data-panel="api"]')) return;
apiManualIncludeDropzoneContent = false;
updateActionVisibility();
if (currentTab === 'api') renderApiSnippet();
});
const mo = new MutationObserver(() => updateActionVisibility());
mo.observe(root, { childList: true, subtree: true });
}
function closeAllViciLookups({ duration = 240 } = {}) {
const panels = Array.from(root.querySelectorAll('.vici-lookup'))
.filter(p => p.style.display !== 'none' || p.classList.contains('open'));
if (panels.length === 0) return Promise.resolve();
return new Promise((resolve) => {
let remaining = panels.length;
panels.forEach((p) => {
slideClose(p, {
duration,
after: () => {
remaining--;
if (remaining <= 0) resolve();
}
});
});
// hard fallback in case transitions don't fire
setTimeout(resolve, duration + 120);
});
}
function bindReset() {
resetBtn?.addEventListener('click', async () => {
try {
// close lookups first so the animation is visible
await closeAllViciLookups({ duration: 240 });
root.querySelectorAll('.vici-toggle-btn.active').forEach((b) => {
b.classList.remove('active');
b.setAttribute('aria-expanded', 'false');
});
adapter.reset?.({ root, modelSlug, modelKey, baselineState: deepClone(baselineState) });
// clear dropzones
dropZoneControllers.forEach((c) => { try { c.clear(); } catch {} });
apiManualIncludeDropzoneContent = false;
renderApiSnippet({ forceDefault: false, toast: false });
updateActionVisibility();
showToast?.('success', 'Form reset.');
} catch (err) {
console.error(err);
showToast?.('error', 'Reset failed.');
}
});
}
function bindPresets() {
presetBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const key = btn.dataset.example;
const preset = window.MODEL_PRESETS?.[key];
if (!preset) {
showToast?.('error', `Preset "${key}" not found.`);
return;
}
try {
preset({ root, modelSlug, modelKey, setTab });
updateActionVisibility();
renderApiSnippet();
showToast?.('success', `Loaded preset: ${key}.`);
} catch (err) {
console.error(err);
showToast?.('error', 'Could not apply preset.');
}
});
});
}
function bindApiControls() {
apiLangTabs.forEach(btn => {
btn.addEventListener('click', () => {
const lang = btn.dataset.lang || 'python';
if (!API_LANGS.includes(lang)) return;
currentApiLang = lang;
apiLangTabs.forEach(b => {
const active = b.dataset.lang === lang;
b.classList.toggle('is-active', active);
b.setAttribute('aria-selected', active ? 'true' : 'false');
});
renderApiSnippet();
});
});
const [syncBtn, copyBtn, resetApiBtn] = apiActionBtns;
syncBtn?.addEventListener('click', () => {
pulseBtn(syncBtn, 'pulse-blue');
apiManualIncludeDropzoneContent = true; // include templates content_b64
renderApiSnippet({ toast: true });
});
copyBtn?.addEventListener('click', () => {
const text = currentApiSnippet?.text?.trim();
if (!text) { showToast?.('error', 'Nothing to copy yet.'); return; }
pulseBtn(copyBtn, 'pulse-green');
copyTextRobust(text)
.then(() => showToast?.('success', 'Copied API snippet.'))
.catch(() => showToast?.('error', 'Copy failed. Select code and copy manually.'));
});
resetApiBtn?.addEventListener('click', () => {
pulseBtn(resetApiBtn, 'pulse-red');
apiManualIncludeDropzoneContent = false;
renderApiSnippet({ forceDefault: true, toast: true });
});
}
async function getMemberId() {
const start = Date.now();
while (!window.$memberstackDom && Date.now() - start < 2000) {
await new Promise(r => setTimeout(r, 50));
}
const ms = window.$memberstackDom;
if (!ms || !ms.getCurrentMember) return null;
try {
const res = await ms.getCurrentMember();
return res?.data?.id || null;
} catch {
return null;
}
}
function getExecutionContextForMember(memberId) {
const out = { member_id: memberId };
try {
const ctxPayload = window.ViciContext?.payloadFor?.(memberId);
if (ctxPayload?.team_id) out.team_id = ctxPayload.team_id;
} catch {}
return out;
}
async function submitModelJob() {
const execBtn = executeBtnMembers;
const built = buildJob({ requireName: true, validate: true, toast: true });
if (!built || built.error || !built.payload) return;
if (typeof window.guardSubmitOrToast === 'function') {
const ok = await window.guardSubmitOrToast({ planned: 1, minCredit: 1.0, buttonSelector: execBtn });
if (!ok) return;
}
const memberId = await getMemberId();
if (!memberId) {
showToast?.('error', 'Please sign in to submit jobs.');
window.location.assign('/sign-up');
return;
}
if (!window.ViciExec?.post) {
showToast?.('error', 'ViciExec.post is not available on this page.');
return;
}
if (execBtn) {
execBtn.disabled = true;
execBtn.setAttribute('aria-busy', 'true');
}
UX?.overlay?.show?.('Submitting');
UX?.progress?.start?.();
UX?.progress?.trickle?.();
try {
const execCtx = getExecutionContextForMember(memberId);
const body = { ...built.payload, ...execCtx };
await window.ViciExec.post(MODEL_WORKFLOW_ENDPOINT, memberId, body);
window.ViciSidebar?.refresh?.().catch?.(() => {});
UX?.progress?.finishOk?.();
UX?.overlay?.show?.('Submitted');
document.querySelector('#page-overlay .label')?.removeAttribute('data-ellipsis');
showToast?.('success', 'Job submitted. Redirecting...');
setTimeout(() => {
UX?.overlay?.hide?.();
window.location.assign('/dashboard');
}, 650);
} catch (err) {
console.error(err);
UX?.progress?.finishFail?.();
UX?.overlay?.show?.('Submission failed');
document.querySelector('#page-overlay .label')?.removeAttribute('data-ellipsis');
showToast?.('error', err?.message || 'Submission failed. Please try again.');
setTimeout(() => UX?.overlay?.hide?.(), 320);
} finally {
if (execBtn) {
execBtn.disabled = false;
execBtn.removeAttribute('aria-busy');
}
}
}
function bindExecute() {
if (executeBtnMembers && executeBtnMembers.tagName.toLowerCase() === 'button') {
executeBtnMembers.type = 'button';
executeBtnMembers.addEventListener('click', (e) => { e.preventDefault(); submitModelJob(); });
}
if (executeBtnGuest && executeBtnGuest.tagName.toLowerCase() === 'a') {
// guest button is a link
}
}
function bindNameCanonicalization() {
const nameInput = document.getElementById('jobname');
if (!nameInput) return;
nameInput.addEventListener('blur', () => {
const raw = nameInput.value;
if (!raw.trim()) return;
const safe = canonicalizeRunName(raw);
if (safe && safe !== raw) {
nameInput.value = safe;
nameInput.dispatchEvent(new Event('input', { bubbles: true }));
nameInput.dispatchEvent(new Event('change', { bubbles: true }));
}
});
}
function blockFileDropsOnRoot() {
root.addEventListener('dragover', (e) => {
const isFile = Array.from(e.dataTransfer?.types || []).includes('Files');
if (isFile) e.preventDefault();
});
root.addEventListener('drop', (e) => {
const isFile = Array.from(e.dataTransfer?.types || []).includes('Files');
if (isFile) e.preventDefault();
});
}
function init() {
adapter.onInit?.({ root, modelSlug, modelKey });
bindNameCanonicalization();
bindDirtyTracking();
bindReset();
bindPresets();
bindApiControls();
bindExecute();
blockFileDropsOnRoot();
bindApiDefinitionPopout();
bindHashRouting();
initTabs();
window.ViciLookup?.init?.(root);
baselineState = deepClone(captureState());
initDefaultApiJob();
renderApiSnippet({ forceDefault: false, toast: false });
updateActionVisibility();
window.ModelPage = {
root,
modelSlug,
modelKey,
setTab,
getCurrentTab: () => currentTab,
isDirty,
updateActionVisibility,
captureState,
applyState,
resetForm: () => adapter.reset?.({ root, modelSlug, modelKey, baselineState: deepClone(baselineState) }),
buildJob,
submitJob: submitModelJob,
renderApiSnippet,
endpoints: {
workflow: MODEL_WORKFLOW_ENDPOINT,
api: MODEL_API_ENDPOINT,
status: MODEL_STATUS_ENDPOINT
}
};
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();