const CARET_UP_SVG = ` `; const CARET_DOWN_SVG = ` `; const TRASH_SVG = ` `; const CHAI_WORKFLOW_ENDPOINT = window.CHAI_WORKFLOW_ENDPOINT || 'https://ayambabu23--workflow-execute-workflow.modal.run/'; const CHAI_API_ENDPOINT = window.CHAI_API_ENDPOINT || 'https://vici-bio--api-execute-workflow.modal.run/'; window.CHAI_WORKFLOW_ENDPOINT = CHAI_WORKFLOW_ENDPOINT; window.CHAI_API_ENDPOINT = CHAI_API_ENDPOINT; function bindMsaTemplatesCoupling() { const msaSel = document.getElementById('msa'); const tmplSel = document.getElementById('templates'); if (!msaSel || !tmplSel) return; const syncFromTemplates = () => { if (tmplSel.value === 'mmseq2' && msaSel.value !== 'mmseq2') { msaSel.value = 'mmseq2'; try { msaSel.classList.remove('pulse-clear'); void msaSel.offsetWidth; msaSel.classList.add('pulse-clear'); } catch {} showToast?.('success', 'Templates require MMseqs2. MSA set to MMseqs2.'); } }; const onMsaChange = () => { if (tmplSel.value === 'mmseq2' && msaSel.value !== 'mmseq2') { msaSel.value = 'mmseq2'; showToast?.('info', 'Templates require MMseqs2.'); } }; tmplSel.addEventListener('change', syncFromTemplates); msaSel.addEventListener('change', onMsaChange); syncFromTemplates(); } function inferMolTypeFromRecord(record, fallbackType = 'protein') { const header = String(record?.header || '').toLowerCase(); const raw = String(record?.raw_sequence ?? record?.sequence ?? '').trim(); const seq = raw.replace(/\s+/g, ''); if (/\b(smiles?|ligand|chembl|pubchem|cid)\b/i.test(header)) return 'ligand'; if (/\b(glycan|ccd)\b/i.test(header)) return 'glycan'; if (/\b(dna)\b/i.test(header)) return 'dna'; if (/\b(rna)\b/i.test(header)) return 'rna'; if (/\b(protein|peptide|amino|aa)\b/i.test(header)) return 'protein'; if (/^\s*(smiles?|ligand)\s*[:|]/i.test(raw)) return 'ligand'; if (/^\s*glycan\s*[:|]/i.test(raw)) return 'glycan'; if (/^\s*dna\s*[:|]/i.test(raw)) return 'dna'; if (/^\s*rna\s*[:|]/i.test(raw)) return 'rna'; if (/^\s*(protein|peptide)\s*[:|]/i.test(raw)) return 'protein'; const hasSmilesSyntax = /[=#@\[\]\(\)\\/\.]/.test(seq) || /[bcnops]/.test(seq); if (typeof isValidSMILES === 'function' && isValidSMILES(seq) && hasSmilesSyntax) { return 'ligand'; } if (typeof isValidGlycanCCD === 'function' && isValidGlycanCCD(raw)) return 'glycan'; if (typeof isValidDNASeq === 'function' && isValidDNASeq(seq)) return 'dna'; if (typeof isValidRNASeq === 'function' && isValidRNASeq(seq)) return 'rna'; if (typeof isValidProteinSeq === 'function' && isValidProteinSeq(seq)) return 'protein'; if ((fallbackType || '').toLowerCase() === 'ligand' && typeof isValidSMILES === 'function' && isValidSMILES(seq)) { return 'ligand'; } return fallbackType || 'protein'; } function normalizeRecordValueForType(type, record) { const raw = String(record?.raw_sequence ?? record?.sequence ?? '').trim(); const stripped = raw.replace(/^\s*(protein|peptide|dna|rna|glycan|smiles?|ligand)\s*[:|]\s*/i, ''); if (type === 'glycan') return stripped.trim(); if (type === 'ligand') return stripped.replace(/\s+/g, ''); return normalizeSequenceForMolType(type, stripped); } const CHAI_IMPORT_FASTA_EXTS = new Set(['fasta', 'fa', 'faa', 'fna', 'fas', 'fsa', 'seq', 'txt']); const CHAI_IMPORT_STRUCT_EXTS = new Set(['pdb', 'ent']); function getFileExtension(name = '') { const n = String(name || '').trim().toLowerCase(); const i = n.lastIndexOf('.'); return i >= 0 ? n.slice(i + 1) : ''; } function sniffImportFormat(text, extension = '') { const ext = String(extension || '').toLowerCase(); const raw = String(text || ''); if (ext === 'pdb' || ext === 'ent') return 'pdb'; if (ext === 'cif' || ext === 'mmcif') return 'cif'; if (CHAI_IMPORT_FASTA_EXTS.has(ext)) return 'fasta'; if (/^\s*>/m.test(raw)) return 'fasta'; if (/^\s*(HEADER|TITLE|COMPND|SEQRES|ATOM |HETATM)/m.test(raw)) return 'pdb'; if (/_entity_poly\.|_entity_poly_seq\./.test(raw)) return 'cif'; return 'fasta'; } const PDB_AA3_TO_1 = { ALA:'A', ARG:'R', ASN:'N', ASP:'D', CYS:'C', GLN:'Q', GLU:'E', GLY:'G', HIS:'H', ILE:'I', LEU:'L', LYS:'K', MET:'M', PHE:'F', PRO:'P', SER:'S', THR:'T', TRP:'W', TYR:'Y', VAL:'V', MSE:'M', SEC:'U', PYL:'O', ASX:'B', GLX:'Z', XLE:'J' }; const PDB_NUC_TO_1 = { A:'A', C:'C', G:'G', U:'U', I:'I', DA:'A', DC:'C', DG:'G', DT:'T', DU:'U', DI:'I', ADE:'A', CYT:'C', GUA:'G', URI:'U', THY:'T' }; function classifyResidueList3(names) { const clean = names.map(x => String(x || '').trim().toUpperCase()).filter(Boolean); if (!clean.length) return null; const aaCount = clean.filter(r => !!PDB_AA3_TO_1[r]).length; const nucCount = clean.filter(r => !!PDB_NUC_TO_1[r]).length; if (aaCount === clean.length) return 'protein'; if (nucCount === clean.length) { const hasU = clean.some(r => ['U','URI','DU'].includes(r)); const hasT = clean.some(r => ['DT','THY'].includes(r)); if (hasU && !hasT) return 'rna'; return 'dna'; } if (aaCount >= Math.max(5, Math.floor(clean.length * 0.8))) return 'protein'; if (nucCount >= Math.max(5, Math.floor(clean.length * 0.8))) { const one = clean.map(r => PDB_NUC_TO_1[r] || '').join(''); return one.includes('U') && !one.includes('T') ? 'rna' : 'dna'; } return null; } function residues3ToSeq(names, type) { if (type === 'protein') { return names.map(r => PDB_AA3_TO_1[String(r).toUpperCase()] || 'X').join(''); } if (type === 'dna' || type === 'rna') { let s = names.map(r => PDB_NUC_TO_1[String(r).toUpperCase()] || 'N').join(''); if (type === 'rna') s = s.replace(/T/g, 'U'); if (type === 'dna') s = s.replace(/U/g, 'T'); return s; } return ''; } function parsePdbSequenceRecords(text) { const raw = String(text || '').replace(/\r\n/g, '\n'); const seqresByChain = new Map(); for (const line of raw.split('\n')) { if (!line.startsWith('SEQRES')) continue; const chain = (line.slice(11, 12).trim() || '?'); const rest = line.slice(19).trim(); if (!rest) continue; const resNames = rest.split(/\s+/).filter(Boolean); if (!seqresByChain.has(chain)) seqresByChain.set(chain, []); seqresByChain.get(chain).push(...resNames); } if (!seqresByChain.size) { const seen = new Set(); for (const line of raw.split('\n')) { if (!/^ATOM /.test(line)) continue; const resn = line.slice(17, 20).trim().toUpperCase(); const chain = (line.slice(21, 22).trim() || '?'); const resi = line.slice(22, 27).trim(); if (!resn || !resi) continue; const key = `${chain}:${resi}`; if (seen.has(key)) continue; seen.add(key); if (!seqresByChain.has(chain)) seqresByChain.set(chain, []); seqresByChain.get(chain).push(resn); } } const records = []; for (const [chain, names] of seqresByChain.entries()) { const t = classifyResidueList3(names); if (!t) continue; const seq = residues3ToSeq(names, t); if (!seq) continue; records.push({ header: `pdb chain ${chain} ${t}`, raw_sequence: seq, sequence: seq }); } return records; } function parseMmcifSequenceRecords(text) { const raw = String(text || '').replace(/\r\n/g, '\n'); if (!/_entity_poly\.pdbx_seq_one_letter_code(?:_can)?/.test(raw)) return []; const records = []; const blocks = raw.match(/\n;[\s\S]*?\n;/g) || []; for (const blk of blocks) { const seqText = blk.slice(2, -2).trim(); const compact = seqText.replace(/\s+/g, '').replace(/\(.+?\)/g, ''); if (!compact) continue; const looksPolymer = /^[A-Za-z*]+$/.test(compact) && compact.length >= 8; if (!looksPolymer) continue; records.push({ header: 'mmcif polymer sequence', raw_sequence: compact, sequence: compact }); } const seen = new Set(); return records.filter(r => { const k = r.sequence; if (!k || seen.has(k)) return false; seen.add(k); return true; }); } function parseImportedMoleculeRecords(text, fileDetail = {}) { const raw = String(text || ''); const ext = String(fileDetail?.extension || getFileExtension(fileDetail?.name || '')).toLowerCase(); const fmt = sniffImportFormat(raw, ext); if (fmt === 'pdb') { return parsePdbSequenceRecords(raw); } if (fmt === 'cif') { return parseMmcifSequenceRecords(raw); } return parseFastaRecords(raw); } function syncViciLookupForMolType(block){ const vici = block?.querySelector('.vici-lookup'); const typeSel = block?.querySelector('.mol-type'); const eyeBtn = block?.querySelector('.vici-toggle-btn'); if (!vici || !typeSel || !window.ViciLookup) return; const t = typeSel.value; const suppressVis = block?.dataset?.viciSuppressVis === '1'; if (t === 'glycan'){ ViciLookup.clear(vici, { keepTarget: true }); if (!suppressVis) viciSlideCollapse(vici); if (eyeBtn){ eyeBtn.disabled = true; eyeBtn.classList.remove('active'); eyeBtn.setAttribute('aria-expanded','false'); eyeBtn.innerHTML = EYE_SVG; eyeBtn.style.opacity = "0.5"; eyeBtn.style.cursor = "not-allowed"; } return; } if (eyeBtn){ eyeBtn.disabled = false; eyeBtn.style.opacity = "1"; eyeBtn.style.cursor = "pointer"; } if (t === 'protein') ViciLookup.setMode(vici, 'protein'); else if (t === 'dna') ViciLookup.setMode(vici, 'nucleotide', 'dna'); else if (t === 'rna') ViciLookup.setMode(vici, 'nucleotide', 'rna'); else if (t === 'ligand') ViciLookup.setMode(vici, 'smiles'); else ViciLookup.setMode(vici, 'protein'); // Key part: during programmatic clears, do not expand or collapse anything if (suppressVis) return; const userOpen = !!eyeBtn?.classList.contains('active'); if (userOpen){ viciSlideExpand(vici); } else { vici.dataset.viciCollapsed = "1"; vici.classList.remove('open'); vici.style.display = 'none'; vici.style.maxHeight = '0px'; vici.style.opacity = '0'; } } function parseFastaRecords(text){ const raw = String(text || '').replace(/\r\n/g, '\n').trim(); if (!raw) return []; if (!raw.includes('>')) { const single = raw.trim(); return [{ header: '', raw_sequence: single, sequence: single.replace(/\s+/g, '') }]; } const lines = raw.split('\n'); const records = []; let current = null; const finalizeCurrent = () => { if (!current) return; const rawSeq = (current._lines || []).join('\n').trim(); current.raw_sequence = rawSeq; current.sequence = rawSeq.replace(/\s+/g, ''); delete current._lines; if (current.raw_sequence || current.sequence) records.push(current); }; for (const lineRaw of lines){ const line = lineRaw.replace(/\r/g, ''); const trimmed = line.trim(); if (!trimmed) continue; if (trimmed.startsWith('>')){ finalizeCurrent(); current = { header: trimmed.slice(1).trim(), _lines: [] }; continue; } if (trimmed.startsWith(';')) continue; if (!current) current = { header: '', _lines: [] }; current._lines.push(line); } finalizeCurrent(); return records.filter(r => (r.raw_sequence || r.sequence)); } function normalizeSequenceForMolType(type, value){ let v = String(value || '').trim(); if (type === 'protein') { return v.replace(/\s+/g, '').toUpperCase(); } if (type === 'dna') { return v.replace(/\s+/g, '').toUpperCase().replace(/U/g, 'T'); } if (type === 'rna') { return v.replace(/\s+/g, '').toUpperCase().replace(/T/g, 'U'); } return v; } function getOwningMoleculeBlockFromDetail(detail){ const src = detail?.block; if (!src || !src.closest) return null; if (src.classList?.contains('molecule-block')) return src; return src.closest('.molecule-block'); } function getChaiMoleculeBlocks(){ return Array.from(document.querySelectorAll('#chai-ui .molecule-block:not([data-removing])')); } function ensureChaiMoleculeCount(minCount){ let blocks = getChaiMoleculeBlocks(); while (blocks.length < minCount) { if (typeof addMolecule !== 'function') break; addMolecule({ animate:false }); blocks = getChaiMoleculeBlocks(); } return blocks; } function setMoleculeType(block, type){ const sel = block?.querySelector('.mol-type'); if (!sel) return; if (sel.value !== type){ sel.value = type; sel.dispatchEvent(new Event('change', { bubbles:true })); } } function setMoleculeInputValue(block, value){ const seqInput = block?.querySelector('.mol-sequence'); if (!seqInput) return false; seqInput.value = value; seqInput.dispatchEvent(new Event('input', { bubbles:true })); seqInput.dispatchEvent(new Event('change', { bubbles:true })); return true; } function readTextFile(file) { if (!file) return Promise.reject(new Error('No file')); if (typeof file.text === 'function') return file.text(); return new Promise((resolve, reject) => { const fr = new FileReader(); fr.onload = () => resolve(String(fr.result || '')); fr.onerror = () => reject(fr.error || new Error('Failed to read file')); fr.readAsText(file); }); } function attachMoleculeDropImport(block) { if (!block) return; if (block.dataset.dropImportBound === '1') return; block.dataset.dropImportBound = '1'; const ta = block.querySelector('.mol-sequence'); if (!ta) return; const clearHover = () => block.classList.remove('file-drag-over'); const hasFiles = (e) => { const dt = e.dataTransfer; if (!dt) return false; if (dt.files && dt.files.length) return true; return Array.from(dt.types || []).includes('Files'); }; const onDragOver = (e) => { if (!hasFiles(e)) return; e.preventDefault(); e.stopPropagation(); if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; block.classList.add('file-drag-over'); }; const onDragEnter = (e) => { if (!hasFiles(e)) return; e.preventDefault(); e.stopPropagation(); block.classList.add('file-drag-over'); }; const onDragLeave = (e) => { if (!block.contains(e.relatedTarget)) clearHover(); }; const onDrop = async (e) => { const files = Array.from(e.dataTransfer?.files || []); if (!files.length) return; // only handle actual files here e.preventDefault(); e.stopPropagation(); clearHover(); const file = files[0]; const ext = getFileExtension(file.name); const allowed = new Set([ ...CHAI_IMPORT_FASTA_EXTS, ...CHAI_IMPORT_STRUCT_EXTS ]); if (ext && !allowed.has(ext)) { showToast?.('error', 'Unsupported file type. Drop FASTA or PDB text files.'); return; } try { const text = await readTextFile(file); applyFileToChaiMoleculeBlocks(block, { name: file.name, extension: ext, fileContent: text }); } catch (err) { console.error(err); showToast?.('error', 'Could not read dropped file.'); } }; [block, ta].forEach((el) => { el.addEventListener('dragover', onDragOver); el.addEventListener('dragenter', onDragEnter); el.addEventListener('dragleave', onDragLeave); el.addEventListener('drop', onDrop); }); } function bindMoleculeDropImportsInChaiUI() { getChaiMoleculeBlocks().forEach(attachMoleculeDropImport); } function applyFileToChaiMoleculeBlocks(moleculeBlock, fileDetail){ if (!moleculeBlock || !fileDetail) return; const startType = (moleculeBlock.querySelector('.mol-type')?.value || 'protein').toLowerCase(); const rawText = String(fileDetail.fileContent || ''); if (!rawText.trim()){ showToast?.('error', 'Selected file is empty.'); return; } const detectedFmt = sniffImportFormat(rawText, fileDetail?.extension || getFileExtension(fileDetail?.name || '')); if (detectedFmt === 'cif') { showToast?.('error', 'mmCIF import is temporarily disabled. Please use FASTA or PDB.'); return; } const records = parseImportedMoleculeRecords(rawText, fileDetail); if (!records.length){ if (detectedFmt === 'pdb' || detectedFmt === 'cif') { showToast?.('error', `No polymer sequence could be extracted from this ${detectedFmt.toUpperCase()} file.`); } else { showToast?.('error', 'Selected file has no readable FASTA records.'); } return; } let blocks = getChaiMoleculeBlocks(); const startIdx = blocks.indexOf(moleculeBlock); if (startIdx < 0){ showToast?.('error', 'Could not find target molecule block.'); return; } const neededCount = startIdx + records.length; blocks = ensureChaiMoleculeCount(neededCount); if (blocks.length < neededCount){ showToast?.('error', 'Could not create enough molecule blocks for all records.'); return; } let filled = 0; const assigned = []; for (let i = 0; i < records.length; i++){ const rec = records[i]; const targetBlock = blocks[startIdx + i]; if (!targetBlock) continue; const inferredType = inferMolTypeFromRecord(rec, startType); setMoleculeType(targetBlock, inferredType); const value = normalizeRecordValueForType(inferredType, rec); if (!value) continue; const ok = setMoleculeInputValue(targetBlock, value); if (ok) { filled++; assigned.push({ chain: targetBlock.querySelector('.mol-chain')?.value || '?', type: inferredType, len: value.length }); } } if (filled === 0){ showToast?.('error', 'No records were inserted.'); return; } if (records.length === 1){ const a = assigned[0]; showToast?.('success', `Inserted 1 record as ${a?.type || startType}.`); } else { const summary = assigned.map(a => `${a.chain}:${a.type}`).join(', '); showToast?.('success', `Inserted ${filled} records (${summary}).`); } updateActionVisibility?.(); } if (!window.__chaiViciFileSelectedBound){ window.__chaiViciFileSelectedBound = true; window.addEventListener('vici:file-selected', (event) => { const detail = event?.detail; if (!detail) return; const moleculeBlock = getOwningMoleculeBlockFromDetail(detail); if (!moleculeBlock) return; if (!moleculeBlock.closest('#chai-ui')) return; const ext = String(detail.extension || '').toLowerCase(); const allowed = new Set([ 'fasta', 'fa', 'faa', 'fna', 'fas', 'fsa', 'seq', 'txt', 'pdb', 'ent' ]); if (ext && !allowed.has(ext)){ showToast?.('error', 'Please choose a FASTA or PDB text file.'); return; } try { if (event.cancelable) event.preventDefault(); } catch {} applyFileToChaiMoleculeBlocks(moleculeBlock, detail); }); } function refreshMoleculeChains() { const blocks = document.querySelectorAll(".molecule-block:not([data-removing])"); blocks.forEach((block, i) => { const letter = String.fromCharCode(65 + i); block.querySelector(".mol-label").innerText = `Molecule ${letter}`; block.querySelector(".mol-chain").value = letter; }); updateAllChainDropdowns(); } function moveMolecule(block, dir) { const container = document.getElementById("molecule-container"); const siblings = Array.from(container.children); const index = siblings.indexOf(block); if (dir === "up" && index > 0) { container.insertBefore(block, siblings[index - 1]); } else if (dir === "down" && index < siblings.length - 1) { container.insertBefore(siblings[index + 1], block); } refreshMoleculeChains(); } function getChainLetters() { return Array.from(document.querySelectorAll(".molecule-block:not([data-removing]) .mol-chain")) .map(inp => inp.value) .filter(Boolean); } function populateChainSelect(sel) { if (!sel) return; const prev = sel.value; const letters = getChainLetters(); sel.innerHTML = ""; letters.forEach(L => { const opt = document.createElement("option"); opt.value = L; opt.textContent = L; sel.appendChild(opt); }); if (letters.includes(prev)) sel.value = prev; } function updateAllChainDropdowns() { document.querySelectorAll(".restraint-block .chain1, .restraint-block .chain2") .forEach(populateChainSelect); } function restraintTypeChanged(block) { const type = block.querySelector(".restraint-type").value; const res1Wrap = block.querySelector(".res1-wrap"); const distWrap = block.querySelector(".distance-wrap"); const res1 = block.querySelector(".res1"); const res2 = block.querySelector(".res2"); if (type === "pocket") { res1Wrap.style.display = "none"; distWrap.style.display = ""; if (res1) res1.value = ""; res2.placeholder = "Residue 2 (e.g., D57)"; } else if (type === "covalent") { res1Wrap.style.display = ""; distWrap.style.display = "none"; res1.placeholder = "Residue 1 (e.g., N436@N)"; res2.placeholder = "Residue 2 (e.g., @C1)"; } else { res1Wrap.style.display = ""; distWrap.style.display = ""; res1.placeholder = "Residue 1 (e.g., A123)"; res2.placeholder = "Residue 2 (e.g., B45)"; } } function onMolTypeChange(e) { const block = e.target.closest(".molecule-block"); const ta = block.querySelector(".mol-sequence"); const t = e.target.value; if (t === "protein") { ta.placeholder = "Enter amino-acid sequence (1-letter; e.g., MKTIIALSYI...)"; } else if (t === "dna") { ta.placeholder = "Enter DNA sequence (A/C/G/T; e.g., ATGGCCATTGTA...)"; } else if (t === "rna") { ta.placeholder = "Enter RNA sequence (A/C/G/U; e.g., AUGGCCAUGG...)"; } else if (t === "ligand") { ta.placeholder = "Enter SMILES string (e.g., Cn1cnc2n(C)c(=O)n(C)c(=O)c12)"; } else if (t === "glycan") { ta.placeholder = "Enter CCD glycan string (e.g., NAG)"; } else { ta.placeholder = "Enter sequence here..."; } const sanitizeBio = (val) => val.replace(/\s+/g, "").toUpperCase(); if (ta._bioSanitizer) { ta.removeEventListener("input", ta._bioSanitizer); ta._bioSanitizer = null; } if (t === "protein" || t === "dna" || t === "rna") { const handler = (evt) => { const cur = evt.target.value; const cleaned = sanitizeBio(cur); if (cur !== cleaned) { const selStart = evt.target.selectionStart; const delta = cleaned.length - cur.length; evt.target.value = cleaned; try { evt.target.setSelectionRange(selStart + delta, selStart + delta); } catch {} } }; ta.addEventListener("input", handler); ta._bioSanitizer = handler; } syncViciLookupForMolType(block); } function addMolecule(opts = {}) { const animate = opts.animate !== false; const container = document.getElementById("molecule-container"); const index = container.querySelectorAll('.molecule-block:not([data-removing])').length; const chainId = String.fromCharCode(65 + index); const uidChain = `help-chain-${index}-${Math.random().toString(36).slice(2,7)}`; const uidType = `help-type-${index}-${Math.random().toString(36).slice(2,7)}`; const uidInput = `help-input-${index}-${Math.random().toString(36).slice(2,7)}`; const div = document.createElement("div"); div.className = "field-group molecule-block dockq-slide"; div.innerHTML = `
`; container.appendChild(div); attachMoleculeDropImport(div); attachMolAutoResize(div); setSlideMax(div); if (animate){ requestAnimationFrame(() => { div.classList.add('open'); setSlideMax(div); }); } else { div.classList.add('open','no-animate'); setSlideMax(div); requestAnimationFrame(() => div.classList.remove('no-animate')); } const typeSelect = div.querySelector(".mol-type"); typeSelect.addEventListener("change", onMolTypeChange); typeSelect.dispatchEvent(new Event("change")); window.ViciLookup?.init?.(div); updateMolLookupState(div); refreshMoleculeChains(); updateActionVisibility?.(); } function ensureAtLeastOneMoleculeBlock(opts = {}) { const { animate = false } = opts; const container = document.getElementById('molecule-container'); if (!container) return; const blocks = getChaiMoleculeBlocks(); if (blocks.length === 0 && typeof addMolecule === 'function') { addMolecule({ animate }); refreshMoleculeChains?.(); updateActionVisibility?.(); } } function hardCollapseChaiVici(panel, { duration = 0, afterClose } = {}) { if (!panel) { try { afterClose?.(); } catch (err) { console.error(err); } return; } panel.dataset.viciCollapsed = '1'; panel.classList.remove('open'); const finish = () => { panel.style.display = 'none'; panel.style.maxHeight = '0px'; panel.style.opacity = '0'; panel.style.overflow = 'hidden'; panel.style.willChange = ''; panel.style.transition = ''; try { afterClose?.(); } catch (err) { console.error(err); } }; if (panel._chaiViciCollapseTimer) { clearTimeout(panel._chaiViciCollapseTimer); panel._chaiViciCollapseTimer = null; } if (duration <= 0) { finish(); return; } const computed = window.getComputedStyle(panel); const alreadyHidden = computed.display === 'none' || (panel.offsetHeight === 0 && panel.scrollHeight === 0); if (alreadyHidden) { finish(); return; } const startHeight = Math.max(panel.scrollHeight, panel.offsetHeight); panel.style.display = 'block'; panel.style.overflow = 'hidden'; panel.style.willChange = 'max-height, opacity'; panel.style.transition = 'none'; panel.style.maxHeight = `${startHeight}px`; panel.style.opacity = computed.opacity === '0' ? '1' : computed.opacity; void panel.offsetHeight; let done = false; const cleanup = () => { if (done) return; done = true; panel.removeEventListener('transitionend', onEnd); if (panel._chaiViciCollapseTimer) { clearTimeout(panel._chaiViciCollapseTimer); panel._chaiViciCollapseTimer = null; } finish(); }; const onEnd = (e) => { if (e.target !== panel) return; if (e.propertyName !== 'max-height') return; cleanup(); }; panel.addEventListener('transitionend', onEnd); panel.style.transition = `max-height ${duration}ms ease, opacity ${duration}ms ease`; panel.style.maxHeight = '0px'; panel.style.opacity = '0'; panel._chaiViciCollapseTimer = setTimeout(cleanup, duration + 80); } function clearMoleculeViciContents(block, { clearTarget = false } = {}) { if (!block) return; const vici = block.querySelector('.vici-lookup'); if (!vici) return; try { window.ViciLookup?.clear?.(vici, { keepInput: false, keepTarget: !clearTarget }); } catch (err) { console.error(err); } updateMolLookupState?.(block); } function collapseMoleculeVici(block, { clearTarget = false, duration = 240 } = {}) { return new Promise((resolve) => { if (!block) { resolve(); return; } const vici = block.querySelector('.vici-lookup'); const eyeBtn = block.querySelector('.vici-toggle-btn'); if (eyeBtn) { eyeBtn.classList.remove('active'); eyeBtn.setAttribute('aria-expanded', 'false'); eyeBtn.innerHTML = EYE_SVG; } hardCollapseChaiVici(vici, { duration, afterClose: () => { clearMoleculeViciContents(block, { clearTarget }); block.classList.remove('lookup-open'); updateMolLookupState?.(block); resolve(); } }); }); } async function clearMoleculeBlock(block, { resetType = true, collapseLookup = false, lookupDuration = 240 } = {}) { if (!block) return; // Preserve current lookup open state and suppress any visibility changes during programmatic resets block.dataset.viciSuppressVis = '1'; try { if (collapseLookup) { await collapseMoleculeVici(block, { clearTarget: true, duration: lookupDuration }); } else { clearMoleculeViciContents(block, { clearTarget: true }); } const typeSel = block.querySelector('.mol-type'); const seqEl = block.querySelector('.mol-sequence'); // Avoid forcing a change event if it's already protein (prevents any extra UI churn) if (resetType && typeSel && typeSel.value !== 'protein') { 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 })); } block.classList.remove('pulse-clear'); void block.offsetWidth; block.classList.add('pulse-clear'); } finally { delete block.dataset.viciSuppressVis; } updateMolLookupState?.(block); setSlideMax?.(block); refreshMoleculeChains?.(); updateActionVisibility?.(); } async function removeMolecule(btn){ const block = btn.closest('.molecule-block'); if (!block) return; if (getChaiMoleculeBlocks().length <= 1) { await clearMoleculeBlock(block, { resetType: true, collapseLookup: false }); return; } clearMoleculeViciContents(block, { clearTarget: false }); await slideRemove(block); } function addRestraint() { const container = document.getElementById("restraint-container"); const idx = container.querySelectorAll('.restraint-block:not([data-removing])').length; const base = `rest-${idx}-${Math.random().toString(36).slice(2,6)}`; const ids = { type: `${base}-type`, chain1: `${base}-chain1`, res1: `${base}-res1`, chain2: `${base}-chain2`, res2: `${base}-res2`, dist: `${base}-dist`, }; const helps = { type: `${base}-help-type`, chain1: `${base}-help-chain1`, res1: `${base}-help-res1`, chain2: `${base}-help-chain2`, res2: `${base}-help-res2`, dist: `${base}-help-dist`, }; const div = document.createElement("div"); div.className = "field-group restraint-block dockq-slide"; div.innerHTML = `
`; 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: `` }, '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. ` }, 'templates': { title: 'templates', html: `Template search mode. Requires msa="mmseq2".` }, 'recycles': { title: 'recycles', html: `Number of recycle iterations inside Chai-1. Higher can improve accuracy but increases runtime.
Supported range: 1–20.` }, 'diffusion-steps': { title: 'diffusion_steps', html: `Number of denoising steps in diffusion sampling. Higher generally improves quality but is slower.
Supported range: 100–2000.` }, 'seed': { title: 'seed', html: `Integer random seed controlling stochastic sampling. Keep the same seed to reproduce runs (given identical inputs).` }, 'samples': { title: 'samples', html: `How many independent structures to generate in this run.
Supported range: 1–10.` }, 'mol-type': { title: 'molecules[].type', html: `Chain/input type. Allowed: protein | dna | rna | ligand | glycan.` }, 'chain-id': { title: 'molecules[].chain_id', html: 'Chain label (A, B, C, …). Used to connect molecules to restraints.' }, 'sequence': { title: 'molecules[].sequence', html: `Input sequence string: ` }, 'restraint-type': { title: 'restraints[].type', html: `Restraint mode: ` }, 'restraint-chain': { title: 'restraints[].chain{1,2}', html: 'Chain IDs referenced by this restraint. Must match your molecules’ chain_id values.' }, 'restraint-res': { title: 'restraints[].res{1,2}', html: `Residue or atom selector. Examples: R84, C217@SG, @C1.` }, 'restraint-distance': { title: 'restraints[].distance', html: `Distance cutoff in Ångström for contact and pocket. Smaller values force tighter proximity. Not used for covalent.` } }; function cloneDefaultJob(){ return JSON.parse(JSON.stringify(DEFAULT_API_JOB)); } const API_TEMPLATES = { python: (payloadText, payloadHtml) => ({ text: [ '# POST a Chai-1 job (Python)', '# Set TOKEN_ID and TOKEN_SECRET to your values.', 'import json', 'import requests', '', `API_URL = "${CHAI_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 Chai-1 job (Python)', '# Set TOKEN_ID and TOKEN_SECRET to your values.', 'import json', 'import requests', '', `API_URL = "${escapeHtml(CHAI_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') }), curl: (payloadText, payloadHtml) => ({ text: [ '# POST a Chai-1 job (cURL)', '# Set TOKEN_ID and TOKEN_SECRET to your values.', '', `curl -X POST "${CHAI_API_ENDPOINT}" \\`, ' -H "Content-Type: application/json" \\', ' -H "Token-ID: " \\', ' -H "Token-Secret: " \\', ` --data-binary @- <<'__VICI_PAYLOAD_JSON__'`, payloadText, '__VICI_PAYLOAD_JSON__' ].join('\n'), html: [ '# POST a Chai-1 job (cURL)', '# Set TOKEN_ID and TOKEN_SECRET to your values.', '', `curl -X POST "${escapeHtml(CHAI_API_ENDPOINT)}" \\`, ' -H "Content-Type: application/json" \\', ' -H "Token-ID: <TOKEN_ID>" \\', ' -H "Token-Secret: <TOKEN_SECRET>" \\', ` --data-binary @- <<'__VICI_PAYLOAD_JSON__'`, payloadHtml, `__VICI_PAYLOAD_JSON__` ].join('\n') }), javascript: (payloadText, payloadHtml) => ({ text: [ '// POST a Chai-1 job (JavaScript)', '// Set TOKEN_ID and TOKEN_SECRET to your values.', '', '(async () => {', ` const API_URL = "${CHAI_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 Chai-1 job (JavaScript)', '// Set TOKEN_ID and TOKEN_SECRET to your values.', '', '(async () => {', ` const API_URL = "${escapeHtml(CHAI_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 buildApiSnippet(job, lang){ const payloadBlock = stringifyChaiPayload(job); if (!payloadBlock) return null; const tpl = API_TEMPLATES[lang]; if (!tpl) return null; return tpl(payloadBlock.text, payloadBlock.html); } 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'); document.body.removeChild(ta); ok ? resolve() : reject(); } catch (e) { reject(e); } }); } function escapeHtml(str){ return String(str || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function stringifyChaiPayload(job){ if (!job) return null; const markers = []; const mark = (val, defKey, type='str') => { const token = `__CHAI_MARK_${markers.length}__`; markers.push({ token, value: val ?? '', defKey, type }); return token; }; const payload = { workflow_name: mark(job.name || 'my_chai_run', 'workflow-name'), chai1: { name: mark(job.name || 'my_chai_run', 'workflow-name'), msa: mark(job.msa || 'mmseq2', 'msa'), templates: mark(job.templates || 'none', 'templates'), recycles: mark(job.recycles, 'recycles', 'num'), diffusion_steps: mark(job.diffusion_steps, 'diffusion-steps', 'num'), seed: mark(job.seed, 'seed', 'num'), samples: mark(job.samples, 'samples', 'num'), molecules: (job.molecules || []).map((m) => ({ type: mark(m.type, 'mol-type'), chain_id: mark(m.chain_id, 'chain-id'), sequence: mark(m.sequence, 'sequence'), })), restraints: (job.restraints || []).map((r) => { const base = { type: mark(r.type, 'restraint-type'), chain1: mark(r.chain1, 'restraint-chain'), chain2: mark(r.chain2, 'restraint-chain'), }; if (r.res1) base.res1 = mark(r.res1, 'restraint-res'); if (r.res2) base.res2 = mark(r.res2, 'restraint-res'); if (typeof r.distance !== 'undefined' && r.type !== 'covalent') { base.distance = mark(String(r.distance), 'restraint-distance', 'num'); } return base; }) } }; const jsonText = JSON.stringify(payload, null, 2); let text = jsonText; let html = escapeHtml(jsonText); markers.forEach((m) => { const quoted = `"${m.token}"`; const quotedHtml = `"${m.token}"`; const isNum = (m.type === 'num'); // For strings, JSON.stringify returns a valid JSON string literal including quotes and escaped newlines. const jsonLiteral = isNum ? String(m.value) : JSON.stringify(String(m.value)); const textVal = jsonLiteral; const htmlVal = isNum ? `${escapeHtml(String(m.value))}` : `${escapeHtml(jsonLiteral)}`; text = text.split(quoted).join(textVal); html = html.split(quotedHtml).join(htmlVal); }); return { text, html }; } function hydrateApiHelp(){ document.querySelectorAll('.help-popover[data-def] .help-popover__body').forEach((body) => { const key = body.parentElement?.dataset.def; const def = API_DEF_CONTENT[key]; if (!def) return; const title = def.title ? `
${def.title}
` : ''; body.innerHTML = `${title}${def.html || ''}`; }); } let apiGlossaryBound = false; let apiFlyout = null; let apiHideTimer = null; function cancelApiHide(){ if (apiHideTimer){ clearTimeout(apiHideTimer); apiHideTimer = null; } } function scheduleApiHide(){ cancelApiHide(); apiHideTimer = setTimeout(() => hideApiFlyout(), 140); } function getApiFlyout(){ if (apiFlyout) return apiFlyout; apiFlyout = document.createElement('div'); apiFlyout.className = 'help-popover api-def-flyout'; apiFlyout.setAttribute('role', 'tooltip'); apiFlyout.setAttribute('aria-hidden', 'true'); apiFlyout.innerHTML = '
'; document.body.appendChild(apiFlyout); apiFlyout.addEventListener('mouseenter', cancelApiHide); apiFlyout.addEventListener('mouseleave', scheduleApiHide); return apiFlyout; } function hideApiFlyout(){ cancelApiHide(); if (!apiFlyout) return; apiFlyout.classList.remove('open'); apiFlyout.setAttribute('aria-hidden','true'); } function showApiFlyout(target, key){ const def = API_DEF_CONTENT[key]; if (!def) return; const fly = getApiFlyout(); const title = def.title ? `
${def.title}
` : ''; fly.querySelector('.help-popover__body').innerHTML = `${title}${def.html || ''}`; fly.classList.add('open'); fly.removeAttribute('aria-hidden'); const rect = target.getBoundingClientRect(); const popRect = fly.getBoundingClientRect(); const margin = 10; const vw = window.innerWidth; const vh = window.innerHeight; const centerX = rect.left + rect.width / 2; let left = centerX - popRect.width / 2; left = Math.max(margin, Math.min(left, vw - popRect.width - margin)); let top = rect.bottom + margin; let above = false; if (top + popRect.height > vh - margin) { top = rect.top - popRect.height - margin; above = true; } top = Math.max(margin, Math.min(top, vh - popRect.height - margin)); fly.style.left = `${left}px`; fly.style.top = `${top}px`; fly.classList.toggle('pop-above', above); const arrow = fly.querySelector('.help-popover__arrow'); if (arrow){ let ax = centerX - left - 6; ax = Math.max(6, Math.min(ax, popRect.width - 18)); arrow.style.left = `${ax}px`; } } function initApiGlossaryHover(){ if (apiGlossaryBound) return; apiGlossaryBound = true; document.addEventListener('mouseover', (e) => { const el = e.target.closest('[data-def-ref]'); if (!el || !el.closest('#chai-api-code')) return; cancelApiHide(); showApiFlyout(el, el.dataset.defRef); }); document.addEventListener('mouseout', (e) => { const from = e.target.closest('[data-def-ref]'); if (!from || !from.closest('#chai-api-code')) return; const to = e.relatedTarget; if (to && (to.closest('.api-def-flyout') || to.closest('[data-def-ref]'))) return; scheduleApiHide(); }); } function setApiLang(lang, opts = {}){ if (!API_LANGS.includes(lang)) return; currentApiLang = lang; document.querySelectorAll('.api-lang-tab').forEach((btn) => { const active = btn.dataset.lang === lang; btn.classList.toggle('is-active', active); btn.setAttribute('aria-selected', active ? 'true' : 'false'); btn.disabled = active; }); renderChaiApiSnippet(opts); } function renderChaiApiSnippet(opts = {}){ const codeEl = document.getElementById('chai-api-code'); if (!codeEl) return; const { job } = (typeof buildChaiJob === 'function') ? buildChaiJob({ requireName: false, requireSequences: false, validate: false, toast: opts.toast }) : { job: null }; const usedFormData = !opts.forceDefault && job && Array.isArray(job.molecules) && job.molecules.length > 0; const sourceJob = usedFormData ? job : cloneDefaultJob(); const snippet = buildApiSnippet(sourceJob, currentApiLang); if (!snippet) { codeEl.textContent = 'Unable to generate snippet.'; currentApiSnippet = { text: '', html: '' }; return; } codeEl.innerHTML = snippet.html; currentApiSnippet = snippet; if (opts.toast) { const msg = opts.forceDefault ? 'Reset API snippet to defaults.' : usedFormData ? 'Synced API snippet from form.' : 'Loaded default API snippet.'; showToast?.('success', msg); } } 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 resetChaiApi(btn){ pulseBtn(btn, 'pulse-red'); renderChaiApiSnippet({ forceDefault: true, toast: true }); } function copyChaiApi(btn){ const text = currentApiSnippet.text?.trim(); if (!text) { showToast?.('error', 'Nothing to copy yet. Sync from the form first.'); return; } pulseBtn(btn, 'pulse-green'); copyTextRobust(text) .then(() => showToast?.('success', 'Copied API snippet.')) .catch(() => showToast?.('error', 'Copy failed. Select code and copy manually.')); } function syncChaiApi(btn){ pulseBtn(btn, 'pulse-blue'); renderChaiApiSnippet({ toast: true }); } function initApiTabClicks(){ const row = document.querySelector('.api-controls-row'); if (!row) return; row.addEventListener('click', (e) => { const btn = e.target.closest('.api-lang-tab'); if (!btn) return; setApiLang(btn.dataset.lang); }); } window.addEventListener('DOMContentLoaded', () => { const chaiRoot = document.getElementById('chai-ui'); if (chaiRoot) { chaiRoot.addEventListener('dragover', (e) => { const isFile = Array.from(e.dataTransfer?.types || []).includes('Files'); if (isFile) e.preventDefault(); }); chaiRoot.addEventListener('drop', (e) => { const isFile = Array.from(e.dataTransfer?.types || []).includes('Files'); if (isFile) e.preventDefault(); }); } }); window.addEventListener('DOMContentLoaded', () => { initApiTabClicks(); hydrateApiHelp(); initApiGlossaryHover(); setApiLang('python'); renderChaiApiSnippet(); });