(() => {
'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;
/* -----------------------------
Shared utilities
------------------------------ */
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 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 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 svgUp() {
return `
`;
}
function svgDown() {
return `
`;
}
function svgTrash() {
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');
try { window.ViciLookup?.clear?.(panel, { keepInput: false, keepTarget: true }); } catch {}
slideClose(panel, { duration: 240 });
return;
}
btn.classList.add('active');
btn.setAttribute('aria-expanded', 'true');
panel.style.display = 'block';
panel.style.maxHeight = '0px';
panel.style.opacity = '0';
scheduleViciInit();
requestAnimationFrame(() => {
slideOpen(panel, { duration: 240 });
setTimeout(() => {
if (panel.style.display !== 'none' && panel.classList.contains('open')) {
panel.style.maxHeight = `${Math.max(panel.scrollHeight, 1)}px`;
}
}, 50);
});
};
/* -----------------------------
OpenFold-3 adapter (model-specific)
------------------------------ */
const DEFAULT_GLOBAL_MSA = 'mmseq2'; // mmseq2 | none
const DEFAULT_TEMPLATES = 'true'; // "true" | "false" (string because select)
const DEFAULT_SEED = 42;
const DEFAULT_SAMPLES = 1;
const OPENFOLD_SAFE_NAME_RE = /^[a-z0-9][a-z0-9_-]{1,62}[a-z0-9]$/;
const AA20 = 'ACDEFGHIKLMNPQRSTVWY';
const RE_PROT_SEQ = new RegExp(`^[${AA20}]+$`);
function isValidProteinSeq(seq){ return RE_PROT_SEQ.test((seq||'').toUpperCase()); }
function isValidDNASeq(seq){ return /^[ACGTU]+$/.test((seq||'').toUpperCase()); }
function isValidRNASeq(seq){ return /^[ACGU]+$/.test((seq||'').toUpperCase()); }
function isValidSMILES(s){
if (!s) return false;
if (/\s/.test(s)) return false;
return /^[A-Za-z0-9@+\-\[\]\(\)=#$%\\/\.]+$/.test(s) && /[A-Za-z]/.test(s);
}
function isValidGlycanCCD(s){
const str = String(s||'').trim();
if (!str) return false;
if (!/^[A-Za-z0-9()\-\s]+$/.test(str)) return false;
if (!/[A-Za-z]{3}/.test(str)) return false;
let bal = 0;
for (const ch of str){
if (ch === '(') bal++;
else if (ch === ')'){ bal--; if (bal < 0) return false; }
}
return bal === 0;
}
function detectLigandSequenceType(raw){
const sequence = String(raw || '').trim();
if (!sequence) return null;
const canonical = sequence.toUpperCase();
const lettersOnly = /^[A-Za-z]+$/.test(sequence);
const smilesTokens = /[=#[\]@\\/0-9a-z]/.test(sequence);
if (isValidSMILES(sequence) && smilesTokens && !(lettersOnly && sequence.length <= 4)){
return 'smiles';
}
if (isValidGlycanCCD(canonical)){
return 'ccd_codes';
}
if (isValidSMILES(sequence)){
return 'smiles';
}
if (isValidGlycanCCD(sequence)){
return 'ccd_codes';
}
return null;
}
function normalizeChainId(raw) {
const id = String(raw || '').trim().toUpperCase();
if (!id) return '';
if (!/^[A-Z0-9]{1,2}$/.test(id)) return '';
return id;
}
function normalizeNoSpaceUpper(raw) {
return String(raw || '').replace(/\s+/g, '').toUpperCase();
}
function 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';
}
/* ---- element helpers (support your current IDs + preferred OpenFold IDs) ---- */
function byId(id) { return document.getElementById(id); }
function getMolListEl() {
return byId('openfold-molecule-list') || byId('rosetta-molecule-list');
}
function getAddMolBtnEl() {
return byId('openfold-add-molecule') || byId('rosetta-add-molecule');
}
function getGlobalMsaEl() {
return byId('openfold-msa'); // recommended
}
function getTemplatesEl() {
return byId('openfold-templates'); // recommended
}
function getSeedEl() {
return byId('openfold-seed'); // recommended
}
function getSamplesEl() {
return byId('openfold-samples') || byId('rosetta-samples'); // optional fallback
}
/* ---- molecule UI ---- */
const molList = getMolListEl();
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;
blocks[0].dataset.base = '1';
return blocks[0];
}
function getChainIds() {
return Array.from(root.querySelectorAll('.molecule-block .mol-chain'))
.map(el => String(el.value || '').trim())
.filter(Boolean);
}
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 setPerMolMsaOptions(block, type) {
const row = block.querySelector('.mol-msa-row');
const sel = block.querySelector('.mol-msas');
if (!row || !sel) return;
const prev = sel.value;
sel.innerHTML = '';
if (type === 'protein') {
sel.appendChild(new Option('Paired (paired MSA)', 'paired'));
sel.appendChild(new Option('Unpaired', 'unpaired'));
sel.appendChild(new Option('Both (paired + unpaired)', 'both'));
sel.appendChild(new Option('None', 'none'));
row.style.display = '';
sel.disabled = false;
sel.value = ['paired','unpaired','both','none'].includes(prev) ? prev : 'paired';
return;
}
if (type === 'rna') {
sel.appendChild(new Option('Both (paired + unpaired)', 'both'));
sel.appendChild(new Option('Unpaired', 'unpaired'));
sel.appendChild(new Option('None', 'none'));
row.style.display = '';
sel.disabled = false;
sel.value = ['both','unpaired','none'].includes(prev) ? prev : 'both';
return;
}
// dna / ligand: no msa strategies
sel.appendChild(new Option('None', 'none'));
sel.value = 'none';
sel.disabled = true;
row.style.display = 'none';
}
function onMolTypeChange(block) {
const sel = block.querySelector('.mol-type');
const ta = block.querySelector('.mol-seq');
const viciPanel = block.querySelector('.vici-lookup');
const t = String(sel?.value || 'protein');
// placeholder + sanitizer
if (ta?._bioSanitizer) {
ta.removeEventListener('input', ta._bioSanitizer);
ta._bioSanitizer = null;
}
if (t === 'protein') {
ta.placeholder = 'Enter amino-acid sequence (1-letter; e.g., MKTIIALSYI...)';
} else if (t === 'dna') {
ta.placeholder = 'Enter DNA sequence (A/C/G/T; e.g., ATGGCC...)';
} else if (t === 'rna') {
ta.placeholder = 'Enter RNA sequence (A/C/G/U; e.g., AUGGCC...)';
} else {
ta.placeholder = 'Enter ligand string; auto-detects SMILES vs CCD (e.g., CC(=O)... or ATP).';
}
if (t === 'protein' || t === 'dna' || t === 'rna') {
const handler = (evt) => {
const cur = evt.target.value;
const cleaned = normalizeNoSpaceUpper(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;
}
// per-molecule msas
setPerMolMsaOptions(block, t);
// optional ViciLookup mode
if (viciPanel) {
viciPanel.setAttribute('data-vici-mode', t === 'ligand' ? 'ligand' : 'protein');
}
}
function addMolecule({
chainId = null,
type = 'protein',
sequence = '',
msas = null,
animate = true,
isBase = false
} = {}) {
const existing = new Set(getChainIds());
const id = normalizeChainId(chainId) || nextAutoChainId(existing);
const uid = `of3-mol-${Math.random().toString(36).slice(2, 8)}`;
const chainHelpId = `${uid}-chain-help`;
const typeHelpId = `${uid}-type-help`;
const msaHelpId = `${uid}-msas-help`;
const div = document.createElement('div');
div.className = 'form-card field-group molecule-block';
div.dataset.uid = uid;
if (getMoleculeBlocks().length === 0) {
isBase = true;
animate = false;
}
div.dataset.base = isBase ? '1' : '0';
div.innerHTML = `
Protein/DNA/RNA expects 1-letter sequences. Ligand/Glycan accepts SMILES or CCD identifiers.
How OpenFold should source MSAs for this chain. Shown for Protein and RNA only.
`;
molList.appendChild(div);
const typeSel = div.querySelector('.mol-type');
if (typeSel) typeSel.value = (['protein','dna','rna','ligand'].includes(type) ? type : 'protein');
const seqEl = div.querySelector('.mol-seq');
if (seqEl) seqEl.value = String(sequence || '');
onMolTypeChange(div);
// apply msas after options exist
const msasSel = div.querySelector('.mol-msas');
if (msasSel && msas != null) {
if (![...msasSel.options].some(o => o.value === String(msas))) {
msasSel.appendChild(new Option(String(msas), String(msas)));
}
msasSel.value = String(msas);
}
updateMolLabel(div);
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 {
const base = getBaseMoleculeBlock();
if (base) base.dataset.base = '1';
}
}
function clearMoleculeBlock(block, { collapseLookup = true } = {}) {
if (!block) return;
const typ = block.querySelector('.mol-type');
if (typ) {
typ.value = 'protein';
typ.dispatchEvent(new Event('change', { bubbles: true }));
}
const msas = block.querySelector('.mol-msas');
if (msas) msas.value = 'paired';
const ta = block.querySelector('.mol-seq');
if (ta) {
ta.value = '';
ta.dispatchEvent(new Event('input', { bubbles: true }));
ta.dispatchEvent(new Event('change', { bubbles: true }));
}
onMolTypeChange(block);
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';
}
}
}
scheduleViciInit();
}
function removeMolecule(block) {
const blocks = getMoleculeBlocks();
const base = getBaseMoleculeBlock();
const isBase = (block === base) || block?.dataset.base === '1' || blocks[0] === block;
if (isBase) {
clearMoleculeBlock(block, { collapseLookup: true });
showToast?.('success', 'Molecule cleared.');
return;
}
slideClose(block, { duration: 240, after: () => { block.remove(); } });
}
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 aMsas = a.querySelector('.mol-msas');
const bMsas = b.querySelector('.mol-msas');
// swap type
const t = aType?.value;
if (aType && bType) {
aType.value = bType.value;
bType.value = t;
}
// swap sequence
const s = aSeq?.value;
if (aSeq && bSeq) {
aSeq.value = bSeq.value;
bSeq.value = s;
}
// swap msas (after we refresh options)
const m = aMsas?.value;
onMolTypeChange(a);
onMolTypeChange(b);
if (aMsas && bMsas) {
aMsas.value = bMsas.value;
bMsas.value = m;
}
[aType, bType, aSeq, bSeq, aMsas, bMsas].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;
// move by swapping content (chain IDs stay fixed)
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 setMoleculesFromArray(mols, { animateAdds = false, animateRemovals = false } = {}) {
const desired = Array.isArray(mols) && mols.length ? mols : [{
chain_id: 'A', type: 'protein', sequence: '', msas: 'paired'
}];
let base = getBaseMoleculeBlock();
if (!base) {
base = addMolecule({
chainId: desired[0]?.chain_id || 'A',
type: desired[0]?.type || 'protein',
sequence: desired[0]?.sequence || '',
msas: desired[0]?.msas ?? null,
animate: false,
isBase: true
});
} else {
base.dataset.base = '1';
}
const blocks = getMoleculeBlocks();
blocks.forEach((b) => {
if (b === base) return;
if (animateRemovals) slideClose(b, { duration: 240, after: () => b.remove() });
else b.remove();
});
clearMoleculeBlock(base, { collapseLookup: true });
const baseChain = normalizeChainId(desired[0]?.chain_id) || 'A';
const chainEl = base.querySelector('.mol-chain');
if (chainEl) chainEl.value = baseChain;
updateMolLabel(base);
const typeSel = base.querySelector('.mol-type');
if (typeSel) typeSel.value = (desired[0]?.type || 'protein');
const seqEl = base.querySelector('.mol-seq');
if (seqEl) seqEl.value = String(desired[0]?.sequence || '');
onMolTypeChange(base);
const msasSel = base.querySelector('.mol-msas');
if (msasSel && desired[0]?.msas != null) msasSel.value = String(desired[0]?.msas);
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 || '',
msas: m.msas ?? null,
animate: animateAdds,
isBase: false
});
}
scheduleViciInit();
}
/* ---- presets (optional) ---- */
window.MODEL_PRESETS = window.MODEL_PRESETS || {};
window.MODEL_PRESETS.example1 = function example1() {
molList.innerHTML = '';
addMolecule({
chainId: 'A',
type: 'protein',
msas: 'paired',
sequence: `MKKGHHHHHHGAISLISALVRAHVDSNPAMTSLDYSRFQANPDYQMSGDDTQHIQQFYDLLTGSMEIIRGWAEKIPGFADLPKADQDLLFESAFLELFVLRLAYRSNPVEGKLIFCNGVVLHRLQCVRGFGEWIDSIVEFSSNLQNMNIDISAFSCIAALAMVTERHGLKEPKRVEELQNKIVNTLKDHVTFNNGGLNRPNYLSKLLGKLPELRTLCTQGLQRIFYLKLEDLVPPPAIIDKLFLDTLPF`
});
addMolecule({
chainId: 'B',
type: 'ligand',
sequence: `c1cc(c(cc1OCC(=O)NCCS)Cl)Cl`
});
getGlobalMsaEl() && (getGlobalMsaEl().value = 'mmseq2');
getTemplatesEl() && (getTemplatesEl().value = 'true');
getSeedEl() && (getSeedEl().value = String(DEFAULT_SEED));
getSamplesEl() && (getSamplesEl().value = '1');
};
window.MODEL_PRESETS.example2 = function example2() {
molList.innerHTML = '';
const seq =
`LPSSEEYKVAYELLPGLSEVPDPSNIPQMHAGHIPLRSEDADEQDSSDLEYFFWKFTNNDSNGNVDRPLIIWLNGGPGCSSMDGALVESGPFRVNSDGKLYLNEGSWISKGDLLFIDQPTGTGFSVEQNKDEGKIDKNKFDEDLEDVTKHFMDFLENYFKIFPEDLTRKIILSGESYAGQYIPFFANAILNHNKFSKIDGDTYDLKALLIGNGWIDPNTQSLSYLPFAMEKKLIDESNPNFKHLTNAHENCQNLINSASTDEAAHFSYQECENILNLLLSYTRESSQKGTADCLNMYNFNLKDSYPSCGMNWPKDISFVSKFFSTPGVIDSLHLDSDKIDHWKECTNSVGTKLSNPISKPSIHLLPGLLESGIEIVLFNGDKDLICNNKGVLDTIDNLKWGGIKGFSDDAVSFDWIHKSKSTDDSEEFSGYVKYDRNLTFVSVYNASHMVPFDKSLVSRGIVDIYSNDVMIIDNNGKNVMITT`;
addMolecule({ chainId: 'A', type: 'protein', msas: 'paired', sequence: seq });
// CCD example (glycan/ligand)
addMolecule({ chainId: 'B', type: 'ligand', sequence: 'NAG' });
getGlobalMsaEl() && (getGlobalMsaEl().value = 'mmseq2');
getTemplatesEl() && (getTemplatesEl().value = 'true');
getSeedEl() && (getSeedEl().value = String(DEFAULT_SEED));
getSamplesEl() && (getSamplesEl().value = '1');
};
window.ModelPageAdapter = {
onInit() {
// populate advanced controls
const globalMsa = getGlobalMsaEl();
if (globalMsa) {
globalMsa.innerHTML = '';
globalMsa.appendChild(new Option('MMseqs2', 'mmseq2'));
globalMsa.appendChild(new Option('None', 'none'));
globalMsa.value = DEFAULT_GLOBAL_MSA;
}
const templates = getTemplatesEl();
if (templates) {
templates.innerHTML = '';
templates.appendChild(new Option('True', 'true'));
templates.appendChild(new Option('False', 'false'));
templates.value = DEFAULT_TEMPLATES;
}
const seed = getSeedEl();
if (seed) {
if (!seed.value) seed.value = String(DEFAULT_SEED);
}
const samples = getSamplesEl();
if (samples) {
samples.innerHTML = '';
for (let i = 1; i <= 10; i++) {
const opt = document.createElement('option');
opt.value = String(i);
opt.textContent = String(i);
if (i === DEFAULT_SAMPLES) opt.selected = true;
samples.appendChild(opt);
}
}
ensureAtLeastOneMolecule();
getAddMolBtnEl()?.addEventListener('click', (e) => {
e.preventDefault();
addMolecule({});
});
// delegated actions
root.addEventListener('click', (e) => {
const molBlock = e.target.closest('.molecule-block');
if (!molBlock) return;
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; }
});
root.addEventListener('change', (e) => {
const molBlock = e.target.closest('.molecule-block');
if (molBlock && e.target.classList.contains('mol-type')) {
onMolTypeChange(molBlock);
}
});
},
captureState() {
const name = String(byId('jobname')?.value || '');
const adv = {
msa_global: getGlobalMsaEl()?.value || DEFAULT_GLOBAL_MSA,
templates: getTemplatesEl()?.value || DEFAULT_TEMPLATES,
seed: getSeedEl()?.value || String(DEFAULT_SEED),
samples: getSamplesEl()?.value || String(DEFAULT_SAMPLES)
};
const molecules = getMoleculeBlocks().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 || ''),
msas: String(b.querySelector('.mol-msas')?.value || 'none')
}));
return {
tab: (root.classList.contains('is-tab-api') ? 'api' : root.classList.contains('is-tab-advanced') ? 'advanced' : 'basic'),
name,
adv,
molecules
};
},
applyState(state) {
if (!state) return;
const nameEl = byId('jobname');
if (nameEl && typeof state.name === 'string') nameEl.value = state.name;
const adv = state.adv || {};
getGlobalMsaEl() && adv.msa_global != null && (getGlobalMsaEl().value = String(adv.msa_global));
getTemplatesEl() && adv.templates != null && (getTemplatesEl().value = String(adv.templates));
getSeedEl() && adv.seed != null && (getSeedEl().value = String(adv.seed));
getSamplesEl() && adv.samples != null && (getSamplesEl().value = String(adv.samples));
molList.innerHTML = '';
(state.molecules || []).forEach((m) => {
addMolecule({
chainId: m.chain_id || null,
type: m.type || 'protein',
sequence: m.sequence || '',
msas: m.msas ?? null
});
});
ensureAtLeastOneMolecule();
},
reset({ baselineState }) {
const state = deepClone(baselineState || {});
const adv = state.adv || {};
const nameEl = byId('jobname');
if (nameEl) nameEl.value = String(state.name || '');
const gm = getGlobalMsaEl();
if (gm) gm.value = String(adv.msa_global ?? DEFAULT_GLOBAL_MSA);
const tpl = getTemplatesEl();
if (tpl) tpl.value = String(adv.templates ?? DEFAULT_TEMPLATES);
const seed = getSeedEl();
if (seed) seed.value = String(adv.seed ?? DEFAULT_SEED);
const samples = getSamplesEl();
if (samples) samples.value = String(adv.samples ?? DEFAULT_SAMPLES);
const desired = Array.isArray(state.molecules) && state.molecules.length
? state.molecules
: [{ chain_id: 'A', type: 'protein', sequence: '', msas: 'paired' }];
setMoleculesFromArray(desired, { animateAdds: false, animateRemovals: true });
},
buildJob(opts = {}, ctx = {}) {
const requireName = opts.requireName !== false;
const validateName = opts.validate !== false;
const toast = !!opts.toast;
const nameInput = byId('jobname');
const rawName = String(nameInput?.value || '').trim();
const runName = canonicalizeRunName(rawName || `my_${ctx.modelKey || 'openfold3'}_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 && !OPENFOLD_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.' };
}
const msaChoice = getGlobalMsaEl()?.value || DEFAULT_GLOBAL_MSA;
const templateChoice = getTemplatesEl()?.value || DEFAULT_TEMPLATES;
const useMsa = msaChoice !== 'none';
const useTemplates = String(templateChoice) === 'true';
let seedVal = parseInt(getSeedEl()?.value || String(DEFAULT_SEED), 10);
if (!Number.isFinite(seedVal) || seedVal < 0) seedVal = DEFAULT_SEED;
let samplesVal = parseInt(getSamplesEl()?.value || String(DEFAULT_SAMPLES), 10);
if (!Number.isFinite(samplesVal) || samplesVal < 1) samplesVal = DEFAULT_SAMPLES;
const blocks = getMoleculeBlocks();
if (!blocks.length) {
if (toast) showToast?.('error', 'At least one molecule is required.');
return { error: 'No molecules.' };
}
const molecules = [];
const seenChains = new Set();
for (const b of blocks) {
const chain_id = normalizeChainId(b.querySelector('.mol-chain')?.value) || '';
const type = String(b.querySelector('.mol-type')?.value || 'protein');
const rawSeq = String(b.querySelector('.mol-seq')?.value || '').trim();
if (!chain_id) {
if (toast) showToast?.('error', 'Each molecule must have a Chain ID.');
return { error: 'Missing chain_id.' };
}
if (seenChains.has(chain_id)) {
if (toast) showToast?.('error', `Duplicate chain ID ${chain_id}.`);
return { error: 'Duplicate chain.' };
}
seenChains.add(chain_id);
if (!rawSeq) {
if (toast) showToast?.('error', `Molecule ${chain_id}: provide a sequence or string.`);
return { error: 'Missing sequence.' };
}
let sequenceType = 'sequence';
let sequenceValue = rawSeq;
if (type === 'protein') {
sequenceValue = normalizeNoSpaceUpper(rawSeq);
if (!isValidProteinSeq(sequenceValue)) {
if (toast) showToast?.('error', `Chain ${chain_id}: protein sequence must use ACDEFGHIKLMNPQRSTVWY.`);
return { error: 'Bad protein sequence.' };
}
} else if (type === 'dna') {
sequenceValue = normalizeNoSpaceUpper(rawSeq);
if (!isValidDNASeq(sequenceValue)) {
if (toast) showToast?.('error', `Chain ${chain_id}: DNA sequence must use A/C/G/T.`);
return { error: 'Bad DNA sequence.' };
}
} else if (type === 'rna') {
sequenceValue = normalizeNoSpaceUpper(rawSeq);
if (!isValidRNASeq(sequenceValue)) {
if (toast) showToast?.('error', `Chain ${chain_id}: RNA sequence must use A/C/G/U.`);
return { error: 'Bad RNA sequence.' };
}
} else if (type === 'ligand') {
const detected = detectLigandSequenceType(rawSeq);
if (!detected) {
if (toast) showToast?.('error', `Chain ${chain_id}: unable to detect if this is SMILES or CCD codes.`);
return { error: 'Bad ligand.' };
}
sequenceType = detected;
if (sequenceType === 'ccd_codes') sequenceValue = rawSeq.toUpperCase();
}
const entry = {
type,
chain_id,
sequence_type: sequenceType,
sequence: sequenceValue
};
if (type === 'protein' || type === 'rna') {
entry.msas = String(b.querySelector('.mol-msas')?.value || 'none');
}
molecules.push(entry);
}
const job = {
class: 'OpenFold3',
name: runName || `my_${ctx.modelKey || 'openfold3'}_run`,
msa: useMsa,
templates: useTemplates,
seed: seedVal,
samples: samplesVal,
molecules
};
const payload = {
workflow_name: job.name,
openfold3: job
};
return { job, payload };
},
getDefaultApiPayload({ modelKey }) {
const name = `my_${modelKey}_run`;
return {
workflow_name: name,
openfold3: {
class: 'OpenFold3',
name,
msa: true,
templates: true,
seed: 42,
samples: 1,
molecules: [
{ type: 'protein', chain_id: 'A', sequence_type: 'sequence', sequence: 'MKTIIALSYIFCLVFAD', msas: 'paired' },
{ type: 'ligand', chain_id: 'B', sequence_type: 'ccd_codes', sequence: 'ATP' }
]
}
};
}
};
/* -----------------------------
Framework (tab UX, API snippet, submit, dirty tracking)
------------------------------ */
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 || 'openfold3').trim() || 'openfold3';
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;
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 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;
}
// ---------- API coloured renderer (paste this) ----------
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 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 your value.';
if (type === 'number') typeHint = 'Expected type: number.';
if (type === 'boolean') typeHint = 'Expected type: boolean (true or false).';
let extra = 'Replace this example value with a valid value.';
if (pathLabel.toLowerCase().endsWith('.name')) {
extra = 'Run/job name. Keep 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(console.error);'
].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(console.error);'
].join('\n')
};
}
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 = `
`;
el.addEventListener('mouseenter', () => {
if (apiDefHideTimer) { clearTimeout(apiDefHideTimer); apiDefHideTimer = null; }
});
el.addEventListener('mouseleave', () => scheduleHideApiDefPopout());
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);
}
});
}
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
});
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 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`,
[modelKey]: { name: `my_${modelKey}_run` }
};
}
}
function bindDirtyTracking() {
root.addEventListener('input', (e) => {
if (e.target.closest('[data-panel="api"]')) return;
updateActionVisibility();
});
root.addEventListener('change', (e) => {
if (e.target.closest('[data-panel="api"]')) return;
updateActionVisibility();
if (currentTab === 'api') renderApiSnippet();
});
const mo = new MutationObserver(() => updateActionVisibility());
mo.observe(root, { childList: true, subtree: true });
}
async 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;
await new Promise((resolve) => {
let remaining = panels.length;
panels.forEach((p) => {
slideClose(p, {
duration,
after: () => {
remaining--;
if (remaining <= 0) resolve();
}
});
});
setTimeout(resolve, duration + 120);
});
}
function bindReset() {
resetBtn?.addEventListener('click', async () => {
try {
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) });
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');
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');
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;
// IMPORTANT: RF3 credits should scale with samples (matches your old RF3 submit logic)
const planned = Math.max(1, parseInt(built.job?.samples || '1', 10) || 1);
if (typeof window.guardSubmitOrToast === 'function') {
const ok = await window.guardSubmitOrToast({ planned, minCredit: planned, 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(); });
}
// guest button remains 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();
bindApiDefinitionPopout();
bindExecute();
blockFileDropsOnRoot();
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,
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();
}
})();