function show(el) { el.classList.remove("hidden"); } function hide(el) { el.classList.add("hidden"); } function escapeHtml(str) { return String(str) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function splitCsvLine(line) { const out = []; let cur = ""; let inQuotes = false; for (let i = 0; i < line.length; i++) { const ch = line[i]; if (ch === '"') { if (inQuotes && line[i + 1] === '"') { cur += '"'; i++; } else { inQuotes = !inQuotes; } } else if (ch === "," && !inQuotes) { out.push(cur); cur = ""; } else { cur += ch; } } out.push(cur); return out.map(v => v.trim()); } const fileInput = document.getElementById("file_input"); const step2 = document.getElementById("step-2"); const step3 = document.getElementById("step-3"); const step4 = document.getElementById("step-4"); const step5 = document.getElementById("step-5"); const labelSelect = document.getElementById("label_select"); const labelError = document.getElementById("label_error"); const normalBox = document.getElementById("normal_values"); const normalError = document.getElementById("normal_error"); const featureList = document.getElementById("feature_list"); const featureCounter = document.getElementById("feature_counter"); const featureError = document.getElementById("feature_error"); const btnStep2Next = document.getElementById("btn_step2_next"); const btnStep3Next = document.getElementById("btn_step3_next"); const btnRun = document.getElementById("btn_run"); const MAX_FEATURES = 5; const MAX_FILE_SIZE = 30 * 1024 * 1024; const BUSY_MESSAGE = "Another algorithm is already running. Please wait until it finishes."; let headers = []; let rows = []; let labelIndex = -1; let numericColumnIdx = []; function showPopup(message) { if (typeof showErrorPopup === "function") { showErrorPopup(message); } else { alert(message); } } function resetUploadFlow() { fileInput.value = ""; headers = []; rows = []; labelIndex = -1; numericColumnIdx = []; labelSelect.innerHTML = ``; normalBox.innerHTML = ""; featureList.innerHTML = ""; featureCounter.textContent = `0 / ${MAX_FEATURES} selected`; hide(step2); hide(step3); hide(step4); hide(step5); hide(labelError); hide(normalError); hide(featureError); } async function isBackendBusy() { try { const res = await fetch("get-status"); if (!res.ok) { showPopup("Backend status could not be checked."); return true; } const status = await res.json(); return status.running === true; } catch { showPopup("Connection to backend was lost."); return true; } } async function readErrorMessage(response, fallback) { try { const data = await response.clone().json(); return data.error || data.message || fallback; } catch { return fallback; } } fileInput.addEventListener("change", async () => { if (!fileInput.files.length) return; const file = fileInput.files[0]; if (file.size > MAX_FILE_SIZE) { showPopup("File is too big! Maximum size is 30MB."); resetUploadFlow(); return; } if (!file.name.toLowerCase().endsWith(".csv")) { showPopup("Only CSV files are allowed."); resetUploadFlow(); return; } if (await isBackendBusy()) { showPopup(BUSY_MESSAGE); resetUploadFlow(); return; } const formData = new FormData(); formData.append("file", file); let uploadResponse; try { uploadResponse = await fetch("upload-dataset", { method: "POST", body: formData }); } catch { showPopup("Dataset upload failed. Backend server is not available."); resetUploadFlow(); return; } if (!uploadResponse.ok) { if (uploadResponse.status !== 409) { const message = await readErrorMessage(uploadResponse, "Dataset upload failed."); showPopup(message); } resetUploadFlow(); return; } let text; try { text = await file.text(); } catch { showPopup("File could not be read."); resetUploadFlow(); return; } const lines = text.split(/\r?\n/).filter(l => l.trim() !== ""); if (lines.length < 2) { showPopup("CSV file must contain a header and at least one data row."); resetUploadFlow(); return; } headers = splitCsvLine(lines[0]); rows = lines.slice(1).map(splitCsvLine); labelSelect.innerHTML = ``; headers.forEach((h, idx) => { const opt = document.createElement("option"); opt.value = idx; opt.textContent = h; labelSelect.appendChild(opt); }); numericColumnIdx = detectNumericColumns(headers, rows, 30); show(step2); hide(step3); hide(step4); hide(step5); hide(labelError); }); function detectNumericColumns(headers, rows, sampleCount) { const result = []; const samples = Math.min(sampleCount, rows.length); for (let c = 0; c < headers.length; c++) { let valid = 0; let total = 0; for (let r = 0; r < samples; r++) { const v = rows[r][c]; if (v === undefined || v === "") continue; total++; if (!isNaN(Number(v))) { valid++; } } if (total > 0 && valid / total >= 0.9) { result.push(c); } } return result; } btnStep2Next.addEventListener("click", async () => { if (await isBackendBusy()) { showPopup(BUSY_MESSAGE); resetUploadFlow(); return; } if (labelSelect.value === "") { show(labelError); return; } hide(labelError); labelIndex = Number(labelSelect.value); const uniques = new Set(); rows.forEach(r => { const v = r[labelIndex]; if (v !== undefined && v !== "") { uniques.add(v.trim()); } }); normalBox.innerHTML = ""; uniques.forEach(val => { const label = document.createElement("label"); label.className = "flex items-center gap-3 bg-gray-900/40 border border-gray-700 rounded-lg px-4 py-2"; label.innerHTML = ` ${escapeHtml(val)} `; normalBox.appendChild(label); }); normalBox.querySelectorAll("input").forEach(cb => { cb.addEventListener("change", () => { if (cb.checked) { normalBox.querySelectorAll("input").forEach(o => { if (o !== cb) o.checked = false; }); } }); }); show(step3); hide(step4); hide(step5); hide(normalError); }); btnStep3Next.addEventListener("click", async () => { if (await isBackendBusy()) { showPopup(BUSY_MESSAGE); resetUploadFlow(); return; } const checked = normalBox.querySelectorAll("input:checked"); if (checked.length !== 1) { normalError.textContent = "Select exactly one NORMAL value."; show(normalError); return; } hide(normalError); featureList.innerHTML = ""; numericColumnIdx .filter(idx => idx !== labelIndex) .forEach(idx => { const label = document.createElement("label"); label.className = "flex items-center justify-between bg-gray-900/40 border border-gray-700 rounded-lg px-4 py-2"; label.innerHTML = `