(() => {
'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: [
'',
'',
'import json',
'import requests',
'',
`API_URL = "${escapeHtml(MODEL_API_ENDPOINT)}"`,
`TOKEN_ID = "<TOKEN_ID>"`,
`TOKEN_SECRET = "<TOKEN_SECRET>"`,
'',
'payload = json.loads(r"""',
payloadHtml,
'""")',
'',
'resp = requests.post(',
' API_URL,',
' headers={',
' "Content-Type": "application/json",',
' "Token-ID": TOKEN_ID,',
' "Token-Secret": TOKEN_SECRET,',
' },',
' json=payload',
')',
'',
'resp.raise_for_status()',
'print(resp.json())'
].join('\n')
};
}
if (lang === 'curl') {
return {
text: [
'# POST a model job (cURL)',
'# Set TOKEN_ID and TOKEN_SECRET to your values.',
'',
`curl -X POST "${MODEL_API_ENDPOINT}" \\`,
' -H "Content-Type: application/json" \\',
' -H "Token-ID: " \\',
' -H "Token-Secret: " \\',
` --data-binary @- <<'${HEREDOC_TAG}'`,
payloadText,
HEREDOC_TAG
].join('\n'),
html: [
'',
'',
'',
`curl -X POST "${escapeHtml(MODEL_API_ENDPOINT)}" \\`,
' -H "Content-Type: application/json" \\',
' -H "Token-ID: <TOKEN_ID>" \\',
' -H "Token-Secret: <TOKEN_SECRET>" \\',
` --data-binary @- <<'${escapeHtml(HEREDOC_TAG)}'`,
payloadHtml,
`${escapeHtml(HEREDOC_TAG)}`
].join('\n')
};
}
return {
text: [
'// POST a model job (JavaScript)',
'// Set TOKEN_ID and TOKEN_SECRET to your values.',
'',
'(async () => {',
` const API_URL = "${MODEL_API_ENDPOINT}";`,
' const TOKEN_ID = "";',
' const TOKEN_SECRET = "";',
'',
` const payload = ${payloadText};`,
'',
' const resp = await fetch(API_URL, {',
' method: "POST",',
' headers: {',
' "Content-Type": "application/json",',
' "Token-ID": TOKEN_ID,',
' "Token-Secret": TOKEN_SECRET,',
' },',
' body: JSON.stringify(payload),',
' });',
'',
' if (!resp.ok) throw new Error(`Request failed: ${resp.status}`);',
'',
' console.log(await resp.json());',
'})().catch((err) => {',
' console.error(err);',
'});'
].join('\n'),
html: [
'',
'',
'',
'(async () => {',
` const API_URL = "${escapeHtml(MODEL_API_ENDPOINT)}";`,
` const TOKEN_ID = "<TOKEN_ID>";`,
` const TOKEN_SECRET = "<TOKEN_SECRET>";`,
'',
` const payload = ${payloadHtml};`,
'',
' const resp = await fetch(API_URL, {',
' method: "POST",',
' headers: {',
' "Content-Type": "application/json",',
' "Token-ID": TOKEN_ID,',
' "Token-Secret": TOKEN_SECRET,',
' },',
' body: JSON.stringify(payload),',
' });',
'',
' if (!resp.ok) throw new Error(`Request failed: ${resp.status}`);',
'',
' console.log(await resp.json());',
'})().catch((err) => {',
' console.error(err);',
'});'
].join('\n')
};
}
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();
}
})();