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 = ` ${idx + 1} ${r.sequence} ${r.length} ${pretty(r.prediction_label)} ${(maxProb * 100).toFixed(2)}% `; tr.addEventListener("click", () => openFastaModal(idx)); tbody.appendChild(tr); }); } // FASTA modal (no charts) function openFastaModal(i) { const r = fastaResults[i]; if (!r) return; document.getElementById("fastaModalTitle").textContent = `Sequence: ${r.sequence}`; document.getElementById("fastaModalMeta").textContent = `Length: ${r.length} | Predicted: ${pretty(r.prediction_label)}`; updateProbList(document.getElementById("fastaProbList"), r.probabilities); document.getElementById("fastaModal").classList.remove("hidden"); } function closeFastaModal() { document.getElementById("fastaModal").classList.add("hidden"); } // ------------------ BIG CHART MODALS ------------------ function openBigBar() { if (!barChart) return; const modal = document.getElementById("bigBarModal"); const ctx = document.getElementById("bigBarChart").getContext("2d"); if (bigBarChartObj) bigBarChartObj.destroy(); bigBarChartObj = new Chart(ctx, { type: barChart.config.type, data: barChart.config.data, options: { responsive: false, animation: false, scales: { y: { beginAtZero: true, max: 1 }, x: { ticks: { color: "#e5e7eb" } } } } }); modal.classList.remove("hidden"); } function closeBigBar() { document.getElementById("bigBarModal").classList.add("hidden"); } function openBigRadar() { if (!radarChart) return; const modal = document.getElementById("bigRadarModal"); const ctx = document.getElementById("bigRadarChart").getContext("2d"); if (bigRadarChartObj) bigRadarChartObj.destroy(); bigRadarChartObj = new Chart(ctx, { type: radarChart.config.type, data: radarChart.config.data, options: { responsive: false, animation: false, scales: { r: { beginAtZero: true, max: 1, grid: { color: "rgba(55,65,81,0.4)" }, angleLines: { color: "rgba(55,65,81,0.4)" }, pointLabels: { color: "#e4e4e4ff", font: { size: 18 } }, ticks: { display: false } } } } }); modal.classList.remove("hidden"); } function closeBigRadar() { document.getElementById("bigRadarModal").classList.add("hidden"); } // DOWNLOAD CSV function downloadCSV() { if (!fastaResults || fastaResults.length === 0) { alert("No batch results available to download."); return; } fetch("/api/download_csv", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(fastaResults) }) .then(res => { if (!res.ok) throw new Error("Failed to generate CSV"); return res.blob(); }) .then(blob => { const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "canloc_results.csv"; document.body.appendChild(a); a.click(); a.remove(); window.URL.revokeObjectURL(url); }) .catch(err => { alert("Download error: " + err.message); }); }