(() => { '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/'; let adapter = window.ModelPageAdapter || {}; const root = document.getElementById('model-ui'); if (!root) return; 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 || 'example-model').trim() || 'example-model'; const modelKey = modelSlug .toLowerCase() .replace(/[^a-z0-9]+/g, '_') .replace(/^_+|_+$/g, '') || 'model'; if (modelKey === 'posebusters') { adapter = createPosebustersAdapter(); } 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; const API_DEF_CONTENT = window.VICI_API_DEF_CONTENT || { 'token-id': { title: 'Token-ID', html: ` Your Vici Token ID. Send it as the Token-ID header. Generate it in your Account . ` }, 'token-secret': { title: 'Token-Secret', html: ` Your Vici Token Secret. Send it as the Token-Secret header. You only see this once when you generate it. Generate it in your Account . ` }, 'workflow-name': { title: `workflow_name / ${modelKey}.name`, html: `A friendly run name shown in your Dashboard. The outer workflow_name and inner ${escapeHtml(modelKey)}.name should match.` } }; let apiDefPopoutEl = null; let apiDefAnchorEl = null; let apiDefHideTimer = null; let apiDynamicDefContent = {}; function tabFromHash(hash = window.location.hash) { const h = String(hash || '').trim().toLowerCase(); if (h === '#basic') return 'basic'; if (h === '#advanced') return 'advanced'; if (h === '#api') return 'api'; return null; } function syncHashToTab(tab, { replace = true } = {}) { const nextHash = `#${tab}`; if (String(window.location.hash || '').toLowerCase() === nextHash) return; try { const url = new URL(window.location.href); url.hash = nextHash; if (replace && window.history?.replaceState) { window.history.replaceState(null, '', url.toString()); } else if (!replace && window.history?.pushState) { window.history.pushState(null, '', url.toString()); } else { window.location.hash = nextHash; } } catch { window.location.hash = nextHash; } } function bindHashRouting() { window.addEventListener('hashchange', () => { const next = tabFromHash(); if (!next || next === currentTab) return; setTab(next, { silent: true, syncHash: false }); }); } function inferInitialTab() { const fromHash = tabFromHash(); if (fromHash) return fromHash; if (root.classList.contains('is-tab-api')) return 'api'; if (root.classList.contains('is-tab-advanced')) return 'advanced'; return 'basic'; } function escapeHtml(str) { return String(str ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function deepClone(v) { return JSON.parse(JSON.stringify(v)); } function isPlainObject(v) { return Object.prototype.toString.call(v) === '[object Object]'; } function extOf(name) { const s = String(name || '').trim(); const i = s.lastIndexOf('.'); return i >= 0 ? s.slice(i + 1).toLowerCase() : ''; } function stripExecutionContextForApi(value) { const blocked = new Set(['member_id', 'msid', 'user_id', 'team_id']); if (Array.isArray(value)) { return value.map(stripExecutionContextForApi); } if (!isPlainObject(value)) { return value; } const out = {}; Object.entries(value).forEach(([k, v]) => { if (blocked.has(k)) return; out[k] = stripExecutionContextForApi(v); }); return out; } function 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 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 pulseBtn(btn, cls) { if (!btn) return; btn.classList.remove(cls); void btn.offsetWidth; btn.classList.add(cls); const onEnd = () => { btn.classList.remove(cls); btn.removeEventListener('animationend', onEnd); }; btn.addEventListener('animationend', onEnd); } function copyTextRobust(text) { if (navigator.clipboard && window.isSecureContext) { return navigator.clipboard.writeText(text); } return new Promise((resolve, reject) => { try { const ta = document.createElement('textarea'); ta.value = text; ta.setAttribute('readonly', ''); ta.style.position = 'fixed'; ta.style.left = '-9999px'; ta.style.top = '-9999px'; document.body.appendChild(ta); ta.select(); ta.setSelectionRange(0, ta.value.length); const ok = document.execCommand('copy'); ta.remove(); ok ? resolve() : reject(new Error('copy failed')); } catch (err) { reject(err); } }); } function getFieldKey(el, idx) { return ( el.getAttribute('data-field-key') || el.id || el.name || `${el.tagName.toLowerCase()}_${idx}` ); } function readFieldValue(el) { const tag = el.tagName.toLowerCase(); const type = (el.type || '').toLowerCase(); if (type === 'checkbox') return !!el.checked; if (type === 'radio') return el.checked ? el.value : undefined; if (type === 'file') { const files = Array.from(el.files || []); return files.map(f => ({ name: f.name, size: f.size, type: f.type })); } if (tag === 'select' && el.multiple) { return Array.from(el.selectedOptions).map(o => o.value); } return el.value; } function writeFieldValue(el, value) { const tag = el.tagName.toLowerCase(); const type = (el.type || '').toLowerCase(); if (type === 'checkbox') { el.checked = !!value; } else if (type === 'radio') { el.checked = String(el.value) === String(value); } else if (type === 'file') { } else if (tag === 'select' && el.multiple && Array.isArray(value)) { Array.from(el.options).forEach(opt => { opt.selected = value.includes(opt.value); }); } else { el.value = value ?? ''; } el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } function dzEl(ref, scope = document) { if (!ref) return null; if (typeof ref === 'string') return scope.querySelector(ref); return ref; } function dzDispatchValueEvents(el) { if (!el) return; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } function dzFormatBytes(bytes) { const n = Number(bytes); if (!Number.isFinite(n) || n <= 0) return ''; if (n < 1024) return `${n} B`; if (n < 1024 * 1024) return `${(n / 1024).toFixed(1).replace(/\.0$/, '')} KB`; return `${(n / (1024 * 1024)).toFixed(2).replace(/\.00$/, '')} MB`; } function dzReadFileAsText(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => resolve(String(e.target?.result || '')); reader.onerror = () => reject(new Error('Failed to read file.')); reader.readAsText(file); }); } function dzDefaultMetaHtml({ label = 'Loaded content', sizeBytes, content = '', includeLines = true, includeChars = true, extraBits = [] } = {}) { const bits = [String(label || 'Loaded content')]; const sizeLabel = dzFormatBytes(sizeBytes); if (sizeLabel) bits.push(sizeLabel); if (includeLines) { const lines = String(content || '').split(/\r?\n/).length; bits.push(`${lines} lines`); } if (includeChars) { const chars = String(content || '').length; bits.push(`${chars.toLocaleString()} chars`); } (Array.isArray(extraBits) ? extraBits : []).forEach((v) => { if (v != null && String(v).trim()) bits.push(String(v)); }); return bits .map((v, i) => { const isSize = /(?:\d+\s?(?:KB|MB|B))$/.test(v); if (i === 0) return escapeHtml(v); return ` ${escapeHtml(v)}`; }) .join(' '); } function createDropZoneController(config = {}) { const scope = config.scope || document; const refs = { drop: dzEl(config.dropZone, scope), input: dzEl(config.fileInput, scope), meta: dzEl(config.metaEl, scope), removeBtn: dzEl(config.removeBtn, scope), contentField: dzEl(config.contentField, scope), filenameField: dzEl(config.filenameField, scope), sourceField: dzEl(config.sourceField, scope) }; const opts = { emptyMetaText: config.emptyMetaText || 'Drop file here or click to upload', uploadSourceValue: config.uploadSourceValue || 'upload', clearInputOnSet: config.clearInputOnSet !== false, readFile: typeof config.readFile === 'function' ? config.readFile : dzReadFileAsText, acceptDrop: typeof config.acceptDrop === 'function' ? config.acceptDrop : ((e) => Array.from(e.dataTransfer?.types || []).includes('Files')), pickFile: typeof config.pickFile === 'function' ? config.pickFile : ((files) => (files && files[0]) || null) }; if (!refs.drop || !refs.input) { return null; } let bound = false; function getState() { const content = String(refs.contentField?.value || ''); const filename = String(refs.filenameField?.value || '').trim(); const source = String(refs.sourceField?.value || '').trim(); const sizeBytes = Number(refs.drop?.dataset?.fileSizeBytes || NaN); const format = String(refs.drop?.dataset?.fileFormat || extOf(filename) || '').trim().toLowerCase(); return { content, filename, source, sizeBytes, format, hasContent: content.trim().length > 0 }; } function renderMeta() { if (!refs.drop || !refs.meta) return; const state = getState(); refs.drop.classList.toggle('is-filled', state.hasContent); if (!state.hasContent) { if (typeof config.renderEmptyMeta === 'function') { const emptyOut = config.renderEmptyMeta({ refs, state, controller }); if (typeof emptyOut === 'string') { refs.meta.innerHTML = emptyOut; return; } } refs.meta.textContent = opts.emptyMetaText; return; } if (typeof config.renderFilledMeta === 'function') { const out = config.renderFilledMeta({ refs, state, controller, formatBytes: dzFormatBytes }); if (typeof out === 'string') { refs.meta.innerHTML = out; return; } if (out && typeof out === 'object') { refs.meta.innerHTML = dzDefaultMetaHtml({ label: out.label, sizeBytes: out.sizeBytes ?? state.sizeBytes, content: out.content ?? state.content, includeLines: out.includeLines !== false, includeChars: out.includeChars !== false, extraBits: out.extraBits || [] }); return; } } const fallbackLabel = state.filename || (state.source ? `Loaded (${state.source})` : 'Loaded content'); refs.meta.innerHTML = dzDefaultMetaHtml({ label: fallbackLabel, sizeBytes: state.sizeBytes, content: state.content }); } function setValue(text, meta = {}) { const nextText = String(text || ''); if (refs.contentField) refs.contentField.value = nextText; if (refs.filenameField && Object.prototype.hasOwnProperty.call(meta, 'name')) { refs.filenameField.value = String(meta.name || ''); } if (refs.sourceField && Object.prototype.hasOwnProperty.call(meta, 'source')) { refs.sourceField.value = String(meta.source || ''); } if (refs.drop) { if (Number.isFinite(Number(meta.sizeBytes))) { refs.drop.dataset.fileSizeBytes = String(Number(meta.sizeBytes)); } else { delete refs.drop.dataset.fileSizeBytes; } const resolvedName = String( Object.prototype.hasOwnProperty.call(meta, 'name') ? meta.name || '' : refs.filenameField?.value || '' ).trim(); const resolvedFormat = String(meta.format || extOf(resolvedName) || '').trim().toLowerCase(); if (resolvedFormat) { refs.drop.dataset.fileFormat = resolvedFormat; } else { delete refs.drop.dataset.fileFormat; } } if (refs.input && opts.clearInputOnSet) { refs.input.value = ''; } dzDispatchValueEvents(refs.contentField); dzDispatchValueEvents(refs.filenameField); dzDispatchValueEvents(refs.sourceField); renderMeta(); if (typeof config.onSet === 'function') { try { config.onSet({ refs, controller, text: nextText, meta }); } catch (err) { console.error(err); } } } function clearValue(extra = {}) { if (refs.contentField) refs.contentField.value = ''; if (refs.filenameField) refs.filenameField.value = ''; if (refs.sourceField) refs.sourceField.value = ''; if (refs.input) refs.input.value = ''; if (refs.drop) { delete refs.drop.dataset.fileSizeBytes; delete refs.drop.dataset.fileFormat; } dzDispatchValueEvents(refs.contentField); dzDispatchValueEvents(refs.filenameField); dzDispatchValueEvents(refs.sourceField); renderMeta(); if (typeof config.onClear === 'function') { try { config.onClear({ refs, controller, extra }); } catch (err) { console.error(err); } } } async function readAndSet(file) { if (!file) return; try { if (typeof config.beforeRead === 'function') { const shouldContinue = await config.beforeRead({ refs, controller, file }); if (shouldContinue === false) return; } const raw = await opts.readFile(file); if (typeof config.validateFileContent === 'function') { const verdict = await config.validateFileContent({ refs, controller, file, raw }); if (verdict === false) return; if (typeof verdict === 'string') { throw new Error(verdict); } } let mapped = { text: raw, meta: { name: file.name || '', source: opts.uploadSourceValue, sizeBytes: file.size } }; if (typeof config.mapFileToValue === 'function') { const custom = await config.mapFileToValue({ refs, controller, file, raw }); if (custom && typeof custom === 'object') { mapped = { text: Object.prototype.hasOwnProperty.call(custom, 'text') ? custom.text : raw, meta: { name: file.name || '', source: opts.uploadSourceValue, sizeBytes: file.size, ...(custom.meta || {}) } }; } } setValue(mapped.text, mapped.meta); } catch (err) { console.error(err); if (typeof config.onError === 'function') { config.onError(err, { refs, controller, file }); } else { window.showToast?.('error', err?.message || 'Failed to read file.'); } } } function bind() { if (bound) return controller; bound = true; refs.drop.addEventListener('click', () => { refs.input.click(); }); refs.drop.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); refs.input.click(); } }); refs.drop.addEventListener('dragover', (e) => { if (!opts.acceptDrop(e)) return; e.preventDefault(); refs.drop.classList.add('dragover'); }); refs.drop.addEventListener('dragleave', () => { refs.drop.classList.remove('dragover'); }); refs.drop.addEventListener('drop', (e) => { if (!opts.acceptDrop(e)) return; const files = Array.from(e.dataTransfer?.files || []); const file = opts.pickFile(files); if (!file) return; e.preventDefault(); refs.drop.classList.remove('dragover'); readAndSet(file); }); refs.input.addEventListener('change', () => { const files = Array.from(refs.input.files || []); const file = opts.pickFile(files); if (file) readAndSet(file); }); refs.removeBtn?.addEventListener('click', () => { clearValue({ fromRemoveButton: true }); if (config.clearToastMessage) { showToast?.('success', config.clearToastMessage); } }); renderMeta(); return controller; } function refresh() { renderMeta(); return controller; } const controller = { refs, bind, refresh, getState, setValue, clearValue, readAndSet }; return controller; } function createPosebustersAdapter() { const EXAMPLE_LIGAND_URL = 'https://cdn.prod.website-files.com/685009238c2e9a16d785299f/69a57ec25a84ca5675e468dd_Ligand.txt'; const cache = new Map(); const FIXED = { complex: null, ideal: null }; let predictedUid = 0; const ROW_ANIM_MS = 240; const TRASH_SVG = ` `; const PLUS_SVG = ` `; function toggleComplexLookupPanel() { const panel = root.querySelector('#pb-complex-lookup'); const btn = root.querySelector('#pb-complex-card .pb-eye-autofill'); if (!panel || !btn) return; if (!window.ViciLookup?.attach) { window.showToast?.('error', 'Vici Lookup global script is not loaded.'); return; } const isOpen = btn.classList.contains('active'); if (isOpen) { closeComplexLookup(); return; } try { window.ViciLookup.attach(panel); } catch (err) { console.error(err); window.showToast?.('error', 'Failed to attach Vici Lookup.'); return; } panel.style.display = ''; if (typeof window.viciSlideExpand === 'function') { window.viciSlideExpand(panel); } else { panel.classList.add('open'); } btn.classList.add('active'); btn.setAttribute('aria-expanded', 'true'); requestAnimationFrame(() => { panel.querySelector('.vici-lookup-input')?.focus(); }); } let syncingComplexLookup = false; function closeComplexLookup() { const panel = root.querySelector('#pb-complex-lookup'); const btn = root.querySelector('#pb-complex-card .pb-eye-autofill'); if (!panel || !btn) return; hardCollapseViciLookup(panel, { duration: 240 }); btn.classList.remove('active'); btn.setAttribute('aria-expanded', 'false'); } function clearComplexLookupUi({ collapse = true } = {}) { const panel = root.querySelector('#pb-complex-lookup'); const btn = root.querySelector('#pb-complex-card .pb-eye-autofill'); if (!panel) return; const doClear = () => { try { // keepTarget:true because the complex file field is already being cleared // by the Remove button itself. We only want to clear the lookup UI state. window.ViciLookup?.clear?.(panel, { keepInput: false, keepTarget: true }); } catch (err) { console.error(err); } }; if (collapse) { hardCollapseViciLookup(panel, { duration: 240, afterClose: doClear }); } else { doClear(); } if (collapse && btn) { btn.classList.remove('active'); btn.setAttribute('aria-expanded', 'false'); } } function bindComplexLookupSync() { const panel = root.querySelector('#pb-complex-lookup'); const contentField = root.querySelector('#pb-complex-content'); if (!panel || !contentField || panel.dataset.pbLookupBound === 'true') return; panel.dataset.pbLookupBound = 'true'; window.validatePosebustersComplexLookupFile = ({ name, fileContent }) => { return validateComplexFilePayload(name || '', fileContent || ''); }; if (window.ViciLookup?.attach) { try { window.ViciLookup.attach(panel); } catch (err) { console.error(err); } } window.addEventListener('vici:file-selected', (e) => { const detail = e.detail || {}; if (detail.block !== panel) return; const content = String(detail.fileContent || ''); const filename = String(detail.name || 'lookup_result.pdb'); // Prevent the change listener below from re-running this same sync syncingComplexLookup = true; FIXED.complex?.setValue(content, { name: filename, format: extOf(filename), source: 'lookup', sizeBytes: blobBytes(content) }); syncingComplexLookup = false; // Do NOT close the lookup panel here window.showToast?.('success', `Loaded ${filename} from Vici Lookup.`); }); contentField.addEventListener('change', () => { if (syncingComplexLookup) return; if (!btnIsOpen()) return; const content = String(contentField.value || '').trim(); if (!content) return; const pickedId = String( panel.querySelector('.vici-lookup-results')?.value || 'lookup_result' ).trim(); const format = /^\s*data_/i.test(content) ? 'cif' : 'pdb'; const fileName = `${pickedId}.${format}`; syncingComplexLookup = true; FIXED.complex?.setValue(content, { name: fileName, format, source: 'lookup', sizeBytes: blobBytes(content) }); syncingComplexLookup = false; // Do NOT close the lookup panel here window.showToast?.('success', `Loaded ${fileName} from Vici Lookup.`); }); function btnIsOpen() { const btn = root.querySelector('#pb-complex-card .pb-eye-autofill'); return !!btn?.classList.contains('active'); } } function extOf(name) { const s = String(name || '').trim(); const i = s.lastIndexOf('.'); return i >= 0 ? s.slice(i + 1).toLowerCase() : ''; } function blobBytes(text) { try { return new Blob([String(text || '')]).size; } catch { return undefined; } } function safeSize(value) { const n = Number(value); return Number.isFinite(n) && n > 0 ? n : undefined; } function hasBinaryNoise(text) { return /[\x00-\x08\x0B\x0C\x0E-\x1F]/.test(String(text || '').slice(0, 4000)); } function validatePdbText(text) { const s = String(text || ''); const hasAtom = /^(ATOM |HETATM)/m.test(s); const hasPdbLike = /^(HEADER|TITLE |COMPND|SOURCE|REMARK|MODEL |ATOM |HETATM)/m.test(s); return hasAtom && hasPdbLike; } function validateCifText(text) { const s = String(text || ''); return /^\s*data_/mi.test(s) && /_atom_site\./.test(s); } function validateSdfText(text) { const s = String(text || ''); return /\$\$\$\$/m.test(s) && /M END/m.test(s); } function validateMolText(text) { const s = String(text || ''); return s.split(/\r?\n/).length >= 4 && /M END/m.test(s); } function validateMol2Text(text) { const s = String(text || ''); return /@MOLECULE/i.test(s) && /@ATOM/i.test(s); } function validateComplexFilePayload(filename, raw) { const ext = extOf(filename); const text = String(raw || ''); if (!text.trim()) return 'File is empty.'; if (hasBinaryNoise(text)) return 'That file looks binary or corrupted. Please upload a real .pdb or .cif text structure file.'; if (ext === 'pdb' && !validatePdbText(text)) { return 'That does not look like a valid PDB structure file.'; } if (ext === 'cif' && !validateCifText(text)) { return 'That does not look like a valid mmCIF structure file.'; } if (!['pdb', 'cif'].includes(ext)) { return 'Only .pdb and .cif files are allowed here.'; } return true; } function validateLigandFilePayload(filename, raw) { const ext = extOf(filename); const text = String(raw || ''); if (!text.trim()) return 'File is empty.'; if (hasBinaryNoise(text)) return 'That file looks binary or corrupted. Please upload a real ligand text file.'; if (ext === 'sdf' && !validateSdfText(text)) { return 'That does not look like a valid SDF file.'; } if (ext === 'mol' && !validateMolText(text)) { return 'That does not look like a valid MOL file.'; } if (ext === 'mol2' && !validateMol2Text(text)) { return 'That does not look like a valid MOL2 file.'; } if (!['sdf', 'mol', 'mol2'].includes(ext)) { return 'Only .sdf, .mol, and .mol2 files are allowed here.'; } return true; } function buildFileMeta(state = {}, fallbackName = '') { const name = String(state.filename || fallbackName || '').trim(); if (!name) return {}; const format = String(state.format || extOf(name) || '').trim().toLowerCase(); return format ? { name, format } : { name }; } function decorateRemoveButton(btn) { if (!btn) return; btn.classList.remove('btn--outline'); btn.classList.add('btn--remove', 'btn--sm'); if (!btn.dataset.pbDecorated) { btn.dataset.pbDecorated = 'true'; btn.innerHTML = `${TRASH_SVG}Remove`; } } function decorateAddButton() { const btn = root.querySelector('#pb-add-predicted-btn'); if (!btn || btn.dataset.pbDecorated) return; btn.dataset.pbDecorated = 'true'; btn.innerHTML = `${PLUS_SVG}Add Ligand`; } function moveControlsToHeader(cardSel) { const card = root.querySelector(cardSel); const head = card?.querySelector('.form-card__head.field-line'); const controls = card?.querySelector('.pb-inline-controls'); if (!card || !head || !controls) return; if (controls.parentElement !== head) { controls.classList.add('pb-inline-controls--header'); head.appendChild(controls); } } function setEyeButtonIcon() { const btn = root.querySelector('#pb-complex-card .pb-eye-autofill'); if (!btn) return; btn.classList.add('vici-toggle-btn'); btn.setAttribute('aria-label', 'Open Vici Lookup'); btn.setAttribute('title', 'Open Vici Lookup'); if (btn.dataset.pbEyeDecorated === 'true') return; btn.dataset.pbEyeDecorated = 'true'; const svg = window.VICI_EYE_SVG || ` `; btn.innerHTML = svg; } function decorateFixedButtons() { decorateRemoveButton(root.querySelector('#pb-complex-card .pb-clear-file')); decorateRemoveButton(root.querySelector('#pb-ideal-card .pb-clear-file')); } function decoratePredictedRow(row) { if (!row) return; decorateRemoveButton(row.querySelector('.pb-remove-row')); } async function fetchExampleText(url) { const key = String(url || ''); if (cache.has(key)) return cache.get(key); const res = await fetch(key, { headers: { Accept: 'text/plain' } }); if (!res.ok) throw new Error(`Failed to fetch example (HTTP ${res.status})`); const txt = await res.text(); cache.set(key, txt); return txt; } function makeMetaHtml(filename, text, sizeBytes) { const bits = []; const name = String(filename || '').trim(); bits.push(name || 'Loaded file'); const sizeLabel = dzFormatBytes(sizeBytes); if (sizeLabel) bits.push(sizeLabel); const lines = String(text || '').split(/\r?\n/).length; bits.push(`${lines} lines`); const chars = String(text || '').length; bits.push(`${chars.toLocaleString()} chars`); return bits .map((v, i) => { const isSize = /(?:\d+\s?(?:KB|MB|B))$/.test(v); if (i === 0) return escapeHtml(v); return ` ${escapeHtml(v)}`; }) .join(' '); } function buildController({ cardSel, inputSel, contentSel, filenameSel, sourceSel, allowedExts, emptyMetaText, validatePayload }) { return createDropZoneController({ scope: root, dropZone: `${cardSel} .pb-drop-zone`, fileInput: inputSel, metaEl: `${cardSel} .pb-file-meta`, removeBtn: `${cardSel} .pb-clear-file`, contentField: contentSel, filenameField: filenameSel, sourceField: sourceSel, emptyMetaText, beforeRead: ({ file }) => { const ext = extOf(file?.name || ''); if (!allowedExts.includes(ext)) { window.showToast?.( 'error', `Invalid file type. Accepted: ${allowedExts.map(x => '.' + x).join(', ')}` ); return false; } return true; }, validateFileContent: ({ file, raw }) => { if (typeof validatePayload === 'function') { return validatePayload(file?.name || '', raw); } return true; }, renderFilledMeta: ({ state }) => makeMetaHtml(state.filename, state.content, state.sizeBytes) }); } function bindFixed() { if (!FIXED.complex) { FIXED.complex = buildController({ cardSel: '#pb-complex-card', inputSel: '#pb-complex-input', contentSel: '#pb-complex-content', filenameSel: '#pb-complex-filename', sourceSel: '#pb-complex-source', allowedExts: ['pdb', 'cif'], emptyMetaText: 'Accepted: .pdb, .cif', validatePayload: validateComplexFilePayload }); FIXED.complex?.bind(); } if (!FIXED.ideal) { FIXED.ideal = buildController({ cardSel: '#pb-ideal-card', inputSel: '#pb-ideal-input', contentSel: '#pb-ideal-content', filenameSel: '#pb-ideal-filename', sourceSel: '#pb-ideal-source', allowedExts: ['sdf', 'mol', 'mol2'], emptyMetaText: 'Accepted: .sdf, .mol, .mol2', validatePayload: validateLigandFilePayload }); FIXED.ideal?.bind(); } moveControlsToHeader('#pb-complex-card'); moveControlsToHeader('#pb-ideal-card'); decorateFixedButtons(); setEyeButtonIcon(); const complexClearBtn = root.querySelector('#pb-complex-card .pb-clear-file'); if (complexClearBtn && !complexClearBtn.dataset.pbLookupClearBound) { complexClearBtn.dataset.pbLookupClearBound = 'true'; complexClearBtn.addEventListener('click', () => { clearComplexLookupUi({ collapse: false }); }); } } function getPredictedRows() { return Array.from(root.querySelectorAll('.pb-predicted-row')); } function syncPredictedRowsUi() { const rows = getPredictedRows(); rows.forEach((row) => { const btn = row.querySelector('.pb-remove-row'); const textEl = btn?.querySelector('.btn__text'); if (!btn) return; if (textEl) { textEl.textContent = 'Remove'; } btn.setAttribute('aria-label', 'Remove ligand'); btn.setAttribute('title', 'Remove ligand'); }); } function ensureAtLeastOnePredictedRow() { const list = root.querySelector('#pb-predicted-list'); if (!list) return null; if (!list.children.length) { return addPredictedRow(null, { animate: false }); } syncPredictedRowsUi(); return list.querySelector('.pb-predicted-row'); } function bubblePosebustersChange() { root.dispatchEvent(new Event('input', { bubbles: true })); root.dispatchEvent(new Event('change', { bubbles: true })); } function animateRowIn(row) { if (!row) return; row.classList.add('is-entering'); row.style.maxHeight = '0px'; requestAnimationFrame(() => { row.style.maxHeight = `${Math.max(row.scrollHeight, 110)}px`; row.classList.remove('is-entering'); }); window.setTimeout(() => { if (row.isConnected && !row.classList.contains('is-removing')) { row.style.maxHeight = 'none'; } }, ROW_ANIM_MS + 40); } function animateRowOut(row) { if (!row || row.classList.contains('is-removing')) return; row.style.maxHeight = `${Math.max(row.scrollHeight, 110)}px`; requestAnimationFrame(() => { row.classList.add('is-removing'); row.style.maxHeight = '0px'; }); window.setTimeout(() => { row.remove(); syncPredictedRowsUi(); bubblePosebustersChange(); }, ROW_ANIM_MS); } function handlePredictedRemove(row) { const list = root.querySelector('#pb-predicted-list'); const total = list ? list.children.length : 0; if (total <= 1) { row.__pbController?.clearValue(); syncPredictedRowsUi(); bubblePosebustersChange(); return; } animateRowOut(row); } function addPredictedRow(initialState = null, { animate = true } = {}) { const list = root.querySelector('#pb-predicted-list'); const tpl = root.querySelector('#pb-predicted-template'); if (!list || !tpl) return null; const fragment = tpl.content.cloneNode(true); const row = fragment.querySelector('.pb-predicted-row'); if (!row) return null; predictedUid += 1; row.dataset.pbPredictedId = String(predictedUid); const ctrl = createDropZoneController({ scope: row, dropZone: '.pb-drop-zone', fileInput: '.pb-file-input', metaEl: '.pb-file-meta', contentField: '.pb-content', filenameField: '.pb-filename', sourceField: '.pb-source', emptyMetaText: 'Accepted: .sdf, .mol, .mol2', beforeRead: ({ file }) => { const ext = extOf(file?.name || ''); const ok = ['sdf', 'mol', 'mol2'].includes(ext); if (!ok) { window.showToast?.('error', 'Invalid file type. Accepted: .sdf, .mol, .mol2'); return false; } return true; }, validateFileContent: ({ file, raw }) => validateLigandFilePayload(file?.name || '', raw), renderFilledMeta: ({ state }) => makeMetaHtml(state.filename, state.content, state.sizeBytes) }); row.__pbController = ctrl; decoratePredictedRow(row); list.appendChild(row); ctrl?.bind(); row.querySelector('.pb-remove-row')?.addEventListener('click', () => { handlePredictedRemove(row); }); if (initialState?.content || initialState?.filename) { ctrl?.setValue(String(initialState.content || ''), { name: String(initialState.filename || ''), source: String(initialState.source || 'preset'), sizeBytes: initialState.sizeBytes }); } syncPredictedRowsUi(); if (animate) { animateRowIn(row); } else { row.style.maxHeight = 'none'; } bubblePosebustersChange(); return row; } function clearPredictedRows({ keepOneBlank = true } = {}) { const list = root.querySelector('#pb-predicted-list'); if (!list) return; list.innerHTML = ''; if (keepOneBlank) { addPredictedRow(null, { animate: false }); return; } syncPredictedRowsUi(); bubblePosebustersChange(); } function collectPredictedRows() { return Array.from(root.querySelectorAll('.pb-predicted-row')) .map(row => row.__pbController?.getState?.()) .filter(Boolean) .filter(s => String(s.content || '').trim() && String(s.filename || '').trim()); } async function applyExample({ toast = true } = {}) { bindFixed(); const ligandTxt = await fetchExampleText(EXAMPLE_LIGAND_URL); const jobnameEl = root.querySelector('#jobname'); if (jobnameEl) { jobnameEl.value = '2519'; } FIXED.complex?.clearValue(); FIXED.ideal?.clearValue(); clearPredictedRows({ keepOneBlank: true }); const firstRow = ensureAtLeastOnePredictedRow(); firstRow?.__pbController?.setValue(ligandTxt, { name: '2519.sdf', source: 'preset', sizeBytes: blobBytes(ligandTxt) }); setTab('basic', { silent: true }); if (toast) { window.showToast?.('success', 'Loaded preset: 2519.'); } } function buildPosebustersPayload(runName, opts = {}) { bindFixed(); const complex = FIXED.complex?.getState?.() || {}; const ideal = FIXED.ideal?.getState?.() || {}; const predicted = collectPredictedRows(); const complexText = String(complex.content || ''); const idealText = String(ideal.content || ''); if (opts.validate && !predicted.length) { if (opts.toast) { showToast?.('error', 'At least one predicted ligand is required.'); } return { error: 'At least one predicted ligand is required.' }; } const inner = { user_id: '', class: 'PoseBusters', name: runName }; if (complexText.trim()) { inner.file = complexText; inner.file_meta = buildFileMeta(complex, complex.filename || 'complex_structure.pdb'); } if (idealText.trim()) { inner.ideal_sdf = idealText; inner.ideal_sdf_meta = buildFileMeta(ideal, ideal.filename || 'reference_ligand.sdf'); } inner.predicted_sdfs = predicted.map((p, idx) => { const name = p.filename || `predicted_${idx + 1}.sdf`; return { [name]: String(p.content || '') }; }); inner.predicted_sdf_meta = predicted.map((p, idx) => { const fallback = `predicted_${idx + 1}.sdf`; return buildFileMeta(p, p.filename || fallback); }); return { job: inner, payload: { workflow_name: runName, [modelKey]: inner } }; } return { onInit() { bindFixed(); bindComplexLookupSync(); decorateAddButton(); const btns = Array.from(root.querySelectorAll('.model-preset-btn[data-example]')); btns.forEach((b, i) => { if (i === 0) { b.dataset.example = 'example1'; b.textContent = '2519'; } else { b.remove(); } }); const eyeBtn = root.querySelector('#pb-complex-card .pb-eye-autofill'); if (eyeBtn && !eyeBtn.dataset.bound) { eyeBtn.dataset.bound = 'true'; eyeBtn.addEventListener('click', (e) => { e.preventDefault(); toggleComplexLookupPanel(); }); } const addBtn = root.querySelector('#pb-add-predicted-btn'); if (addBtn && !addBtn.dataset.bound) { addBtn.dataset.bound = 'true'; addBtn.addEventListener('click', () => addPredictedRow(null, { animate: true })); } ensureAtLeastOnePredictedRow(); syncPredictedRowsUi(); }, applyPreset(key) { if (key !== 'example1') return; applyExample({ toast: false }).catch((err) => { console.error(err); window.showToast?.('error', err?.message || 'Failed to load example.'); }); }, getPresetLabel(key) { if (key === 'example1') return '2519'; return key; }, captureState() { bindFixed(); return { tab: currentTab, jobname: String(root.querySelector('#jobname')?.value || ''), complex: FIXED.complex?.getState?.() || {}, ideal: FIXED.ideal?.getState?.() || {}, predicted: collectPredictedRows() }; }, applyState(state) { bindFixed(); if (!state) return; const jobnameEl = root.querySelector('#jobname'); if (jobnameEl) jobnameEl.value = String(state.jobname || ''); const c = state.complex || {}; if (String(c.content || '').trim() || String(c.filename || '').trim()) { FIXED.complex?.setValue(String(c.content || ''), { name: String(c.filename || ''), source: String(c.source || ''), sizeBytes: c.sizeBytes }); } else { FIXED.complex?.clearValue(); } const i = state.ideal || {}; if (String(i.content || '').trim() || String(i.filename || '').trim()) { FIXED.ideal?.setValue(String(i.content || ''), { name: String(i.filename || ''), source: String(i.source || ''), sizeBytes: i.sizeBytes }); } else { FIXED.ideal?.clearValue(); } const list = root.querySelector('#pb-predicted-list'); if (list) list.innerHTML = ''; const predictedRows = Array.isArray(state.predicted) ? state.predicted : []; if (predictedRows.length) { predictedRows.forEach((p) => { addPredictedRow(p, { animate: false }); }); } else { ensureAtLeastOnePredictedRow(); } syncPredictedRowsUi(); }, reset({ baselineState }) { if (baselineState) { this.applyState(baselineState); } else { FIXED.complex?.clearValue(); FIXED.ideal?.clearValue(); const list = root.querySelector('#pb-predicted-list'); if (list) list.innerHTML = ''; ensureAtLeastOnePredictedRow(); syncPredictedRowsUi(); } FIXED.complex?.refresh?.(); FIXED.ideal?.refresh?.(); bubblePosebustersChange(); }, buildJob(opts = {}) { bindFixed(); const nameInput = root.querySelector('#jobname'); const rawName = String(nameInput?.value || '').trim(); const runName = canonicalizeRunName(rawName || `my_${modelKey}_run`); if (nameInput && rawName && runName !== rawName) { nameInput.value = runName; if (opts.toast) { showToast?.('success', `Name adjusted to "${runName}".`); } } if (opts.requireName && !runName) { if (opts.toast) showToast?.('error', 'Name is required.'); return { error: 'Name is required.' }; } if (opts.validate && runName && !SAFE_NAME_RE.test(runName)) { if (opts.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.' }; } return buildPosebustersPayload(runName, opts); } }; } function collectSerializableFields(scope = root) { const els = Array.from(scope.querySelectorAll('input, select, textarea')) .filter(el => !el.closest('[data-panel="api"]')) .filter(el => !el.disabled) .filter(el => { const type = (el.type || '').toLowerCase(); return type !== 'button' && type !== 'submit' && type !== 'reset'; }); const out = {}; els.forEach((el, idx) => { const key = getFieldKey(el, idx); const val = readFieldValue(el); if (typeof val === 'undefined') return; if (Object.prototype.hasOwnProperty.call(out, key)) { if (!Array.isArray(out[key])) out[key] = [out[key]]; out[key].push(val); } else { out[key] = val; } }); return out; } function applySerializableFields(values = {}, scope = root) { const els = Array.from(scope.querySelectorAll('input, select, textarea')) .filter(el => !el.closest('[data-panel="api"]')); els.forEach((el, idx) => { const key = getFieldKey(el, idx); if (!Object.prototype.hasOwnProperty.call(values, key)) return; const next = values[key]; if (Array.isArray(next) && !el.multiple && (el.type || '').toLowerCase() !== 'checkbox') { return; } writeFieldValue(el, next); }); } function captureState() { if (typeof adapter.captureState === 'function') { try { return adapter.captureState({ root, modelSlug, modelKey }); } catch (err) { console.error(err); } } return { tab: currentTab, fields: collectSerializableFields(root) }; } function applyState(state) { if (!state) return; if (typeof adapter.applyState === 'function') { try { adapter.applyState(state, { root, modelSlug, modelKey }); } catch (err) { console.error(err); } } else { applySerializableFields(state.fields || {}, root); } if (state.tab && ['basic','advanced','api'].includes(state.tab)) { setTab(state.tab, { silent: true }); } else { setTab('basic', { silent: true }); } if (typeof adapter.afterApplyState === 'function') { try { adapter.afterApplyState(state, { root, modelSlug, modelKey }); } catch (err) { console.error(err); } } } function isDirty() { if (typeof adapter.isDirty === 'function') { try { return !!adapter.isDirty({ root, modelSlug, modelKey, baselineState }); } catch (err) { console.error(err); } } if (!baselineState) return false; const current = captureState() || {}; const base = baselineState || {}; const { tab: _currentTabIgnored, ...currentComparable } = current; const { tab: _baseTabIgnored, ...baseComparable } = base; return stableSerialize(currentComparable) !== stableSerialize(baseComparable); } 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(); if (!silent && typeof adapter.onTabChange === 'function') { try { adapter.onTabChange(tab, { root, modelSlug, modelKey }); } catch (err) { console.error(err); } } } function initTabs() { tabs.forEach(btn => { btn.addEventListener('click', () => setTab(btn.dataset.tab)); btn.addEventListener('keydown', (e) => { if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) return; e.preventDefault(); const i = tabs.indexOf(btn); let next = i; if (e.key === 'ArrowRight') next = (i + 1) % tabs.length; if (e.key === 'ArrowLeft') next = (i - 1 + tabs.length) % tabs.length; if (e.key === 'Home') next = 0; if (e.key === 'End') next = tabs.length - 1; tabs[next]?.focus(); setTab(tabs[next]?.dataset.tab || 'basic'); }); }); setTab(currentTab || 'basic', { silent: true, syncHash: true, replaceHash: true }); } function buildGenericJob({ requireName = true, validate = true, toast = false } = {}) { const nameInput = document.getElementById('jobname'); const rawName = String(nameInput?.value || '').trim(); let runName = canonicalizeRunName(rawName || `my_${modelKey}_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 (validate && runName && !SAFE_NAME_RE.test(runName)) { if (toast) showToast?.('error', 'Name must be 3-64 chars using a-z, 0-9, _ or - and start/end with letter or digit.'); return { error: 'Invalid name.' }; } const allFields = collectSerializableFields(root); const params = { ...allFields }; delete params.jobname; Object.keys(params).forEach((k) => { const v = params[k]; if (Array.isArray(v)) return; if (typeof v !== 'string') return; const trimmed = v.trim(); if (trimmed === '') return; if (/^-?\d+$/.test(trimmed)) params[k] = parseInt(trimmed, 10); else if (/^-?\d+\.\d+$/.test(trimmed)) params[k] = parseFloat(trimmed); }); const inner = { name: runName || `my_${modelKey}_run`, ...params }; const payload = { workflow_name: inner.name, [modelKey]: inner }; return { job: inner, payload }; } function buildJob(opts = {}) { if (typeof adapter.buildJob === 'function') { try { return adapter.buildJob(opts, { root, modelSlug, modelKey }); } catch (err) { console.error(err); return { error: err?.message || 'Failed to build job.' }; } } return buildGenericJob(opts); } function toDefRefSafe(path) { return String(path) .replace(/[^a-zA-Z0-9._:-]+/g, '_') .slice(0, 180); } function humanizeKey(key) { return String(key || '') .replace(/\[\d+\]/g, '') .replace(/[_-]+/g, ' ') .replace(/\s+/g, ' ') .trim() || 'field'; } function valueTypeLabel(v) { if (Array.isArray(v)) return 'array'; if (v === null) return 'null'; return typeof v; } function buildGenericPayloadDef(path, value) { const pathLabel = String(path || 'payload'); const type = valueTypeLabel(value); let typeHint = `Expected type: ${escapeHtml(type)}.`; if (type === 'string') typeHint = 'Expected type: string. Replace with the value for your run.'; if (type === 'number') typeHint = 'Expected type: number. Use an integer or decimal that your model supports.'; if (type === 'boolean') typeHint = 'Expected type: boolean (true or false).'; let extra = 'Replace this example value with a valid value for your model.'; const pathLower = pathLabel.toLowerCase(); if (pathLower.endsWith('.name')) { extra = 'Run/job name for this model block. Keep this aligned with workflow_name unless your backend expects otherwise.'; } else if (pathLower.includes('seed')) { extra = 'Random seed for reproducibility. Use an integer.'; } else if (pathLower.includes('num') || pathLower.includes('count') || pathLower.includes('steps')) { extra = 'Numeric model parameter. Use a supported range from your model docs.'; } else if (String(value) === '...') { extra = 'Placeholder. Replace with additional model-specific parameters or remove this field.'; } 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]; // recurse into objects/arrays 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((err) => {', ' console.error(err);', '});' ].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((err) => {', ' console.error(err);', '});' ].join('\n') }; } let apiRenderQueued = false; function queueApiSnippetRender() { if (apiRenderQueued) return; apiRenderQueued = true; requestAnimationFrame(() => { apiRenderQueued = false; renderApiSnippet(); }); } function makeApiPreviewPayload(payload) { const out = deepClone(payload || {}); const block = out?.[modelKey]; if (modelKey !== 'posebusters' || !block || !isPlainObject(block)) { return out; } const complexName = String(block.file_meta?.name || 'complex_structure.pdb').trim() || 'complex_structure.pdb'; if (typeof block.file === 'string') { block.file = block.file.trim() ? `` : ''; } if (typeof block.ideal_sdf === 'string') { const idealName = String(block.ideal_sdf_meta?.name || 'ideal_ligand.sdf').trim() || 'ideal_ligand.sdf'; block.ideal_sdf = block.ideal_sdf.trim() ? `` : ''; } if (Array.isArray(block.predicted_sdfs)) { block.predicted_sdfs = block.predicted_sdfs.map((entry, idx) => { if (!entry || typeof entry !== 'object') return entry; const fileName = Object.keys(entry)[0] || block.predicted_sdf_meta?.[idx]?.name || `predicted_${idx + 1}.sdf`; const rawValue = String(entry[fileName] ?? Object.values(entry)[0] ?? ''); return { [fileName]: rawValue.trim() ? `` : '' }; }); } return out; } let lastSyncedPosebustersFileKey = ''; function getPosebustersApiFileKey() { if (modelKey !== 'posebusters') return ''; try { const state = captureState() || {}; return stableSerialize({ complex: { filename: String(state?.complex?.filename || ''), content: String(state?.complex?.content || '') }, ideal: { filename: String(state?.ideal?.filename || ''), content: String(state?.ideal?.content || '') }, predicted: Array.isArray(state?.predicted) ? state.predicted.map((row) => ({ filename: String(row?.filename || ''), content: String(row?.content || '') })) : [] }); } catch (err) { console.error(err); return ''; } } function shouldIncludePosebustersApiFiles(includeFileContents) { if (modelKey !== 'posebusters') return true; if (includeFileContents) return true; const currentKey = getPosebustersApiFileKey(); return !!lastSyncedPosebustersFileKey && currentKey === lastSyncedPosebustersFileKey; } function renderApiSnippet({ forceDefault = false, toast = false, includeFileContents = 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 || { workflow_name: `my_${modelKey}_run`, [modelKey]: { name: `my_${modelKey}_run`, param_1: 'option-a', param_2: 42 } }); } payloadSource = stripExecutionContextForApi(payloadSource); if ( !forceDefault && modelKey === 'posebusters' && !shouldIncludePosebustersApiFiles(includeFileContents) ) { payloadSource = makeApiPreviewPayload(payloadSource); } const payloadBlock = stringifyPayloadWithMarkers(payloadSource); apiDynamicDefContent = payloadBlock.defs || {}; const snippet = getApiTemplate( currentApiLang, payloadBlock.text, payloadBlock.html ); currentApiSnippet = snippet; apiCodeEl.innerHTML = snippet.html; if (!forceDefault && modelKey === 'posebusters' && includeFileContents) { lastSyncedPosebustersFileKey = getPosebustersApiFileKey(); } if (toast) { window.showToast?.( 'success', forceDefault ? 'Reset API snippet to defaults.' : includeFileContents ? 'Synced API snippet from form.' : 'Updated API snippet.' ); } } finally { isRenderingApiSnippet = false; } } 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) { if (!targetEl) return; 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(); const titleEl = pop.querySelector('.api-def-popout__title'); const bodyEl = pop.querySelector('.api-def-popout__body'); titleEl.textContent = def.title || defRef; bodyEl.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 setApiLang(lang) { if (!API_LANGS.includes(lang)) return; currentApiLang = lang; apiLangTabs.forEach(btn => { const active = btn.dataset.lang === lang; btn.classList.toggle('is-active', active); btn.setAttribute('aria-selected', active ? 'true' : 'false'); }); renderApiSnippet(); } function findApiActionButtons() { let syncBtn = null, copyBtn = null, resetApiBtn = null; apiActionBtns.forEach((btn, i) => { const label = `${btn.getAttribute('aria-label') || ''} ${btn.title || ''} ${btn.textContent || ''}`.toLowerCase(); if (!syncBtn && label.includes('sync')) syncBtn = btn; else if (!copyBtn && label.includes('copy')) copyBtn = btn; else if (!resetApiBtn && label.includes('reset')) resetApiBtn = btn; // fallback by order if (i === 0 && !syncBtn) syncBtn = btn; if (i === 1 && !copyBtn) copyBtn = btn; if (i === 2 && !resetApiBtn) resetApiBtn = btn; }); return { syncBtn, copyBtn, resetApiBtn }; } function getPresets() { if (modelKey === 'posebusters') { return { example1: { __posebusters_example: true } }; } return window.MODEL_PRESETS || {}; } function applyPreset(key) { const presets = getPresets(); const preset = presets[key]; if (!preset) { showToast?.('error', `Preset "${key}" not found.`); return; } try { if (typeof adapter.applyPreset === 'function') { adapter.applyPreset(key, preset, { root, modelSlug, modelKey }); } else if (typeof preset === 'function') { preset({ root, modelSlug, modelKey, setTab }); } else if (isPlainObject(preset)) { const targetTab = preset.tab; const fieldMap = isPlainObject(preset.fields) ? preset.fields : preset; Object.entries(fieldMap).forEach(([k, v]) => { if (k === 'tab') return; const byId = document.getElementById(k); if (byId) { writeFieldValue(byId, v); return; } const byName = root.querySelector(`[name="${CSS.escape(k)}"]`); if (byName) { writeFieldValue(byName, v); return; } const byKey = root.querySelector(`[data-field-key="${CSS.escape(k)}"]`); if (byKey) { writeFieldValue(byKey, v); } }); if (targetTab && ['basic','advanced','api'].includes(targetTab)) { setTab(targetTab); } } if (typeof adapter.afterPreset === 'function') { adapter.afterPreset(key, preset, { root, modelSlug, modelKey }); } updateActionVisibility(); renderApiSnippet(); const toastLabel = (typeof adapter.getPresetLabel === 'function' ? adapter.getPresetLabel(key, preset, { root, modelSlug, modelKey }) : null) || key; window.showToast?.('success', `Loaded preset: ${toastLabel}.`); } catch (err) { console.error(err); showToast?.('error', 'Could not apply preset.'); } } function hardCollapseViciLookup(panel, { duration = 240, afterClose } = {}) { if (!panel) { try { afterClose?.(); } catch (err) { console.error(err); } return; } panel.dataset.viciCollapsed = '1'; panel.classList.remove('open'); if (panel._viciCollapseTimer) { clearTimeout(panel._viciCollapseTimer); panel._viciCollapseTimer = null; } let finished = false; function finish() { if (finished) return; finished = true; panel.removeEventListener('transitionend', onEnd); if (panel._viciCollapseTimer) { clearTimeout(panel._viciCollapseTimer); panel._viciCollapseTimer = null; } panel.style.display = 'none'; panel.style.maxHeight = '0px'; panel.style.opacity = '0'; panel.style.overflow = 'hidden'; panel.style.willChange = ''; panel.style.transition = ''; try { afterClose?.(); } catch (err) { console.error(err); } } function onEnd(e) { if (e.target !== panel) return; finish(); } const computed = window.getComputedStyle(panel); const alreadyHidden = computed.display === 'none' || (panel.offsetHeight === 0 && panel.scrollHeight === 0); if (alreadyHidden) { finish(); return; } const startHeight = Math.max(panel.scrollHeight, panel.offsetHeight); panel.style.display = 'block'; panel.style.overflow = 'hidden'; panel.style.willChange = 'max-height, opacity'; panel.style.transition = 'none'; panel.style.maxHeight = `${startHeight}px`; panel.style.opacity = computed.opacity === '0' ? '1' : computed.opacity; void panel.offsetHeight; panel.addEventListener('transitionend', onEnd); panel.style.transition = `max-height ${duration}ms ease, opacity ${duration}ms ease`; panel.style.maxHeight = '0px'; panel.style.opacity = '0'; panel._viciCollapseTimer = setTimeout(finish, duration + 80); } function resetAllViciLookups(scope = root) { const panel = scope.querySelector('#pb-complex-lookup'); if (!panel) return; hardCollapseViciLookup(panel, { duration: 240, afterClose: () => { try { // keepTarget:true so we do not wipe the real complex field // after adapter.reset has already restored it window.ViciLookup?.clear?.(panel, { keepInput: false, keepTarget: true }); } catch (err) { console.error(err); } } }); const toggleBtn = scope.querySelector('#pb-complex-card .pb-eye-autofill'); if (toggleBtn) { toggleBtn.classList.remove('active'); toggleBtn.setAttribute('aria-expanded', 'false'); } } function resetFormUI({ toast = true } = {}) { try { const tabBeforeReset = currentTab; if (typeof adapter.reset === 'function') { adapter.reset({ root, modelSlug, modelKey, baselineState: deepClone(baselineState) }); if (tabBeforeReset && ['basic', 'advanced', 'api'].includes(tabBeforeReset)) { setTab(tabBeforeReset, { silent: true }); } } else if (baselineState) { const nextState = deepClone(baselineState); nextState.tab = tabBeforeReset; applyState(nextState); } resetAllViciLookups(root); const jobnameEl = document.getElementById('jobname'); if (jobnameEl && jobnameEl.value) { const safe = canonicalizeRunName(jobnameEl.value); if (safe) jobnameEl.value = safe; } lastSyncedPosebustersFileKey = ''; renderApiSnippet({ forceDefault: false, toast: false }); updateActionVisibility(); if (toast) { showToast?.('success', 'Form reset.'); } } catch (err) { console.error(err); showToast?.('error', 'Reset failed.'); } } 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 (err) { console.warn('Could not read ViciContext payload', err); } return out; } async function submitModelJob() { const execBtn = executeBtnMembers; const built = buildJob({ requireName: true, validate: true, toast: true }); if (!built || built.error || !built.payload) return; if (typeof window.guardSubmitOrToast === 'function') { try { const ok = await window.guardSubmitOrToast({ planned: 1, minCredit: 1.0, buttonSelector: execBtn }); if (!ok) return; } catch (err) { console.warn('guardSubmitOrToast failed, continuing', err); } } 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 }; if (modelKey === 'posebusters' && body?.[modelKey]) { body[modelKey].user_id = execCtx.team_id ? { member_id: memberId, team_id: execCtx.team_id } : memberId; } 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'); } } } async function checkModelJobStatus({ job_id, member_id } = {}) { if (!job_id) throw new Error('job_id is required'); const body = { job_id }; if (member_id) body.member_id = member_id; const res = await fetch(MODEL_STATUS_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!res.ok) { throw new Error(`Status request failed: ${res.status}`); } return res.json(); } function bindDirtyTracking() { root.addEventListener('input', (e) => { if (e.target.closest('[data-panel="api"]')) return; // Any form change disarms manual file-content injection lastSyncedPosebustersFileKey = ''; updateActionVisibility(); }); root.addEventListener('change', (e) => { if (e.target.closest('[data-panel="api"]')) return; // Any form change disarms manual file-content injection lastSyncedPosebustersFileKey = ''; updateActionVisibility(); if (currentTab === 'api') queueApiSnippetRender(); }); const mo = new MutationObserver((mutations) => { const hasRealUiMutation = mutations.some((m) => { const target = m.target && m.target.nodeType === 1 ? m.target : m.target?.parentElement; if (!target) return false; if (target.closest?.('.api-code-wrap')) return false; if (target.closest?.('.api-def-popout')) return false; return true; }); if (!hasRealUiMutation) return; updateActionVisibility(); }); mo.observe(root, { childList: true, subtree: true }); } function bindReset() { resetBtn?.addEventListener('click', () => resetFormUI({ toast: false })); } function bindPresets() { presetBtns.forEach(btn => { btn.addEventListener('click', (e) => { e.preventDefault(); applyPreset(btn.dataset.example); }); }); } function bindApiControls() { apiLangTabs.forEach(btn => { btn.addEventListener('click', () => setApiLang(btn.dataset.lang || 'python')); btn.addEventListener('keydown', (e) => { if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) return; e.preventDefault(); const i = apiLangTabs.indexOf(btn); let next = i; if (e.key === 'ArrowRight') next = (i + 1) % apiLangTabs.length; if (e.key === 'ArrowLeft') next = (i - 1 + apiLangTabs.length) % apiLangTabs.length; if (e.key === 'Home') next = 0; if (e.key === 'End') next = apiLangTabs.length - 1; apiLangTabs[next]?.focus(); setApiLang(apiLangTabs[next]?.dataset.lang || 'python'); }); }); const { syncBtn, copyBtn, resetApiBtn } = findApiActionButtons(); syncBtn?.addEventListener('click', () => { pulseBtn(syncBtn, 'pulse-blue'); renderApiSnippet({ toast: true, includeFileContents: 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'); lastSyncedPosebustersFileKey = ''; renderApiSnippet({ forceDefault: true, toast: true }); }); } function bindExecute() { if (executeBtnMembers && executeBtnMembers.tagName.toLowerCase() === 'button') { executeBtnMembers.type = 'button'; executeBtnMembers.addEventListener('click', (e) => { e.preventDefault(); submitModelJob(); }); } if (executeBtnGuest && executeBtnGuest.tagName.toLowerCase() === 'a') { } } 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 initDefaultApiJob() { if (typeof adapter.getDefaultApiPayload === 'function') { try { defaultApiJob = adapter.getDefaultApiPayload({ root, modelSlug, modelKey }); if (defaultApiJob) { defaultApiJob = stripExecutionContextForApi(defaultApiJob); return; } } catch (err) { console.error(err); } } const built = buildJob({ requireName: false, validate: false, toast: false }); if (built && !built.error && built.payload) { defaultApiJob = stripExecutionContextForApi(deepClone(built.payload)); return; } defaultApiJob = { workflow_name: `my_${modelKey}_run`, [modelKey]: { name: `my_${modelKey}_run`, param_1: 'option-a', param_2: 42 } }; } function init() { if (typeof adapter.onInit === 'function') { try { adapter.onInit({ root, modelSlug, modelKey }); } catch (err) { console.error(err); } } bindNameCanonicalization(); bindDirtyTracking(); bindReset(); bindPresets(); bindApiControls(); bindApiDefinitionPopout(); bindExecute(); blockFileDropsOnRoot(); bindHashRouting(); initTabs(); baselineState = deepClone(captureState()); initDefaultApiJob(); setApiLang('python'); renderApiSnippet({ forceDefault: false, toast: false }); updateActionVisibility(); window.ModelPage = { root, modelSlug, modelKey, setTab, getCurrentTab: () => currentTab, isDirty, updateActionVisibility, captureState, applyState, resetForm: resetFormUI, applyPreset, buildJob, submitJob: submitModelJob, checkStatus: checkModelJobStatus, renderApiSnippet, setApiLang, getApiSnippet: () => ({ ...currentApiSnippet }), 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(); } })();