(() => { '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 OPENMM_EXAMPLE_PROTEIN_URL = 'https://cdn.prod.website-files.com/685009238c2e9a16d785299f/69aa0271493a30a4a672a63b_3ptb.txt'; const OPENMM_DEFAULT_WATER_FFXML_BY_FAMILY = { amber19: 'amber19/opc.xml', amber14: 'amber14/tip3p.xml', charmm36: 'charmm36_2024/water.xml' }; let API_DEF_CONTENT = window.VICI_API_DEF_CONTENT || null; let apiDefPopoutEl = null; let apiDefAnchorEl = null; let apiDefHideTimer = null; let apiDynamicDefContent = {}; function openmmEl(id) { return document.getElementById(id); } function openmmNum(id, fallback, asInt = false) { const el = openmmEl(id); const raw = String(el?.value ?? '').trim(); if (!raw) return fallback; const n = asInt ? parseInt(raw, 10) : parseFloat(raw); return Number.isFinite(n) ? n : fallback; } function openmmStr(id, fallback = '') { const el = openmmEl(id); const v = String(el?.value ?? '').trim(); return v || fallback; } function openmmBool(id, fallback = false) { const v = String(openmmEl(id)?.value ?? '').trim().toLowerCase(); if (v === 'true') return true; if (v === 'false') return false; return fallback; } function openmmClamp(n, min, max) { if (!Number.isFinite(n)) return min; return Math.min(max, Math.max(min, n)); } function openmmRound(n, dp = 6) { if (!Number.isFinite(n)) return n; const p = Math.pow(10, dp); return Math.round(n * p) / p; } function openmmCsvToArray(s) { return String(s || '') .split(',') .map(x => x.trim()) .filter(Boolean); } function openmmHasProteinStructure() { return !!String(openmmEl('protein_structure_content')?.value || '').trim(); } function openmmHasAnyLigandFiles() { return Array.from(root.querySelectorAll('.openmm-ligand-item')).some((card) => { return !!String(card.querySelector('.openmm-ligand-file-content')?.value || '').trim(); }); } function openmmMaybeAutoEnableProteinLigand() { if (openmmHasProteinStructure() && openmmHasAnyLigandFiles()) { openmmSetSystemType('protein-ligand'); } } function openmmInferLigandFormat(filename, content) { const name = String(filename || '').toLowerCase(); if (name.endsWith('.mol2')) return 'mol2'; if (name.endsWith('.sdf')) return 'sdf'; if (name.endsWith('.mol')) return 'mol'; const txt = String(content || '').slice(0, 4000); if (/@molecule/i.test(txt)) return 'mol2'; if (/^\$\$\$\$/m.test(txt) || /^>\s*<[^>]+>/m.test(txt)) return 'sdf'; if (/^\s*\d+\s+\d+\s+.*V(2000|3000)\s*$/m.test(txt) || /^\s*M END\s*$/m.test(txt)) { return 'mol'; } return 'sdf'; } function openmmInferProteinFormat(filename, content) { const name = String(filename || '').toLowerCase(); if (name.endsWith('.cif') || name.endsWith('.mmcif')) return 'cif'; if (name.endsWith('.pdb') || name.endsWith('.ent')) return 'pdb'; const txt = String(content || '').slice(0, 500); if (/^data_/mi.test(txt) || /_atom_site\./i.test(txt)) return 'cif'; if (/^(HEADER|ATOM|HETATM|MODEL|REMARK)/mi.test(txt)) return 'pdb'; return 'pdb'; } function openmmFormatBytes(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 openmmInferFfFamily(proteinFfxml) { const s = String(proteinFfxml || '').toLowerCase(); if (s.startsWith('amber19')) return 'amber19'; if (s.startsWith('amber14')) return 'amber14'; if (s.startsWith('charmm36_2024')) return 'charmm36'; return ''; } function openmmIsAllowedFileForKind(kind, file) { if (!file) return false; const name = String(file.name || '').toLowerCase(); if (kind === 'protein') { return ( name.endsWith('.pdb') || name.endsWith('.cif') || name.endsWith('.ent') || name.endsWith('.mmcif') ); } if (kind === 'ligand') { return ( name.endsWith('.sdf') || name.endsWith('.mol') || name.endsWith('.mol2') ); } return false; } async function openmmFetchText(url, label = 'file') { const res = await fetch(url, { cache: 'no-store' }); if (!res.ok) throw new Error(`Failed to fetch ${label} (${res.status})`); return res.text(); } function openmmReadFileText(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 openmmDispatchValueEvents(el) { if (!el) return; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } function openmmHydrateHelpDots(scope) { const heads = Array.from(scope.querySelectorAll('.field-head[data-help-text]')); heads.forEach((head, idx) => { if (head.querySelector('.help-dot')) return; const helpText = String(head.getAttribute('data-help-text') || '').trim(); if (!helpText) return; const uid = `help-openmm-${idx}-${Math.random().toString(36).slice(2,7)}`; const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'help-trigger help-dot'; btn.setAttribute('aria-controls', uid); btn.setAttribute('aria-expanded', 'false'); btn.textContent = 'i'; const pop = document.createElement('div'); pop.id = uid; pop.className = 'help-popover'; pop.setAttribute('role', 'dialog'); pop.setAttribute('aria-hidden', 'true'); pop.innerHTML = `

${escapeHtml(helpText)}

`; head.append(btn, pop); }); } function openmmGetKindRefs(kind) { const isProtein = kind === 'protein'; return { kind, block: openmmEl(isProtein ? 'protein-structure-block' : 'ligand-structure-block'), drop: openmmEl(isProtein ? 'protein-drop-zone' : 'ligand-drop-zone'), meta: openmmEl(isProtein ? 'protein-drop-meta' : 'ligand-drop-meta'), input: openmmEl(isProtein ? 'protein_file_input' : 'ligand_file_input'), content: openmmEl(isProtein ? 'protein_structure_content' : 'ligand_file_content'), filename: openmmEl(isProtein ? 'protein_structure_filename' : 'ligand_file_filename'), source: openmmEl(isProtein ? 'protein_structure_source' : 'ligand_file_source'), removeBtn: openmmEl(isProtein ? 'protein-remove-btn' : 'ligand-remove-btn') }; } const OPENMM_CCD_EXCLUDE = new Set([ 'HOH', 'WAT', 'SOL', 'NA', 'K', 'CL', 'CA', 'MG', 'ZN', 'MN', 'FE', 'CU' ]); function openmmTokenizeCifRow(line) { return String(line).match(/'[^']*'|"[^"]*"|\S+/g) || []; } function openmmExtractLigandCcdsFromProtein(text, format) { const seen = new Set(); const add = (code) => { const c = String(code || '').trim().toUpperCase(); if (!c || OPENMM_CCD_EXCLUDE.has(c)) return; seen.add(c); }; if (format === 'pdb') { String(text || '').split(/\r?\n/).forEach((line) => { if (!line.startsWith('HETATM')) return; add(line.slice(17, 20)); }); return Array.from(seen); } const lines = String(text || '').split(/\r?\n/); for (let i = 0; i < lines.length; i++) { if (lines[i].trim() !== 'loop_') continue; const headers = []; let j = i + 1; while (j < lines.length && /^_atom_site\./.test(lines[j].trim())) { headers.push(lines[j].trim()); j++; } if (!headers.length) continue; const groupIdx = headers.indexOf('_atom_site.group_PDB'); const labelIdx = headers.indexOf('_atom_site.label_comp_id'); const authIdx = headers.indexOf('_atom_site.auth_comp_id'); if (groupIdx === -1 || (labelIdx === -1 && authIdx === -1)) continue; while (j < lines.length) { const raw = lines[j].trim(); if (!raw || raw === '#' || raw === 'loop_' || raw.startsWith('_')) break; const toks = openmmTokenizeCifRow(raw).map(t => t.replace(/^['"]|['"]$/g, '')); if (toks[groupIdx] === 'HETATM') { add(toks[labelIdx] || toks[authIdx]); } j++; } } return Array.from(seen); } function openmmSetLigandCcdInputs(values = []) { const input = openmmEl('ligand_ccds_csv_inline'); if (!input) return; input.value = Array.isArray(values) ? values.map(v => String(v || '').trim().toUpperCase()).filter(Boolean).join(', ') : ''; openmmDispatchValueEvents(input); openmmSyncSystemTypeUI(); } function openmmSetStructure(kind, text, meta = {}) { const refs = openmmGetKindRefs(kind); if (!refs.content) return; refs.content.value = String(text || ''); if (refs.filename && meta.name != null) refs.filename.value = String(meta.name || ''); if (refs.source && meta.source != null) refs.source.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) refs.input.value = ''; openmmDispatchValueEvents(refs.content); if (refs.filename) openmmDispatchValueEvents(refs.filename); if (refs.source) openmmDispatchValueEvents(refs.source); if (kind === 'protein') { const proteinFormat = openmmInferProteinFormat( String(meta.name || openmmEl('protein_structure_filename')?.value || ''), String(text || '') ); const detectedCcds = openmmExtractLigandCcdsFromProtein(String(text || ''), proteinFormat); if (detectedCcds.length) { openmmSetSystemType('protein-ligand'); openmmSetLigandCcdInputs(detectedCcds); } else { openmmSetLigandCcdInputs([]); openmmMaybeAutoEnableProteinLigand(); } } openmmRefreshStructurePreview(kind); } function openmmClearStructure(kind, opts = {}) { const refs = openmmGetKindRefs(kind); if (refs.content) refs.content.value = ''; if (refs.filename) refs.filename.value = ''; if (refs.source) refs.source.value = ''; if (refs.input) refs.input.value = ''; if (refs.drop) delete refs.drop.dataset.fileSizeBytes; if (kind === 'ligand' && opts.clearCharge) { const chargeEl = openmmEl('ligand_charge'); if (chargeEl) { chargeEl.value = ''; openmmDispatchValueEvents(chargeEl); } } if (kind === 'protein') { openmmSetLigandCcdInputs([]); } if (refs.content) openmmDispatchValueEvents(refs.content); if (refs.filename) openmmDispatchValueEvents(refs.filename); if (refs.source) openmmDispatchValueEvents(refs.source); openmmRefreshStructurePreview(kind); } function openmmRefreshStructurePreview(kind) { const refs = openmmGetKindRefs(kind); if (!refs.drop || !refs.meta || !refs.content) return; const content = String(refs.content.value || ''); const filename = String(refs.filename?.value || '').trim(); const source = String(refs.source?.value || '').trim(); const hasContent = content.trim().length > 0; refs.drop.classList.toggle('is-filled', hasContent); if (!hasContent) { refs.meta.textContent = kind === 'protein' ? 'Accepted: .pdb, .cif' : 'Accepted: .sdf, .mol, .mol2'; return; } const lines = content.split(/\r?\n/).length; const chars = content.length; const sizeLabel = openmmFormatBytes(Number(refs.drop.dataset.fileSizeBytes || 0)); let label = filename || (source === 'vici-lookup' ? 'Loaded from Vici Lookup' : 'Loaded content'); const bits = [label]; if (kind === 'protein') { const fmt = openmmInferProteinFormat(filename, content).toUpperCase(); bits.push(fmt); } if (sizeLabel) bits.push(sizeLabel); bits.push(`${lines} lines`); bits.push(`${chars.toLocaleString()} chars`); refs.meta.innerHTML = bits .map((v, i) => i === 0 ? escapeHtml(v) : ` ${escapeHtml(v)}`) .join(' '); } function openmmBindDropzone(kind) { const refs = openmmGetKindRefs(kind); if (!refs.drop || !refs.input) return; const readAndSet = async (file) => { if (!file) return; if (!openmmIsAllowedFileForKind(kind, file)) { showToast?.( 'error', kind === 'protein' ? 'Protein upload only accepts .pdb, .cif, .ent, or .mmcif files.' : 'Ligand upload only accepts .sdf, .mol, or .mol2 files.' ); if (refs.input) refs.input.value = ''; return; } try { const text = await openmmReadFileText(file); openmmSetStructure(kind, text, { name: file.name, source: 'upload', sizeBytes: file.size }); } catch (err) { console.error(err); showToast?.('error', err?.message || 'Failed to read file.'); } }; 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) => { const hasFiles = Array.from(e.dataTransfer?.types || []).includes('Files'); if (!hasFiles) return; e.preventDefault(); refs.drop.classList.add('dragover'); }); refs.drop.addEventListener('dragleave', () => { refs.drop.classList.remove('dragover'); }); refs.drop.addEventListener('drop', (e) => { const files = Array.from(e.dataTransfer?.files || []); if (!files.length) return; e.preventDefault(); refs.drop.classList.remove('dragover'); readAndSet(files[0]); }); refs.input.addEventListener('change', () => { const file = refs.input.files?.[0]; if (file) readAndSet(file); }); refs.removeBtn?.addEventListener('click', () => { openmmClearStructure(kind, { clearCharge: kind === 'ligand' }); if (kind === 'protein') { const vici = openmmEl('protein-vici-lookup'); if (window.ViciLookup?.clear && vici) { try { window.ViciLookup.clear(vici); } catch {} } } showToast?.('success', `${kind === 'protein' ? 'Protein' : 'Ligand'} cleared.`); }); } function openmmSyncSystemTypeUI() { const type = openmmStr('system_type', 'protein'); const ligandWrap = openmmEl('ligand-block-wrap'); const ccdWrap = openmmEl('ligand-ccd-wrap'); const slider = openmmEl('system_type_slider'); const isLigand = type === 'protein-ligand'; if (slider) slider.checked = isLigand; root.querySelectorAll('.openmm-system-chip[data-system-type]').forEach((btn) => { const active = btn.dataset.systemType === type; btn.classList.toggle('is-active', active); btn.setAttribute('aria-pressed', active ? 'true' : 'false'); }); if (ligandWrap) { ligandWrap.classList.toggle('is-open', isLigand); ligandWrap.setAttribute('aria-hidden', isLigand ? 'false' : 'true'); } if (ccdWrap) { ccdWrap.classList.toggle('is-open', isLigand); ccdWrap.setAttribute('aria-hidden', isLigand ? 'false' : 'true'); } } function openmmSetSystemType(type) { const normalized = (type === 'protein-ligand') ? 'protein-ligand' : 'protein'; const hidden = openmmEl('system_type'); if (!hidden) return; hidden.value = normalized; openmmDispatchValueEvents(hidden); openmmSyncSystemTypeUI(); } function openmmBindSystemToggle() { const slider = openmmEl('system_type_slider'); slider?.addEventListener('change', () => { openmmSetSystemType(slider.checked ? 'protein-ligand' : 'protein'); }); root.querySelectorAll('.openmm-system-chip[data-system-type]').forEach(btn => { btn.addEventListener('click', () => { openmmSetSystemType(btn.dataset.systemType || 'protein'); }); }); openmmEl('system_type')?.addEventListener('change', openmmSyncSystemTypeUI); openmmSyncSystemTypeUI(); } function openmmGetEyeSvgMarkup() { return ( window.EYE_SVG || window.VICI_EYE_SVG || `` ); } function openmmBindEyeButtonAndLookup() { const eyeBtn = openmmEl('protein-vici-toggle'); if (eyeBtn) { const applyEye = () => { eyeBtn.innerHTML = openmmGetEyeSvgMarkup(); eyeBtn.classList.remove('btn__text'); }; applyEye(); // If the shared icon script loads a little later, retry a few times if (!window.EYE_SVG && !window.VICI_EYE_SVG) { let tries = 0; const t = setInterval(() => { tries += 1; if (window.EYE_SVG || window.VICI_EYE_SVG || tries > 20) { clearInterval(t); applyEye(); } }, 150); } eyeBtn.addEventListener('click', () => { if (typeof window.toggleViciLookup === 'function') { window.toggleViciLookup(eyeBtn); } else { showToast?.('error', 'Vici Lookup is not ready yet.'); } }); } const viciBlock = openmmEl('protein-vici-lookup'); if (!viciBlock) return; let tries = 0; const attach = () => { if (window.ViciLookup?.attach) { try { window.ViciLookup.attach(viciBlock); window.ViciLookup.setMode?.(viciBlock, 'pdb'); } catch (e) { console.error(e); } return; } tries += 1; if (tries < 30) setTimeout(attach, 250); }; attach(); } function openmmBindHiddenSync() { ['protein', 'ligand'].forEach((kind) => { const refs = openmmGetKindRefs(kind); refs.content?.addEventListener('input', () => openmmRefreshStructurePreview(kind)); refs.filename?.addEventListener('input', () => openmmRefreshStructurePreview(kind)); refs.source?.addEventListener('input', () => openmmRefreshStructurePreview(kind)); openmmRefreshStructurePreview(kind); }); window.addEventListener('vici:file-selected', (e) => { const detail = e.detail || {}; const block = detail.block; if (!block) return; const moleculeBlock = block.closest('.molecule-block'); if (!moleculeBlock) return; if (moleculeBlock.id === 'protein-structure-block') { openmmSetStructure('protein', String(detail.fileContent || ''), { name: detail.name || 'lookup_structure.txt', source: 'vici-lookup', sizeBytes: NaN }); } }); // If Vici lookup writes directly to hidden textarea via target, keep source label helpful openmmEl('protein_structure_content')?.addEventListener('change', () => { const src = openmmEl('protein_structure_source'); const name = openmmEl('protein_structure_filename'); const content = openmmEl('protein_structure_content')?.value || ''; if (content.trim() && src && !src.value) src.value = 'vici-lookup'; if (content.trim() && name && !name.value) name.value = 'lookup_structure'; openmmRefreshStructurePreview('protein'); }); } function openmmBindForceFieldDefaultWaterSync() { const proteinFfEl = openmmEl('protein_ffxml'); const waterEl = openmmEl('water_ffxml'); if (!proteinFfEl || !waterEl) return; proteinFfEl.addEventListener('change', () => { const family = openmmInferFfFamily(proteinFfEl.value); const preferred = OPENMM_DEFAULT_WATER_FFXML_BY_FAMILY[family]; if (!preferred) return; const current = String(waterEl.value || ''); const currentIsAmber19 = current.startsWith('amber19/'); const currentIsAmber14 = current.startsWith('amber14/'); const currentIsCharmm = current.startsWith('charmm36_2024/'); const mismatch = (family === 'amber19' && (currentIsAmber14 || currentIsCharmm)) || (family === 'amber14' && (currentIsAmber19 || currentIsCharmm)) || (family === 'charmm36' && (currentIsAmber19 || currentIsAmber14)); if (mismatch) { waterEl.value = preferred; openmmDispatchValueEvents(waterEl); } }); } function openmmGetWaterFfxml(proteinFfxml, waterChosen) { const chosen = String(waterChosen || '').trim(); if (chosen) return chosen; const family = openmmInferFfFamily(proteinFfxml); return OPENMM_DEFAULT_WATER_FFXML_BY_FAMILY[family] || 'amber19/opc.xml'; } function openmmBuildPayload(opts = {}) { const requireName = opts.requireName !== false; const validate = opts.validate !== false; const previewMode = typeof opts.previewMode === 'boolean' ? opts.previewMode : !validate; const nameInput = openmmEl('jobname'); const rawName = String(nameInput?.value || '').trim(); let runName = canonicalizeRunName(rawName || `my_${modelKey}_run`); if (nameInput && rawName && runName !== rawName) nameInput.value = runName; if (requireName && !runName) { if (opts.toast) showToast?.('error', 'Name is required.'); return { error: 'Name is required.' }; } if (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.' }; } const systemType = openmmStr('system_type', 'protein') === 'protein-ligand' ? 'protein-ligand' : 'protein'; const proteinContentRaw = String(openmmEl('protein_structure_content')?.value || ''); if (validate && !proteinContentRaw.trim()) { if (opts.toast) showToast?.('error', 'Protein structure is required.'); return { error: 'Protein structure is required.' }; } const protein_ffxml = openmmStr('protein_ffxml', 'amber19/protein.ff19SB.xml'); const water_ffxml = openmmGetWaterFfxml(protein_ffxml, openmmStr('water_ffxml', '')); const timestep_fs_equi = openmmNum('timestep_fs_equi', 2); const timestep_fs_prod = openmmNum('timestep_fs_prod', 2); const steps_equi = openmmNum( 'steps_equi', Math.max(1, Math.round((openmmNum('equilibration_time_ns', 0.2) * 1e6) / timestep_fs_equi)), true ); const steps_prod = openmmNum( 'steps_prod', Math.max(1, Math.round((openmmNum('production_time_ns', 2) * 1e6) / timestep_fs_prod)), true ); const equilibration_time_ns = openmmNum('equilibration_time_ns', 0.2); const production_time_ns = openmmNum('production_time_ns', 2); const box_size_angstrom = openmmNum('box_size_angstrom', 12); const ion_concentration_m = openmmNum('ion_concentration_m', 0.15); const proteinFileName = openmmStr('protein_structure_filename', ''); const proteinContent = previewMode ? '' : proteinContentRaw; const proteinFileFormat = openmmInferProteinFormat( proteinFileName, proteinContentRaw || proteinContent ); const ligandCcds = openmmCsvToArray(openmmStr('ligand_ccds_csv_inline', '')) .map(v => v.toUpperCase()); const ligandFiles = systemType === 'protein-ligand' ? openmmCollectLigandFiles({ previewMode }) : []; if (validate && systemType === 'protein-ligand' && !ligandFiles.length && !ligandCcds.length) { if (opts.toast) showToast?.('error', 'Provide at least one ligand file or ligand CCD code for protein-ligand mode.'); return { error: 'Ligand input is required.' }; } const inner = { user_id: previewMode ? '' : '', name: runName || `my_${modelKey}_run`, file: proteinContent, ph: openmmNum('ph', 7.0), protein_ffxml, water_ffxml, system_type: systemType, max_minimization_steps: openmmNum('max_minimization_steps', 10000, true), temperature_kelvin_equi: openmmNum('temperature_kelvin_equi', 298), temperature_kelvin_prod: openmmNum('temperature_kelvin_prod', 298), pressure_bar_equi: openmmNum('pressure_bar_equi', 1), pressure_bar_prod: openmmNum('pressure_bar_prod', 1), timestep_fs_equi, timestep_fs_prod, friction_ps_equi: openmmNum('friction_ps_equi', 1.0), friction_ps_prod: openmmNum('friction_ps_prod', 1.0), steps_equi, steps_prod, record_freq_equi: openmmNum('record_freq_equi', 1000, true), record_freq_prod: openmmNum('record_freq_prod', 1000, true), equilibration_time_ns, production_time_ns, box_size_angstrom, ion_concentration_m, starting_restraint_force_constant: openmmNum('starting_restraint_force_constant', 1), remove_waters: openmmBool('remove_waters', true), rmsd_2d_step_size: openmmNum('rmsd_2d_step_size', 5, true), protein_filename: proteinFileName || (previewMode ? 'protein_structure.pdb' : ''), protein_file_format: proteinFileFormat, protein_structure_source: openmmStr('protein_structure_source', ''), ligand_ccds: ligandCcds, ligand_files: ligandFiles, }; return { job: inner, payload: { workflow_name: inner.name, [modelKey]: inner } }; } function openmmInstallInlineAdvancedSection() { const commonPanel = root?.querySelector?.('[data-panel="common"]'); const advancedGrid = root?.querySelector?.('.openmm-advanced-grid'); const systemTypeCard = openmmEl('openmm-system-type-card'); if (!commonPanel || !advancedGrid || !systemTypeCard) return; let wrap = openmmEl('openmm-inline-advanced-wrap'); if (!wrap) { wrap = document.createElement('div'); wrap.id = 'openmm-inline-advanced-wrap'; wrap.className = 'openmm-advanced-inline-wrap'; wrap.setAttribute('aria-hidden', 'true'); } if (!wrap.contains(advancedGrid)) { wrap.appendChild(advancedGrid); } if (wrap.parentNode !== commonPanel) { commonPanel.insertBefore(wrap, systemTypeCard); } openmmSyncInlineAdvancedSection(); } function openmmSyncInlineAdvancedSection() { const wrap = openmmEl('openmm-inline-advanced-wrap'); if (!wrap) return; const isOpen = currentTab === 'advanced'; wrap.classList.toggle('is-open', isOpen); wrap.setAttribute('aria-hidden', isOpen ? 'false' : 'true'); } async function openmmLoadExample(which, ctx = {}) { const applyBaseDefaults = () => { writeFieldValue(openmmEl('max_minimization_steps'), 5000); writeFieldValue(openmmEl('equilibration_time_ns'), 0.02); writeFieldValue(openmmEl('production_time_ns'), 0.2); writeFieldValue(openmmEl('protein_ffxml'), 'amber19/protein.ff19SB.xml'); writeFieldValue(openmmEl('water_ffxml'), 'amber19/opc.xml'); writeFieldValue(openmmEl('box_size_angstrom'), 12); writeFieldValue(openmmEl('ion_concentration_m'), 0.15); writeFieldValue(openmmEl('starting_restraint_force_constant'), 1); writeFieldValue(openmmEl('remove_waters'), 'true'); writeFieldValue(openmmEl('rmsd_2d_step_size'), 5); writeFieldValue(openmmEl('ph'), 7.0); openmmSetLigandCcdInputs([]); writeFieldValue(openmmEl('timestep_fs_equi'), 2); writeFieldValue(openmmEl('timestep_fs_prod'), 2); writeFieldValue(openmmEl('temperature_kelvin_equi'), 298); writeFieldValue(openmmEl('temperature_kelvin_prod'), 298); writeFieldValue(openmmEl('pressure_bar_equi'), 1); writeFieldValue(openmmEl('pressure_bar_prod'), 1); writeFieldValue(openmmEl('friction_ps_equi'), 1.0); writeFieldValue(openmmEl('friction_ps_prod'), 1.0); writeFieldValue(openmmEl('steps_equi'), 2000); writeFieldValue(openmmEl('steps_prod'), 20000); writeFieldValue(openmmEl('record_freq_equi'), 200); writeFieldValue(openmmEl('record_freq_prod'), 1000); }; applyBaseDefaults(); openmmResetLigands(); const proteinText = await openmmFetchText(OPENMM_EXAMPLE_PROTEIN_URL, '3PTB protein'); openmmSetStructure('protein', proteinText, { name: '3PTB.pdb', source: 'example' }); const presetName = String(which || '').toUpperCase() === '3PTB' ? '3ptb_openmm' : `my_${modelKey}_run`; writeFieldValue(openmmEl('jobname'), presetName); openmmSyncSystemTypeUI(); openmmRefreshStructurePreview('protein'); window.ModelPage?.renderApiSnippet?.(); window.ModelPage?.updateActionVisibility?.(); } function openmmInstallPresets() { const existing = window.MODEL_PRESETS || {}; window.MODEL_PRESETS = { ...existing, // Map the button’s data-example="3PTB" to this preset '3PTB': (ctx) => { openmmLoadExample('3PTB', ctx).catch((e) => { console.error(e); showToast?.('error', e?.message || 'Failed to load 3PTB preset.'); }); } }; } function openmmMoveAdvancedGridAboveSystemToggle() { const commonPanel = root.querySelector('[data-panel="common"]'); const advancedPanel = root.querySelector('[data-panel="advanced"]'); const advancedGrid = advancedPanel?.querySelector('.openmm-advanced-grid'); const systemTypeCard = openmmEl('openmm-system-type-card'); if (!commonPanel || !advancedGrid || !systemTypeCard) return; if (advancedGrid.dataset.movedToCommon === '1') return; commonPanel.insertBefore(advancedGrid, systemTypeCard); advancedGrid.dataset.movedToCommon = '1'; } function openmmEnsureAdvancedGridInAdvancedPanel() { const advancedPanel = root?.querySelector?.('[data-panel="advanced"]'); if (!advancedPanel) return; const grids = Array.from(root.querySelectorAll('.openmm-advanced-grid')); if (!grids.length) return; // If duplicate grids exist for any reason, keep the first and remove extras const primary = grids[0]; grids.slice(1).forEach(g => { try { g.remove(); } catch {} }); if (!primary.closest('[data-panel="advanced"]')) { const modelSlot = advancedPanel.querySelector('.model-slot'); advancedPanel.insertBefore(primary, modelSlot || advancedPanel.firstChild); } } function openmmBeautifyForcefieldSelectLabels() { ['protein_ffxml', 'water_ffxml'].forEach((id) => { const sel = openmmEl(id); if (!sel) return; Array.from(sel.querySelectorAll('option')).forEach((opt) => { const original = opt.dataset.originalLabel || opt.textContent || ''; if (!opt.dataset.originalLabel) opt.dataset.originalLabel = original; // UI label only, keep option.value unchanged for backend opt.textContent = String(original).replace(/\.xml$/i, ''); }); }); } function openmmRefreshLigandCardPreview(card) { if (!card) return; const drop = card.querySelector('.openmm-ligand-drop-zone'); const meta = card.querySelector('.openmm-drop-zone__meta'); const contentEl = card.querySelector('.openmm-ligand-file-content'); const nameEl = card.querySelector('.openmm-ligand-file-name'); const sourceEl = card.querySelector('.openmm-ligand-file-source'); if (!drop || !meta || !contentEl) return; const content = String(contentEl.value || ''); const filename = String(nameEl?.value || '').trim(); const source = String(sourceEl?.value || '').trim(); const hasContent = content.trim().length > 0; drop.classList.toggle('is-filled', hasContent); if (!hasContent) { meta.textContent = 'Accepted: .sdf, .mol, .mol2'; return; } const lines = content.split(/\r?\n/).length; const chars = content.length; const sizeLabel = openmmFormatBytes(Number(drop.dataset.fileSizeBytes || 0)); const fmt = openmmInferLigandFormat(filename, content).toUpperCase(); const label = filename || (source === 'example' ? 'Loaded example ligand' : 'Loaded ligand'); const bits = [label, fmt]; if (sizeLabel) bits.push(sizeLabel); bits.push(`${lines} lines`); bits.push(`${chars.toLocaleString()} chars`); meta.innerHTML = bits .map((v, i) => i === 0 ? escapeHtml(v) : ` ${escapeHtml(v)}`) .join(' '); } function openmmClearLigandCard(card, opts = {}) { if (!card) return; const drop = card.querySelector('.openmm-ligand-drop-zone'); const input = card.querySelector('.openmm-ligand-file-input'); const contentEl = card.querySelector('.openmm-ligand-file-content'); const nameEl = card.querySelector('.openmm-ligand-file-name'); const sourceEl = card.querySelector('.openmm-ligand-file-source'); const chargeEl = card.querySelector('.openmm-ligand-charge'); if (contentEl) contentEl.value = ''; if (nameEl) nameEl.value = ''; if (sourceEl) sourceEl.value = ''; if (input) input.value = ''; if (drop) delete drop.dataset.fileSizeBytes; if (opts.clearCharge !== false && chargeEl) { chargeEl.value = ''; openmmDispatchValueEvents(chargeEl); } if (contentEl) openmmDispatchValueEvents(contentEl); if (nameEl) openmmDispatchValueEvents(nameEl); if (sourceEl) openmmDispatchValueEvents(sourceEl); openmmRefreshLigandCardPreview(card); } function openmmBindLigandCard(card) { if (!card) return; const list = openmmEl('ligand-list'); const wrap = card.closest('.openmm-ligand-item-wrap') || card; const drop = card.querySelector('.openmm-ligand-drop-zone'); const input = card.querySelector('.openmm-ligand-file-input'); const contentEl = card.querySelector('.openmm-ligand-file-content'); const nameEl = card.querySelector('.openmm-ligand-file-name'); const sourceEl = card.querySelector('.openmm-ligand-file-source'); const addBtn = card.querySelector('[data-ligand-add]'); const removeBtn = card.querySelector('[data-ligand-remove]'); if (!drop || !input || !contentEl || !nameEl || !sourceEl) return; const readAndSet = async (file) => { if (!file) return; if (!openmmIsAllowedFileForKind('ligand', file)) { showToast?.('error', 'Ligand upload only accepts .sdf, .mol, or .mol2 files.'); if (input) input.value = ''; return; } try { const text = await openmmReadFileText(file); contentEl.value = String(text || ''); nameEl.value = String(file.name || ''); sourceEl.value = 'upload'; drop.dataset.fileSizeBytes = String(Number(file.size || 0)); input.value = ''; openmmDispatchValueEvents(contentEl); openmmDispatchValueEvents(nameEl); openmmDispatchValueEvents(sourceEl); openmmRefreshLigandCardPreview(card); openmmMaybeAutoEnableProteinLigand(); } catch (err) { console.error(err); showToast?.('error', err?.message || 'Failed to read ligand file.'); } }; drop.addEventListener('click', () => input.click()); drop.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); input.click(); } }); drop.addEventListener('dragover', (e) => { const hasFiles = Array.from(e.dataTransfer?.types || []).includes('Files'); if (!hasFiles) return; e.preventDefault(); drop.classList.add('dragover'); }); drop.addEventListener('dragleave', () => { drop.classList.remove('dragover'); }); drop.addEventListener('drop', (e) => { const files = Array.from(e.dataTransfer?.files || []); if (!files.length) return; e.preventDefault(); drop.classList.remove('dragover'); readAndSet(files[0]); }); input.addEventListener('change', () => { const file = input.files?.[0]; if (file) readAndSet(file); }); addBtn?.addEventListener('click', () => { openmmAddLigandCard(); }); removeBtn?.addEventListener('click', () => { const cards = Array.from(root.querySelectorAll('.openmm-ligand-item')); if (cards.length <= 1) { openmmClearLigandCard(card, { clearCharge: true }); showToast?.('success', 'Ligand cleared.'); return; } wrap.remove(); showToast?.('success', 'Ligand removed.'); window.ModelPage?.updateActionVisibility?.(); if (currentTab === 'api') window.ModelPage?.renderApiSnippet?.(); }); contentEl.addEventListener('input', () => openmmRefreshLigandCardPreview(card)); nameEl.addEventListener('input', () => openmmRefreshLigandCardPreview(card)); sourceEl.addEventListener('input', () => openmmRefreshLigandCardPreview(card)); openmmRefreshLigandCardPreview(card); } function openmmAddLigandCard(opts = {}) { const list = openmmEl('ligand-list'); const tpl = openmmEl('openmm-ligand-template'); if (!list || !tpl || !tpl.content?.firstElementChild) return null; const wrap = tpl.content.firstElementChild.cloneNode(true); const card = wrap.querySelector('.openmm-ligand-item') || wrap; list.appendChild(wrap); openmmHydrateHelpDots(wrap); openmmBindLigandCard(card); openmmRefreshLigandCardPreview(card); if (opts.open !== false) { requestAnimationFrame(() => { wrap.classList.add('is-open'); }); } else { wrap.classList.add('is-open'); } window.ModelPage?.updateActionVisibility?.(); return card; } function openmmResetLigands(count = 1) { const list = openmmEl('ligand-list'); if (!list) return; list.innerHTML = ''; const total = Math.max(1, parseInt(count, 10) || 1); for (let i = 0; i < total; i++) { openmmAddLigandCard({ open: false }); } if (currentTab === 'api') window.ModelPage?.renderApiSnippet?.(); } function openmmCollectLigandFiles({ previewMode = false } = {}) { return Array.from(root.querySelectorAll('.openmm-ligand-item')).map((card, idx) => { const raw = String(card.querySelector('.openmm-ligand-file-content')?.value || ''); const filename = String(card.querySelector('.openmm-ligand-file-name')?.value || ''); const chargeText = String(card.querySelector('.openmm-ligand-charge')?.value ?? '').trim(); const file = previewMode ? `` : raw; let charge = 'auto'; if (chargeText !== '' && chargeText.toLowerCase() !== 'auto') { const parsed = parseInt(chargeText, 10); if (Number.isFinite(parsed)) { charge = openmmClamp(parsed, -5, 5); } } return { file, file_format: openmmInferLigandFormat(filename, raw || file), charge }; }).filter(item => String(item.file || '').trim()); } function openmmInitUi() { if (!root || root.dataset.model !== 'openmm') return; openmmHydrateHelpDots(root); openmmBeautifyForcefieldSelectLabels(); setTimeout(openmmBeautifyForcefieldSelectLabels, 0); requestAnimationFrame(openmmBeautifyForcefieldSelectLabels); openmmInstallInlineAdvancedSection(); openmmBindSystemToggle(); openmmBindEyeButtonAndLookup(); openmmBindDropzone('protein'); openmmResetLigands(); openmmBindHiddenSync(); openmmBindForceFieldDefaultWaterSync(); // Ensure defaults exist even if HTML gets reused if (!openmmStr('system_type')) writeFieldValue(openmmEl('system_type'), 'protein'); if (!openmmStr('protein_ffxml')) writeFieldValue(openmmEl('protein_ffxml'), 'amber19/protein.ff19SB.xml'); if (!openmmStr('water_ffxml')) writeFieldValue(openmmEl('water_ffxml'), 'amber19/opc.xml'); if (!openmmStr('starting_restraint_force_constant')) writeFieldValue(openmmEl('starting_restraint_force_constant'), 1); if (!openmmStr('remove_waters')) writeFieldValue(openmmEl('remove_waters'), 'true'); openmmSyncSystemTypeUI(); openmmRefreshStructurePreview('protein'); openmmInstallPresets(); // Better API help text const apiHelp = document.getElementById('help-api'); if (apiHelp) { const body = apiHelp.querySelector('.help-popover__body') || apiHelp.querySelector('p') || apiHelp; if (body) { body.innerHTML = 'The API snippet mirrors the OpenMM payload shape. Required defaults are always included so backend execution does not fail when optional UI fields are untouched.'; } } } adapter.onInit = function () { if (!root || root.dataset.model !== 'openmm') return; openmmInitUi(); }; adapter.afterApplyState = function () { if (!root || root.dataset.model !== 'openmm') return; openmmInstallInlineAdvancedSection(); openmmBeautifyForcefieldSelectLabels(); openmmSyncSystemTypeUI(); openmmRefreshStructurePreview('protein'); }; adapter.afterPreset = function () { if (!root || root.dataset.model !== 'openmm') return; openmmInstallInlineAdvancedSection(); openmmBeautifyForcefieldSelectLabels(); openmmSyncSystemTypeUI(); openmmRefreshStructurePreview('protein'); }; adapter.getDefaultApiPayload = function () { if (!root || root.dataset.model !== 'openmm') return null; const built = openmmBuildPayload({ requireName: false, validate: false, toast: false }); return built?.payload || null; }; adapter.buildJob = function (opts = {}) { if (!root || root.dataset.model !== 'openmm') { return buildGenericJob(opts); } return openmmBuildPayload(opts); }; 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; 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 buildBaseApiDefContent() { return { 'token-id': { title: 'Token-ID', html: ` Your Vici Token ID. Send it as the Token-ID header. Generate it in your . ` }, '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 . ` }, '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.` } }; } 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', 'protein_structure_source', 'ligand_file_source' ]); 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) { if (!el) return; 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 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(); openmmSyncInlineAdvancedSection(); 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 jsonValueToHighlightedHtml(value, markersByToken, indent = 0) { const sp = ' '.repeat(indent); if (Array.isArray(value)) { if (!value.length) return '[]'; const inner = value.map(v => `${' '.repeat(indent + 1)}${jsonValueToHighlightedHtml(v, markersByToken, indent + 1)}`).join(',\n'); return `[\n${inner}\n${sp}]`; } if (value && typeof value === 'object') { const keys = Object.keys(value); if (!keys.length) return '{}'; const rows = keys.map((k) => { const keyHtml = `"${escapeHtml(k)}"`; const valHtml = jsonValueToHighlightedHtml(value[k], markersByToken, indent + 1); return `${' '.repeat(indent + 1)}${keyHtml}: ${valHtml}`; }).join(',\n'); return `{\n${rows}\n${sp}}`; } if (typeof value === 'string') { const marker = markersByToken.get(value); if (marker) { if (marker.kind === 'number') { return `${escapeHtml(String(marker.value))}`; } return `"${escapeHtml(String(marker.value))}"`; } return `"${escapeHtml(value)}"`; } if (typeof value === 'number') { return `${escapeHtml(String(value))}`; } if (typeof value === 'boolean') { return `${value ? 'true' : 'false'}`; } return `null`; } function stringifyPayloadWithMarkers(payloadObj) { const markers = []; const dynamicDefs = {}; const mark = (value, kind = 'string', defRef = '') => { const token = `__MARK_${markers.length}__`; markers.push({ token, value, kind, defRef }); return token; }; const payload = deepClone(payloadObj); function walk(node, pathParts = []) { if (Array.isArray(node)) { for (let i = 0; i < node.length; i++) { const v = node[i]; const childPath = [...pathParts, `[${i}]`]; if (v && typeof v === 'object') { walk(v, childPath); continue; } const pathStr = childPath.join('.'); const defRef = toDefRefSafe(`payload:${pathStr}`); dynamicDefs[defRef] = buildGenericPayloadDef(pathStr, v); let kind = 'string'; if (typeof v === 'number') kind = 'number'; else if (typeof v === 'boolean') kind = 'boolean'; else if (v === null) kind = 'null'; node[i] = mark(v, kind, defRef); } return; } if (!node || typeof node !== 'object') return; Object.keys(node).forEach((key) => { const v = node[key]; const childPath = [...pathParts, key]; // recurse into objects/arrays if (v && typeof v === 'object') { walk(v, childPath); return; } const pathStr = childPath.join('.'); const isWorkflowName = pathStr === 'workflow_name'; const isInnerModelName = pathStr === `${modelKey}.name`; let defRef = 'workflow-name'; if (!isWorkflowName && !isInnerModelName) { defRef = toDefRefSafe(`payload:${pathStr}`); dynamicDefs[defRef] = buildGenericPayloadDef(pathStr, v); } let kind = 'string'; if (typeof v === 'number') kind = 'number'; else if (typeof v === 'boolean') kind = 'boolean'; else if (v === null) kind = 'null'; node[key] = mark(v, kind, defRef); }); } walk(payload, []); const jsonText = JSON.stringify(payload, null, 2); let text = jsonText; let html = escapeHtml(jsonText); markers.forEach((m) => { const quotedToken = `"${m.token}"`; const quotedTokenHtml = `"${m.token}"`; const jsonEscaped = JSON.stringify(String(m.value)); let textVal = jsonEscaped; let htmlVal = `${escapeHtml(jsonEscaped)}`; if (m.kind === 'number') { textVal = String(m.value); htmlVal = `${escapeHtml(String(m.value))}`; } else if (m.kind === 'boolean') { textVal = m.value ? 'true' : 'false'; htmlVal = `${m.value ? 'true' : 'false'}`; } else if (m.kind === 'null') { textVal = 'null'; htmlVal = `null`; } text = text.split(quotedToken).join(textVal); html = html.split(quotedTokenHtml).join(htmlVal); }); return { text, html, defs: dynamicDefs }; } function getApiTemplate(lang, payloadText, payloadHtml) { const HEREDOC_TAG = '__VICI_PAYLOAD_JSON__'; if (lang === 'python') { return { text: [ '# POST a model job (Python)', '# Set TOKEN_ID and TOKEN_SECRET to your values.', 'import json', 'import requests', '', `API_URL = "${MODEL_API_ENDPOINT}"`, 'TOKEN_ID = ""', 'TOKEN_SECRET = ""', '', 'payload = json.loads(r"""', payloadText, '""")', '', 'resp = requests.post(', ' API_URL,', ' headers={', ' "Content-Type": "application/json",', ' "Token-ID": TOKEN_ID,', ' "Token-Secret": TOKEN_SECRET,', ' },', ' json=payload', ')', '', 'resp.raise_for_status()', 'print(resp.json())' ].join('\n'), html: [ '# POST a model job (Python)', '# Set TOKEN_ID and TOKEN_SECRET to your values.', 'import json', 'import requests', '', `API_URL = "${escapeHtml(MODEL_API_ENDPOINT)}"`, `TOKEN_ID = "<TOKEN_ID>"`, `TOKEN_SECRET = "<TOKEN_SECRET>"`, '', 'payload = json.loads(r"""', payloadHtml, '""")', '', 'resp = requests.post(', ' API_URL,', ' headers={', ' "Content-Type": "application/json",', ' "Token-ID": TOKEN_ID,', ' "Token-Secret": TOKEN_SECRET,', ' },', ' json=payload', ')', '', 'resp.raise_for_status()', 'print(resp.json())' ].join('\n') }; } if (lang === 'curl') { return { text: [ '# POST a model job (cURL)', '# Set TOKEN_ID and TOKEN_SECRET to your values.', '', `curl -X POST "${MODEL_API_ENDPOINT}" \\`, ' -H "Content-Type: application/json" \\', ' -H "Token-ID: " \\', ' -H "Token-Secret: " \\', ` --data-binary @- <<'${HEREDOC_TAG}'`, payloadText, HEREDOC_TAG ].join('\n'), html: [ '# POST a model job (cURL)', '# Set TOKEN_ID and TOKEN_SECRET to your values.', '', `curl -X POST "${escapeHtml(MODEL_API_ENDPOINT)}" \\`, ' -H "Content-Type: application/json" \\', ' -H "Token-ID: <TOKEN_ID>" \\', ' -H "Token-Secret: <TOKEN_SECRET>" \\', ` --data-binary @- <<'${escapeHtml(HEREDOC_TAG)}'`, payloadHtml, `${escapeHtml(HEREDOC_TAG)}` ].join('\n') }; } return { text: [ '// POST a model job (JavaScript)', '// Set TOKEN_ID and TOKEN_SECRET to your values.', '', '(async () => {', ` const API_URL = "${MODEL_API_ENDPOINT}";`, ' const TOKEN_ID = "";', ' const TOKEN_SECRET = "";', '', ` const payload = ${payloadText};`, '', ' const resp = await fetch(API_URL, {', ' method: "POST",', ' headers: {', ' "Content-Type": "application/json",', ' "Token-ID": TOKEN_ID,', ' "Token-Secret": TOKEN_SECRET,', ' },', ' body: JSON.stringify(payload),', ' });', '', ' if (!resp.ok) throw new Error(`Request failed: ${resp.status}`);', '', ' console.log(await resp.json());', '})().catch((err) => {', ' console.error(err);', '});' ].join('\n'), html: [ '// POST a model job (JavaScript)', '// Set TOKEN_ID and TOKEN_SECRET to your values.', '', '(async () => {', ` const API_URL = "${escapeHtml(MODEL_API_ENDPOINT)}";`, ` const TOKEN_ID = "<TOKEN_ID>";`, ` const TOKEN_SECRET = "<TOKEN_SECRET>";`, '', ` const payload = ${payloadHtml};`, '', ' const resp = await fetch(API_URL, {', ' method: "POST",', ' headers: {', ' "Content-Type": "application/json",', ' "Token-ID": TOKEN_ID,', ' "Token-Secret": TOKEN_SECRET,', ' },', ' body: JSON.stringify(payload),', ' });', '', ' if (!resp.ok) throw new Error(`Request failed: ${resp.status}`);', '', ' console.log(await resp.json());', '})().catch((err) => {', ' console.error(err);', '});' ].join('\n') }; } function renderApiSnippet({ forceDefault = false, toast = false, includeFiles = 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, previewMode: includeFiles ? false : undefined // key fix }); 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); 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.' : (includeFiles ? 'Synced API snippet from form (including file content).' : 'Synced API snippet from form.') ); } } finally { isRenderingApiSnippet = false; } } function toDefRefSafe(s) { return String(s || '') .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 120) || 'field'; } function getFieldMetaById(id) { if (!id) return null; const el = document.getElementById(id); if (!el) return null; const label = root.querySelector(`label[for="${CSS.escape(id)}"]`); const head = label?.closest('.field-head') || el.closest('.field-group')?.querySelector('.field-head[data-help-text]'); return { label: (label?.textContent || id).trim(), help: (head?.getAttribute('data-help-text') || '').trim() }; } function buildGenericPayloadDef(pathStr, value) { const path = String(pathStr || ''); const lastKeyMatch = path.match(/([a-zA-Z0-9_]+)(?:\[\d+\])?$/); const key = lastKeyMatch ? lastKeyMatch[1] : ''; const meta = getFieldMetaById(key); const fallbackDescriptions = { workflow_name: 'Top-level workflow name shown in your dashboard and used for the run.', user_id: 'Your member ID for associating the run with your account.', file: 'Protein structure file contents (PDB/mmCIF text).', ligand_file: 'Ligand file contents (SDF/mol/mol2 text).', protein_filename: 'Original uploaded protein filename.', ligand_filename: 'Original uploaded ligand filename.', protein_file_format: 'Detected protein file format passed to the backend.', protein_structure_source: 'Where the protein content came from (upload, preset, or Vici Lookup).', ligand_file_source: 'Where the ligand content came from (upload or preset).', ligand_file_format: 'Detected ligand file format passed to the backend.' }; const label = meta?.label || key || path; const help = meta?.help || fallbackDescriptions[key] || `Payload field sent to backend at ${escapeHtml(path)}.`; let valuePreview = ''; if (typeof value === 'string') { const short = value.length > 80 ? `${value.slice(0, 77)}...` : value; valuePreview = `
Current value: ${escapeHtml(short)}
`; } else if (typeof value === 'number' || typeof value === 'boolean') { valuePreview = `
Current value: ${escapeHtml(String(value))}
`; } return { title: label, html: `${help}${valuePreview}` }; } function ensureApiDefPopout() { if (apiDefPopoutEl && document.body.contains(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 hideApiDefPopout() { if (!apiDefPopoutEl) return; apiDefPopoutEl.classList.remove('is-visible'); apiDefPopoutEl.setAttribute('aria-hidden', 'true'); apiDefAnchorEl = null; } function scheduleHideApiDefPopout() { if (apiDefHideTimer) clearTimeout(apiDefHideTimer); apiDefHideTimer = setTimeout(() => { hideApiDefPopout(); apiDefHideTimer = null; }, 100); } function positionApiDefPopout(anchorEl) { if (!anchorEl || !apiDefPopoutEl) return; const anchorRect = anchorEl.getBoundingClientRect(); const popRect = apiDefPopoutEl.getBoundingClientRect(); const pad = 8; let top = anchorRect.bottom + 10; let left = anchorRect.left; if (left + popRect.width > window.innerWidth - pad) { left = window.innerWidth - popRect.width - pad; } if (left < pad) left = pad; if (top + popRect.height > window.innerHeight - pad) { top = anchorRect.top - popRect.height - 10; } if (top < pad) top = pad; apiDefPopoutEl.style.left = `${Math.round(left)}px`; apiDefPopoutEl.style.top = `${Math.round(top)}px`; } function showApiDefPopoutFor(anchorEl) { if (!anchorEl) return; const pop = ensureApiDefPopout(); if (apiDefHideTimer) { clearTimeout(apiDefHideTimer); apiDefHideTimer = null; } const defRef = String(anchorEl.dataset.defRef || '').trim(); if (!defRef) return; const def = (apiDynamicDefContent && apiDynamicDefContent[defRef]) || (API_DEF_CONTENT && API_DEF_CONTENT[defRef]); if (!def) return; const titleEl = pop.querySelector('.api-def-popout__title'); const bodyEl = pop.querySelector('.api-def-popout__body'); if (titleEl) titleEl.textContent = def.title || 'Field'; if (bodyEl) bodyEl.innerHTML = def.html || ''; apiDefAnchorEl = anchorEl; pop.classList.add('is-visible'); pop.setAttribute('aria-hidden', 'false'); positionApiDefPopout(anchorEl); } 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 || {}; } 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(); showToast?.('success', `Loaded preset: ${key}.`); } 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 resetOpenmmViciLookup({ keepTarget = true } = {}) { const panel = openmmEl('protein-vici-lookup'); const btn = openmmEl('protein-vici-toggle'); if (!panel) return; hardCollapseViciLookup(panel, { duration: 240, afterClose: () => { try { // keepTarget:true so reset-restored protein text is not wiped window.ViciLookup?.clear?.(panel, { keepInput: false, keepTarget: !!keepTarget }); } catch (err) { console.error(err); } } }); if (btn) { btn.classList.remove('active'); btn.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); } resetOpenmmViciLookup({ keepTarget: true }); const jobnameEl = document.getElementById('jobname'); if (jobnameEl && jobnameEl.value) { const safe = canonicalizeRunName(jobnameEl.value); if (safe) jobnameEl.value = safe; } renderApiSnippet({ forceDefault: false, toast: false }); updateActionVisibility(); openmmBeautifyForcefieldSelectLabels(); } 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; } } 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 body = { ...built.payload, member_id: memberId }; if (body && body[modelKey] && typeof body[modelKey] === 'object') { body[modelKey].user_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; updateActionVisibility(); }); root.addEventListener('change', (e) => { if (e.target.closest('[data-panel="api"]')) return; updateActionVisibility(); // Only refresh API snippet when non-API form fields change // and only if API tab is currently open. if (currentTab === 'api') renderApiSnippet(); }); const mo = new MutationObserver((mutations) => { updateActionVisibility(); // DO NOT call renderApiSnippet() here. // renderApiSnippet mutates #api-code-block.innerHTML, which retriggers this observer // and causes an infinite loop / "crash". }); mo.observe(root, { childList: true, subtree: true }); } function bindReset() { resetBtn?.addEventListener('click', () => resetFormUI({ toast: true })); } 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, includeFiles: true }); }); copyBtn?.addEventListener('click', () => { const text = currentApiSnippet?.text?.trim(); if (!text) { showToast?.('error', 'Nothing to copy yet.'); return; } pulseBtn(copyBtn, 'pulse-green'); copyTextRobust(text) .then(() => showToast?.('success', 'Copied API snippet.')) .catch(() => showToast?.('error', 'Copy failed. Select code and copy manually.')); }); resetApiBtn?.addEventListener('click', () => { pulseBtn(resetApiBtn, 'pulse-red'); renderApiSnippet({ forceDefault: true, toast: true }); }); } 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(deepClone(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(); bindExecute(); blockFileDropsOnRoot(); bindHashRouting(); initTabs(); if (!API_DEF_CONTENT) API_DEF_CONTENT = buildBaseApiDefContent(); bindApiDefinitionPopout(); 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(); } })();