let barChart = null; let radarChart = null; let bigBarChartObj = null; let bigRadarChartObj = null; let fastaResults = []; const API_BASE = ""; // same origin //------------------ Safe Fetch ------------------ async function safeFetchJSON(url, options = {}) { const res = await fetch(url, options); const text = await res.text(); let data; try { data = JSON.parse(text); } catch { throw new Error(text || `HTTP ${res.status}`); } if (!res.ok) { throw new Error(data.error || `HTTP ${res.status}`); } return data; } // ------------------ INIT ------------------ document.addEventListener("DOMContentLoaded", () => { // Tabs const tabSeq = document.getElementById("tab-seq"); const tabFasta = document.getElementById("tab-fasta"); const panelSeq = document.getElementById("panel-seq"); const panelFasta = document.getElementById("panel-fasta"); if (tabSeq && tabFasta && panelSeq && panelFasta) { tabSeq.addEventListener("click", () => { tabSeq.classList.add("active"); tabFasta.classList.remove("active"); panelSeq.classList.remove("hidden"); panelFasta.classList.add("hidden"); }); tabFasta.addEventListener("click", () => { tabFasta.classList.add("active"); tabSeq.classList.remove("active"); panelFasta.classList.remove("hidden"); panelSeq.classList.add("hidden"); }); } // Buttons const predictBtn = document.getElementById("predictBtn"); if (predictBtn) predictBtn.addEventListener("click", predictSequence); const predictFastaBtn = document.getElementById("predictFastaBtn"); if (predictFastaBtn) predictFastaBtn.addEventListener("click", predictFasta); // FASTA modal close events const fastaBackdrop = document.getElementById("fastaModalBackdrop"); const fastaClose = document.getElementById("fastaModalClose"); if (fastaBackdrop) fastaBackdrop.addEventListener("click", closeFastaModal); if (fastaClose) fastaClose.addEventListener("click", closeFastaModal); // Big chart click const barCanvas = document.getElementById("barChart"); const radarCanvas = document.getElementById("radarChart"); if (barCanvas) barCanvas.addEventListener("click", openBigBar); if (radarCanvas) radarCanvas.addEventListener("click", openBigRadar); }); // ------------------ HELPERS ------------------ function validateSequence(seq) { const s = seq.trim().toUpperCase(); if (!s) return [false, "Sequence is empty."]; const validAA = /^[ACDEFGHIKLMNPQRSTVWYUBZX*]+$/; if (!validAA.test(s)) return [false, "Invalid characters in sequence."]; if (s.length < 15) return [false, "Sequence too short (min 15 AA)."]; return [true, null]; } // Capitalize class names function pretty(name) { return name.charAt(0).toUpperCase() + name.slice(1); } // Probability severity function getConfidenceMeta(maxProb) { if (maxProb >= 0.75) return { color: "#4ade80", text: "High confidence" }; if (maxProb >= 0.6) return { color: "#facc15", text: "Medium confidence" }; return { color: "#f87171", text: "Low confidence – interpret cautiously" }; } // Bar chart colors function buildBarColors(values) { const max = Math.max(...values); return values.map(v => v === max ? "rgba(45,212,191,0.95)" : "rgba(166,184,184,0.9)" ); } // Probability list builder function updateProbList(container, probs) { if (!container) return; container.innerHTML = ""; Object.entries(probs).forEach(([k, v]) => { const div = document.createElement("div"); div.className = "prob-item"; const left = document.createElement("span"); left.textContent = pretty(k); // CAPITALIZED const right = document.createElement("span"); right.textContent = (v * 100).toFixed(2) + "%"; // PERCENT div.appendChild(left); div.appendChild(right); container.appendChild(div); }); } // ------------------ CHART DRAWING ------------------ function drawCharts(classLabels, rawValues) { const barCanvas = document.getElementById("barChart"); const radarCanvas = document.getElementById("radarChart"); if (!barCanvas || !radarCanvas) return; const labels = classLabels.map(pretty); // CAPITALIZE const values = rawValues; if (barChart) barChart.destroy(); if (radarChart) radarChart.destroy(); // Bar Chart barChart = new Chart(barCanvas.getContext("2d"), { type: "bar", data: { labels, datasets: [ { data: values, backgroundColor: buildBarColors(values), borderRadius: 6 } ] }, options: { responsive: true, plugins: { legend: { display: false }, tooltip: { callbacks: { label: ctx => (ctx.parsed.y * 100).toFixed(2) + "%" // PERCENT } } }, scales: { y: { beginAtZero: true, max: 1 }, x: { ticks: { color: "#e5e7eb" } } } } }); // Radar Chart radarChart = new Chart(radarCanvas.getContext("2d"), { type: "radar", data: { labels, datasets: [ { data: values, backgroundColor: "rgba(45,212,191,0.18)", borderColor: "rgba(45,212,191,0.9)", borderWidth: 2 } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { callbacks: { label: ctx => (ctx.parsed.r * 100).toFixed(2) + "%" // PERCENT } } }, scales: { r: { beginAtZero: true, max: 1, grid: { color: "rgba(55,65,81,0.4)" }, angleLines: { color: "rgba(55,65,81,0.4)" }, pointLabels: { color: "#e5e7eb", font: { size: 16 } }, ticks: { display: false } } } } }); } // ------------------ SINGLE SEQUENCE ------------------ async function predictSequence() { const seqInput = document.getElementById("sequenceInput"); const seq = seqInput.value.trim(); const [ok, err] = validateSequence(seq); if (!ok) return alert(err); const loading = document.getElementById("loadingSeq"); const resultCard = document.getElementById("seqResultCard"); const resultHeader = document.getElementById("seqResultHeader"); const warningEl = document.getElementById("seqConfidenceWarning"); const probList = document.getElementById("seqProbList"); loading.classList.remove("hidden"); resultCard.classList.add("hidden"); try { const res = await fetch(`${API_BASE}/api/predict_sequence`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sequence: seq }) }); const data = await res.json(); loading.classList.add("hidden"); if (!res.ok || data.error) return alert(data.error || "Prediction failed."); const label = pretty(data.prediction_label); // CAPITALIZED const probs = data.probabilities || {}; const labels = Object.keys(probs); const values = labels.map(k => probs[k]); const maxProb = Math.max(...values); const meta = getConfidenceMeta(maxProb); resultHeader.innerHTML = `Predicted Location: ${label}`; warningEl.textContent = meta.text; warningEl.style.color = meta.color; updateProbList(probList, probs); drawCharts(labels, values); resultCard.classList.remove("hidden"); } catch (e) { loading.classList.add("hidden"); alert("Server error: " + e); } } // ------------------ FASTA ------------------ async function predictFasta() { const fileInput = document.getElementById("fastaFile"); const file = fileInput.files[0]; if (!file) return alert("Choose a FASTA file."); const loading = document.getElementById("loadingFasta"); const wrapper = document.getElementById("fastaResultsWrapper"); const tbody = document.getElementById("fastaTableBody"); loading.classList.remove("hidden"); wrapper.classList.add("hidden"); tbody.innerHTML = ""; fastaResults = []; try { const form = new FormData(); form.append("file", file); const res = await fetch(`${API_BASE}/api/predict_fasta`, { method: "POST", body: form }); const data = await res.json(); loading.classList.add("hidden"); if (!res.ok || data.error) return alert(data.error || "FASTA read error."); fastaResults = data.results; renderFastaTable(); wrapper.classList.remove("hidden"); } catch (e) { loading.classList.add("hidden"); alert("Server error: " + e); } } function renderFastaTable() { const tbody = document.getElementById("fastaTableBody"); tbody.innerHTML = ""; fastaResults.forEach((r, idx) => { const probs = r.probabilities || {}; const maxProb = Math.max(...Object.values(probs)); const tr = document.createElement("tr"); tr.dataset.index = idx; tr.innerHTML = `