(() => { '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; const API_DEF_CONTENT = window.VICI_API_DEF_CONTENT || { 'token-id': { title: 'Token-ID', html: ` Your Vici Token ID. Send it as the Token-ID header. Generate it in your Account . ` }, 'token-secret': { title: 'Token-Secret', html: ` Your Vici Token Secret. Send it as the Token-Secret header. You only see this once when you generate it. Generate it in your Account . ` }, 'workflow-name': { title: `workflow_name / ${modelKey}.name`, html: `A friendly run name shown in your Dashboard. The outer workflow_name and inner ${escapeHtml(modelKey)}.name should match.` } }; let apiDefPopoutEl = null; let apiDefAnchorEl = null; let apiDefHideTimer = null; let apiDynamicDefContent = {}; function tabFromHash(hash = window.location.hash) { const h = String(hash || '').trim().toLowerCase(); if (h === '#basic') return 'basic'; if (h === '#advanced') return 'advanced'; if (h === '#api') return 'api'; return null; } function syncHashToTab(tab, { replace = true } = {}) { const nextHash = `#${tab}`; if (String(window.location.hash || '').toLowerCase() === nextHash) return; try { const url = new URL(window.location.href); url.hash = nextHash; if (replace && window.history?.replaceState) { window.history.replaceState(null, '', url.toString()); } else if (!replace && window.history?.pushState) { window.history.pushState(null, '', url.toString()); } else { window.location.hash = nextHash; } } catch { window.location.hash = nextHash; } } function bindHashRouting() { window.addEventListener('hashchange', () => { const next = tabFromHash(); if (!next || next === currentTab) return; setTab(next, { silent: true, syncHash: false }); }); } function inferInitialTab() { const fromHash = tabFromHash(); if (fromHash) return fromHash; if (root.classList.contains('is-tab-api')) return 'api'; if (root.classList.contains('is-tab-advanced')) return 'advanced'; return 'basic'; } function escapeHtml(str) { return String(str ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function deepClone(v) { return JSON.parse(JSON.stringify(v)); } function isPlainObject(v) { return Object.prototype.toString.call(v) === '[object Object]'; } function stripExecutionContextForApi(value) { const blocked = new Set(['member_id', 'msid', 'user_id', 'team_id']); if (Array.isArray(value)) { return value.map(stripExecutionContextForApi); } if (!isPlainObject(value)) { return value; } const out = {}; Object.entries(value).forEach(([k, v]) => { if (blocked.has(k)) return; out[k] = stripExecutionContextForApi(v); }); return out; } function stableSerialize(value) { const sortRec = (v) => { if (Array.isArray(v)) return v.map(sortRec); if (!isPlainObject(v)) return v; const out = {}; Object.keys(v).sort().forEach((k) => { out[k] = sortRec(v[k]); }); return out; }; return JSON.stringify(sortRec(value)); } function canonicalizeRunName(raw) { let s = String(raw || '').trim(); if (!s) return ''; s = s.replace(/\s+/g, '_'); try { s = s.normalize('NFKD'); } catch {} s = s.replace(/[^\w-]+/g, ''); s = s.replace(/_+/g, '_').toLowerCase(); s = s.replace(/^[^a-z0-9]+/, ''); s = s.replace(/[^a-z0-9]+$/, ''); return s.slice(0, 64); } const SAFE_NAME_RE = /^[a-z0-9](?:[a-z0-9_-]{1,62}[a-z0-9])?$/; function pulseBtn(btn, cls) { if (!btn) return; btn.classList.remove(cls); void btn.offsetWidth; btn.classList.add(cls); const onEnd = () => { btn.classList.remove(cls); btn.removeEventListener('animationend', onEnd); }; btn.addEventListener('animationend', onEnd); } function copyTextRobust(text) { if (navigator.clipboard && window.isSecureContext) { return navigator.clipboard.writeText(text); } return new Promise((resolve, reject) => { try { const ta = document.createElement('textarea'); ta.value = text; ta.setAttribute('readonly', ''); ta.style.position = 'fixed'; ta.style.left = '-9999px'; ta.style.top = '-9999px'; document.body.appendChild(ta); ta.select(); ta.setSelectionRange(0, ta.value.length); const ok = document.execCommand('copy'); ta.remove(); ok ? resolve() : reject(new Error('copy failed')); } catch (err) { reject(err); } }); } function getFieldKey(el, idx) { return ( el.getAttribute('data-field-key') || el.id || el.name || `${el.tagName.toLowerCase()}_${idx}` ); } function readFieldValue(el) { const tag = el.tagName.toLowerCase(); const type = (el.type || '').toLowerCase(); if (type === 'checkbox') return !!el.checked; if (type === 'radio') return el.checked ? el.value : undefined; if (type === 'file') { const files = Array.from(el.files || []); return files.map(f => ({ name: f.name, size: f.size, type: f.type })); } if (tag === 'select' && el.multiple) { return Array.from(el.selectedOptions).map(o => o.value); } return el.value; } function writeFieldValue(el, value) { const tag = el.tagName.toLowerCase(); const type = (el.type || '').toLowerCase(); if (type === 'checkbox') { el.checked = !!value; } else if (type === 'radio') { el.checked = String(el.value) === String(value); } else if (type === 'file') { } else if (tag === 'select' && el.multiple && Array.isArray(value)) { Array.from(el.options).forEach(opt => { opt.selected = value.includes(opt.value); }); } else { el.value = value ?? ''; } el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } function dzEl(ref, scope = document) { if (!ref) return null; if (typeof ref === 'string') return scope.querySelector(ref); return ref; } function dzDispatchValueEvents(el) { if (!el) return; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } function dzFormatBytes(bytes) { const n = Number(bytes); if (!Number.isFinite(n) || n <= 0) return ''; if (n < 1024) return `${n} B`; if (n < 1024 * 1024) return `${(n / 1024).toFixed(1).replace(/\.0$/, '')} KB`; return `${(n / (1024 * 1024)).toFixed(2).replace(/\.00$/, '')} MB`; } function dzReadFileAsText(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => resolve(String(e.target?.result || '')); reader.onerror = () => reject(new Error('Failed to read file.')); reader.readAsText(file); }); } function readFileAsArrayBuffer(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => resolve(e.target?.result); reader.onerror = () => reject(new Error('Failed to read file.')); reader.readAsArrayBuffer(file); }); } function readFileAsBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { const result = String(e.target?.result || ''); const base64 = result.includes(',') ? result.split(',').pop() : result; resolve(base64 || ''); }; reader.onerror = () => reject(new Error('Failed to read file.')); reader.readAsDataURL(file); }); } function detectHumatchColumnsFromHeaders(headers = []) { const cleanHeaders = (Array.isArray(headers) ? headers : []) .map((v) => String(v || '').trim()) .filter(Boolean); if (!cleanHeaders.length) return null; const scored = cleanHeaders.map((header, index) => ({ header, index, heavyScore: scoreHumatchHeader(header, 'heavy'), lightScore: scoreHumatchHeader(header, 'light') })); const heavyPick = [...scored] .sort((a, b) => (b.heavyScore - a.heavyScore) || (a.index - b.index)) .find((row) => row.heavyScore > 0); const lightPick = [...scored] .filter((row) => row.header !== heavyPick?.header) .sort((a, b) => (b.lightScore - a.lightScore) || (a.index - b.index)) .find((row) => row.lightScore > 0); if (!heavyPick && !lightPick) return null; return { heavy: heavyPick?.header || '', light: lightPick?.header || '', headers: cleanHeaders }; } function dzDefaultMetaHtml({ label = 'Loaded content', sizeBytes, content = '', includeLines = true, includeChars = true, extraBits = [] } = {}) { const bits = [String(label || 'Loaded content')]; const sizeLabel = dzFormatBytes(sizeBytes); if (sizeLabel) bits.push(sizeLabel); if (includeLines) { const lines = String(content || '').split(/\r?\n/).length; bits.push(`${lines} lines`); } if (includeChars) { const chars = String(content || '').length; bits.push(`${chars.toLocaleString()} chars`); } (Array.isArray(extraBits) ? extraBits : []).forEach((v) => { if (v != null && String(v).trim()) bits.push(String(v)); }); return bits .map((v, i) => { const isSize = /(?:\d+\s?(?:KB|MB|B))$/.test(v); if (i === 0) return escapeHtml(v); return ` ${escapeHtml(v)}`; }) .join(' '); } function createDropZoneController(config = {}) { const scope = config.scope || document; const refs = { drop: dzEl(config.dropZone, scope), input: dzEl(config.fileInput, scope), meta: dzEl(config.metaEl, scope), removeBtn: dzEl(config.removeBtn, scope), contentField: dzEl(config.contentField, scope), filenameField: dzEl(config.filenameField, scope), sourceField: dzEl(config.sourceField, scope) }; const opts = { emptyMetaText: config.emptyMetaText || 'Drop file here or click to upload', uploadSourceValue: config.uploadSourceValue || 'upload', clearInputOnSet: config.clearInputOnSet !== false, readFile: typeof config.readFile === 'function' ? config.readFile : dzReadFileAsText, acceptDrop: typeof config.acceptDrop === 'function' ? config.acceptDrop : ((e) => Array.from(e.dataTransfer?.types || []).includes('Files')), pickFile: typeof config.pickFile === 'function' ? config.pickFile : ((files) => (files && files[0]) || null) }; if (!refs.drop || !refs.input) { return null; } let bound = false; function getState() { const content = String(refs.contentField?.value || ''); const filename = String(refs.filenameField?.value || '').trim(); const source = String(refs.sourceField?.value || '').trim(); const sizeBytes = Number(refs.drop?.dataset?.fileSizeBytes || NaN); 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; } let humatchFileDz = null; let lastHumatchBulkState = null; function isHumatchModel() { return modelKey === 'humatch'; } function byId(id) { return document.getElementById(id); } function clearHumatchSingleInputs() { const heavy = byId('heavy-seq'); const light = byId('light-seq'); if (heavy) writeFieldValue(heavy, ''); if (light) writeFieldValue(light, ''); } function clearHumatchBulkInputs() { if (humatchFileDz) { humatchFileDz.clearValue({ silent: true }); return; } const heavyCol = byId('heavy-colname'); const lightCol = byId('light-colname'); if (heavyCol) writeFieldValue(heavyCol, ''); if (lightCol) writeFieldValue(lightCol, ''); } function parseDelimitedLine(line, delimiter) { const out = []; let cur = ''; let inQuotes = false; for (let i = 0; i < line.length; i += 1) { const ch = line[i]; if (ch === '"') { if (inQuotes && line[i + 1] === '"') { cur += '"'; i += 1; continue; } inQuotes = !inQuotes; continue; } if (ch === delimiter && !inQuotes) { out.push(cur.trim()); cur = ''; continue; } cur += ch; } out.push(cur.trim()); return out; } function detectDelimitedSeparator(text, extHint = '') { if (extHint === '.tsv') return '\t'; if (extHint === '.csv') return ','; const firstLine = String(text || '') .split(/\r?\n/) .find((line) => String(line || '').trim()) || ''; const commaCount = (firstLine.match(/,/g) || []).length; const tabCount = (firstLine.match(/\t/g) || []).length; const semiCount = (firstLine.match(/;/g) || []).length; if (tabCount > commaCount && tabCount > semiCount) return '\t'; if (semiCount > commaCount) return ';'; return ','; } function getHumatchHeaderColumnsFromText(text, extHint = '') { const firstLine = String(text || '') .split(/\r?\n/) .find((line) => String(line || '').trim()); if (!firstLine) return []; const delimiter = detectDelimitedSeparator(text, extHint); return parseDelimitedLine(firstLine, delimiter) .map((v) => String(v || '').trim()) .filter(Boolean); } function normalizeHumatchHeader(name) { return String(name || '') .toLowerCase() .replace(/[^a-z0-9]+/g, ''); } function scoreHumatchHeader(name, type) { const norm = normalizeHumatchHeader(name); if (!norm) return -1; let score = 0; if (type === 'heavy') { if (norm === 'heavy') score += 120; if (norm === 'vh') score += 140; if (norm === 'hc') score += 90; if (norm.includes('heavychain')) score += 220; if (norm.includes('heavyseq')) score += 210; if (norm.includes('heavysequence')) score += 210; if (norm.includes('heavy')) score += 100; if (norm.includes('vh')) score += 80; if (norm.includes('chainh')) score += 70; if (norm.endsWith('seq')) score += 10; if (norm.includes('sequence')) score += 10; } else { if (norm === 'light') score += 120; if (norm === 'vl') score += 140; if (norm === 'lc') score += 90; if (norm.includes('lightchain')) score += 220; if (norm.includes('lightseq')) score += 210; if (norm.includes('lightsequence')) score += 210; if (norm.includes('light')) score += 100; if (norm.includes('vl')) score += 80; if (norm.includes('chainl')) score += 70; if (norm.endsWith('seq')) score += 10; if (norm.includes('sequence')) score += 10; } return score; } async function tryDetectHumatchExcelColumns(file) { if (!window.XLSX?.read || !window.XLSX?.utils?.sheet_to_json) { return null; } const buffer = await readFileAsArrayBuffer(file); const workbook = window.XLSX.read(buffer, { type: 'array' }); const firstSheetName = Array.isArray(workbook?.SheetNames) ? workbook.SheetNames[0] : ''; if (!firstSheetName) return null; const sheet = workbook.Sheets[firstSheetName]; if (!sheet) return null; const rows = window.XLSX.utils.sheet_to_json(sheet, { header: 1, raw: false, blankrows: false }); const headers = Array.isArray(rows?.[0]) ? rows[0] : []; return detectHumatchColumnsFromHeaders(headers); } function detectHumatchColumnsFromText(text, extHint = '') { const headers = getHumatchHeaderColumnsFromText(text, extHint); return detectHumatchColumnsFromHeaders(headers); } function maybeAutofillHumatchColumn(id, nextValue) { const el = byId(id); if (!el || !nextValue) return; const current = String(el.value || '').trim(); if (current) return; writeFieldValue(el, nextValue); } function applyHumatchDetectedColumns(detected) { if (!detected) return; maybeAutofillHumatchColumn('heavy-colname', detected.heavy); maybeAutofillHumatchColumn('light-colname', detected.light); } const HUMATCH_ALLOWED_EXTENSIONS = new Set(['csv', 'tsv', 'xlsx', 'xls']); function getHumatchFileExtension(file) { const name = String(file?.name || '').trim().toLowerCase(); const match = name.match(/\.([a-z0-9]+)$/i); return match ? match[1].toLowerCase() : ''; } function validateHumatchBulkFile(file) { const ext = getHumatchFileExtension(file); if (!ext || !HUMATCH_ALLOWED_EXTENSIONS.has(ext)) { throw new Error('Unsupported file type. Please upload CSV, TSV, XLSX, or XLS.'); } return ext; } function getHumatchFileFormat(file) { const ext = validateHumatchBulkFile(file); return `.${ext}`; } async function readHumatchBulkFilePayload(file) { if (!file) throw new Error('No file selected.'); const ext = validateHumatchBulkFile(file); const format = `.${ext}`; // Plain text formats if (ext === 'csv' || ext === 'tsv') { const text = await dzReadFileAsText(file); if (!String(text || '').trim()) throw new Error('File is empty.'); return { text: String(text), payloadFormat: format, transport: 'text', detectedColumns: detectHumatchColumnsFromText(String(text), format) }; } // Excel formats const base64 = await readFileAsBase64(file); if (!String(base64 || '').trim()) throw new Error('File is empty.'); let detectedColumns = null; try { detectedColumns = await tryDetectHumatchExcelColumns(file); } catch (err) { console.warn('Excel header parsing failed. Sending raw file to backend.', err); } return { text: base64, payloadFormat: format, transport: 'base64', detectedColumns }; } function setSectionEnabled(sectionEl, enabled) { if (!sectionEl) return; const isSmoothHumatchBulk = sectionEl.id === 'humatch-bulk-section'; const wasOpen = sectionEl.dataset.open === '1'; if (isSmoothHumatchBulk && wasOpen !== enabled) { clearTimeout(sectionEl._toggleTimer); sectionEl.dataset.open = enabled ? '1' : '0'; if (enabled) { sectionEl.classList.remove('is-hidden'); sectionEl.style.overflow = 'hidden'; sectionEl.style.maxHeight = '0px'; sectionEl.style.opacity = '0'; sectionEl.style.transform = 'translateY(-10px)'; requestAnimationFrame(() => { requestAnimationFrame(() => { sectionEl.style.maxHeight = `${sectionEl.scrollHeight}px`; sectionEl.style.opacity = '1'; sectionEl.style.transform = 'translateY(0)'; }); }); sectionEl._toggleTimer = setTimeout(() => { sectionEl.style.maxHeight = `${sectionEl.scrollHeight}px`; sectionEl.style.overflow = 'hidden'; }, 340); } else { sectionEl.style.overflow = 'hidden'; const currentHeight = sectionEl.getBoundingClientRect().height; sectionEl.style.maxHeight = `${Math.ceil(currentHeight)}px`; sectionEl.style.opacity = '1'; sectionEl.style.transform = 'translateY(0)'; void sectionEl.offsetHeight; requestAnimationFrame(() => { sectionEl.style.maxHeight = '0px'; sectionEl.style.opacity = '0'; sectionEl.style.transform = 'translateY(-10px)'; }); sectionEl._toggleTimer = setTimeout(() => { sectionEl.classList.add('is-hidden'); }, 340); } } else if (!isSmoothHumatchBulk) { sectionEl.classList.toggle('is-hidden', !enabled); } else { sectionEl.dataset.open = enabled ? '1' : '0'; } Array.from(sectionEl.querySelectorAll('input, select, textarea, button')).forEach((el) => { if (el.getAttribute('data-internal-field') === 'true') return; el.disabled = !enabled; }); } function syncHumatchUi() { if (!isHumatchModel()) return; const bulk = !!byId('humatch-bulk-toggle')?.checked; const mode = String(byId('humatch-mode')?.value || 'humanise').trim() || 'humanise'; const singleSection = byId('humatch-single-section'); const bulkSection = byId('humatch-bulk-section'); const inputModeText = byId('humatch-input-mode-text'); const bulkChanged = lastHumatchBulkState !== null && bulk !== lastHumatchBulkState; if (bulkChanged) { if (bulk) { clearHumatchSingleInputs(); } else { clearHumatchBulkInputs(); } } lastHumatchBulkState = bulk; setSectionEnabled(singleSection, !bulk); setSectionEnabled(bulkSection, bulk); if (inputModeText) { inputModeText.textContent = bulk ? 'Bulk file input' : 'Single sequence input'; } const alignedInput = byId('humatch-aligned'); const summaryInput = byId('humatch-summary'); if (alignedInput) alignedInput.disabled = mode === 'align'; if (summaryInput) summaryInput.disabled = mode !== 'classify'; humatchFileDz?.refresh(); } async function hydrateHumatchContext() { if (!isHumatchModel()) return; const userEl = byId('humatch-user-id'); const teamEl = byId('humatch-team-id'); const memberId = await getMemberId().catch(() => null); if (userEl) userEl.value = memberId || ''; try { const ctx = memberId ? window.ViciContext?.payloadFor?.(memberId) : null; if (teamEl) teamEl.value = String(ctx?.team_id || ''); } catch (err) { console.warn('Could not read Vici team context', err); } } function initHumatchUi() { if (!isHumatchModel()) return; if (currentTab === 'advanced') { currentTab = 'basic'; } byId('humatch-mode')?.addEventListener('change', syncHumatchUi); byId('humatch-bulk-toggle')?.addEventListener('change', syncHumatchUi); if (!humatchFileDz) { humatchFileDz = createDropZoneController({ scope: root, dropZone: '#humatch-file-dropzone', fileInput: '#humatch-file-input', metaEl: '#humatch-file-meta', removeBtn: '#humatch-remove-file', contentField: '#input-file-content', filenameField: '#input-file-name', sourceField: '#input-file-format', emptyMetaText: 'CSV, TSV, XLSX or XLS', mapFileToValue: async ({ file }) => { const payload = await readHumatchBulkFilePayload(file); return { text: payload.text, meta: { name: file.name || '', source: payload.payloadFormat, sizeBytes: file.size, transport: payload.transport, detectedColumns: payload.detectedColumns || null } }; }, onSet: ({ meta }) => { const transportEl = byId('input-file-transport'); if (transportEl) writeFieldValue(transportEl, meta?.transport || ''); if (meta?.detectedColumns) { applyHumatchDetectedColumns(meta.detectedColumns); } }, onClear: () => { const heavyCol = byId('heavy-colname'); const lightCol = byId('light-colname'); const transportEl = byId('input-file-transport'); if (heavyCol) writeFieldValue(heavyCol, ''); if (lightCol) writeFieldValue(lightCol, ''); if (transportEl) writeFieldValue(transportEl, ''); }, renderFilledMeta: ({ state }) => { const transport = String(byId('input-file-transport')?.value || '').trim(); const isText = transport === 'text'; return { label: state.filename || 'input file', content: state.content, includeChars: isText, includeLines: isText, extraBits: [ state.source || '', transport === 'base64' ? 'binary pass-through' : 'text' ].filter(Boolean) }; }, clearToastMessage: 'Input file removed.' }); humatchFileDz.bind(); } window.ViciLookup?.init?.(root); hydrateHumatchContext(); syncHumatchUi(); } function collectSerializableFields(scope = root) { const els = Array.from(scope.querySelectorAll('input, select, textarea')) .filter(el => !el.closest('[data-panel="api"]')) .filter(el => el.getAttribute('data-internal-field') !== 'true') .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"]')) .filter(el => el.getAttribute('data-internal-field') !== 'true'); 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; applySerializableFields(state.fields || {}, root); if (state.tab && ['basic','advanced','api'].includes(state.tab)) { setTab(state.tab, { silent: true }); } else { setTab('basic', { silent: true }); } if (isHumatchModel()) { syncHumatchUi(); } } 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) { // 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 } = {}) { 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 buildHumatchJob({ requireName = true, validate = true, toast = false } = {}) { const nameInput = document.getElementById('jobname'); const rawName = String(nameInput?.value || '').trim(); let runName = canonicalizeRunName(rawName || 'my_humatch_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 mode = String(byId('humatch-mode')?.value || 'humanise').trim() || 'humanise'; const bulk = !!byId('humatch-bulk-toggle')?.checked; const inner = { name: runName || 'my_humatch_run', mode }; const cachedUserId = String(byId('humatch-user-id')?.value || '').trim(); if (cachedUserId) { inner.user_id = cachedUserId; } if (bulk) { const inputFile = String(byId('input-file-content')?.value || ''); const inputFileName = String(byId('input-file-name')?.value || '').trim(); const inputFileFormat = String(byId('input-file-format')?.value || '').trim(); const inputFileTransport = String(byId('input-file-transport')?.value || '').trim(); const heavyCol = String(byId('heavy-colname')?.value || '').trim(); const lightCol = String(byId('light-colname')?.value || '').trim(); if (validate && !inputFile.trim()) { if (toast) showToast?.('error', 'Input file is required in bulk mode.'); return { error: 'Input file is required.' }; } if (validate && !inputFileName) { if (toast) showToast?.('error', 'File name is missing. Please re-upload the file.'); return { error: 'File name is required.' }; } if (validate && !inputFileFormat) { if (toast) showToast?.('error', 'File format is missing. Please re-upload the file.'); return { error: 'File format is required.' }; } if (validate && !inputFileTransport) { if (toast) showToast?.('error', 'File transport is missing. Please re-upload the file.'); return { error: 'File transport is required.' }; } if (validate && !heavyCol) { if (toast) showToast?.('error', 'Heavy column name is required in bulk mode.'); return { error: 'Heavy column name is required.' }; } if (validate && !lightCol) { if (toast) showToast?.('error', 'Light column name is required in bulk mode.'); return { error: 'Light column name is required.' }; } if (inputFile.trim()) inner.input_file = inputFile; if (inputFileName) inner.input_file_name = inputFileName; if (inputFileFormat) inner.input_file_format = inputFileFormat; if (inputFileTransport) inner.input_file_transport = inputFileTransport; if (heavyCol) inner.heavy_colname = heavyCol; if (lightCol) inner.light_colname = lightCol; } else { const heavySeq = String(byId('heavy-seq')?.value || '').trim(); const lightSeq = String(byId('light-seq')?.value || '').trim(); if (validate && !heavySeq) { if (toast) showToast?.('error', 'Heavy chain sequence is required.'); return { error: 'Heavy chain sequence is required.' }; } if (validate && !lightSeq) { if (toast) showToast?.('error', 'Light chain sequence is required.'); return { error: 'Light chain sequence is required.' }; } if (heavySeq) inner.heavy_chain_seq = heavySeq; if (lightSeq) inner.light_chain_seq = lightSeq; } if (mode !== 'align') { inner.aligned = !!byId('humatch-aligned')?.checked; } if (mode === 'classify') { inner.summary = !!byId('humatch-summary')?.checked; } const payload = { workflow_name: inner.name, [modelKey]: inner }; return { job: inner, payload }; } function buildJob(opts = {}) { if (isHumatchModel()) { return buildHumatchJob(opts); } return buildGenericJob(opts); } function toDefRefSafe(path) { return String(path) .replace(/[^a-zA-Z0-9._:-]+/g, '_') .slice(0, 180); } function humanizeKey(key) { return String(key || '') .replace(/\[\d+\]/g, '') .replace(/[_-]+/g, ' ') .replace(/\s+/g, ' ') .trim() || 'field'; } function valueTypeLabel(v) { if (Array.isArray(v)) return 'array'; if (v === null) return 'null'; return typeof v; } function buildGenericPayloadDef(path, value) { const pathLabel = String(path || 'payload'); const type = valueTypeLabel(value); let typeHint = `Expected type: ${escapeHtml(type)}.`; if (type === 'string') typeHint = 'Expected type: string. Replace with the value for your run.'; if (type === 'number') typeHint = 'Expected type: number. Use an integer or decimal that your model supports.'; if (type === 'boolean') typeHint = 'Expected type: boolean (true or false).'; let extra = 'Replace this example value with a valid value for your model.'; const pathLower = pathLabel.toLowerCase(); if (pathLower.endsWith('.name')) { extra = 'Run/job name for this model block. Keep this aligned with workflow_name unless your backend expects otherwise.'; } else if (pathLower.includes('seed')) { extra = 'Random seed for reproducibility. Use an integer.'; } else if (pathLower.includes('num') || pathLower.includes('count') || pathLower.includes('steps')) { extra = 'Numeric model parameter. Use a supported range from your model docs.'; } else if (String(value) === '...') { extra = 'Placeholder. Replace with additional model-specific parameters or remove this field.'; } return { title: pathLabel, html: `
${escapeHtml(pathLabel)}
${typeHint}
${extra}
` }; } function stringifyPayloadWithMarkers(payloadObj) { const markers = []; const dynamicDefs = {}; const mark = (value, kind = 'string', defRef = '') => { const token = `__MARK_${markers.length}__`; markers.push({ token, value, kind, defRef }); return token; }; const payload = deepClone(payloadObj); function walk(node, pathParts = []) { if (Array.isArray(node)) { for (let i = 0; i < node.length; i++) { const v = node[i]; const childPath = [...pathParts, `[${i}]`]; if (v && typeof v === 'object') { walk(v, childPath); continue; } const pathStr = childPath.join('.'); const defRef = toDefRefSafe(`payload:${pathStr}`); dynamicDefs[defRef] = buildGenericPayloadDef(pathStr, v); let kind = 'string'; if (typeof v === 'number') kind = 'number'; else if (typeof v === 'boolean') kind = 'boolean'; else if (v === null) kind = 'null'; node[i] = mark(v, kind, defRef); } return; } if (!node || typeof node !== 'object') return; Object.keys(node).forEach((key) => { const v = node[key]; const childPath = [...pathParts, key]; // recurse into objects/arrays if (v && typeof v === 'object') { walk(v, childPath); return; } const pathStr = childPath.join('.'); const isWorkflowName = pathStr === 'workflow_name'; const isInnerModelName = pathStr === `${modelKey}.name`; let defRef = 'workflow-name'; if (!isWorkflowName && !isInnerModelName) { defRef = toDefRefSafe(`payload:${pathStr}`); dynamicDefs[defRef] = buildGenericPayloadDef(pathStr, v); } let kind = 'string'; if (typeof v === 'number') kind = 'number'; else if (typeof v === 'boolean') kind = 'boolean'; else if (v === null) kind = 'null'; node[key] = mark(v, kind, defRef); }); } walk(payload, []); const jsonText = JSON.stringify(payload, null, 2); let text = jsonText; let html = escapeHtml(jsonText); markers.forEach((m) => { const quotedToken = `"${m.token}"`; const quotedTokenHtml = `"${m.token}"`; const jsonEscaped = JSON.stringify(String(m.value)); let textVal = jsonEscaped; let htmlVal = `${escapeHtml(jsonEscaped)}`; if (m.kind === 'number') { textVal = String(m.value); htmlVal = `${escapeHtml(String(m.value))}`; } else if (m.kind === 'boolean') { textVal = m.value ? 'true' : 'false'; htmlVal = `${m.value ? 'true' : 'false'}`; } else if (m.kind === 'null') { textVal = 'null'; htmlVal = `null`; } text = text.split(quotedToken).join(textVal); html = html.split(quotedTokenHtml).join(htmlVal); }); return { text, html, defs: dynamicDefs }; } function getApiTemplate(lang, payloadText, payloadHtml) { const HEREDOC_TAG = '__VICI_PAYLOAD_JSON__'; if (lang === 'python') { return { text: [ '# POST a model job (Python)', '# Set TOKEN_ID and TOKEN_SECRET to your values.', 'import json', 'import requests', '', `API_URL = "${MODEL_API_ENDPOINT}"`, 'TOKEN_ID = ""', 'TOKEN_SECRET = ""', '', 'payload = json.loads(r"""', payloadText, '""")', '', 'resp = requests.post(', ' API_URL,', ' headers={', ' "Content-Type": "application/json",', ' "Token-ID": TOKEN_ID,', ' "Token-Secret": TOKEN_SECRET,', ' },', ' json=payload', ')', '', 'resp.raise_for_status()', 'print(resp.json())' ].join('\n'), html: [ '# POST a model job (Python)', '# Set TOKEN_ID and TOKEN_SECRET to your values.', 'import json', 'import requests', '', `API_URL = "${escapeHtml(MODEL_API_ENDPOINT)}"`, `TOKEN_ID = "<TOKEN_ID>"`, `TOKEN_SECRET = "<TOKEN_SECRET>"`, '', 'payload = json.loads(r"""', payloadHtml, '""")', '', 'resp = requests.post(', ' API_URL,', ' headers={', ' "Content-Type": "application/json",', ' "Token-ID": TOKEN_ID,', ' "Token-Secret": TOKEN_SECRET,', ' },', ' json=payload', ')', '', 'resp.raise_for_status()', 'print(resp.json())' ].join('\n') }; } if (lang === 'curl') { return { text: [ '# POST a model job (cURL)', '# Set TOKEN_ID and TOKEN_SECRET to your values.', '', `curl -X POST "${MODEL_API_ENDPOINT}" \\`, ' -H "Content-Type: application/json" \\', ' -H "Token-ID: " \\', ' -H "Token-Secret: " \\', ` --data-binary @- <<'${HEREDOC_TAG}'`, payloadText, HEREDOC_TAG ].join('\n'), html: [ '# POST a model job (cURL)', '# Set TOKEN_ID and TOKEN_SECRET to your values.', '', `curl -X POST "${escapeHtml(MODEL_API_ENDPOINT)}" \\`, ' -H "Content-Type: application/json" \\', ' -H "Token-ID: <TOKEN_ID>" \\', ' -H "Token-Secret: <TOKEN_SECRET>" \\', ` --data-binary @- <<'${escapeHtml(HEREDOC_TAG)}'`, payloadHtml, `${escapeHtml(HEREDOC_TAG)}` ].join('\n') }; } return { text: [ '// POST a model job (JavaScript)', '// Set TOKEN_ID and TOKEN_SECRET to your values.', '', '(async () => {', ` const API_URL = "${MODEL_API_ENDPOINT}";`, ' const TOKEN_ID = "";', ' const TOKEN_SECRET = "";', '', ` const payload = ${payloadText};`, '', ' const resp = await fetch(API_URL, {', ' method: "POST",', ' headers: {', ' "Content-Type": "application/json",', ' "Token-ID": TOKEN_ID,', ' "Token-Secret": TOKEN_SECRET,', ' },', ' body: JSON.stringify(payload),', ' });', '', ' if (!resp.ok) throw new Error(`Request failed: ${resp.status}`);', '', ' console.log(await resp.json());', '})().catch((err) => {', ' console.error(err);', '});' ].join('\n'), html: [ '// POST a model job (JavaScript)', '// Set TOKEN_ID and TOKEN_SECRET to your values.', '', '(async () => {', ` const API_URL = "${escapeHtml(MODEL_API_ENDPOINT)}";`, ` const TOKEN_ID = "<TOKEN_ID>";`, ` const TOKEN_SECRET = "<TOKEN_SECRET>";`, '', ` const payload = ${payloadHtml};`, '', ' const resp = await fetch(API_URL, {', ' method: "POST",', ' headers: {', ' "Content-Type": "application/json",', ' "Token-ID": TOKEN_ID,', ' "Token-Secret": TOKEN_SECRET,', ' },', ' body: JSON.stringify(payload),', ' });', '', ' if (!resp.ok) throw new Error(`Request failed: ${resp.status}`);', '', ' console.log(await resp.json());', '})().catch((err) => {', ' console.error(err);', '});' ].join('\n') }; } function renderApiSnippet({ forceDefault = false, toast = false } = {}) { if (!apiCodeEl) return; if (isRenderingApiSnippet) return; isRenderingApiSnippet = true; try { let payloadSource = null; apiDynamicDefContent = {}; if (!forceDefault) { const built = buildJob({ requireName: false, validate: false, toast: false }); if (built && !built.error && built.payload) { payloadSource = built.payload; } } if (!payloadSource) { payloadSource = deepClone(defaultApiJob || { workflow_name: `my_${modelKey}_run`, [modelKey]: { name: `my_${modelKey}_run`, param_1: 'option-a', param_2: 42 } }); } payloadSource = stripExecutionContextForApi(payloadSource); const payloadBlock = stringifyPayloadWithMarkers(payloadSource, { literalMode: 'json' }); 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() { if (isHumatchModel()) { return { coltuximab: { jobname: 'humatch_Coltuximab', 'humatch-mode': 'humanise', 'humatch-aligned': false, 'humatch-summary': true, 'humatch-bulk-toggle': false, 'heavy-seq': 'QVQLVQPGAEVVKPGASVKLSCKTSGYTFTSNWMHWVKQAPGQGLEWIGEIDPSDSYTNYNQNFQGKAKLTVDKSTSTAYMEVSSLRSDDTAVYYCARGSNPYYYAMDYWGQGTSVTVSS', 'light-seq': 'EIVLTQSPAIMSASPGERVTMTCSASSGVNYMHWYQQKPGTSPRRWIYDTSKLASGVPARFSGSGSGTDYSLTISSMEPEDAATYYCHQRGSXXYTFGGGTKLEIK', 'heavy-colname': '', 'light-colname': '', 'input-file-content': '', 'input-file-name': '', 'input-file-format': '' } }; } 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(); if (isHumatchModel()) syncHumatchUi(); renderApiSnippet(); showToast?.('success', `Loaded preset: ${key}.`); } catch (err) { console.error(err); showToast?.('error', 'Could not apply preset.'); } } function smoothCollapseViciLookup(panel) { if (!panel) return; clearTimeout(panel._viciResetTimer); panel.dataset.viciCollapsed = '1'; panel.classList.remove('open'); const startHeight = Math.max( panel.scrollHeight || 0, panel.getBoundingClientRect().height || 0 ); panel.style.display = ''; panel.style.overflow = 'hidden'; panel.style.transition = 'none'; panel.style.maxHeight = `${Math.ceil(startHeight)}px`; panel.style.opacity = '1'; requestAnimationFrame(() => { requestAnimationFrame(() => { panel.style.transition = 'max-height 220ms ease, opacity 180ms ease'; panel.style.maxHeight = '0px'; panel.style.opacity = '0'; }); }); panel._viciResetTimer = setTimeout(() => { panel.style.display = 'none'; panel.style.maxHeight = '0px'; panel.style.opacity = '0'; panel.style.overflow = ''; panel.style.transition = ''; }, 260); } function resetAllViciLookups(scope = root) { const panels = Array.from(scope.querySelectorAll('.vici-lookup')); panels.forEach((panel) => { const block = panel.closest('.molecule-block') || panel.parentElement; const toggleBtn = block?.querySelector('.vici-toggle-btn'); const isOpen = panel.classList.contains('open') || toggleBtn?.classList.contains('active') || toggleBtn?.getAttribute('aria-expanded') === 'true'; if (isOpen && typeof toggleBtn?.click === 'function') { try { // lets the component use its own close animation if it has one toggleBtn.click(); } catch (err) { console.error(err); smoothCollapseViciLookup(panel); } } else if (isOpen) { smoothCollapseViciLookup(panel); } requestAnimationFrame(() => { try { window.ViciLookup?.clear?.(panel, { keepInput: false, keepTarget: false }); } catch (err) { console.error(err); } }); if (toggleBtn) { toggleBtn.classList.remove('active'); toggleBtn.setAttribute('aria-expanded', 'false'); } }); root.querySelectorAll('.lookup-target-text').forEach((el) => { el.value = ''; dzDispatchValueEvents(el); }); } function resetFormUI({ toast = true } = {}) { try { const tabBeforeReset = currentTab; if (typeof adapter.reset === 'function') { adapter.reset({ root, modelSlug, modelKey, baselineState: deepClone(baselineState) }); if (tabBeforeReset && ['basic', 'advanced', 'api'].includes(tabBeforeReset)) { setTab(tabBeforeReset, { silent: true }); } } else if (baselineState) { const nextState = deepClone(baselineState); nextState.tab = tabBeforeReset; applyState(nextState); } resetAllViciLookups(root); const jobnameEl = document.getElementById('jobname'); if (jobnameEl && jobnameEl.value) { const safe = canonicalizeRunName(jobnameEl.value); if (safe) jobnameEl.value = safe; } 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; 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: false })); } function bindPresets() { presetBtns.forEach(btn => { btn.addEventListener('click', (e) => { e.preventDefault(); applyPreset(btn.dataset.example); }); }); } function bindApiControls() { apiLangTabs.forEach(btn => { btn.addEventListener('click', () => setApiLang(btn.dataset.lang || 'python')); btn.addEventListener('keydown', (e) => { if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) return; e.preventDefault(); const i = apiLangTabs.indexOf(btn); let next = i; if (e.key === 'ArrowRight') next = (i + 1) % apiLangTabs.length; if (e.key === 'ArrowLeft') next = (i - 1 + apiLangTabs.length) % apiLangTabs.length; if (e.key === 'Home') next = 0; if (e.key === 'End') next = apiLangTabs.length - 1; apiLangTabs[next]?.focus(); setApiLang(apiLangTabs[next]?.dataset.lang || 'python'); }); }); const { syncBtn, copyBtn, resetApiBtn } = findApiActionButtons(); syncBtn?.addEventListener('click', () => { pulseBtn(syncBtn, 'pulse-blue'); renderApiSnippet({ toast: true }); }); 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 (isHumatchModel()) { defaultApiJob = { workflow_name: 'my_humatch_run', [modelKey]: { name: 'my_humatch_run', mode: 'humanise', heavy_chain_seq: 'QVQLVQSGAEVKKPGASVKVSCKASGYTFTSYGMH', light_chain_seq: 'DIQMTQSPSSLSASVGDRVTITCRASQSVSSYLAWY', aligned: false } }; defaultApiJob = stripExecutionContextForApi(defaultApiJob); return; } 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() { initHumatchUi(); bindNameCanonicalization(); bindDirtyTracking(); bindReset(); bindPresets(); bindApiControls(); bindApiDefinitionPopout(); bindExecute(); blockFileDropsOnRoot(); bindHashRouting(); initTabs(); baselineState = deepClone(captureState()); initDefaultApiJob(); setApiLang('python'); renderApiSnippet({ forceDefault: false, toast: false }); updateActionVisibility(); window.ModelPage = { root, modelSlug, modelKey, setTab, getCurrentTab: () => currentTab, isDirty, updateActionVisibility, captureState, applyState, resetForm: resetFormUI, applyPreset, buildJob, submitJob: submitModelJob, checkStatus: checkModelJobStatus, renderApiSnippet, setApiLang, getApiSnippet: () => ({ ...currentApiSnippet }), endpoints: { workflow: MODEL_WORKFLOW_ENDPOINT, api: MODEL_API_ENDPOINT, status: MODEL_STATUS_ENDPOINT } }; } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init, { once: true }); } else { init(); } })();