(() => {
'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 ` `;
})
.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)}