Spaces:
Running
Running
Update index.html
Browse files- 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="
|
| 1396 |
-
<div class="example-item-title"><span class="example-badge badge-amp">AMP</span>
|
| 1397 |
-
<div class="example-item-seq">
|
| 1398 |
</div>
|
| 1399 |
-
<div class="example-item" data-seq="
|
| 1400 |
-
<div class="example-item-title"><span class="example-badge badge-amp">AMP</span>
|
| 1401 |
-
<div class="example-item-seq">
|
| 1402 |
</div>
|
| 1403 |
-
<div class="example-item" data-seq="
|
| 1404 |
-
<div class="example-item-title"><span class="example-badge badge-nonamp">Non-AMP</span>
|
| 1405 |
-
<div class="example-item-seq">
|
| 1406 |
</div>
|
| 1407 |
-
<div class="example-item" data-seq="
|
| 1408 |
-
<div class="example-item-title"><span class="example-badge badge-nonamp">Non-AMP</span>
|
| 1409 |
-
<div class="example-item-seq">
|
| 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>
|
| 1652 |
-
<tr><td>2</td><td>
|
| 1653 |
-
<tr><td>3</td><td>
|
| 1654 |
-
<tr><td>4</td><td>
|
| 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">×</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="
|
| 1785 |
</footer>
|
| 1786 |
-
<script
|
| 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';
|
| 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;
|
| 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;
|
| 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;
|
| 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"/>' +
|
| 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(() => {});
|
| 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
|
| 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 |
-
|
|
|
|
| 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;
|
| 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-2 — 23 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 A — 37 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 fragment — 59 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 construct — 50 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">×</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>
|