Spaces:
Sleeping
Sleeping
| 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"); | |
| const downloadBtn = document.getElementById("downloadCsvBtn"); | |
| if (downloadBtn) { | |
| downloadBtn.addEventListener("click", downloadCSV); | |
| } | |
| 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 = `<span style="color:${meta.color}">Predicted Location: ${label}</span>`; | |
| 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 = ` | |
| <td>${idx + 1}</td> | |
| <td>${r.sequence}</td> | |
| <td>${r.length}</td> | |
| <td>${pretty(r.prediction_label)}</td> | |
| <td>${(maxProb * 100).toFixed(2)}%</td> | |
| `; | |
| 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); | |
| }); | |
| } | |