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 = `
${escapeHtml(headers[idx])}
numeric `; featureList.appendChild(label); }); featureList.querySelectorAll("input").forEach(cb => cb.addEventListener("change", updateCounter) ); updateCounter(); show(step4); hide(step5); hide(featureError); }); function updateCounter() { const all = featureList.querySelectorAll("input"); const selected = featureList.querySelectorAll("input:checked").length; featureCounter.textContent = `${selected} / ${MAX_FEATURES} selected`; all.forEach(cb => { cb.disabled = selected >= MAX_FEATURES && !cb.checked; cb.classList.toggle("opacity-60", cb.disabled); }); } btnRun.addEventListener("click", async () => { if (await isBackendBusy()) { showPopup(BUSY_MESSAGE); resetUploadFlow(); return; } const selectedFeatures = Array.from( featureList.querySelectorAll("input:checked") ).map(cb => headers[cb.dataset.col]); if (selectedFeatures.length !== MAX_FEATURES) { featureError.textContent = `You must select exactly ${MAX_FEATURES} numeric features.`; show(featureError); return; } hide(featureError); const labelColumnName = headers[labelIndex]; const normalInput = normalBox.querySelector("input:checked"); if (!normalInput) { normalError.textContent = "Select exactly one NORMAL value."; show(normalError); return; } let normalValue = normalInput.value; if (!isNaN(normalValue)) { normalValue = Number(normalValue); } const config = { dataset: { file_path: "temp/upload.csv", dataset_name: "custom_user_dataset" }, labeling: { label_column: labelColumnName, normal_value: normalValue, normal_label: 0, anomaly_label: 1 }, features: { selected_columns: selectedFeatures, expected_feature_count: selectedFeatures.length }, algorithm: { name: "custom_dataset" } }; let saveResponse; try { saveResponse = await fetch("save-config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(config) }); } catch { showPopup("Configuration could not be saved. Backend server is not available."); return; } if (!saveResponse.ok) { if (saveResponse.status !== 409) { const message = await readErrorMessage(saveResponse, "Configuration could not be saved."); showPopup(message); } hide(step5); return; } show(step5); });