(() => { '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 = `
`; 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: [ '# POST a model job (Python)', '# Set TOKEN_ID and TOKEN_SECRET to your values.', '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: [ '# POST a model job (cURL)', '# Set TOKEN_ID and TOKEN_SECRET to your values.', '', `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: [ '// POST a model job (JavaScript)', '// Set TOKEN_ID and TOKEN_SECRET to your values.', '', '(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(); } })();