436 lines
10 KiB
JavaScript
436 lines
10 KiB
JavaScript
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 = `<option value="">-- select label column --</option>`;
|
|
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 = `<option value="">-- select label column --</option>`;
|
|
|
|
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 = `
|
|
<input type="checkbox" value="${escapeHtml(val)}" class="accent-orange-500">
|
|
<span>${escapeHtml(val)}</span>
|
|
`;
|
|
|
|
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 = `
|
|
<div class="flex items-center gap-3">
|
|
<input type="checkbox" data-col="${idx}" class="accent-orange-500">
|
|
<span>${escapeHtml(headers[idx])}</span>
|
|
</div>
|
|
<span class="text-xs text-gray-500">numeric</span>
|
|
`;
|
|
|
|
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);
|
|
}); |