nonzeroexit commited on
Commit
90bc2d9
·
verified ·
1 Parent(s): f5f7b90

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +37 -211
index.html CHANGED
@@ -1392,21 +1392,21 @@ kbd {
1392
  <i class="fas fa-flask"></i> Try Example
1393
  </button>
1394
  <div class="example-dropdown" id="example-dropdown">
1395
- <div class="example-item" data-seq="MEKAALIFIGLLLFSTCTQILAQSCNNDSDCTNLKCATKNIKCEQNKCQCLDERYIRAISLNTRSPRCNVQSCIDHCKAIGEVIYVCFTYHCYCRKPPM" data-label="AMP · Long (99 aa)" data-type="amp">
1396
- <div class="example-item-title"><span class="example-badge badge-amp">AMP</span> Long peptide 99 aa</div>
1397
- <div class="example-item-seq">MEKAALIFIGLLLFSTCTQILAQSCNNDSDCTNLKCATK…</div>
1398
  </div>
1399
- <div class="example-item" data-seq="SLQGGAPNFPQPSQQNGGWQVSPDLGRDDKGNTRGQIEIQ" data-label="AMP · Short (41 aa)" data-type="amp">
1400
- <div class="example-item-title"><span class="example-badge badge-amp">AMP</span> Short peptide41 aa</div>
1401
- <div class="example-item-seq">SLQGGAPNFPQPSQQNGGWQVSPDLGRDDKGNTRGQIEIQ</div>
1402
  </div>
1403
- <div class="example-item" data-seq="MKSLLPLAILAALAVAALCYESHESMESYEVSPFTTRRNANTFISPQQRWHAKAQERVRELN" data-label="Non-AMP · Long (61 aa)" data-type="nonamp">
1404
- <div class="example-item-title"><span class="example-badge badge-nonamp">Non-AMP</span> Long peptide61 aa</div>
1405
- <div class="example-item-seq">MKSLLPLAILAALAVAALCYESHESMESYEVSPFTTRR…</div>
1406
  </div>
1407
- <div class="example-item" data-seq="MKPLKQKVSITLDEDVIKNLKTLAEECDRSLSQYINLILK" data-label="Non-AMP · Short (41 aa)" data-type="nonamp">
1408
- <div class="example-item-title"><span class="example-badge badge-nonamp">Non-AMP</span> Short peptide41 aa</div>
1409
- <div class="example-item-seq">MKPLKQKVSITLDEDVIKNLKTLAEECDRSLSQYINLILK</div>
1410
  </div>
1411
  </div>
1412
  </div>
@@ -1567,10 +1567,6 @@ kbd {
1567
  </div>
1568
  <div class="tab-panel" id="panel-model">
1569
  <div class="card">
1570
- <div class="card-header">
1571
- <div class="card-header-icon"><i class="fas fa-check-circle"></i></div>
1572
- <div class="card-header-title">Classifier Performance Metrics</div>
1573
- <<div class="card">
1574
  <div class="card-header">
1575
  <div class="card-header-icon"><i class="fas fa-check-circle"></i></div>
1576
  <div class="card-header-title">MLP Classifier Performance Metrics</div>
@@ -1594,6 +1590,12 @@ kbd {
1594
  </p>
1595
  </div>
1596
  </div>
 
 
 
 
 
 
1597
  <div class="card-body">
1598
  <p class="prose">Separate regression models predict MIC for each organism. Performance evaluated via MSE (log-scale), R², Pearson correlation, and Kendall's tau.</p>
1599
  <table class="metrics-table">
@@ -1648,10 +1650,10 @@ kbd {
1648
  <table class="metrics-table" style="margin-top:8px;">
1649
  <thead><tr><th>#</th><th>Description</th><th>Expected</th><th>Sequence (truncated)</th></tr></thead>
1650
  <tbody>
1651
- <tr><td>1</td><td>Long (99 aa)</td><td><span class="status-badge green">P-AMP</span></td><td style="font-family:var(--font-mono);font-size:11px;">MEKAALIFIGLLLFSTCTQIL…</td></tr>
1652
- <tr><td>2</td><td>Long (99 aa)</td><td><span class="status-badge red">Non-AMP</span></td><td style="font-family:var(--font-mono);font-size:11px;">MKSLLPLAILAALAVAALCYE…</td></tr>
1653
- <tr><td>3</td><td>Short (51 aa)</td><td><span class="status-badge green">P-AMP</span></td><td style="font-family:var(--font-mono);font-size:11px;">SLQGGAPNFPQPSQQNGGRWQ…</td></tr>
1654
- <tr><td>4</td><td>Short (50 aa)</td><td><span class="status-badge red">Non-AMP</span></td><td style="font-family:var(--font-mono);font-size:11px;">MKPLKQKVSITLDEDVIKNL…</td></tr>
1655
  <tr><td>5</td><td>Invalid chars</td><td><span class="status-badge gray">Rejected</span></td><td style="font-family:var(--font-mono);font-size:11px;">MEKAALIFIG(XX)…</td></tr>
1656
  </tbody>
1657
  </table>
@@ -1698,7 +1700,6 @@ kbd {
1698
  </ul>
1699
  <div class="section-h3">Contact</div>
1700
  <p class="prose">For questions, collaboration inquiries, or feedback: <a href="mailto:epicamp.sup@gmail.com" style="color:var(--ncbi-blue);">epicamp.sup@gmail.com</a></p>
1701
-
1702
  </div>
1703
  </div>
1704
  </div>
@@ -1768,6 +1769,7 @@ kbd {
1768
  </div>
1769
  </main>
1770
  <div class="aa-tooltip-box" id="aa-tooltip"></div>
 
1771
  <div id="demo-modal" class="modal-overlay">
1772
  <div class="modal-box">
1773
  <button class="modal-close" id="modal-close-btn">&times;</button>
@@ -1781,9 +1783,9 @@ kbd {
1781
  <footer>
1782
  <p style="margin-bottom:4px;">© 2025 Bioinformatics and Computational Biology Unit (BCBU) — Zewail City</p>
1783
  <address style="font-style:normal;color:rgba(255,255,255,0.55);font-size:11px;">Ahmed Zewail Street, October Gardens, Giza, Egypt</address>
1784
- <p style="margin-top:6px;font-size:11px;"><a href="/cdn-cgi/l/email-protection#1d786d747e7c706d336e686d5d7a707c7471337e7270" style="color:rgba(160,200,255,0.85);text-decoration:none;"><span class="__cf_email__" data-cfemail="badfcad3d9dbd7ca94c9cfcafaddd7dbd3d694d9d5d7">[email&#160;protected]</span></a></p>
1785
  </footer>
1786
- <script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script type="module">
1787
  import { Client } from "https://esm.sh/@gradio/client";
1788
  /* ── CONFIG ─────────────────────────────────────────────── */
1789
  const GRADIO_PREDICTION_TARGET_ID = "nonzeroexit/AMP-Classifier";
@@ -1791,7 +1793,7 @@ kbd {
1791
  const MAILER_SPACE_ID = "nonzeroexit/AMP-Mailer";
1792
  const HF_TOKEN = null; // ← if Space is Private: paste your HF read token "hf_xxxx"
1793
  // ← if Space is Public: leave as null
1794
- const SHEET_WEBHOOK_URL = 'https://script.google.com/macros/s/AKfycbzF8_W8uwshY2THg7atOc9TmRqerLKAJyZbs9_-hP5kj99LMYTqn2e4Ki8SwNzZo8Oc/exec'; // ← if Space is Public: leave as null
1795
  /* ── AA PROPERTY MAP ─────────────────────────────────────── */
1796
  const AA_PROPS = {
1797
  A:'hydrophobic', V:'hydrophobic', I:'hydrophobic', L:'hydrophobic',
@@ -1871,17 +1873,8 @@ kbd {
1871
  const step1Sub = document.getElementById('step-1-sub');
1872
  const step2Sub = document.getElementById('step-2-sub');
1873
  const step3Sub = document.getElementById('step-3-sub');
1874
- // lineWidths: percent of the connector bar filled at each logical state
1875
- // 0 = nothing, 50 = between step1 and step2 done, 100 = all done
1876
  const LINE_WIDTHS = { 1: '0%', 2: '50%', 3: '100%' };
1877
- /**
1878
- * setStep(n, state, statusMsg)
1879
- * n : which step is NOW the focal point (1, 2, or 3)
1880
- * state : 'active' | 'processing' | 'done' | 'error'
1881
- * statusMsg : text shown in the stepper status bar
1882
- */
1883
  function setStep(n, state, statusMsg) {
1884
- // Mark steps before n as done, step n as state, steps after n as idle
1885
  for (let i = 1; i <= 3; i++) {
1886
  const el = stepEls[i];
1887
  if (!el) continue;
@@ -1891,9 +1884,7 @@ kbd {
1891
  } else if (i === n) {
1892
  el.classList.add(state);
1893
  }
1894
- // steps after n get no class = default idle look
1895
  }
1896
- // Connector line
1897
  if (stepperLine) {
1898
  if (n === 1) stepperLine.style.width = '0%';
1899
  else if (n === 2 && state === 'done') stepperLine.style.width = '50%';
@@ -1922,7 +1913,6 @@ kbd {
1922
  }, duration);
1923
  }
1924
  /* ── SEQUENCE STATS ──────────────────────────────────────── */
1925
- // Approximate monoisotopic residue weights (Da)
1926
  const MW_TABLE = {A:89,R:174,N:132,D:133,C:121,Q:146,E:147,G:75,H:155,I:131,
1927
  L:131,K:146,M:149,F:165,P:115,S:105,T:119,W:204,Y:181,V:117};
1928
  const HYDRO_SET = new Set(['A','V','I','L','M','F','W','P']);
@@ -1931,17 +1921,16 @@ kbd {
1931
  if (!seq || seq.length < 3) { if (seqStatsRow) seqStatsRow.style.display = 'none'; return; }
1932
  if (seqStatsRow) seqStatsRow.style.display = 'flex';
1933
  const n = seq.length;
1934
- let hydro = 0, cation = 0, mw = 18; // 18 = water
1935
  for (const ch of seq) {
1936
  if (HYDRO_SET.has(ch)) hydro++;
1937
  if (CATION_SET.has(ch)) cation++;
1938
- mw += (MW_TABLE[ch] || 110) - 18; // subtract water for each peptide bond
1939
  }
1940
  if (statLength) statLength.textContent = n;
1941
  if (statHydro) statHydro.textContent = Math.round(hydro / n * 100) + '%';
1942
  if (statCation) statCation.textContent = Math.round(cation / n * 100) + '%';
1943
  if (statMw) statMw.textContent = mw.toLocaleString();
1944
- // Char badge colour class
1945
  if (charBadgeWrap) {
1946
  charBadgeWrap.classList.remove('valid','warning','invalid');
1947
  if (n >= 10 && n <= 100 && !/[^ACDEFGHIKLMNPQRSTVWY]/i.test(seq)) {
@@ -1960,24 +1949,20 @@ kbd {
1960
  e.stopPropagation();
1961
  exampleDropdown.classList.toggle('open');
1962
  });
1963
- // Close on outside click
1964
  document.addEventListener('click', (e) => {
1965
  if (examplePicker && !examplePicker.contains(e.target)) {
1966
  exampleDropdown.classList.remove('open');
1967
  }
1968
  });
1969
- // Item click
1970
  exampleDropdown.querySelectorAll('.example-item').forEach(item => {
1971
  item.addEventListener('click', () => {
1972
  const seq = item.dataset.seq;
1973
  const label = item.dataset.label;
1974
  const type = item.dataset.type;
1975
  if (!seq) return;
1976
- // Fill sequence
1977
  sequenceInput.value = seq.toUpperCase();
1978
  exampleDropdown.classList.remove('open');
1979
  onSeqInput();
1980
- // Pre-select all bacteria for AMP examples
1981
  if (type === 'amp') {
1982
  micCheckboxes.forEach(cb => { cb.disabled = false; cb.checked = true; });
1983
  }
@@ -2026,13 +2011,10 @@ kbd {
2026
  modalCloseBtn?.addEventListener('click', closeModal);
2027
  demoModal?.addEventListener('click', e => { if (e.target === demoModal) closeModal(); });
2028
  clearAll();
2029
- // Global AA tooltip hide on move away
2030
  document.addEventListener('mousemove', e => {
2031
  if (!e.target.classList.contains('aa-char')) aaTooltip.style.display = 'none';
2032
  });
2033
- // Example picker
2034
  setupExamplePicker();
2035
- // Ctrl+Enter shortcut to submit
2036
  document.addEventListener('keydown', e => {
2037
  if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
2038
  e.preventDefault();
@@ -2042,7 +2024,6 @@ kbd {
2042
  showToast('Submitting via Ctrl+Enter…', 'info', 1800);
2043
  }
2044
  }
2045
- // Escape closes example dropdown
2046
  if (e.key === 'Escape' && exampleDropdown) {
2047
  exampleDropdown.classList.remove('open');
2048
  }
@@ -2062,7 +2043,6 @@ kbd {
2062
  updateMicCheckboxes();
2063
  updateBtnState();
2064
  updateSeqStats(sequenceInput.value.toUpperCase());
2065
- // Auto-clear results dashboard whenever the sequence is changed
2066
  resetDashboard();
2067
  setStep(1, 'active', 'Enter a valid sequence to begin.');
2068
  }
@@ -2107,7 +2087,6 @@ kbd {
2107
  function updateBtnState() {
2108
  const v = validateSequence(sequenceInput.value);
2109
  if (predictBtn) predictBtn.disabled = !v.isValid || !clientInstance;
2110
- // Update step 1 appearance based on validity
2111
  const seq = sequenceInput ? sequenceInput.value : '';
2112
  if (!seq) {
2113
  setStep(1, 'active', 'Enter a valid sequence to begin.');
@@ -2130,7 +2109,6 @@ kbd {
2130
  }
2131
  if (alignViewer) alignViewer.style.display = 'block';
2132
  if (aaCompBarWrap) aaCompBarWrap.style.display = 'block';
2133
- // Ruler
2134
  aaRuler.innerHTML = '';
2135
  for (let i = 0; i < seq.length; i++) {
2136
  const tick = document.createElement('div');
@@ -2138,7 +2116,6 @@ kbd {
2138
  tick.textContent = (i + 1) % 5 === 0 ? (i + 1) : (i === 0 ? '1' : '');
2139
  aaRuler.appendChild(tick);
2140
  }
2141
- // Residues
2142
  aaSeqDisplay.innerHTML = '';
2143
  for (let i = 0; i < seq.length; i++) {
2144
  const ch = seq[i];
@@ -2220,7 +2197,6 @@ kbd {
2220
  }
2221
  function resetDashboard() {
2222
  ampClassOutput.innerHTML = '<div class="class-result-display"><div class="class-label pending">Awaiting input…</div></div>';
2223
- // Reset confidence panel to placeholder (no gauge-fill in initial state)
2224
  confidenceOutput.innerHTML =
2225
  '<div class="gauge-wrap">' +
2226
  '<svg class="gauge-arc-svg" viewBox="0 0 150 80">' +
@@ -2232,9 +2208,6 @@ kbd {
2232
  '<div class="gauge-sub">Model confidence</div>' +
2233
  '</div>';
2234
  micChartOutput.innerHTML = '<div class="mic-empty">MIC results will appear here after analysis. Select bacteria above before submitting.</div>';
2235
- // Reset the report panel WITHOUT destroying #download-link or #email-status-result
2236
- const reportPlaceholder = additionalOutput.querySelector('.report-placeholder');
2237
- // Remove any old placeholder text nodes / divs, but keep the persistent elements
2238
  Array.from(additionalOutput.childNodes).forEach(node => {
2239
  if (node !== downloadLink && node !== emailStatusResult) node.remove();
2240
  });
@@ -2254,7 +2227,7 @@ kbd {
2254
  function setGauge(fraction, color) {
2255
  const arc = 188.5;
2256
  const fill = document.getElementById('gauge-fill');
2257
- if (!fill) return; // gauge SVG may not be in DOM yet
2258
  fill.style.strokeDashoffset = (arc - fraction * arc).toString();
2259
  fill.style.stroke = color;
2260
  }
@@ -2310,17 +2283,14 @@ kbd {
2310
  const v = validateSequence(seq);
2311
  if (!v.isValid) { showError(v.message); return; }
2312
  clearError();
2313
- // UI: loading state
2314
  predictBtn.disabled = true;
2315
  predictBtn.innerHTML = '<i class="fas fa-circle-notch spin"></i> Processing…';
2316
  setStep(2, 'processing', 'Model is analysing your sequence…');
2317
  if (step2Sub) step2Sub.textContent = 'Running…';
2318
- // Show loading in all panels -- preserve persistent elements in additionalOutput
2319
  const loadingHTML = '<div class="loading-pulse"><div class="pulse-dots"><div class="pulse-dot"></div><div class="pulse-dot"></div><div class="pulse-dot"></div></div><span>Analysing...</span></div>';
2320
  ampClassOutput.innerHTML = loadingHTML;
2321
  confidenceOutput.innerHTML = loadingHTML;
2322
  micChartOutput.innerHTML = loadingHTML;
2323
- // Safe reset: remove all children except the persistent link and status divs
2324
  Array.from(additionalOutput.childNodes).forEach(function(node) {
2325
  if (node !== downloadLink && node !== emailStatusResult) node.remove();
2326
  });
@@ -2336,7 +2306,6 @@ kbd {
2336
  elapsed++;
2337
  if (processingInfo) processingInfo.textContent = 'Processing — ' + elapsed + 's elapsed…';
2338
  }, 1000);
2339
- /* ── parse helpers ── */
2340
  let ampLabel = 'Unknown', ampConf = 0;
2341
  let micData = {}, limeFeatures = [];
2342
  try {
@@ -2361,7 +2330,6 @@ kbd {
2361
  if (m) limeFeatures.push({ feature: m[1].trim(), value: parseFloat(m[2]) });
2362
  }
2363
  }
2364
- /* ── Update Classification ── */
2365
  const isAMP = ampLabel.toLowerCase().includes('amp') && !ampLabel.toLowerCase().includes('non-amp');
2366
  ampClassOutput.innerHTML = `
2367
  <div class="class-result-display">
@@ -2372,10 +2340,7 @@ kbd {
2372
  </span>
2373
  </div>
2374
  </div>`;
2375
- /* ── Update Gauge ── */
2376
  const gColor = isAMP ? '#2e7d32' : '#c62828';
2377
- const gOffset = (188.5 - ampConf * 188.5).toFixed(1);
2378
- // Write the SVG first, THEN call setGauge (so gauge-fill exists in DOM)
2379
  confidenceOutput.innerHTML =
2380
  '<div class="gauge-wrap">' +
2381
  '<svg class="gauge-arc-svg" viewBox="0 0 150 80">' +
@@ -2383,14 +2348,12 @@ kbd {
2383
  '<path id="gauge-fill" class="gauge-fill" d="M15,75 A60,60 0 0,1 135,75"' +
2384
  ' stroke="' + gColor + '"' +
2385
  ' stroke-dasharray="188.5"' +
2386
- ' stroke-dashoffset="188.5"/>' + // start at 0, then animate
2387
  '</svg>' +
2388
  '<div class="gauge-value-label" style="color:' + gColor + '">' + (ampConf * 100).toFixed(1) + '%</div>' +
2389
  '<div class="gauge-sub">Model confidence</div>' +
2390
  '</div>';
2391
- // Now gauge-fill is in the DOM — animate it
2392
  requestAnimationFrame(() => setGauge(ampConf, gColor));
2393
- /* ── Update MIC Chart ── */
2394
  const checkedBacteria = [...micCheckboxes].filter(cb => cb.checked).map(cb => {
2395
  return cb.parentElement.querySelector('label em')?.textContent?.trim() || cb.value;
2396
  });
@@ -2411,20 +2374,15 @@ kbd {
2411
  } else {
2412
  micChartOutput.innerHTML = `<div class="mic-empty">${!isAMP ? 'Sequence classified as Non-AMP — MIC prediction not applicable.' : 'No bacteria selected or no MIC predictions returned.'}</div>`;
2413
  }
2414
- /* ── Advance stepper to step 3 ── */
2415
  setStep(3, 'active', 'Analysis complete — results below.');
2416
  if (step3Sub) step3Sub.textContent = ampLabel;
2417
- // Scroll results area into view smoothly
2418
  const ra = document.getElementById('results-area');
2419
  if (ra) ra.scrollIntoView({ behavior: 'smooth', block: 'start' });
2420
  showToast('Classification: ' + ampLabel + ' (' + (ampConf*100).toFixed(1) + '% confidence)', isAMP ? 'success' : 'info', 4000);
2421
- /* ── Enable bacteria checkboxes if AMP ── */
2422
  if (isAMP) micCheckboxes.forEach(cb => cb.disabled = false);
2423
- /* ── Generate PDF ── */
2424
  let pdfDoc = null;
2425
  try { pdfDoc = await buildPDF(seq, ampLabel, ampConf, micData, limeFeatures); }
2426
  catch (pdfErr) { console.error("PDF gen error:", pdfErr); }
2427
- // Helper: safely clear additionalOutput without removing persistent elements
2428
  function clearAdditionalOutput() {
2429
  Array.from(additionalOutput.childNodes).forEach(function(node) {
2430
  if (node !== downloadLink && node !== emailStatusResult) node.remove();
@@ -2446,7 +2404,6 @@ kbd {
2446
  setStep(3, 'done', 'Report ready — download below.');
2447
  if (step3Sub) step3Sub.textContent = 'PDF ready';
2448
  showToast('PDF report ready to download', 'success', 3000);
2449
- // ── Log user to Google Sheet ──────────────────
2450
  if (SHEET_WEBHOOK_URL && userEmailInput?.value?.trim()) {
2451
  fetch(SHEET_WEBHOOK_URL, {
2452
  method: 'POST',
@@ -2458,22 +2415,19 @@ kbd {
2458
  confidence: (ampConf * 100).toFixed(1) + '%',
2459
  sequence: seq.slice(0, 50) + (seq.length > 50 ? '…' : '')
2460
  })
2461
- }).catch(() => {}); // silent — never blocks the user
2462
  }
2463
  if (emailStatusResult) {
2464
  additionalOutput.appendChild(emailStatusResult);
2465
  emailStatusResult.style.display = 'flex';
2466
-
2467
  if (IS_EMAIL_SENDING_ENABLED && userEmailInput?.value?.trim()) {
2468
  const toEmail = userEmailInput.value.trim();
2469
  const pdfB64 = pdfDoc.output('datauristring').split(',')[1];
2470
  const seqPrev = seq.slice(0, 50) + (seq.length > 50 ? '…' : '');
2471
-
2472
  emailStatusResult.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending report to ' + toEmail + '…';
2473
-
2474
  try {
2475
  const mailer = await Client.connect(MAILER_SPACE_ID, {
2476
- hf_token: HF_TOKEN // set to null if Space is public
2477
  });
2478
  const result = await mailer.predict("/send", [
2479
  toEmail, pdfB64, ampLabel,
@@ -2510,7 +2464,6 @@ kbd {
2510
  ampClassOutput.innerHTML = '<div class="class-result-display"><div class="class-label pending" style="color:var(--accent-red)">Error</div></div>';
2511
  confidenceOutput.innerHTML = '<div class="mic-empty">—</div>';
2512
  micChartOutput.innerHTML = '<div class="mic-empty">Unavailable due to prediction error.</div>';
2513
- // Safe clear additionalOutput
2514
  Array.from(additionalOutput.childNodes).forEach(function(node) {
2515
  if (node !== downloadLink && node !== emailStatusResult) node.remove();
2516
  });
@@ -2554,14 +2507,14 @@ kbd {
2554
  const LGRAY= [210, 218, 232];
2555
  const isAMP = label.toLowerCase().includes('amp') && !label.toLowerCase().includes('non-amp');
2556
  const dateStr = new Date().toLocaleDateString('en-GB', { day:'numeric', month:'long', year:'numeric' });
2557
-
2558
- // helper: repeating page header
 
2559
  function pageHeader(sectionTitle) {
2560
  pdf.setFillColor(0, 40, 90);
2561
  pdf.rect(0, 0, W, 18, 'F');
2562
  pdf.setFillColor(26, 111, 196);
2563
  pdf.rect(0, 15, W, 3, 'F');
2564
- // Logo on left if available
2565
  if (logoB64) {
2566
  try { pdf.addImage(logoB64, 'PNG', 8, 2, 13, 13); } catch(e) {}
2567
  }
@@ -2572,8 +2525,6 @@ kbd {
2572
  pdf.setFont('helvetica','normal'); pdf.setFontSize(7.5); pdf.setTextColor(200, 220, 255);
2573
  pdf.text(sectionTitle, W - 10, 11, { align:'right' });
2574
  }
2575
-
2576
- // helper: page footer
2577
  function pageFooter(pageNum, total) {
2578
  pdf.setFillColor(245, 247, 252);
2579
  pdf.rect(0, H - 14, W, 14, 'F');
@@ -2586,8 +2537,6 @@ kbd {
2586
  pdf.setTextColor(160, 168, 185);
2587
  pdf.text('Page ' + pageNum + ' of ' + total, W - 10, H - 5.5, { align:'right' });
2588
  }
2589
-
2590
- // helper: section title with underline
2591
  function sectionTitle(text, yPos) {
2592
  pdf.setFont('helvetica','bold'); pdf.setFontSize(14); pdf.setTextColor(0, 63, 125);
2593
  pdf.text(text, 15, yPos);
@@ -2595,30 +2544,17 @@ kbd {
2595
  pdf.line(15, yPos + 2.5, W - 15, yPos + 2.5);
2596
  return yPos + 12;
2597
  }
2598
-
2599
- // preload assets
2600
- const logoB64 = await getBase64(document.querySelector('.header-logo img')?.src || '');
2601
- const banner64 = await getBase64('image2.png');
2602
- const shapB64 = await getBase64('shap.png');
2603
-
2604
- // ══════════════════════════════════════════════════
2605
- // PAGE 1 — COVER (image2.png header + summary + TOC)
2606
- // ══════════════════════════════════════════════════
2607
-
2608
- // ── Header banner (image2.png) ──────────────────
2609
- var bannerH = 52; // height reserved for the header image
2610
  if (banner64) {
2611
  const bi = new Image(); bi.src = banner64;
2612
  await new Promise(r => { bi.onload = r; bi.onerror = r; });
2613
  if (bi.naturalWidth) {
2614
- // scale to full page width, preserve aspect
2615
  var bw = W, bh = bw * (bi.naturalHeight / bi.naturalWidth);
2616
- if (bh > 70) bh = 70; // cap at 70 mm
2617
  bannerH = bh;
2618
  pdf.addImage(banner64, 'PNG', 0, 0, bw, bh);
2619
  }
2620
  } else {
2621
- // Fallback: branded gradient bar if image missing
2622
  pdf.setFillColor(0, 40, 90);
2623
  pdf.rect(0, 0, W, bannerH, 'F');
2624
  pdf.setFillColor(26, 111, 196);
@@ -2628,12 +2564,8 @@ kbd {
2628
  pdf.setFont('helvetica','normal'); pdf.setFontSize(10); pdf.setTextColor(160,200,255);
2629
  pdf.text('Explainable Antimicrobial Peptide Platform', W / 2, bannerH / 2 + 7, { align:'center' });
2630
  }
2631
-
2632
- // Thin accent line below banner
2633
  pdf.setFillColor(26, 111, 196);
2634
  pdf.rect(0, bannerH, W, 1.5, 'F');
2635
-
2636
- // ── Report title strip ──────────────────────────
2637
  var by = bannerH + 1.5;
2638
  pdf.setFillColor(247, 249, 253);
2639
  pdf.rect(0, by, W, 20, 'F');
@@ -2642,20 +2574,14 @@ kbd {
2642
  pdf.setFont('helvetica','normal'); pdf.setFontSize(8.5); pdf.setTextColor(100, 115, 145);
2643
  pdf.text('Generated on ' + dateStr + ' | Zewail City of Science and Technology \u00B7 BCBU', W / 2, by + 17, { align:'center' });
2644
  by += 22;
2645
-
2646
- // Divider
2647
  pdf.setDrawColor(210, 218, 232); pdf.setLineWidth(0.4);
2648
  pdf.line(15, by, W - 15, by);
2649
  by += 7;
2650
-
2651
- // ── Quick Summary label ─────────────────────────
2652
  pdf.setFont('helvetica','bold'); pdf.setFontSize(9); pdf.setTextColor(26, 111, 196);
2653
  pdf.text('QUICK SUMMARY', 15, by);
2654
  pdf.setDrawColor(26, 111, 196); pdf.setLineWidth(0.4);
2655
  pdf.line(15, by + 1.5, W - 15, by + 1.5);
2656
  by += 7;
2657
-
2658
- // ── Input Sequence box ──────────────────────────
2659
  pdf.setFillColor(248, 250, 255);
2660
  pdf.roundedRect(15, by, W - 30, 22, 2, 2, 'F');
2661
  pdf.setDrawColor(210, 218, 232); pdf.setLineWidth(0.3);
@@ -2668,8 +2594,6 @@ kbd {
2668
  pdf.setFont('helvetica','normal'); pdf.setFontSize(6.5); pdf.setTextColor(150, 158, 175);
2669
  pdf.text(seq.length + ' amino acids', W - 20, by + 19, { align:'right' });
2670
  by += 26;
2671
-
2672
- // ── Four stat cards row ─────────────────────────
2673
  var hasMIC = Object.keys(micResults).length > 0;
2674
  var hasLIME = limeFeats.length > 0;
2675
  var hasShap = !!shapB64;
@@ -2699,29 +2623,23 @@ kbd {
2699
  var cx = 15 + i * (cw4 + cardGap);
2700
  pdf.setFillColor(c.fill[0], c.fill[1], c.fill[2]);
2701
  pdf.roundedRect(cx, by, cw4, cardH, 2, 2, 'F');
2702
- // left accent bar
2703
  pdf.setFillColor(c.border[0], c.border[1], c.border[2]);
2704
  pdf.rect(cx, by, 2.5, cardH, 'F');
2705
  pdf.setDrawColor(c.border[0], c.border[1], c.border[2]); pdf.setLineWidth(0.4);
2706
  pdf.roundedRect(cx, by, cw4, cardH, 2, 2, 'S');
2707
- // label
2708
  pdf.setFont('helvetica','bold'); pdf.setFontSize(5.5); pdf.setTextColor(150,158,175);
2709
  pdf.text(c.label, cx + 6, by + 7);
2710
- // value — split to fit within card width
2711
  pdf.setFont('helvetica','bold'); pdf.setFontSize(9);
2712
  pdf.setTextColor(c.valColor[0], c.valColor[1], c.valColor[2]);
2713
  var maxTxtW = cw4 - 10;
2714
  var valLines = pdf.splitTextToSize(c.value, maxTxtW);
2715
  pdf.text(valLines, cx + 6, by + 15);
2716
- // sub — always fits, tiny font
2717
  var subY = by + 15 + valLines.length * 5;
2718
  pdf.setFont('helvetica','normal'); pdf.setFontSize(6); pdf.setTextColor(120,128,145);
2719
  var subLines = pdf.splitTextToSize(c.sub, maxTxtW);
2720
  pdf.text(subLines, cx + 6, subY);
2721
  });
2722
  by += cardH + 4;
2723
-
2724
- // ── MIC mini-table (if available) ───────────────
2725
  if (hasMIC) {
2726
  pdf.setFont('helvetica','bold'); pdf.setFontSize(6.5); pdf.setTextColor(26,111,196);
2727
  pdf.text('PREDICTED MIC VALUES (\u00B5M)', 15, by + 5);
@@ -2733,7 +2651,6 @@ kbd {
2733
  micEntries.forEach(function(kv, i) {
2734
  var mx = 15 + i * colW;
2735
  var micVal = typeof kv[1] === 'number' ? kv[1].toFixed(3) : String(kv[1]);
2736
- // card bg
2737
  pdf.setFillColor(244, 247, 253);
2738
  pdf.roundedRect(mx + 1, by, colW - 2, 22, 2, 2, 'F');
2739
  pdf.setDrawColor(210,218,232); pdf.setLineWidth(0.25);
@@ -2747,10 +2664,7 @@ kbd {
2747
  } else {
2748
  by += 4;
2749
  }
2750
-
2751
- // ── Table of Contents ───────────────────────────
2752
  by += 3;
2753
- // TOC header bar
2754
  pdf.setFillColor(0, 40, 90);
2755
  pdf.roundedRect(15, by, W - 30, 10, 2, 2, 'F');
2756
  pdf.setFont('helvetica','bold'); pdf.setFontSize(8); pdf.setTextColor(255,255,255);
@@ -2759,10 +2673,7 @@ kbd {
2759
  pdf.text('Section', W - 45, by + 7);
2760
  pdf.text('Page', W - 21, by + 7, { align:'right' });
2761
  by += 12;
2762
-
2763
  var pg = 2;
2764
- // 4 canonical sections — LIME and SHAP share one "Features" section page
2765
- var featPage = (hasLIME || hasShap) ? pg + (hasMIC ? 1 : 0) + 1 : null;
2766
  var tocData = [
2767
  { num:'1', icon:'SEQ', title:'Input Sequence',
2768
  desc:'Full amino acid sequence with length and composition',
@@ -2777,48 +2688,33 @@ kbd {
2777
  desc:'Local LIME attributions and global SHAP feature importance',
2778
  page: (hasLIME || hasShap) ? pg++ : null }
2779
  ];
2780
-
2781
  var tocRowH = 13;
2782
  tocData.forEach(function(row, idx) {
2783
  var avail = row.page !== null;
2784
  var rowBg = avail ? (idx % 2 === 0 ? [247,249,254] : [255,255,255]) : [250,250,252];
2785
  pdf.setFillColor(rowBg[0], rowBg[1], rowBg[2]);
2786
  pdf.rect(15, by, W - 30, tocRowH, 'F');
2787
-
2788
- // left colour tag
2789
  var tagColor = avail ? (idx===0?[26,111,196]:idx===1?[39,174,96]:idx===2?[230,81,0]:[126,87,194]) : [190,195,205];
2790
  pdf.setFillColor(tagColor[0], tagColor[1], tagColor[2]);
2791
  pdf.rect(15, by, 3, tocRowH, 'F');
2792
-
2793
- // number badge
2794
  pdf.setFillColor(tagColor[0], tagColor[1], tagColor[2]);
2795
  pdf.roundedRect(21, by + 2, 8, 8, 1, 1, 'F');
2796
  pdf.setFont('helvetica','bold'); pdf.setFontSize(6.5); pdf.setTextColor(255,255,255);
2797
  pdf.text(row.num, 25, by + 7.5, { align:'center' });
2798
-
2799
- // icon badge
2800
  pdf.setFillColor(avail ? 235 : 242, avail ? 240 : 243, avail ? 252 : 248);
2801
  pdf.roundedRect(32, by + 2.5, 10, 7, 1, 1, 'F');
2802
  pdf.setFont('helvetica','bold'); pdf.setFontSize(5); pdf.setTextColor(tagColor[0], tagColor[1], tagColor[2]);
2803
  pdf.text(row.icon, 37, by + 7.5, { align:'center' });
2804
-
2805
- // title
2806
  pdf.setFont('helvetica','bold'); pdf.setFontSize(8);
2807
  pdf.setTextColor(avail ? 20 : 160, avail ? 35 : 165, avail ? 80 : 180);
2808
  pdf.text(row.title, 45, by + 6.5);
2809
-
2810
- // description
2811
  pdf.setFont('helvetica','normal'); pdf.setFontSize(6);
2812
  pdf.setTextColor(avail ? 110 : 175, avail ? 118 : 180, avail ? 140 : 195);
2813
  pdf.text(row.desc, 45, by + 11);
2814
-
2815
- // page number
2816
  var pageLabel = avail ? 'pg. ' + row.page : 'N/A';
2817
  pdf.setFont('helvetica', avail ? 'bold' : 'normal'); pdf.setFontSize(8);
2818
  pdf.setTextColor(avail ? tagColor[0] : 180, avail ? tagColor[1] : 185, avail ? tagColor[2] : 195);
2819
  pdf.text(pageLabel, W - 18, by + 7.5, { align:'right' });
2820
-
2821
- // dotted leader
2822
  pdf.setFillColor(210, 218, 232);
2823
  var titleW2 = pdf.getTextWidth(row.title);
2824
  var pgW2 = pdf.getTextWidth(pageLabel);
@@ -2826,19 +2722,13 @@ kbd {
2826
  for (var dx = lx1; dx < lx2 - 1; dx += 2.2) {
2827
  pdf.circle(dx, by + 7, 0.3, 'F');
2828
  }
2829
-
2830
- // row separator
2831
  pdf.setDrawColor(225, 230, 242); pdf.setLineWidth(0.15);
2832
  pdf.line(15, by + tocRowH, W - 15, by + tocRowH);
2833
  by += tocRowH;
2834
  });
2835
-
2836
- // TOC bottom rule
2837
  pdf.setFillColor(26, 111, 196);
2838
  pdf.rect(15, by, W - 30, 1, 'F');
2839
  by += 5;
2840
-
2841
- // Cover page footer strip
2842
  pdf.setFillColor(245, 247, 252);
2843
  pdf.rect(0, H - 14, W, 14, 'F');
2844
  pdf.setDrawColor(210, 218, 232); pdf.setLineWidth(0.3);
@@ -2849,19 +2739,13 @@ kbd {
2849
  pdf.text('epicamp.sup@gmail.com', 10, H - 3);
2850
  pdf.setTextColor(160, 168, 185);
2851
  pdf.text('Page 1', W - 10, H - 5.5, { align:'right' });
2852
-
2853
- // ══════════════════════════════════════════════════
2854
- // PAGE 2 — INPUT SEQUENCE
2855
- // ══════════════════════════════════════════════════
2856
  pdf.addPage();
2857
  pageHeader('Section 1 — Input Sequence');
2858
  var y = 26;
2859
-
2860
  y = sectionTitle('1. Input Sequence', y);
2861
  pdf.setFont('helvetica','normal'); pdf.setFontSize(8.5); pdf.setTextColor(130,138,155);
2862
  pdf.text('Full amino acid sequence submitted for analysis (' + seq.length + ' residues).', 15, y, { maxWidth: W - 30 });
2863
  y += 10;
2864
- // Sequence box
2865
  var seqLines = pdf.splitTextToSize(seq, W - 44);
2866
  var seqBoxH = 10 + seqLines.length * 5.5 + 4;
2867
  pdf.setFillColor(248, 250, 255);
@@ -2877,8 +2761,6 @@ kbd {
2877
  pdf.setFont('helvetica','normal'); pdf.setFontSize(7); pdf.setTextColor(150,158,175);
2878
  pdf.text(seq.length + ' amino acids', W - 20, y + seqBoxH - 3, { align:'right' });
2879
  y += seqBoxH + 10;
2880
-
2881
- // Sequence stats mini-row
2882
  var statsData = [
2883
  { k:'Length', v: seq.length + ' aa' },
2884
  { k:'Hydrophobic', v: (function(){ var h=0; for(var i=0;i<seq.length;i++) if('AVILMFWP'.includes(seq[i]))h++; return Math.round(h/seq.length*100)+'%'; })() },
@@ -2898,14 +2780,9 @@ kbd {
2898
  pdf.text(s.v, sx + sw/2, y + 14, { align:'center' });
2899
  });
2900
  y += 25;
2901
-
2902
- // ══════════════════════════════════════════════════
2903
- // PAGE 3 — CLASSIFICATION & CONFIDENCE
2904
- // ══════════════════════════════════════════════════
2905
  pdf.addPage();
2906
  pageHeader('Section 2 — Classification & Confidence');
2907
  y = 26;
2908
-
2909
  y = sectionTitle('2. Classification & Confidence', y);
2910
  pdf.autoTable({
2911
  startY: y,
@@ -2938,33 +2815,23 @@ kbd {
2938
  margin: { left: 15, right: 15 }
2939
  });
2940
  y = pdf.lastAutoTable.finalY + 10;
2941
-
2942
- // Visual confidence bar
2943
  pdf.setFont('helvetica','bold'); pdf.setFontSize(8); pdf.setTextColor(0,40,90);
2944
  pdf.text('Confidence Indicator', 15, y + 5);
2945
  y += 9;
2946
  var barW = W - 30, barH2 = 7;
2947
- // track
2948
  pdf.setFillColor(220, 226, 240);
2949
  pdf.roundedRect(15, y, barW, barH2, 2, 2, 'F');
2950
- // fill
2951
  var fillC = conf >= 0.7 ? GRN : conf >= 0.5 ? [230,81,0] : RED;
2952
  pdf.setFillColor(fillC[0], fillC[1], fillC[2]);
2953
  pdf.roundedRect(15, y, Math.max(barW * conf, 3), barH2, 2, 2, 'F');
2954
- // label
2955
  pdf.setFont('helvetica','bold'); pdf.setFontSize(7); pdf.setTextColor(fillC[0], fillC[1], fillC[2]);
2956
  pdf.text((conf*100).toFixed(1) + '%', 15 + barW * conf + 2, y + 5.5);
2957
  y += barH2 + 5;
2958
- // scale labels
2959
  pdf.setFont('helvetica','normal'); pdf.setFontSize(6.5); pdf.setTextColor(160,168,185);
2960
  pdf.text('0%', 15, y + 3);
2961
  pdf.text('50%', 15 + barW/2, y + 3, { align:'center' });
2962
  pdf.text('100%', 15 + barW, y + 3, { align:'right' });
2963
  y += 12;
2964
-
2965
- // ══════════════════��═══════════════════════════════
2966
- // PAGE 4 — MIC VALUES (only if available)
2967
- // ══════════════════════════════════════════════════
2968
  var micRows = Object.entries(micResults).map(function(kv) {
2969
  return [kv[0], typeof kv[1] === 'number' ? kv[1].toFixed(3) + ' \u00B5M' : String(kv[1])];
2970
  });
@@ -3011,8 +2878,6 @@ kbd {
3011
  margin: { left: 15, right: 15 }
3012
  });
3013
  y = pdf.lastAutoTable.finalY + 14;
3014
-
3015
- // MIC visual bar chart
3016
  pdf.setFont('helvetica','bold'); pdf.setFontSize(8); pdf.setTextColor(0,40,90);
3017
  pdf.text('MIC Comparison Chart', 15, y + 5);
3018
  y += 11;
@@ -3025,27 +2890,19 @@ kbd {
3025
  var bx = 15 + i * barSlotW + barSlotW * 0.15;
3026
  var bw2 = barSlotW * 0.7;
3027
  var barColor = val < 4 ? GRN : val < 16 ? [230,81,0] : RED;
3028
- // bar
3029
  pdf.setFillColor(barColor[0], barColor[1], barColor[2]);
3030
  pdf.roundedRect(bx, y + maxBarH - bh, bw2, bh, 1, 1, 'F');
3031
- // value label above
3032
  pdf.setFont('helvetica','bold'); pdf.setFontSize(6.5); pdf.setTextColor(barColor[0], barColor[1], barColor[2]);
3033
  pdf.text(val.toFixed(2), bx + bw2 / 2, y + maxBarH - bh - 2, { align:'center' });
3034
- // organism label below
3035
  pdf.setFont('helvetica','italic'); pdf.setFontSize(6); pdf.setTextColor(80,90,110);
3036
  pdf.text(r[0], bx + bw2 / 2, y + maxBarH + 5, { align:'center' });
3037
  });
3038
  y += maxBarH + 12;
3039
  }
3040
-
3041
- // ══════════════════════════════════════════════════
3042
- // PAGE 5 — FEATURES: LIME + SHAP
3043
- // ══════════════════════════════════════════════════
3044
  if (limeFeats.length > 0 || shapB64) {
3045
  pdf.addPage();
3046
  pageHeader('Section 4 — Feature Explanation (LIME & SHAP)');
3047
  y = 26;
3048
-
3049
  if (limeFeats.length > 0) {
3050
  y = sectionTitle('4a. LIME — Local Feature Attribution', y);
3051
  pdf.setFont('helvetica','normal'); pdf.setFontSize(8); pdf.setTextColor(130,138,155);
@@ -3082,7 +2939,6 @@ kbd {
3082
  });
3083
  y = pdf.lastAutoTable.finalY + 12;
3084
  }
3085
-
3086
  if (shapB64) {
3087
  if (y + 50 > H - 20) { pdf.addPage(); pageHeader('Section 4 — Feature Explanation (LIME & SHAP)'); y = 26; }
3088
  y = sectionTitle('4b. SHAP — Global Feature Importance', y);
@@ -3099,43 +2955,13 @@ kbd {
3099
  }
3100
  }
3101
  }
3102
-
3103
- // ══════════════════════════════════════════════════
3104
- // FOOTERS on all pages
3105
- // ══════════════════════════════════════════════════
3106
  var total = pdf.internal.getNumberOfPages();
3107
  for (var p = 1; p <= total; p++) {
3108
  pdf.setPage(p);
3109
  pageFooter(p, total);
3110
  }
3111
  return pdf;
3112
-
3113
- // legacy shap block (unreachable — kept for reference)
3114
- if (shapB64) {
3115
- var img2 = new Image(); img2.src = shapB64;
3116
- await new Promise(r => img2.onload = r);
3117
- if (img2.naturalWidth) {
3118
- var maxW2 = W - 30, maxH2 = 90;
3119
- var iw2 = img2.naturalWidth, ih2 = img2.naturalHeight;
3120
- if (iw2 > maxW2) { ih2 = ih2 * maxW2 / iw2; iw2 = maxW2; }
3121
- if (ih2 > maxH2) { iw2 = iw2 * maxH2 / ih2; ih2 = maxH2; }
3122
- if (y + ih2 + 20 > pdf.internal.pageSize.height - 20) { pdf.addPage(); y = 15; }
3123
- pdf.setFont('helvetica','bold'); pdf.setFontSize(13); pdf.setTextColor(...PRI);
3124
- pdf.text('Global SHAP Feature Importance', 15, y); y += 6;
3125
- pdf.addImage(shapB64, 'PNG', (W - iw) / 2, y, iw, ih); y += ih + 10;
3126
- }
3127
- }
3128
- // Footer on each page
3129
- const n = pdf.internal.getNumberOfPages();
3130
- for (var p = 1; p <= n; p++) {
3131
- pdf.setPage(p);
3132
- pdf.setFontSize(8); pdf.setTextColor(150,150,150);
3133
- pdf.text('Page ' + p + ' of ' + n, W - 15, pdf.internal.pageSize.height - 8, { align:'right' });
3134
- pdf.text('EPIC-AMP - Zewail City - BCBU', 15, pdf.internal.pageSize.height - 8);
3135
- }
3136
- return pdf;
3137
  }
3138
-
3139
  </script>
3140
  </body>
3141
  </html>
 
1392
  <i class="fas fa-flask"></i> Try Example
1393
  </button>
1394
  <div class="example-dropdown" id="example-dropdown">
1395
+ <div class="example-item" data-seq="GIGKFLHSAKKFGKAFVGEIMNS" data-label="AMP · Magainin-2 (23 aa)" data-type="amp">
1396
+ <div class="example-item-title"><span class="example-badge badge-amp">AMP</span> Magainin-223 aa</div>
1397
+ <div class="example-item-seq">GIGKFLHSAKKFGKAFVGEIMNS</div>
1398
  </div>
1399
+ <div class="example-item" data-seq="KWKLFKKIEKVGQNIRDGIIKAGPAVAVVGQATQIAK" data-label="AMP · Cecropin-Melittin CA(1-7)M(2-9) (37 aa)" data-type="amp">
1400
+ <div class="example-item-title"><span class="example-badge badge-amp">AMP</span> Cecropin A37 aa</div>
1401
+ <div class="example-item-seq">KWKLFKKIEKVGQNIRDGIIKAGPAVAVVGQATQIAK</div>
1402
  </div>
1403
+ <div class="example-item" data-seq="MKWVTFISLLFLFSSAYSRGVFRRDAHKSEVAHRFKDLGEENFKALVLIAFAQYLQQCPF" data-label="Non-AMP · Serum albumin fragment (59 aa)" data-type="nonamp">
1404
+ <div class="example-item-title"><span class="example-badge badge-nonamp">Non-AMP</span> Albumin fragment59 aa</div>
1405
+ <div class="example-item-seq">MKWVTFISLLFLFSSAYSRGVFRRDAHKSEVAHRFKDL…</div>
1406
  </div>
1407
+ <div class="example-item" data-seq="MGSSHHHHHHSSGLVPRGSHMASMTGGQQMGRGSEFELRRQACGRSTKDL" data-label="Non-AMP · His-tag fusion construct (50 aa)" data-type="nonamp">
1408
+ <div class="example-item-title"><span class="example-badge badge-nonamp">Non-AMP</span> His-tag construct50 aa</div>
1409
+ <div class="example-item-seq">MGSSHHHHHHSSGLVPRGSHMASMTGGQQMGRGSEFEL…</div>
1410
  </div>
1411
  </div>
1412
  </div>
 
1567
  </div>
1568
  <div class="tab-panel" id="panel-model">
1569
  <div class="card">
 
 
 
 
1570
  <div class="card-header">
1571
  <div class="card-header-icon"><i class="fas fa-check-circle"></i></div>
1572
  <div class="card-header-title">MLP Classifier Performance Metrics</div>
 
1590
  </p>
1591
  </div>
1592
  </div>
1593
+ <div class="card">
1594
+ <div class="card-header">
1595
+ <div class="card-header-icon"><i class="fas fa-chart-line"></i></div>
1596
+ <div class="card-header-title">Regression Performance Metrics (MIC)</div>
1597
+ <div class="card-header-sub">Per-organism MIC regression</div>
1598
+ </div>
1599
  <div class="card-body">
1600
  <p class="prose">Separate regression models predict MIC for each organism. Performance evaluated via MSE (log-scale), R², Pearson correlation, and Kendall's tau.</p>
1601
  <table class="metrics-table">
 
1650
  <table class="metrics-table" style="margin-top:8px;">
1651
  <thead><tr><th>#</th><th>Description</th><th>Expected</th><th>Sequence (truncated)</th></tr></thead>
1652
  <tbody>
1653
+ <tr><td>1</td><td>Magainin-2 (23 aa)</td><td><span class="status-badge green">P-AMP</span></td><td style="font-family:var(--font-mono);font-size:11px;">GIGKFLHSAKKFGKAFVGEIM…</td></tr>
1654
+ <tr><td>2</td><td>Cecropin A (37 aa)</td><td><span class="status-badge green">P-AMP</span></td><td style="font-family:var(--font-mono);font-size:11px;">KWKLFKKIEKVGQNIRDGII…</td></tr>
1655
+ <tr><td>3</td><td>Albumin fragment (59 aa)</td><td><span class="status-badge red">Non-AMP</span></td><td style="font-family:var(--font-mono);font-size:11px;">MKWVTFISLLFLFSSAYSRG…</td></tr>
1656
+ <tr><td>4</td><td>His-tag construct (50 aa)</td><td><span class="status-badge red">Non-AMP</span></td><td style="font-family:var(--font-mono);font-size:11px;">MGSSHHHHHHSSGLVPRGSH…</td></tr>
1657
  <tr><td>5</td><td>Invalid chars</td><td><span class="status-badge gray">Rejected</span></td><td style="font-family:var(--font-mono);font-size:11px;">MEKAALIFIG(XX)…</td></tr>
1658
  </tbody>
1659
  </table>
 
1700
  </ul>
1701
  <div class="section-h3">Contact</div>
1702
  <p class="prose">For questions, collaboration inquiries, or feedback: <a href="mailto:epicamp.sup@gmail.com" style="color:var(--ncbi-blue);">epicamp.sup@gmail.com</a></p>
 
1703
  </div>
1704
  </div>
1705
  </div>
 
1769
  </div>
1770
  </main>
1771
  <div class="aa-tooltip-box" id="aa-tooltip"></div>
1772
+ <div class="toast-wrap" id="toast-wrap"></div>
1773
  <div id="demo-modal" class="modal-overlay">
1774
  <div class="modal-box">
1775
  <button class="modal-close" id="modal-close-btn">&times;</button>
 
1783
  <footer>
1784
  <p style="margin-bottom:4px;">© 2025 Bioinformatics and Computational Biology Unit (BCBU) — Zewail City</p>
1785
  <address style="font-style:normal;color:rgba(255,255,255,0.55);font-size:11px;">Ahmed Zewail Street, October Gardens, Giza, Egypt</address>
1786
+ <p style="margin-top:6px;font-size:11px;"><a href="mailto:epicamp.sup@gmail.com" style="color:rgba(160,200,255,0.85);text-decoration:none;">epicamp.sup@gmail.com</a></p>
1787
  </footer>
1788
+ <script type="module">
1789
  import { Client } from "https://esm.sh/@gradio/client";
1790
  /* ── CONFIG ─────────────────────────────────────────────── */
1791
  const GRADIO_PREDICTION_TARGET_ID = "nonzeroexit/AMP-Classifier";
 
1793
  const MAILER_SPACE_ID = "nonzeroexit/AMP-Mailer";
1794
  const HF_TOKEN = null; // ← if Space is Private: paste your HF read token "hf_xxxx"
1795
  // ← if Space is Public: leave as null
1796
+ const SHEET_WEBHOOK_URL = 'https://script.google.com/macros/s/AKfycbzF8_W8uwshY2THg7atOc9TmRqerLKAJyZbs9_-hP5kj99LMYTqn2e4Ki8SwNzZo8Oc/exec';
1797
  /* ── AA PROPERTY MAP ─────────────────────────────────────── */
1798
  const AA_PROPS = {
1799
  A:'hydrophobic', V:'hydrophobic', I:'hydrophobic', L:'hydrophobic',
 
1873
  const step1Sub = document.getElementById('step-1-sub');
1874
  const step2Sub = document.getElementById('step-2-sub');
1875
  const step3Sub = document.getElementById('step-3-sub');
 
 
1876
  const LINE_WIDTHS = { 1: '0%', 2: '50%', 3: '100%' };
 
 
 
 
 
 
1877
  function setStep(n, state, statusMsg) {
 
1878
  for (let i = 1; i <= 3; i++) {
1879
  const el = stepEls[i];
1880
  if (!el) continue;
 
1884
  } else if (i === n) {
1885
  el.classList.add(state);
1886
  }
 
1887
  }
 
1888
  if (stepperLine) {
1889
  if (n === 1) stepperLine.style.width = '0%';
1890
  else if (n === 2 && state === 'done') stepperLine.style.width = '50%';
 
1913
  }, duration);
1914
  }
1915
  /* ── SEQUENCE STATS ──────────────────────────────────────── */
 
1916
  const MW_TABLE = {A:89,R:174,N:132,D:133,C:121,Q:146,E:147,G:75,H:155,I:131,
1917
  L:131,K:146,M:149,F:165,P:115,S:105,T:119,W:204,Y:181,V:117};
1918
  const HYDRO_SET = new Set(['A','V','I','L','M','F','W','P']);
 
1921
  if (!seq || seq.length < 3) { if (seqStatsRow) seqStatsRow.style.display = 'none'; return; }
1922
  if (seqStatsRow) seqStatsRow.style.display = 'flex';
1923
  const n = seq.length;
1924
+ let hydro = 0, cation = 0, mw = 18;
1925
  for (const ch of seq) {
1926
  if (HYDRO_SET.has(ch)) hydro++;
1927
  if (CATION_SET.has(ch)) cation++;
1928
+ mw += (MW_TABLE[ch] || 110) - 18;
1929
  }
1930
  if (statLength) statLength.textContent = n;
1931
  if (statHydro) statHydro.textContent = Math.round(hydro / n * 100) + '%';
1932
  if (statCation) statCation.textContent = Math.round(cation / n * 100) + '%';
1933
  if (statMw) statMw.textContent = mw.toLocaleString();
 
1934
  if (charBadgeWrap) {
1935
  charBadgeWrap.classList.remove('valid','warning','invalid');
1936
  if (n >= 10 && n <= 100 && !/[^ACDEFGHIKLMNPQRSTVWY]/i.test(seq)) {
 
1949
  e.stopPropagation();
1950
  exampleDropdown.classList.toggle('open');
1951
  });
 
1952
  document.addEventListener('click', (e) => {
1953
  if (examplePicker && !examplePicker.contains(e.target)) {
1954
  exampleDropdown.classList.remove('open');
1955
  }
1956
  });
 
1957
  exampleDropdown.querySelectorAll('.example-item').forEach(item => {
1958
  item.addEventListener('click', () => {
1959
  const seq = item.dataset.seq;
1960
  const label = item.dataset.label;
1961
  const type = item.dataset.type;
1962
  if (!seq) return;
 
1963
  sequenceInput.value = seq.toUpperCase();
1964
  exampleDropdown.classList.remove('open');
1965
  onSeqInput();
 
1966
  if (type === 'amp') {
1967
  micCheckboxes.forEach(cb => { cb.disabled = false; cb.checked = true; });
1968
  }
 
2011
  modalCloseBtn?.addEventListener('click', closeModal);
2012
  demoModal?.addEventListener('click', e => { if (e.target === demoModal) closeModal(); });
2013
  clearAll();
 
2014
  document.addEventListener('mousemove', e => {
2015
  if (!e.target.classList.contains('aa-char')) aaTooltip.style.display = 'none';
2016
  });
 
2017
  setupExamplePicker();
 
2018
  document.addEventListener('keydown', e => {
2019
  if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
2020
  e.preventDefault();
 
2024
  showToast('Submitting via Ctrl+Enter…', 'info', 1800);
2025
  }
2026
  }
 
2027
  if (e.key === 'Escape' && exampleDropdown) {
2028
  exampleDropdown.classList.remove('open');
2029
  }
 
2043
  updateMicCheckboxes();
2044
  updateBtnState();
2045
  updateSeqStats(sequenceInput.value.toUpperCase());
 
2046
  resetDashboard();
2047
  setStep(1, 'active', 'Enter a valid sequence to begin.');
2048
  }
 
2087
  function updateBtnState() {
2088
  const v = validateSequence(sequenceInput.value);
2089
  if (predictBtn) predictBtn.disabled = !v.isValid || !clientInstance;
 
2090
  const seq = sequenceInput ? sequenceInput.value : '';
2091
  if (!seq) {
2092
  setStep(1, 'active', 'Enter a valid sequence to begin.');
 
2109
  }
2110
  if (alignViewer) alignViewer.style.display = 'block';
2111
  if (aaCompBarWrap) aaCompBarWrap.style.display = 'block';
 
2112
  aaRuler.innerHTML = '';
2113
  for (let i = 0; i < seq.length; i++) {
2114
  const tick = document.createElement('div');
 
2116
  tick.textContent = (i + 1) % 5 === 0 ? (i + 1) : (i === 0 ? '1' : '');
2117
  aaRuler.appendChild(tick);
2118
  }
 
2119
  aaSeqDisplay.innerHTML = '';
2120
  for (let i = 0; i < seq.length; i++) {
2121
  const ch = seq[i];
 
2197
  }
2198
  function resetDashboard() {
2199
  ampClassOutput.innerHTML = '<div class="class-result-display"><div class="class-label pending">Awaiting input…</div></div>';
 
2200
  confidenceOutput.innerHTML =
2201
  '<div class="gauge-wrap">' +
2202
  '<svg class="gauge-arc-svg" viewBox="0 0 150 80">' +
 
2208
  '<div class="gauge-sub">Model confidence</div>' +
2209
  '</div>';
2210
  micChartOutput.innerHTML = '<div class="mic-empty">MIC results will appear here after analysis. Select bacteria above before submitting.</div>';
 
 
 
2211
  Array.from(additionalOutput.childNodes).forEach(node => {
2212
  if (node !== downloadLink && node !== emailStatusResult) node.remove();
2213
  });
 
2227
  function setGauge(fraction, color) {
2228
  const arc = 188.5;
2229
  const fill = document.getElementById('gauge-fill');
2230
+ if (!fill) return;
2231
  fill.style.strokeDashoffset = (arc - fraction * arc).toString();
2232
  fill.style.stroke = color;
2233
  }
 
2283
  const v = validateSequence(seq);
2284
  if (!v.isValid) { showError(v.message); return; }
2285
  clearError();
 
2286
  predictBtn.disabled = true;
2287
  predictBtn.innerHTML = '<i class="fas fa-circle-notch spin"></i> Processing…';
2288
  setStep(2, 'processing', 'Model is analysing your sequence…');
2289
  if (step2Sub) step2Sub.textContent = 'Running…';
 
2290
  const loadingHTML = '<div class="loading-pulse"><div class="pulse-dots"><div class="pulse-dot"></div><div class="pulse-dot"></div><div class="pulse-dot"></div></div><span>Analysing...</span></div>';
2291
  ampClassOutput.innerHTML = loadingHTML;
2292
  confidenceOutput.innerHTML = loadingHTML;
2293
  micChartOutput.innerHTML = loadingHTML;
 
2294
  Array.from(additionalOutput.childNodes).forEach(function(node) {
2295
  if (node !== downloadLink && node !== emailStatusResult) node.remove();
2296
  });
 
2306
  elapsed++;
2307
  if (processingInfo) processingInfo.textContent = 'Processing — ' + elapsed + 's elapsed…';
2308
  }, 1000);
 
2309
  let ampLabel = 'Unknown', ampConf = 0;
2310
  let micData = {}, limeFeatures = [];
2311
  try {
 
2330
  if (m) limeFeatures.push({ feature: m[1].trim(), value: parseFloat(m[2]) });
2331
  }
2332
  }
 
2333
  const isAMP = ampLabel.toLowerCase().includes('amp') && !ampLabel.toLowerCase().includes('non-amp');
2334
  ampClassOutput.innerHTML = `
2335
  <div class="class-result-display">
 
2340
  </span>
2341
  </div>
2342
  </div>`;
 
2343
  const gColor = isAMP ? '#2e7d32' : '#c62828';
 
 
2344
  confidenceOutput.innerHTML =
2345
  '<div class="gauge-wrap">' +
2346
  '<svg class="gauge-arc-svg" viewBox="0 0 150 80">' +
 
2348
  '<path id="gauge-fill" class="gauge-fill" d="M15,75 A60,60 0 0,1 135,75"' +
2349
  ' stroke="' + gColor + '"' +
2350
  ' stroke-dasharray="188.5"' +
2351
+ ' stroke-dashoffset="188.5"/>' +
2352
  '</svg>' +
2353
  '<div class="gauge-value-label" style="color:' + gColor + '">' + (ampConf * 100).toFixed(1) + '%</div>' +
2354
  '<div class="gauge-sub">Model confidence</div>' +
2355
  '</div>';
 
2356
  requestAnimationFrame(() => setGauge(ampConf, gColor));
 
2357
  const checkedBacteria = [...micCheckboxes].filter(cb => cb.checked).map(cb => {
2358
  return cb.parentElement.querySelector('label em')?.textContent?.trim() || cb.value;
2359
  });
 
2374
  } else {
2375
  micChartOutput.innerHTML = `<div class="mic-empty">${!isAMP ? 'Sequence classified as Non-AMP — MIC prediction not applicable.' : 'No bacteria selected or no MIC predictions returned.'}</div>`;
2376
  }
 
2377
  setStep(3, 'active', 'Analysis complete — results below.');
2378
  if (step3Sub) step3Sub.textContent = ampLabel;
 
2379
  const ra = document.getElementById('results-area');
2380
  if (ra) ra.scrollIntoView({ behavior: 'smooth', block: 'start' });
2381
  showToast('Classification: ' + ampLabel + ' (' + (ampConf*100).toFixed(1) + '% confidence)', isAMP ? 'success' : 'info', 4000);
 
2382
  if (isAMP) micCheckboxes.forEach(cb => cb.disabled = false);
 
2383
  let pdfDoc = null;
2384
  try { pdfDoc = await buildPDF(seq, ampLabel, ampConf, micData, limeFeatures); }
2385
  catch (pdfErr) { console.error("PDF gen error:", pdfErr); }
 
2386
  function clearAdditionalOutput() {
2387
  Array.from(additionalOutput.childNodes).forEach(function(node) {
2388
  if (node !== downloadLink && node !== emailStatusResult) node.remove();
 
2404
  setStep(3, 'done', 'Report ready — download below.');
2405
  if (step3Sub) step3Sub.textContent = 'PDF ready';
2406
  showToast('PDF report ready to download', 'success', 3000);
 
2407
  if (SHEET_WEBHOOK_URL && userEmailInput?.value?.trim()) {
2408
  fetch(SHEET_WEBHOOK_URL, {
2409
  method: 'POST',
 
2415
  confidence: (ampConf * 100).toFixed(1) + '%',
2416
  sequence: seq.slice(0, 50) + (seq.length > 50 ? '…' : '')
2417
  })
2418
+ }).catch(() => {});
2419
  }
2420
  if (emailStatusResult) {
2421
  additionalOutput.appendChild(emailStatusResult);
2422
  emailStatusResult.style.display = 'flex';
 
2423
  if (IS_EMAIL_SENDING_ENABLED && userEmailInput?.value?.trim()) {
2424
  const toEmail = userEmailInput.value.trim();
2425
  const pdfB64 = pdfDoc.output('datauristring').split(',')[1];
2426
  const seqPrev = seq.slice(0, 50) + (seq.length > 50 ? '…' : '');
 
2427
  emailStatusResult.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending report to ' + toEmail + '…';
 
2428
  try {
2429
  const mailer = await Client.connect(MAILER_SPACE_ID, {
2430
+ hf_token: HF_TOKEN
2431
  });
2432
  const result = await mailer.predict("/send", [
2433
  toEmail, pdfB64, ampLabel,
 
2464
  ampClassOutput.innerHTML = '<div class="class-result-display"><div class="class-label pending" style="color:var(--accent-red)">Error</div></div>';
2465
  confidenceOutput.innerHTML = '<div class="mic-empty">—</div>';
2466
  micChartOutput.innerHTML = '<div class="mic-empty">Unavailable due to prediction error.</div>';
 
2467
  Array.from(additionalOutput.childNodes).forEach(function(node) {
2468
  if (node !== downloadLink && node !== emailStatusResult) node.remove();
2469
  });
 
2507
  const LGRAY= [210, 218, 232];
2508
  const isAMP = label.toLowerCase().includes('amp') && !label.toLowerCase().includes('non-amp');
2509
  const dateStr = new Date().toLocaleDateString('en-GB', { day:'numeric', month:'long', year:'numeric' });
2510
+ const logoB64 = await getBase64(document.querySelector('.header-logo img')?.src || '');
2511
+ const banner64 = await getBase64('image2.png');
2512
+ const shapB64 = await getBase64('shap.png');
2513
  function pageHeader(sectionTitle) {
2514
  pdf.setFillColor(0, 40, 90);
2515
  pdf.rect(0, 0, W, 18, 'F');
2516
  pdf.setFillColor(26, 111, 196);
2517
  pdf.rect(0, 15, W, 3, 'F');
 
2518
  if (logoB64) {
2519
  try { pdf.addImage(logoB64, 'PNG', 8, 2, 13, 13); } catch(e) {}
2520
  }
 
2525
  pdf.setFont('helvetica','normal'); pdf.setFontSize(7.5); pdf.setTextColor(200, 220, 255);
2526
  pdf.text(sectionTitle, W - 10, 11, { align:'right' });
2527
  }
 
 
2528
  function pageFooter(pageNum, total) {
2529
  pdf.setFillColor(245, 247, 252);
2530
  pdf.rect(0, H - 14, W, 14, 'F');
 
2537
  pdf.setTextColor(160, 168, 185);
2538
  pdf.text('Page ' + pageNum + ' of ' + total, W - 10, H - 5.5, { align:'right' });
2539
  }
 
 
2540
  function sectionTitle(text, yPos) {
2541
  pdf.setFont('helvetica','bold'); pdf.setFontSize(14); pdf.setTextColor(0, 63, 125);
2542
  pdf.text(text, 15, yPos);
 
2544
  pdf.line(15, yPos + 2.5, W - 15, yPos + 2.5);
2545
  return yPos + 12;
2546
  }
2547
+ var bannerH = 52;
 
 
 
 
 
 
 
 
 
 
 
2548
  if (banner64) {
2549
  const bi = new Image(); bi.src = banner64;
2550
  await new Promise(r => { bi.onload = r; bi.onerror = r; });
2551
  if (bi.naturalWidth) {
 
2552
  var bw = W, bh = bw * (bi.naturalHeight / bi.naturalWidth);
2553
+ if (bh > 70) bh = 70;
2554
  bannerH = bh;
2555
  pdf.addImage(banner64, 'PNG', 0, 0, bw, bh);
2556
  }
2557
  } else {
 
2558
  pdf.setFillColor(0, 40, 90);
2559
  pdf.rect(0, 0, W, bannerH, 'F');
2560
  pdf.setFillColor(26, 111, 196);
 
2564
  pdf.setFont('helvetica','normal'); pdf.setFontSize(10); pdf.setTextColor(160,200,255);
2565
  pdf.text('Explainable Antimicrobial Peptide Platform', W / 2, bannerH / 2 + 7, { align:'center' });
2566
  }
 
 
2567
  pdf.setFillColor(26, 111, 196);
2568
  pdf.rect(0, bannerH, W, 1.5, 'F');
 
 
2569
  var by = bannerH + 1.5;
2570
  pdf.setFillColor(247, 249, 253);
2571
  pdf.rect(0, by, W, 20, 'F');
 
2574
  pdf.setFont('helvetica','normal'); pdf.setFontSize(8.5); pdf.setTextColor(100, 115, 145);
2575
  pdf.text('Generated on ' + dateStr + ' | Zewail City of Science and Technology \u00B7 BCBU', W / 2, by + 17, { align:'center' });
2576
  by += 22;
 
 
2577
  pdf.setDrawColor(210, 218, 232); pdf.setLineWidth(0.4);
2578
  pdf.line(15, by, W - 15, by);
2579
  by += 7;
 
 
2580
  pdf.setFont('helvetica','bold'); pdf.setFontSize(9); pdf.setTextColor(26, 111, 196);
2581
  pdf.text('QUICK SUMMARY', 15, by);
2582
  pdf.setDrawColor(26, 111, 196); pdf.setLineWidth(0.4);
2583
  pdf.line(15, by + 1.5, W - 15, by + 1.5);
2584
  by += 7;
 
 
2585
  pdf.setFillColor(248, 250, 255);
2586
  pdf.roundedRect(15, by, W - 30, 22, 2, 2, 'F');
2587
  pdf.setDrawColor(210, 218, 232); pdf.setLineWidth(0.3);
 
2594
  pdf.setFont('helvetica','normal'); pdf.setFontSize(6.5); pdf.setTextColor(150, 158, 175);
2595
  pdf.text(seq.length + ' amino acids', W - 20, by + 19, { align:'right' });
2596
  by += 26;
 
 
2597
  var hasMIC = Object.keys(micResults).length > 0;
2598
  var hasLIME = limeFeats.length > 0;
2599
  var hasShap = !!shapB64;
 
2623
  var cx = 15 + i * (cw4 + cardGap);
2624
  pdf.setFillColor(c.fill[0], c.fill[1], c.fill[2]);
2625
  pdf.roundedRect(cx, by, cw4, cardH, 2, 2, 'F');
 
2626
  pdf.setFillColor(c.border[0], c.border[1], c.border[2]);
2627
  pdf.rect(cx, by, 2.5, cardH, 'F');
2628
  pdf.setDrawColor(c.border[0], c.border[1], c.border[2]); pdf.setLineWidth(0.4);
2629
  pdf.roundedRect(cx, by, cw4, cardH, 2, 2, 'S');
 
2630
  pdf.setFont('helvetica','bold'); pdf.setFontSize(5.5); pdf.setTextColor(150,158,175);
2631
  pdf.text(c.label, cx + 6, by + 7);
 
2632
  pdf.setFont('helvetica','bold'); pdf.setFontSize(9);
2633
  pdf.setTextColor(c.valColor[0], c.valColor[1], c.valColor[2]);
2634
  var maxTxtW = cw4 - 10;
2635
  var valLines = pdf.splitTextToSize(c.value, maxTxtW);
2636
  pdf.text(valLines, cx + 6, by + 15);
 
2637
  var subY = by + 15 + valLines.length * 5;
2638
  pdf.setFont('helvetica','normal'); pdf.setFontSize(6); pdf.setTextColor(120,128,145);
2639
  var subLines = pdf.splitTextToSize(c.sub, maxTxtW);
2640
  pdf.text(subLines, cx + 6, subY);
2641
  });
2642
  by += cardH + 4;
 
 
2643
  if (hasMIC) {
2644
  pdf.setFont('helvetica','bold'); pdf.setFontSize(6.5); pdf.setTextColor(26,111,196);
2645
  pdf.text('PREDICTED MIC VALUES (\u00B5M)', 15, by + 5);
 
2651
  micEntries.forEach(function(kv, i) {
2652
  var mx = 15 + i * colW;
2653
  var micVal = typeof kv[1] === 'number' ? kv[1].toFixed(3) : String(kv[1]);
 
2654
  pdf.setFillColor(244, 247, 253);
2655
  pdf.roundedRect(mx + 1, by, colW - 2, 22, 2, 2, 'F');
2656
  pdf.setDrawColor(210,218,232); pdf.setLineWidth(0.25);
 
2664
  } else {
2665
  by += 4;
2666
  }
 
 
2667
  by += 3;
 
2668
  pdf.setFillColor(0, 40, 90);
2669
  pdf.roundedRect(15, by, W - 30, 10, 2, 2, 'F');
2670
  pdf.setFont('helvetica','bold'); pdf.setFontSize(8); pdf.setTextColor(255,255,255);
 
2673
  pdf.text('Section', W - 45, by + 7);
2674
  pdf.text('Page', W - 21, by + 7, { align:'right' });
2675
  by += 12;
 
2676
  var pg = 2;
 
 
2677
  var tocData = [
2678
  { num:'1', icon:'SEQ', title:'Input Sequence',
2679
  desc:'Full amino acid sequence with length and composition',
 
2688
  desc:'Local LIME attributions and global SHAP feature importance',
2689
  page: (hasLIME || hasShap) ? pg++ : null }
2690
  ];
 
2691
  var tocRowH = 13;
2692
  tocData.forEach(function(row, idx) {
2693
  var avail = row.page !== null;
2694
  var rowBg = avail ? (idx % 2 === 0 ? [247,249,254] : [255,255,255]) : [250,250,252];
2695
  pdf.setFillColor(rowBg[0], rowBg[1], rowBg[2]);
2696
  pdf.rect(15, by, W - 30, tocRowH, 'F');
 
 
2697
  var tagColor = avail ? (idx===0?[26,111,196]:idx===1?[39,174,96]:idx===2?[230,81,0]:[126,87,194]) : [190,195,205];
2698
  pdf.setFillColor(tagColor[0], tagColor[1], tagColor[2]);
2699
  pdf.rect(15, by, 3, tocRowH, 'F');
 
 
2700
  pdf.setFillColor(tagColor[0], tagColor[1], tagColor[2]);
2701
  pdf.roundedRect(21, by + 2, 8, 8, 1, 1, 'F');
2702
  pdf.setFont('helvetica','bold'); pdf.setFontSize(6.5); pdf.setTextColor(255,255,255);
2703
  pdf.text(row.num, 25, by + 7.5, { align:'center' });
 
 
2704
  pdf.setFillColor(avail ? 235 : 242, avail ? 240 : 243, avail ? 252 : 248);
2705
  pdf.roundedRect(32, by + 2.5, 10, 7, 1, 1, 'F');
2706
  pdf.setFont('helvetica','bold'); pdf.setFontSize(5); pdf.setTextColor(tagColor[0], tagColor[1], tagColor[2]);
2707
  pdf.text(row.icon, 37, by + 7.5, { align:'center' });
 
 
2708
  pdf.setFont('helvetica','bold'); pdf.setFontSize(8);
2709
  pdf.setTextColor(avail ? 20 : 160, avail ? 35 : 165, avail ? 80 : 180);
2710
  pdf.text(row.title, 45, by + 6.5);
 
 
2711
  pdf.setFont('helvetica','normal'); pdf.setFontSize(6);
2712
  pdf.setTextColor(avail ? 110 : 175, avail ? 118 : 180, avail ? 140 : 195);
2713
  pdf.text(row.desc, 45, by + 11);
 
 
2714
  var pageLabel = avail ? 'pg. ' + row.page : 'N/A';
2715
  pdf.setFont('helvetica', avail ? 'bold' : 'normal'); pdf.setFontSize(8);
2716
  pdf.setTextColor(avail ? tagColor[0] : 180, avail ? tagColor[1] : 185, avail ? tagColor[2] : 195);
2717
  pdf.text(pageLabel, W - 18, by + 7.5, { align:'right' });
 
 
2718
  pdf.setFillColor(210, 218, 232);
2719
  var titleW2 = pdf.getTextWidth(row.title);
2720
  var pgW2 = pdf.getTextWidth(pageLabel);
 
2722
  for (var dx = lx1; dx < lx2 - 1; dx += 2.2) {
2723
  pdf.circle(dx, by + 7, 0.3, 'F');
2724
  }
 
 
2725
  pdf.setDrawColor(225, 230, 242); pdf.setLineWidth(0.15);
2726
  pdf.line(15, by + tocRowH, W - 15, by + tocRowH);
2727
  by += tocRowH;
2728
  });
 
 
2729
  pdf.setFillColor(26, 111, 196);
2730
  pdf.rect(15, by, W - 30, 1, 'F');
2731
  by += 5;
 
 
2732
  pdf.setFillColor(245, 247, 252);
2733
  pdf.rect(0, H - 14, W, 14, 'F');
2734
  pdf.setDrawColor(210, 218, 232); pdf.setLineWidth(0.3);
 
2739
  pdf.text('epicamp.sup@gmail.com', 10, H - 3);
2740
  pdf.setTextColor(160, 168, 185);
2741
  pdf.text('Page 1', W - 10, H - 5.5, { align:'right' });
 
 
 
 
2742
  pdf.addPage();
2743
  pageHeader('Section 1 — Input Sequence');
2744
  var y = 26;
 
2745
  y = sectionTitle('1. Input Sequence', y);
2746
  pdf.setFont('helvetica','normal'); pdf.setFontSize(8.5); pdf.setTextColor(130,138,155);
2747
  pdf.text('Full amino acid sequence submitted for analysis (' + seq.length + ' residues).', 15, y, { maxWidth: W - 30 });
2748
  y += 10;
 
2749
  var seqLines = pdf.splitTextToSize(seq, W - 44);
2750
  var seqBoxH = 10 + seqLines.length * 5.5 + 4;
2751
  pdf.setFillColor(248, 250, 255);
 
2761
  pdf.setFont('helvetica','normal'); pdf.setFontSize(7); pdf.setTextColor(150,158,175);
2762
  pdf.text(seq.length + ' amino acids', W - 20, y + seqBoxH - 3, { align:'right' });
2763
  y += seqBoxH + 10;
 
 
2764
  var statsData = [
2765
  { k:'Length', v: seq.length + ' aa' },
2766
  { k:'Hydrophobic', v: (function(){ var h=0; for(var i=0;i<seq.length;i++) if('AVILMFWP'.includes(seq[i]))h++; return Math.round(h/seq.length*100)+'%'; })() },
 
2780
  pdf.text(s.v, sx + sw/2, y + 14, { align:'center' });
2781
  });
2782
  y += 25;
 
 
 
 
2783
  pdf.addPage();
2784
  pageHeader('Section 2 — Classification & Confidence');
2785
  y = 26;
 
2786
  y = sectionTitle('2. Classification & Confidence', y);
2787
  pdf.autoTable({
2788
  startY: y,
 
2815
  margin: { left: 15, right: 15 }
2816
  });
2817
  y = pdf.lastAutoTable.finalY + 10;
 
 
2818
  pdf.setFont('helvetica','bold'); pdf.setFontSize(8); pdf.setTextColor(0,40,90);
2819
  pdf.text('Confidence Indicator', 15, y + 5);
2820
  y += 9;
2821
  var barW = W - 30, barH2 = 7;
 
2822
  pdf.setFillColor(220, 226, 240);
2823
  pdf.roundedRect(15, y, barW, barH2, 2, 2, 'F');
 
2824
  var fillC = conf >= 0.7 ? GRN : conf >= 0.5 ? [230,81,0] : RED;
2825
  pdf.setFillColor(fillC[0], fillC[1], fillC[2]);
2826
  pdf.roundedRect(15, y, Math.max(barW * conf, 3), barH2, 2, 2, 'F');
 
2827
  pdf.setFont('helvetica','bold'); pdf.setFontSize(7); pdf.setTextColor(fillC[0], fillC[1], fillC[2]);
2828
  pdf.text((conf*100).toFixed(1) + '%', 15 + barW * conf + 2, y + 5.5);
2829
  y += barH2 + 5;
 
2830
  pdf.setFont('helvetica','normal'); pdf.setFontSize(6.5); pdf.setTextColor(160,168,185);
2831
  pdf.text('0%', 15, y + 3);
2832
  pdf.text('50%', 15 + barW/2, y + 3, { align:'center' });
2833
  pdf.text('100%', 15 + barW, y + 3, { align:'right' });
2834
  y += 12;
 
 
 
 
2835
  var micRows = Object.entries(micResults).map(function(kv) {
2836
  return [kv[0], typeof kv[1] === 'number' ? kv[1].toFixed(3) + ' \u00B5M' : String(kv[1])];
2837
  });
 
2878
  margin: { left: 15, right: 15 }
2879
  });
2880
  y = pdf.lastAutoTable.finalY + 14;
 
 
2881
  pdf.setFont('helvetica','bold'); pdf.setFontSize(8); pdf.setTextColor(0,40,90);
2882
  pdf.text('MIC Comparison Chart', 15, y + 5);
2883
  y += 11;
 
2890
  var bx = 15 + i * barSlotW + barSlotW * 0.15;
2891
  var bw2 = barSlotW * 0.7;
2892
  var barColor = val < 4 ? GRN : val < 16 ? [230,81,0] : RED;
 
2893
  pdf.setFillColor(barColor[0], barColor[1], barColor[2]);
2894
  pdf.roundedRect(bx, y + maxBarH - bh, bw2, bh, 1, 1, 'F');
 
2895
  pdf.setFont('helvetica','bold'); pdf.setFontSize(6.5); pdf.setTextColor(barColor[0], barColor[1], barColor[2]);
2896
  pdf.text(val.toFixed(2), bx + bw2 / 2, y + maxBarH - bh - 2, { align:'center' });
 
2897
  pdf.setFont('helvetica','italic'); pdf.setFontSize(6); pdf.setTextColor(80,90,110);
2898
  pdf.text(r[0], bx + bw2 / 2, y + maxBarH + 5, { align:'center' });
2899
  });
2900
  y += maxBarH + 12;
2901
  }
 
 
 
 
2902
  if (limeFeats.length > 0 || shapB64) {
2903
  pdf.addPage();
2904
  pageHeader('Section 4 — Feature Explanation (LIME & SHAP)');
2905
  y = 26;
 
2906
  if (limeFeats.length > 0) {
2907
  y = sectionTitle('4a. LIME — Local Feature Attribution', y);
2908
  pdf.setFont('helvetica','normal'); pdf.setFontSize(8); pdf.setTextColor(130,138,155);
 
2939
  });
2940
  y = pdf.lastAutoTable.finalY + 12;
2941
  }
 
2942
  if (shapB64) {
2943
  if (y + 50 > H - 20) { pdf.addPage(); pageHeader('Section 4 — Feature Explanation (LIME & SHAP)'); y = 26; }
2944
  y = sectionTitle('4b. SHAP — Global Feature Importance', y);
 
2955
  }
2956
  }
2957
  }
 
 
 
 
2958
  var total = pdf.internal.getNumberOfPages();
2959
  for (var p = 1; p <= total; p++) {
2960
  pdf.setPage(p);
2961
  pageFooter(p, total);
2962
  }
2963
  return pdf;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2964
  }
 
2965
  </script>
2966
  </body>
2967
  </html>