Contact: residue–residue proximity with a distance bound.
Pocket: any residue in Chain 1 near a specific residue in Chain 2 (leave Residue 1 blank).
Covalent: explicit atom–atom attachment (no distance).
Contact
Type: Contact
Chain 1: A Residue 1: R84
Chain 2: C Residue 2: G7
Distance: 0.22 Å
Accepted formats: residues are protein tokens like R84, K45, S18 (AA letter + index).
Notes: both sides must be protein residues; distance must be 0–5.5 Å.
Pocket
Type: Pocket
Chain 1: C (pocket, no residue)
Chain 2: A Residue 2: S18
Distance: 0.5 Å
Accepted formats: Residue 2 is a protein token like S18. Leave Residue 1 empty.
Notes: both chains should be protein; distance must be 0–5.5 Å.
Covalent
Type: Covalent
Chain 1: A Residue 1: N436@N
Chain 2: B Residue 2: @C1
Accepted formats:
Protein side: R84@N, R52@C1 (residue token + @ + atom).
Non-protein side (ligand/glycan/nucleic): @C1, @C, @N2, @S.
Atom rule: atom is a single uppercase letter (e.g., C, N, O, S, P) with optional digits (e.g., C1, N2). No multi-letter atoms.
Notes: distance is ignored for covalent restraints.
Pick a chain from your Molecules above.
Protein token like R84, K45. For Pocket, leave empty. For Covalent, protein side like N436@N.
Pick the second chain to restrain against.
Protein token like S18. For Covalent non-protein side use @C1 / @N2 etc.
Used for Contact/Pocket. 0–5.5 Å. Ignored for Covalent.
`;
container.appendChild(div);
setSlideMax(div);
requestAnimationFrame(() => {
div.classList.add('open');
setSlideMax(div);
});
populateChainSelect(div.querySelector(".chain1"));
populateChainSelect(div.querySelector(".chain2"));
const typeSel = div.querySelector(".restraint-type");
typeSel.addEventListener("change", () => restraintTypeChanged(div));
restraintTypeChanged(div);
updateActionVisibility?.();
}
async function removeRestraint(btn){
const block = btn.closest('.restraint-block');
if (!block) return;
await slideRemove(block);
}
async function resetForm({ startEmpty = false, animate, animateFirst } = {}) {
const doAnimate = (animate ?? animateFirst ?? true) !== false && !startEmpty;
const scope = document.getElementById('chai-ui');
const molWrap = document.getElementById('molecule-container');
const resWrap = document.getElementById('restraint-container');
if (scope) {
scope.querySelectorAll('input[type="text"], input[type="number"], textarea').forEach(el => {
if (el.readOnly || el.classList.contains('mol-chain')) return;
if (el.closest('.molecule-block') || el.closest('.restraint-block')) return;
el.value = '';
});
scope.querySelectorAll('select').forEach(sel => {
if (sel.closest('.molecule-block') || sel.closest('.restraint-block')) return;
sel.selectedIndex = 0;
});
}
const molBlocks = molWrap
? [...molWrap.querySelectorAll('.molecule-block:not([data-removing])')]
: [];
const restBlocks = resWrap
? [...resWrap.querySelectorAll('.restraint-block:not([data-removing])')]
: [];
if (startEmpty) {
restBlocks.forEach((r) => r.remove());
molBlocks.forEach((b) => {
clearMoleculeViciContents(b, { clearTarget: true });
b.remove();
});
} else {
const first = molBlocks[0] || null;
const extras = molBlocks.slice(1);
if (doAnimate) {
extras.forEach((b) => clearMoleculeViciContents(b, { clearTarget: false }));
const removals = [
...extras.map((b) => slideRemove(b)),
...restBlocks.map((r) => slideRemove(r))
];
if (first) {
await collapseMoleculeVici(first, {
clearTarget: true,
duration: 240
});
}
await Promise.all(removals);
} else {
restBlocks.forEach((r) => r.remove());
extras.forEach((b) => {
clearMoleculeViciContents(b, { clearTarget: true });
b.remove();
});
if (first) {
clearMoleculeViciContents(first, { clearTarget: true });
}
}
}
if (!startEmpty) {
ensureAtLeastOneMoleculeBlock({ animate: false });
const first = getChaiMoleculeBlocks()[0] || null;
if (first) {
const typeSel = first.querySelector('.mol-type');
const seqEl = first.querySelector('.mol-sequence');
const chain = first.querySelector('.mol-chain');
const label = first.querySelector('.mol-label');
if (typeSel) {
typeSel.value = 'protein';
typeSel.dispatchEvent(new Event('change', { bubbles: true }));
}
if (seqEl) {
seqEl.value = '';
seqEl.dispatchEvent(new Event('input', { bubbles: true }));
seqEl.dispatchEvent(new Event('change', { bubbles: true }));
}
if (chain) chain.value = 'A';
if (label) label.textContent = 'Molecule A';
first.classList.remove('pulse-clear');
void first.offsetWidth;
first.classList.add('pulse-clear');
updateMolLookupState?.(first);
setSlideMax?.(first);
}
}
const r = document.getElementById('recycles'); if (r) r.value = '3';
const d = document.getElementById('diffusion'); if (d) d.value = '200';
const s = document.getElementById('samples'); if (s) s.value = '1';
const resetBtn = document.getElementById('reset-chai');
resetBtn?.classList?.remove('show');
const err = document.getElementById('error-msg');
if (err) err.style.display = 'none';
refreshMoleculeChains?.();
updateActionVisibility?.();
}
const CHAI_SAFE_NAME_RE = /^[a-z0-9][a-z0-9_-]{1,62}[a-z0-9]$/;
function canonicalizeChaiName(raw){
if (!raw) return '';
let s = raw.replace(/\s+/g,'_');
s = s.normalize('NFKD').replace(/[^\w-]+/g,'');
s = s.replace(/_+/g,'_');
s = s.toLowerCase();
s = s.replace(/^[^a-z0-9]+/, '');
s = s.replace(/[^a-z0-9]+$/, '');
return s.slice(0,64);
}
const AA20 = 'ACDEFGHIKLMNPQRSTVWY';
// sequences
const RE_PROT_SEQ = new RegExp(`^[${AA20}]+$`);
function isValidProteinSeq(seq){ return RE_PROT_SEQ.test((seq||'').toUpperCase()); }
function isValidDNASeq(seq){ return /^[ACGT]+$/.test((seq||'').toUpperCase()); }
function isValidRNASeq(seq){ return /^[ACGU]+$/.test((seq||'').toUpperCase()); }
function isValidSMILES(s){
if (!s || /\s/.test(s)) return false;
return /^[A-Za-z0-9@+\-\[\]\(\)=#$%\\/\.]+$/.test(s) && /[A-Za-z]/.test(s);
}
function isValidGlycanCCD(s){
if (!s || !/^[A-Za-z0-9()\-\s]+$/.test(s)) return false;
if (!/[A-Za-z]{3}/.test(s)) return false;
let bal = 0;
for (const ch of s){ if (ch==='(') bal++; else if (ch===')'){ bal--; if (bal<0) return false; } }
return bal === 0;
}
const RE_RESIDUE = new RegExp(`^[${AA20}]\\d+$`);
function isResidueToken(s){ return RE_RESIDUE.test((s||'').trim().toUpperCase()); }
const RE_ATOM_LABEL = /^[A-Z]{1,2}\d*$/;
function isProteinCovalentToken(s){
const m = (s||'').trim();
const i = m.indexOf('@');
if (i < 1) return false;
const res = m.slice(0, i).toUpperCase();
const atom = m.slice(i + 1).toUpperCase();
return isResidueToken(res) && RE_ATOM_LABEL.test(atom);
}
function isLigandAtomToken(s){
return /^@[A-Z]{1,2}\d*$/.test((s||'').trim().toUpperCase());
}
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 buildChaiJob(opts = {}) {
const {
requireName = true,
requireSequences = true,
validate = true,
toast = false,
fallbackName = 'my_chai_run'
} = opts;
const SAFE_RE = (typeof CHAI_SAFE_NAME_RE !== 'undefined')
? CHAI_SAFE_NAME_RE
: /^[a-z0-9][a-z0-9_-]{1,62}[a-z0-9]$/;
const canonicalize = (typeof canonicalizeChaiName === 'function')
? canonicalizeChaiName
: (s)=>String(s||'').trim().toLowerCase().replace(/\s+/g,'_').slice(0,64);
const nameInput = document.getElementById('jobname');
const rawName = (nameInput?.value || '').trim();
let jobName = rawName ? canonicalize(rawName) : canonicalize(fallbackName);
if (rawName && jobName !== rawName) {
nameInput.value = jobName;
if (toast) showToast?.('success', `Name adjusted to "${jobName}".`);
}
if (!jobName && requireName) {
if (toast) showToast?.('error', 'Name is required.');
return { error: 'Name is required.' };
}
if (!SAFE_RE.test(jobName)) {
if (requireName) {
if (toast) showToast?.('error', 'Name must be 3–64 chars: a–z, 0–9, _ or -, start/end with letter/digit.');
return { error: 'Invalid name.' };
}
jobName = canonicalize(fallbackName);
if (nameInput) nameInput.value = jobName;
}
const moleculeBlocks = Array.from(document.querySelectorAll('#chai-ui .molecule-block:not([data-removing])'));
const molecules = [];
for (const block of moleculeBlocks) {
const type = (block.querySelector('.mol-type')?.value || '').trim().toLowerCase();
const chain_id = (block.querySelector('.mol-chain')?.value || '').trim();
const seqEl = block.querySelector('.mol-sequence');
let sequence = String(seqEl?.value || '').trim();
if (!sequence) continue;
if ((type === 'protein' || type === 'dna' || type === 'rna') && sequence.includes('>')) {
const recs = parseFastaRecords(sequence);
if (recs.length) sequence = recs[0].sequence || '';
}
sequence = normalizeSequenceForMolType(type, sequence);
if (seqEl && seqEl.value !== sequence) {
seqEl.value = sequence;
seqEl.dispatchEvent(new Event('input', { bubbles: true }));
seqEl.dispatchEvent(new Event('change', { bubbles: true }));
}
if (validate) {
if (type === 'protein' && !isValidProteinSeq(sequence)) {
const msg = `Chain ${chain_id}: invalid protein sequence.`;
if (toast) showToast?.('error', msg);
return { error: msg };
}
if (type === 'dna' && !isValidDNASeq(sequence)) {
const msg = `Chain ${chain_id}: invalid DNA sequence.`;
if (toast) showToast?.('error', msg);
return { error: msg };
}
if (type === 'rna' && !isValidRNASeq(sequence)) {
const msg = `Chain ${chain_id}: invalid RNA sequence.`;
if (toast) showToast?.('error', msg);
return { error: msg };
}
if (type === 'ligand' && !isValidSMILES(sequence)) {
const msg = `Chain ${chain_id}: invalid SMILES string.`;
if (toast) showToast?.('error', msg);
return { error: msg };
}
if (type === 'glycan' && !isValidGlycanCCD(sequence)) {
const msg = `Chain ${chain_id}: invalid glycan CCD text.`;
if (toast) showToast?.('error', msg);
return { error: msg };
}
}
molecules.push({ type, chain_id, sequence });
}
if (requireSequences && molecules.length < 1) {
if (toast) showToast?.('error', 'At least one molecule sequence must be filled.');
return { error: 'Missing molecule sequence.' };
}
const chainType = Object.fromEntries(molecules.map((m) => [m.chain_id, m.type]));
const restraints = [];
let errorMsg = null;
document.querySelectorAll('.restraint-block:not([data-removing])').forEach((block) => {
if (errorMsg) return;
const type = block.querySelector('.restraint-type')?.value;
const chain1 = (block.querySelector('.chain1')?.value || '').trim();
const chain2 = (block.querySelector('.chain2')?.value || '').trim();
const res1 = (block.querySelector('.res1')?.value || '').trim();
const res2 = (block.querySelector('.res2')?.value || '').trim();
const distEl = block.querySelector('.distance');
const distance = distEl ? parseFloat(distEl.value) : NaN;
if (type && (!chain1 || !chain2)) { errorMsg = 'Each restraint must choose Chain 1 and Chain 2.'; return; }
const t1 = chainType[chain1];
const t2 = chainType[chain2];
if (!validate) {
if (type === 'contact') restraints.push({ type, chain1, res1, chain2, res2, distance });
else if (type === 'pocket') restraints.push({ type, chain1, chain2, res2, distance });
else if (type === 'covalent') restraints.push({ type, chain1, res1, chain2, res2 });
return;
}
if (type === 'contact') {
const okRes = (val) => (typeof isResidueToken === 'function') ? isResidueToken(val) : /^[A-Za-z]\d+$/.test(val);
if (t1 !== 'protein' || t2 !== 'protein') { errorMsg = 'Contact restraints require protein residues on both sides (e.g., R84).'; return; }
if (!okRes(res1) || !okRes(res2)) { errorMsg = 'Contact: residues must be AA+index (e.g., R84, K45).'; return; }
if (!Number.isFinite(distance) || distance < 0 || distance > 5.5) { errorMsg = 'Contact: distance must be between 0 and 5.5 Å.'; return; }
restraints.push({ type, chain1, res1, chain2, res2, distance });
} else if (type === 'pocket') {
const okRes2 = (val) => (typeof isResidueToken === 'function') ? isResidueToken(val) : /^[A-Za-z]\d+$/.test(val);
if (t1 !== 'protein' || t2 !== 'protein') { errorMsg = 'Pocket restraints use protein chains. Leave Residue 1 blank; set Residue 2 like S18.'; return; }
if (res1) { errorMsg = 'Pocket: leave Residue 1 empty (the pocket chain).'; return; }
if (!okRes2(res2)) { errorMsg = 'Pocket: Residue 2 must be AA+index (e.g., S18).'; return; }
if (!Number.isFinite(distance) || distance < 0 || distance > 5.5) { errorMsg = 'Pocket: distance must be between 0 and 5.5 Å.'; return; }
restraints.push({ type, chain1, chain2, res2, distance });
} else if (type === 'covalent') {
const proteinOk = (val) => {
if (typeof isProteinCovalentToken === 'function') return isProteinCovalentToken(val);
const m = String(val || ''); const i = m.indexOf('@');
if (i < 1) return false;
const res = m.slice(0, i).toUpperCase();
const atom = m.slice(i + 1).toUpperCase();
return isResidueToken(res) && /^[A-Z]{1,2}\d*$/.test(atom);
};
const ligandOk = (val) => {
if (typeof isLigandAtomToken === 'function') return isLigandAtomToken(val);
return /^@[A-Z]{1,2}\d*$/.test(String(val || '').toUpperCase());
};
const ok1 = (t1 === 'protein') ? proteinOk(res1) : ligandOk(res1);
const ok2 = (t2 === 'protein') ? proteinOk(res2) : ligandOk(res2);
if (!ok1 || !ok2) { errorMsg = 'Covalent: protein side must be R84@N/C/etc; non-protein side must be @C1/@C/@N2/@CA. Distance is not used.'; return; }
restraints.push({ type, chain1, res1, chain2, res2 });
}
});
if (errorMsg) { if (toast) showToast?.('error', errorMsg); return { error: errorMsg }; }
const msa = document.getElementById('msa')?.value;
const templates = document.getElementById('templates')?.value;
const recycles = parseInt(document.getElementById('recycles')?.value || '3', 10);
const diffusion_steps = parseInt(document.getElementById('diffusion')?.value || '200', 10);
const seed = document.getElementById('seed')?.value ? parseInt(document.getElementById('seed').value, 10) : 42;
if (validate && templates === 'mmseq2' && msa !== 'mmseq2') {
if (toast) showToast?.('error', 'Templates require MSA = MMseq2.');
return { error: 'Templates require MSA = MMseq2.' };
}
let samples = parseInt(document.getElementById('samples')?.value || '1', 10);
if (!Number.isInteger(samples) || samples < 1) samples = 1;
const job = {
name: jobName || fallbackName,
msa,
templates,
recycles,
diffusion_steps,
seed,
samples,
molecules,
restraints,
};
return { job, payload: { workflow_name: job.name, chai1: job } };
}
function getViciTokenFromClient(){
const token_id =
(window.VICI_TOKEN_ID ||
localStorage.getItem('vici_token_id') ||
sessionStorage.getItem('vici_token_id') ||
'').trim();
const token_secret =
(window.VICI_TOKEN_SECRET ||
localStorage.getItem('vici_token_secret') ||
sessionStorage.getItem('vici_token_secret') ||
'').trim();
if (!token_id || !token_secret) return null;
return { token_id, token_secret };
}
async function submitChaiJob() {
const execBtn =
document.querySelector('.chai-actions [data-ms-content="members"].action-button, .chai-actions [data-ms-content="members"].chai-run-button') ||
document.querySelector('.chai-execute-group [data-ms-content="members"].action-button, .chai-execute-group [data-ms-content="members"].chai-run-button');
const planned = 1;
const ok = await guardSubmitOrToast({ planned, minCredit: 1.0, buttonSelector: execBtn });
if (!ok) return;
const { job, payload, error } = buildChaiJob({ requireName: true, requireSequences: true, validate: true, toast: true });
if (error || !job) return;
const memberId = await getMemberId();
if (!memberId) {
showToast?.("error", "Please sign in to submit jobs.");
window.location.assign("/sign-up");
return;
}
if (!window.ViciExec?.post) {
showToast?.("error", "ViciExec.post is not available on this page.");
return;
}
if (execBtn) { execBtn.disabled = true; execBtn.setAttribute('aria-busy','true'); }
UX.overlay?.show?.('Submitting');
UX.progress?.start?.();
UX.progress?.trickle?.();
try {
const body = {
...(payload || { workflow_name: job.name, chai1: job }),
member_id: memberId,
};
const responseText = await window.ViciExec.post(
window.CHAI_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', 'Chai-1 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'); }
}
}
window.addEventListener('DOMContentLoaded', () => {
const recycleSelect = document.getElementById("recycles");
if (recycleSelect) {
recycleSelect.innerHTML = "";
for (let i = 1; i <= 20; i++) {
const opt = document.createElement("option");
opt.value = i; opt.text = i;
if (i === 3) opt.selected = true;
recycleSelect.appendChild(opt);
}
}
const diffusionSelect = document.getElementById("diffusion");
if (diffusionSelect) {
diffusionSelect.innerHTML = "";
for (let i = 100; i <= 2000; i += 100) {
const opt = document.createElement("option");
opt.value = i; opt.text = i;
if (i === 200) opt.selected = true;
diffusionSelect.appendChild(opt);
}
}
const samplesSelect = document.getElementById("samples");
if (samplesSelect) {
samplesSelect.innerHTML = "";
for (let i = 1; i <= 10; i++) {
const opt = document.createElement("option");
opt.value = i; opt.text = i;
if (i === 1) opt.selected = true;
samplesSelect.appendChild(opt);
}
}
const addMolBtn = document.getElementById("addMolBtn");
if (addMolBtn) {
addMolBtn.type = 'button';
addMolBtn.addEventListener("click", (e) => { e.preventDefault(); addMolecule(); });
}
const addRestBtn = document.getElementById("addRestBtn");
if (addRestBtn) {
addRestBtn.type = 'button';
addRestBtn.addEventListener("click", (e) => { e.preventDefault(); addRestraint(); });
}
document.querySelectorAll('.chai-example-buttons [data-example]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
applyChaiExample(btn.dataset.example);
});
});
resetForm({ startEmpty:false, animateFirst:false });
updateActionVisibility();
bindMsaTemplatesCoupling();
bindMoleculeDropImportsInChaiUI();
});
function isChaiDirty(){
const scope = document.getElementById('chai-ui');
if (!scope) return false;
const molCount = document.querySelectorAll('.molecule-block:not([data-removing])').length;
const restCount = document.querySelectorAll('.restraint-block:not([data-removing])').length;
if (molCount > 1 || restCount > 0) return true;
const typed = scope.querySelectorAll('input[type="text"], input[type="number"], textarea');
for (const el of typed) {
if (el.readOnly) continue;
if (el.classList.contains('mol-chain')) continue;
if ((el.value || '').trim() !== '') return true;
}
return false;
}
function updateActionVisibility(){
const root = document.getElementById('chai-ui');
const resetBtn = document.getElementById('reset-chai');
const actions = document.querySelector('.chai-actions');
if (!root) return;
if (root.classList.contains('tab-api')){
resetBtn?.classList.remove('show');
actions?.classList.remove('visible');
return;
}
const dirty = isChaiDirty();
resetBtn?.classList.toggle('show', dirty);
actions?.classList.toggle('visible', dirty);
}
document.addEventListener('input', (e) => {
if (e.target.closest('#chai-ui')) updateActionVisibility();
});
window.addEventListener('DOMContentLoaded', () => {
updateActionVisibility();
});
const _molRO = new WeakMap();
function attachMolAutoResize(block){
if (!block || _molRO.has(block)) return;
let raf = null;
const ro = new ResizeObserver(() => {
if (!block.classList.contains('open')) return;
if (raf) cancelAnimationFrame(raf);
raf = requestAnimationFrame(() => {
setSlideMax(block);
raf = null;
});
});
ro.observe(block);
const vici = block.querySelector('.vici-lookup');
if (vici) ro.observe(vici);
_molRO.set(block, ro);
}
function updateMolLookupState(block){
if (!block) return;
const eyeBtn = block.querySelector('.vici-toggle-btn');
const vici = block.querySelector('.vici-lookup');
const isOpen =
!!eyeBtn?.classList.contains('active') ||
eyeBtn?.getAttribute('aria-expanded') === 'true' ||
vici?.classList.contains('open') ||
(vici && vici.style.display !== 'none' && vici.style.maxHeight !== '0px');
block.classList.toggle('lookup-open', isOpen);
if (block.classList.contains('open')) {
setSlideMax(block);
if (vici){
const onEnd = () => {
setSlideMax(block);
vici.removeEventListener('transitionend', onEnd);
};
vici.addEventListener('transitionend', onEnd);
}
}
}
document.addEventListener('click', (e) => {
const btn = e.target.closest('.vici-toggle-btn');
if (!btn) return;
const block = btn.closest('.molecule-block');
requestAnimationFrame(() => updateMolLookupState(block));
});
function setSlideMax(el){
if (!el) return;
const cs = getComputedStyle(el);
const minH = parseFloat(cs.minHeight) || 0;
const h = Math.max(el.scrollHeight, minH);
el.style.setProperty('--slide-max', `${h}px`);
}
function slideRemove(el, { fallbackMs = 700 } = {}) {
return new Promise((resolve) => {
if (!el || !el.isConnected) {
resolve();
return;
}
if (el.dataset.removing === '1') {
resolve();
return;
}
el.dataset.removing = '1';
el.classList.add('dockq-slide');
const minH = parseFloat(getComputedStyle(el).minHeight) || 0;
const startH = Math.max(el.scrollHeight, minH);
el.style.overflow = 'hidden';
el.style.maxHeight = `${startH}px`;
void el.offsetHeight;
let done = false;
const finish = () => {
if (done) return;
done = true;
el.removeEventListener('transitionend', onEnd);
if (el.isConnected) el.remove();
refreshMoleculeChains?.();
updateActionVisibility?.();
resolve();
};
const onEnd = (ev) => {
if (ev.target !== el) return;
if (ev.propertyName !== 'max-height') return;
finish();
};
el.addEventListener('transitionend', onEnd);
requestAnimationFrame(() => {
el.classList.remove('open');
el.style.maxHeight = '0px';
el.style.opacity = '0';
});
setTimeout(finish, fallbackMs);
});
}
function slideOpen(el){
if (!el) return;
el.classList.add('dockq-slide');
el.classList.remove('open');
setSlideMax(el);
const maxH = el.style.getPropertyValue('--slide-max') || `${el.scrollHeight}px`;
el.style.maxHeight = maxH;
requestAnimationFrame(() => {
el.offsetHeight;
el.classList.add('open');
});
}
const CHAI_EXAMPLES = {
'8cyo': {
name: '8CYO protein-ligand',
molecules: [
{
type: 'protein',
label: '8CYO Protein',
sequence: `
MKKGHHHHHHGAISLISALVRAHVDSNPAMTSLDYSRFQANPDYQMSGDDTQHIQQFYDLLTGSMEIIRGWAEKIPGFADLPKADQDLLFESAFLELFVLRLAYRSNPVEGKLIFCNGVVLHRLQCVRGFGEWIDSIVEFSSNLQNMNIDISAFSCIAALAMVTERHGLKEPKRVEELQNKIVNTLKDHVTFNNGGLNRPNYLSKLLGKLPELRTLCTQGLQRIFYLKLEDLVPPPAIIDKLFLDTLPF
`.trim(),
},
{
type: 'ligand',
label: '8CYO Ligand',
sequence: 'c1cc(c(cc1OCC(=O)NCCS)Cl)Cl',
},
],
restraints: [
{ type: 'covalent', chain1: 'A', res1: 'C217@SG', chain2: 'B', res2: '@S1' },
],
},
'7syz': {
name: '7SYZ antibody complex',
molecules: [
{
type: 'protein',
label: '7SYZ Antigen (A)',
sequence: `
MMADSKLVSLNNNLSGKIKDQGKVIKNYYGTMDIKKINDGLLDSKILGAFNTVIALLGSIIIIVMNIMIIQNYTRTTDNQALIKESLQSVQQQIKALTDKIGTEIGPKVSLIDTSSTITIPANIGLLGSKISQSTSSINENVNDKCKFTLPPLKIHECNISCPNPLPFREYRPISQGVSDLVGLPNQICLQKTTSTILKPRLISYTLPINTREGVCITDPLLAVDNGFFAYSHLEKIGSCTRGIAKQRIIGVGEVLDRGDKVPSMFMTNVWTPPNPSTIHHCSSTYHEDFYYTLCAVSHVGDPILNSTSWTESLSLIRLAVRPKSDSGDYNQKYIAITKVERGKYDKVMPYGPSGIKQGDTLYFPAVGFLPRTEFQYNDSNCPIIHCKYSKAENCRLSMGVNSKSHYILRSGLLKYNLSLGGDIILQFIEIADNRLTIGSPSKIYNSLGQPVFYQASYSWDTMIKLGDVDTVDPLRVQWRNNSVISRPGQSQCPRFNVCPEVCWEGTYNDAFLIDRLNWVSAGVYLNSNQTAENPVFAVFKDNEILYQVPLAEDDTNAQKTITDCFLLENVIWCISLVEIYDTGDSVIRPKLFAVKIPAQCSES
`.trim(),
},
{
type: 'protein',
label: 'Heavy chain (B)',
sequence: `
QIQLVQSGPELKKPGETVKISCTTSGYTFTNYGLNWVKQAPGKGFKWMAWINTYTGEPTYADDFKGRFAFSLETSASTTYLQINNLKNEDMSTYFCARSGYYDGLKAMDYWGQGTSVTVSSAKTTPPSVYPLAPGSAAQTNSMVTLGCLVKGYFPEPVTVTWNSGSLSSGVHTFPAVLQSDLYTLSSSVTVPSSTWPSETVTCNVAHPASSTKVDKKIVPRDC
`.trim(),
},
{
type: 'protein',
label: 'Light chain (C)',
sequence: `
DVLMIQTPLSLPVSLGDQASISCRSSQSLIHINGNTYLEWYLQKPGQSPKLLIYKVSNRFSGVPDRFSGSGSGTDFTLKISRVEAEDLGVYYCFQGSHVPFTFGAGTKLELKRADAAPTVSIFPPSSEQLTSGGASVVCFLNNFYPKDINVKWKIDGSERQNGVLNSWTDQDSKDSTYSMSSTLTLTKDEYERHNSYTCEATHKTSTSPIVKSFNRNECVY
`.trim(),
},
],
restraints: [
{ type: 'contact', chain1: 'A', res1: 'C387', chain2: 'B', res2: 'Y101', distance: '5.5' },
{ type: 'pocket', chain1: 'C', res1: '', chain2: 'A', res2: 'S483', distance: '5.5' },
],
},
};
function applyChaiExample(key){
const ex = CHAI_EXAMPLES[(key || '').toLowerCase()];
if (!ex) return;
resetForm({ startEmpty: true });
const molWrap = document.getElementById('molecule-container');
const resWrap = document.getElementById('restraint-container');
if (molWrap) {
molWrap.innerHTML = '';
ex.molecules.forEach((mol, idx) => {
addMolecule({ animate:false });
const block = molWrap.querySelectorAll('.molecule-block')[idx];
if (!block) return;
const typeSel = block.querySelector('.mol-type');
if (typeSel) {
typeSel.value = mol.type;
typeSel.dispatchEvent(new Event('change'));
}
const seq = block.querySelector('.mol-sequence');
if (seq) seq.value = (mol.sequence || '').trim();
const label = block.querySelector('.mol-label');
if (label && mol.label) label.textContent = mol.label;
const lookupBtn = block.querySelector('.vici-toggle-btn');
if (lookupBtn?.classList.contains('active')) toggleViciLookup(lookupBtn);
});
}
refreshMoleculeChains();
if (resWrap) {
resWrap.innerHTML = '';
ex.restraints.forEach(rest => {
addRestraint();
const block = resWrap.querySelector('.restraint-block:last-of-type');
if (!block) return;
const typeSel = block.querySelector('.restraint-type');
if (typeSel) {
typeSel.value = rest.type || 'contact';
typeSel.dispatchEvent(new Event('change'));
}
const chain1 = block.querySelector('.chain1'); if (chain1) chain1.value = rest.chain1 || '';
const chain2 = block.querySelector('.chain2'); if (chain2) chain2.value = rest.chain2 || '';
const res1 = block.querySelector('.res1'); if (res1) res1.value = rest.res1 || '';
const res2 = block.querySelector('.res2'); if (res2) res2.value = rest.res2 || '';
const dist = block.querySelector('.distance');
if (dist && rest.type !== 'covalent') dist.value = rest.distance ?? '';
});
}
if (ex.name) {
const job = document.getElementById('jobname');
if (job) job.value = ex.name;
}
const root = document.getElementById('chai-ui');
if (root?.classList.contains('tab-api')) setChaiTab('advanced');
updateActionVisibility?.();
}
let _chaiTabOrder = ['basic', 'advanced', 'api'];
let _chaiCurTab = 'advanced';
let _chaiHashRoutingBound = false;
function chaiTabFromHash(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 chaiSyncHashToTab(tab, { replace = true } = {}) {
const nextHash = `#${tab}`;
const curHash = String(window.location.hash || '').toLowerCase();
if (curHash === 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 inferInitialChaiTab() {
const fromHash = chaiTabFromHash();
if (fromHash) return fromHash;
const root = document.getElementById('chai-ui');
if (!root) return 'advanced';
if (root.classList.contains('tab-basic')) return 'basic';
if (root.classList.contains('tab-api')) return 'api';
if (root.classList.contains('tab-advanced')) return 'advanced';
return 'advanced';
}
function bindChaiHashRouting() {
if (_chaiHashRoutingBound) return;
_chaiHashRoutingBound = true;
window.addEventListener('hashchange', () => {
const next = chaiTabFromHash();
if (!next) return;
if (next === _chaiCurTab) return;
setChaiTab(next, { silent: true, syncHash: false });
});
}
function setChaiTab(tab, { silent = false, syncHash = true, replaceHash = false } = {}) {
const root = document.getElementById('chai-ui');
if (!root) return;
if (!_chaiTabOrder.includes(tab)) return;
_chaiCurTab = tab;
const panes = Array.from(root.querySelectorAll('[data-pane]'));
panes.forEach(p => {
p.dataset.wasHidden = (getComputedStyle(p).visibility === 'hidden') ? '1' : '0';
});
root.classList.remove('tab-basic', 'tab-advanced', 'tab-api');
root.classList.add(`tab-${tab}`);
root.querySelectorAll('.chai-tabbar .chai-tab').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) {
chaiSyncHashToTab(tab, { replace: replaceHash || silent });
}
requestAnimationFrame(() => {
panes.forEach(p => {
const wasHidden = p.dataset.wasHidden === '1';
const nowHidden = (getComputedStyle(p).visibility === 'hidden');
if (wasHidden && !nowHidden) {
p.classList.add('pane-enter');
requestAnimationFrame(() => {
p.classList.remove('pane-enter');
});
}
delete p.dataset.wasHidden;
});
});
updateActionVisibility?.();
if (tab === 'api' && typeof renderChaiApiSnippet === 'function') {
renderChaiApiSnippet();
}
if (!silent && typeof window.dispatchEvent === 'function') {
try {
window.dispatchEvent(new CustomEvent('chai:tab-change', { detail: { tab } }));
} catch {}
}
}
function initChaiTabs() {
const root = document.getElementById('chai-ui');
const bar = root?.querySelector('.chai-tabbar');
if (!root || !bar) return;
bindChaiHashRouting();
bar.addEventListener('click', (e) => {
const btn = e.target.closest('.chai-tab');
if (!btn) return;
setChaiTab(btn.dataset.tab, { syncHash: true, replaceHash: false });
});
bar.addEventListener('keydown', (e) => {
const btn = e.target.closest('.chai-tab');
if (!btn) return;
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) return;
e.preventDefault();
const tabs = Array.from(bar.querySelectorAll('.chai-tab'));
const i = tabs.indexOf(btn);
if (i < 0) return;
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();
setChaiTab(tabs[next]?.dataset.tab || 'advanced', { syncHash: true, replaceHash: false });
});
const initial = inferInitialChaiTab();
setChaiTab(initial, { silent: true, syncHash: true, replaceHash: true });
}
window.addEventListener('DOMContentLoaded', initChaiTabs);
const API_LANGS = ['python','curl','javascript'];
let currentApiLang = 'python';
let currentApiSnippet = { text: '', html: '' };
const DEFAULT_API_JOB = {
name: 'my_chai_run',
msa: 'mmseq2',
templates: 'none',
recycles: 3,
diffusion_steps: 200,
seed: 42,
samples: 1,
molecules: [
{ type: 'protein', chain_id: 'A', sequence: 'MKTIIALSYIFCLVFADYKDDDDK' }
],
restraints: [
{ type: 'contact', chain1: 'A', res1: 'R84', chain2: 'A', res2: 'K45', distance: 5 }
]
};
const API_DEF_CONTENT = {
'api-legend': {
title: 'What the colors mean',
html: `
Purple dotted: values you should replace.
Blue text: Vici endpoint + required headers.
Plain text: example defaults.
`
},
'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 / chai1.name',
html: 'A friendly run name shown in your Dashboard. The outer workflow_name and inner chai1.name should match.'
},
'msa': {
title: 'msa',
html: `Multiple sequence alignment mode for protein chains.
"none": no MSA used (faster, less context).
"mmseq2": build MSA with MMseqs2 (better accuracy, slower).