(() => {
'use strict';
const MODEL_WORKFLOW_ENDPOINT =
window.MODEL_WORKFLOW_ENDPOINT ||
'https://ayambabu23--workflow-execute-workflow.modal.run/';
const MODEL_API_ENDPOINT =
window.MODEL_API_ENDPOINT ||
'https://vici-bio--api-execute-workflow.modal.run/';
const MODEL_STATUS_ENDPOINT =
window.MODEL_STATUS_ENDPOINT ||
'https://vici-bio--api-check-status.modal.run/';
const 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';
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;
let apiManualIncludeDropzoneContent = false;
const dropZoneControllers = [];
const DOCKQ_CLASS_NAME = 'DockQ';
const DOCKQ_EXAMPLE_FILES = {
model: {
url: 'https://cdn.prod.website-files.com/685009238c2e9a16d785299f/69a75ec4a47f8246ef1f00ce_1A2K%20model.txt',
name: '1A2K_model.pdb'
},
ref: {
url: 'https://cdn.prod.website-files.com/685009238c2e9a16d785299f/69a75ec460292720cd9d33a5_1A2K%20ref.txt',
name: '1A2K_ref.pdb'
}
};
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 === '#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';
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 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) {
return (
el.getAttribute('data-field-key') ||
el.id ||
el.name ||
''
);
}
function isFrontendOnlyField(el) {
if (!el) return false;
if (el.matches('[data-frontend-only="true"], [data-no-submit="true"]')) {
return true;
}
if (
el.classList?.contains('lookup-target-name') ||
el.classList?.contains('lookup-target-source')
) {
return true;
}
return false;
}
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 dockQArrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
const chunkSize = 0x8000;
let binary = '';
for (let i = 0; i < bytes.length; i += chunkSize) {
const chunk = bytes.subarray(i, i + chunkSize);
binary += String.fromCharCode.apply(null, chunk);
}
return btoa(binary);
}
function getDockQFileKind(fileName) {
const name = String(fileName || '').trim().toLowerCase();
if (!name) return null;
if (name.endsWith('.pdb.gz')) return { suffix: 'pdb_gz', label: 'pdb.gz', isBinary: true };
if (name.endsWith('.cif.gz')) return { suffix: 'cif_gz', label: 'cif.gz', isBinary: true };
if (name.endsWith('.pdb')) return { suffix: 'file', label: 'pdb', isBinary: false };
if (name.endsWith('.cif')) return { suffix: 'cif', label: 'cif', isBinary: false };
if (name.endsWith('.gz')) return { suffix: null, label: 'gz', isBinary: true, ambiguous: true };
return null;
}
function getDockQFieldKey(role, suffix) {
if (suffix === 'file') return `${role}_file`;
return `${role}_${suffix}`;
}
function getDockQRoleLabel(role) {
return role === 'model' ? 'Model' : 'Reference';
}
function getDockQLookupFallbackName(role) {
return role === 'model' ? 'model_lookup.pdb' : 'reference_lookup.pdb';
}
async function readDockQUploadFile(file) {
const kind = getDockQFileKind(file?.name);
if (!kind) throw new Error('Unsupported file type. Use .pdb, .cif, .pdb.gz, or .cif.gz.');
if (kind.ambiguous) throw new Error('Use .pdb.gz or .cif.gz so DockQ can map the gz file correctly.');
if (kind.isBinary) {
const buffer = await file.arrayBuffer();
return dockQArrayBufferToBase64(buffer);
}
return dzReadFileAsText(file);
}
function getDockQFileBlock(role) {
return root.querySelector(`.dockq-file-card[data-dockq-role="${role}"]`);
}
function getDockQFilePayload(role, {
forApi = false,
includeApiDropzoneContent = false,
toast = false
} = {}) {
const block = getDockQFileBlock(role);
const roleLabel = getDockQRoleLabel(role);
if (!block) {
return { error: `${roleLabel} block not found.` };
}
const contentField = block.querySelector('.lookup-target-content');
const nameField = block.querySelector('.lookup-target-name');
const sourceField = block.querySelector('.lookup-target-source');
const content = String(contentField?.value || '');
const source = String(sourceField?.value || '').trim();
const defaultFileName = role === 'model'
? 'model_structure.pdb'
: 'reference_structure.pdb';
const defaultPlaceholder = role === 'model'
? ''
: '';
let fileName = String(nameField?.value || '').trim();
if (!content.trim()) {
if (forApi) {
return {
key: getDockQFieldKey(role, 'file'),
value: defaultPlaceholder,
fileName: defaultFileName,
fileFormat: 'pdb'
};
}
if (toast) showToast?.('error', `${roleLabel} file is required.`);
return { error: `${roleLabel} file is required.` };
}
if (!fileName) {
fileName = source === 'vici_lookup'
? getDockQLookupFallbackName(role)
: defaultFileName;
if (nameField) nameField.value = fileName;
}
const kind = getDockQFileKind(fileName);
if (!kind || !kind.suffix) {
if (toast) {
showToast?.('error', `${roleLabel} file type is invalid. Use .pdb, .cif, .pdb.gz, or .cif.gz.`);
}
return { error: `${roleLabel} file type is invalid.` };
}
const key = getDockQFieldKey(role, kind.suffix);
if (forApi && !includeApiDropzoneContent) {
const placeholderMap = {
file: role === 'model' ? '' : '',
cif: role === 'model' ? '' : '',
pdb_gz: role === 'model' ? '' : '',
cif_gz: role === 'model' ? '' : ''
};
return {
key,
value: placeholderMap[kind.suffix] || '',
fileName,
fileFormat: kind.label
};
}
return {
key,
value: content,
fileName,
fileFormat: kind.label
};
}
async function loadDockQExample() {
const refBlock = getDockQFileBlock('ref');
const modelBlock = getDockQFileBlock('model');
const refController = refBlock?._dockqDropzoneController;
const modelController = modelBlock?._dockqDropzoneController;
if (!refController || !modelController) {
showToast?.('error', 'DockQ example inputs are not ready yet.');
return;
}
try {
const [refResp, modelResp] = await Promise.all([
fetch(DOCKQ_EXAMPLE_FILES.ref.url),
fetch(DOCKQ_EXAMPLE_FILES.model.url)
]);
if (!refResp.ok || !modelResp.ok) {
throw new Error('Could not load example files.');
}
const [refText, modelText] = await Promise.all([
refResp.text(),
modelResp.text()
]);
refController.setValue(refText, {
name: DOCKQ_EXAMPLE_FILES.ref.name,
source: 'example',
sizeBytes: new Blob([refText]).size
});
modelController.setValue(modelText, {
name: DOCKQ_EXAMPLE_FILES.model.name,
source: 'example',
sizeBytes: new Blob([modelText]).size
});
const nameInput = document.getElementById('jobname');
if (nameInput && !String(nameInput.value || '').trim()) {
writeFieldValue(nameInput, 'dockq_1a2k_example');
}
const autoMapEl = document.getElementById('dockq-auto-map');
const nativeChainsEl = document.getElementById('dockq-native-chains');
const modelChainsEl = document.getElementById('dockq-model-chains');
const smallMolEl = document.getElementById('dockq-small-molecule');
if (autoMapEl) writeFieldValue(autoMapEl, true);
if (nativeChainsEl) writeFieldValue(nativeChainsEl, '');
if (modelChainsEl) writeFieldValue(modelChainsEl, '');
if (smallMolEl) writeFieldValue(smallMolEl, false);
apiManualIncludeDropzoneContent = false;
updateActionVisibility();
if (currentTab === 'api') renderApiSnippet();
showToast?.('success', 'Loaded DockQ example files.');
} catch (err) {
console.error(err);
showToast?.('error', err?.message || 'Failed to load DockQ example files.');
}
}
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 getFileFormatFromName(name) {
const s = String(name || '').trim();
if (!s) return '';
const lower = s.toLowerCase();
if (lower.endsWith('.tar.gz')) return 'tar.gz';
if (lower.endsWith('.tar.bz2')) return 'tar.bz2';
if (lower.endsWith('.tar.xz')) return 'tar.xz';
const dot = s.lastIndexOf('.');
if (dot <= 0 || dot >= s.length - 1) return '';
return s.slice(dot + 1).toLowerCase();
}
function getDropZoneFieldEntries(scope = root) {
const blocks = Array.from(scope.querySelectorAll('.molecule-block--dropzone'));
return blocks
.map((block, idx) => {
const contentField = block.querySelector('.lookup-target-content');
if (!contentField) return null;
const nameField = block.querySelector('.lookup-target-name');
const sourceField = block.querySelector('.lookup-target-source');
const contentKey = getFieldKey(contentField, idx);
const baseKey = /_content$/i.test(contentKey)
? contentKey.replace(/_content$/i, '')
: `${contentKey}_file`;
const content = String(contentField.value || '');
const fileName = String(nameField?.value || '').trim();
const source = String(sourceField?.value || '').trim();
const hasContent = content.trim().length > 0;
const isViciLookup = source === 'vici_lookup';
return {
block,
contentField,
nameField,
sourceField,
contentKey,
baseKey,
content,
fileName,
source,
hasContent,
isViciLookup
};
})
.filter(Boolean);
}
function stripFrontendOnlyDropZoneFields(params = {}) {
return { ...params };
}
function applyApiDropZoneContentView(
params = {},
{ includeRealContent = false } = {},
scope = root
) {
const next = { ...params };
getDropZoneFieldEntries(scope).forEach((entry) => {
const fileName = entry.fileName || '';
// content only appears after explicit Sync
next[entry.contentKey] = includeRealContent ? (entry.content || '') : '';
// these always auto-sync
next[`${entry.baseKey}_file_name`] = fileName;
next[`${entry.baseKey}_file_format`] = fileName
? getFileFormatFromName(fileName)
: '';
});
return next;
}
function getDropZoneMetaFields(scope = root) {
const out = {};
getDropZoneFieldEntries(scope).forEach((entry) => {
const fileName = entry.fileName || '';
out[`${entry.baseKey}_file_name`] = fileName;
out[`${entry.baseKey}_file_format`] = fileName
? getFileFormatFromName(fileName)
: '';
});
return out;
}
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);
return {
content,
filename,
source,
sizeBytes,
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;
}
}
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;
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);
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 {
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 clearViciLookupForBlock(block) {
if (!block) return;
const container = block.closest('.molecule-block') || block;
// Clear the ViciLookup widget state (does not close it)
const panel = container.querySelector('.vici-lookup');
if (panel) {
try {
window.ViciLookup?.clear?.(panel, { keepInput: false, keepTarget: false });
} catch (err) {
console.error(err);
}
}
// If you store any extra lookup text fields in this block, clear those too
container.querySelectorAll('.lookup-target-text').forEach((el) => {
el.value = '';
dzDispatchValueEvents(el);
});
}
function bindDockQDropZones(scope = root) {
const blocks = Array.from(scope.querySelectorAll('.dockq-file-card[data-dockq-role]'));
blocks.forEach((block) => {
if (block._dockqDropzoneBound) return;
block._dockqDropzoneBound = true;
const role = block.dataset.dockqRole === 'model' ? 'model' : 'ref';
const controller = createDropZoneController({
scope: block,
dropZone: '.drop-zone',
fileInput: '.lookup-drop-input',
metaEl: '.drop-zone__meta',
removeBtn: '.dockq-remove-file',
contentField: '.lookup-target-content',
filenameField: '.lookup-target-name',
sourceField: '.lookup-target-source',
emptyMetaText: 'Supports .pdb, .cif, .pdb.gz, .cif.gz',
beforeRead: ({ file }) => {
const kind = getDockQFileKind(file?.name);
if (!kind) {
showToast?.('error', 'Unsupported file type. Use .pdb, .cif, .pdb.gz, or .cif.gz.');
return false;
}
if (kind.ambiguous) {
showToast?.('error', 'Use .pdb.gz or .cif.gz so DockQ can map the gz file correctly.');
return false;
}
return true;
},
readFile: (file) => readDockQUploadFile(file),
renderFilledMeta: ({ state }) => {
const kind = getDockQFileKind(state.filename);
const extraBits = [];
if (state.source === 'vici_lookup') extraBits.push('Vici Lookup');
if (state.source === 'example') extraBits.push('Example');
if (kind?.isBinary) extraBits.push('base64');
return {
label: state.filename || `${getDockQRoleLabel(role)} loaded`,
sizeBytes: state.sizeBytes,
content: state.content,
includeLines: !kind?.isBinary,
includeChars: true,
extraBits
};
},
clearToastMessage: `${getDockQRoleLabel(role)} file removed.`,
// This is the new part:
onClear: ({ extra }) => {
// Only clear ViciLookup when the user pressed the remove button
if (extra?.fromRemoveButton) {
clearViciLookupForBlock(block);
}
}
});
if (!controller) return;
const fileInput = block.querySelector('.lookup-drop-input');
if (fileInput) {
fileInput.setAttribute('accept', '.pdb,.cif,.pdb.gz,.cif.gz,.gz');
}
controller.bind();
block._dockqDropzoneController = controller;
dropZoneControllers.push(controller);
const contentField = block.querySelector('.lookup-target-content');
const nameField = block.querySelector('.lookup-target-name');
const sourceField = block.querySelector('.lookup-target-source');
const syncLookupState = () => {
const text = String(contentField?.value || '');
if (!text.trim()) {
if (nameField) nameField.value = '';
if (sourceField) sourceField.value = '';
controller.refresh();
return;
}
if (sourceField && !sourceField.value) {
sourceField.value = 'vici_lookup';
}
if (nameField && !String(nameField.value || '').trim()) {
nameField.value = getDockQLookupFallbackName(role);
}
controller.refresh();
};
contentField?.addEventListener('input', syncLookupState);
contentField?.addEventListener('change', syncLookupState);
syncLookupState();
});
}
function setDockQChainMapOpen(isOpen, { immediate = false } = {}) {
const wrap = document.getElementById('dockq-chain-map-wrap');
const nativeEl = document.getElementById('dockq-native-chains');
const modelEl = document.getElementById('dockq-model-chains');
if (!wrap) return;
[nativeEl, modelEl].forEach((el) => {
if (el) el.disabled = !isOpen;
});
wrap.setAttribute('aria-hidden', isOpen ? 'false' : 'true');
if (isOpen) {
wrap.classList.add('is-open');
if (immediate) {
wrap.style.maxHeight = 'none';
wrap.style.opacity = '1';
return;
}
wrap.style.overflow = 'hidden';
wrap.style.maxHeight = '0px';
wrap.style.opacity = '0';
requestAnimationFrame(() => {
wrap.style.maxHeight = `${wrap.scrollHeight}px`;
wrap.style.opacity = '1';
});
const onEnd = (e) => {
if (e.target !== wrap || e.propertyName !== 'max-height') return;
wrap.style.maxHeight = 'none';
wrap.removeEventListener('transitionend', onEnd);
};
wrap.addEventListener('transitionend', onEnd);
return;
}
const startHeight = wrap.scrollHeight;
wrap.style.overflow = 'hidden';
if (immediate || !startHeight) {
wrap.classList.remove('is-open');
wrap.style.maxHeight = '0px';
wrap.style.opacity = '0';
return;
}
wrap.style.maxHeight = wrap.style.maxHeight === 'none'
? `${startHeight}px`
: (wrap.style.maxHeight || `${startHeight}px`);
wrap.style.opacity = '1';
wrap.offsetHeight;
wrap.classList.remove('is-open');
wrap.style.maxHeight = '0px';
wrap.style.opacity = '0';
}
function syncDockQChainMapUI({ immediate = false } = {}) {
const autoMap = !!document.getElementById('dockq-auto-map')?.checked;
setDockQChainMapOpen(!autoMap, { immediate });
}
function bindDockQChainMapToggle() {
const autoMapEl = document.getElementById('dockq-auto-map');
if (!autoMapEl || autoMapEl._dockqChainBound) return;
autoMapEl._dockqChainBound = true;
autoMapEl.addEventListener('change', () => {
syncDockQChainMapUI();
});
syncDockQChainMapUI({ immediate: true });
}
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 => !isFrontendOnlyField(el))
.filter(el => {
const type = (el.type || '').toLowerCase();
return type !== 'button' && type !== 'submit' && type !== 'reset';
});
const out = {};
els.forEach((el) => {
const key = getFieldKey(el);
if (!key) return;
const val = readFieldValue(el);
if (typeof val === 'undefined') return;
if (Array.isArray(val) && val.length === 0) 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"]'))
.filter(el => !isFrontendOnlyField(el));
els.forEach((el) => {
const key = getFieldKey(el);
if (!key) return;
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','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', '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) {
// use replace on init/internal changes, push on user tab clicks if you want browser back support
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,
forApi = false,
includeApiDropzoneContent = false
} = {}) {
const nameInput = document.getElementById('jobname');
const rawName = String(nameInput?.value || '').trim();
let runName = canonicalizeRunName(rawName || 'my_dockq_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 workflowName = runName || 'my_dockq_run';
const autoMap = !!document.getElementById('dockq-auto-map')?.checked;
const nativeChains = String(document.getElementById('dockq-native-chains')?.value || '').trim();
const modelChains = String(document.getElementById('dockq-model-chains')?.value || '').trim();
const smallMolecule = !!document.getElementById('dockq-small-molecule')?.checked;
if (!autoMap && !nativeChains) {
if (toast) showToast?.('error', 'Native chains is required when Auto map is off.');
return { error: 'Native chains is required.' };
}
if (!autoMap && !modelChains) {
if (toast) showToast?.('error', 'Model chains is required when Auto map is off.');
return { error: 'Model chains is required.' };
}
const refFile = getDockQFilePayload('ref', {
forApi,
includeApiDropzoneContent,
toast
});
if (refFile.error) return { error: refFile.error };
const modelFile = getDockQFilePayload('model', {
forApi,
includeApiDropzoneContent,
toast
});
if (modelFile.error) return { error: modelFile.error };
const dockq = {
name: workflowName,
auto_map: autoMap,
small_molecule: smallMolecule,
[refFile.key]: refFile.value,
ref_file_name: refFile.fileName,
ref_file_format: refFile.fileFormat,
[modelFile.key]: modelFile.value,
model_file_name: modelFile.fileName,
model_file_format: modelFile.fileFormat
};
if (forApi || !autoMap || nativeChains) {
dockq.native_chains = nativeChains;
}
if (forApi || !autoMap || modelChains) {
dockq.model_chains = modelChains;
}
const payload = {
workflow_name: workflowName,
dockq
};
return {
job: payload,
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')
};
}
function renderApiSnippet({ forceDefault = false, toast = false } = {}) {
if (!apiCodeEl) return;
if (isRenderingApiSnippet) return;
isRenderingApiSnippet = true;
try {
let payloadSource = null;
apiDynamicDefContent = {};
if (!forceDefault) {
const built = buildJob({
requireName: false,
validate: false,
toast: false,
forApi: true,
includeApiDropzoneContent: apiManualIncludeDropzoneContent
});
if (built && !built.error && built.payload) {
payloadSource = built.payload;
}
}
if (!payloadSource) {
payloadSource = deepClone(defaultApiJob || {
workflow_name: 'my_dockq_run',
dockq: {
name: 'my_dockq_run',
auto_map: true,
native_chains: '',
model_chains: '',
small_molecule: false,
ref_file: '',
ref_file_name: 'reference_structure.pdb',
ref_file_format: 'pdb',
model_file: '',
model_file_name: 'model_structure.pdb',
model_file_format: 'pdb'
}
});
}
payloadSource = stripExecutionContextForApi(payloadSource);
if (payloadSource && isPlainObject(payloadSource[modelKey])) {
let modelPayload = { ...payloadSource[modelKey] };
// Never allow Vici lookup drop-zone values into API preview
modelPayload = stripFrontendOnlyDropZoneFields(modelPayload, root);
// Always show the drop-zone content key in API,
// but keep it as a placeholder until Sync is clicked
modelPayload = applyApiDropZoneContentView(
modelPayload,
{ includeRealContent: apiManualIncludeDropzoneContent },
root
);
payloadSource = {
...payloadSource,
[modelKey]: modelPayload
};
}
const payloadBlock = stringifyPayloadWithMarkers(payloadSource);
apiDynamicDefContent = payloadBlock.defs || {};
const snippet = getApiTemplate(
currentApiLang,
payloadBlock.text,
payloadBlock.html
);
currentApiSnippet = snippet;
apiCodeEl.innerHTML = snippet.html;
if (toast) {
showToast?.(
'success',
forceDefault ? 'Reset API snippet to defaults.' : 'Synced API snippet from form.'
);
}
} finally {
isRenderingApiSnippet = false;
}
}
function 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() {
return window.MODEL_PRESETS || {};
}
async function applyPreset(key) {
if (key === 'example') {
await loadDockQExample();
return;
}
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','api'].includes(targetTab)) {
setTab(targetTab);
}
}
if (typeof adapter.afterPreset === 'function') {
adapter.afterPreset(key, preset, { root, modelSlug, modelKey });
}
updateActionVisibility();
renderApiSnippet();
showToast?.('success', `Loaded preset: ${key}.`);
} catch (err) {
console.error(err);
showToast?.('error', 'Could not apply preset.');
}
}
function hardCollapseViciLookup(panel, { immediate = false } = {}) {
if (!panel) return;
panel.dataset.viciCollapsed = '1';
panel.classList.remove('open');
panel.style.overflow = 'hidden';
const finish = () => {
panel.style.maxHeight = '0px';
panel.style.opacity = '0';
panel.style.display = 'none';
panel.style.transition = '';
};
if (immediate || getComputedStyle(panel).display === 'none') {
finish();
return;
}
const startHeight = panel.scrollHeight;
if (!startHeight) {
finish();
return;
}
panel.style.display = '';
panel.style.maxHeight = `${startHeight}px`;
panel.style.opacity = '1';
panel.offsetHeight;
panel.style.transition = 'max-height .24s ease, opacity .18s ease';
panel.style.maxHeight = '0px';
panel.style.opacity = '0';
const onEnd = (e) => {
if (e.target !== panel || e.propertyName !== 'max-height') return;
panel.removeEventListener('transitionend', onEnd);
finish();
};
panel.addEventListener('transitionend', onEnd);
setTimeout(() => {
panel.removeEventListener('transitionend', onEnd);
finish();
}, 280);
}
function resetAllViciLookups(scope = root) {
const panels = Array.from(scope.querySelectorAll('.molecule-block > .vici-lookup'));
panels.forEach((panel) => {
const block = panel.closest('.molecule-block');
const toggleBtn = block?.querySelector('.vici-toggle-btn');
if (toggleBtn) {
toggleBtn.classList.remove('active');
toggleBtn.setAttribute('aria-expanded', 'false');
}
const wasVisible =
getComputedStyle(panel).display !== 'none' &&
panel.scrollHeight > 0;
hardCollapseViciLookup(panel, { immediate: !wasVisible });
const clearDelay = wasVisible ? 240 : 0;
setTimeout(() => {
try {
window.ViciLookup?.clear?.(panel, { keepInput: false, keepTarget: false });
} catch (err) {
console.error(err);
}
}, clearDelay);
});
}
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);
}
// Hard clear every drop-zone controller, including hidden name/source fields
dropZoneControllers.forEach((controller) => {
try {
controller.clearValue({ fromReset: true });
} catch (err) {
console.error(err);
}
});
resetAllViciLookups(root);
// Hard clear lookup textareas
root.querySelectorAll('.lookup-target-text').forEach((el) => {
el.value = '';
dzDispatchValueEvents(el);
});
const jobnameEl = document.getElementById('jobname');
if (jobnameEl && jobnameEl.value) {
const safe = canonicalizeRunName(jobnameEl.value);
if (safe) jobnameEl.value = safe;
}
apiManualIncludeDropzoneContent = false;
syncDockQChainMapUI({ immediate: true });
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
};
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 content injection
apiManualIncludeDropzoneContent = false;
updateActionVisibility();
});
root.addEventListener('change', (e) => {
if (e.target.closest('[data-panel="api"]')) return;
// Any form change disarms manual content injection
apiManualIncludeDropzoneContent = false;
updateActionVisibility();
// Auto-sync normal fields to API preview, but with drop-zone content stripped
if (currentTab === 'api') renderApiSnippet();
});
const mo = new MutationObserver(() => {
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');
// Explicitly allow drop-zone content into the API preview
apiManualIncludeDropzoneContent = true;
renderApiSnippet({ toast: true });
});
copyBtn?.addEventListener('click', () => {
const text = currentApiSnippet?.text?.trim();
if (!text) {
showToast?.('error', 'Nothing to copy yet.');
return;
}
pulseBtn(copyBtn, 'pulse-green');
copyTextRobust(text)
.then(() => showToast?.('success', 'Copied API snippet.'))
.catch(() => showToast?.('error', 'Copy failed. Select code and copy manually.'));
});
resetApiBtn?.addEventListener('click', () => {
pulseBtn(resetApiBtn, 'pulse-red');
// Reset back to non-content mode
apiManualIncludeDropzoneContent = false;
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);
if (isPlainObject(defaultApiJob[modelKey])) {
let modelPayload = { ...defaultApiJob[modelKey] };
defaultApiJob = {
...defaultApiJob,
[modelKey]: modelPayload
};
}
return;
}
} catch (err) {
console.error(err);
}
}
const built = buildJob({
requireName: false,
validate: false,
toast: false
});
if (built && !built.error && built.payload) {
let next = stripExecutionContextForApi(deepClone(built.payload));
if (isPlainObject(next[modelKey])) {
let modelPayload = { ...next[modelKey] };
modelPayload = stripFrontendOnlyDropZoneFields(modelPayload, root);
modelPayload = applyApiDropZoneContentView(
modelPayload,
{ includeRealContent: false },
root
);
next = {
...next,
[modelKey]: modelPayload
};
}
defaultApiJob = next;
return;
}
defaultApiJob = {
workflow_name: 'my_dockq_run',
dockq: {
name: 'my_dockq_run',
auto_map: true,
native_chains: '',
model_chains: '',
small_molecule: false,
ref_file: '',
ref_file_name: 'reference_structure.pdb',
ref_file_format: 'pdb',
model_file: '',
model_file_name: 'model_structure.pdb',
model_file_format: 'pdb'
}
};
}
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();
bindDockQDropZones(root);
bindDockQChainMapToggle();
window.ViciLookup?.init?.(root);
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();
}
})();