272 lines
7.0 KiB
JavaScript
272 lines
7.0 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;
|
|
|
|
|
|
let headers = [];
|
|
let rows = [];
|
|
let labelIndex = -1;
|
|
let numericColumnIdx = [];
|
|
|
|
fileInput.addEventListener("change", async () => {
|
|
|
|
const formData = new FormData();
|
|
formData.append("file", fileInput.files[0]);
|
|
|
|
await fetch("/upload-dataset", {
|
|
method: "POST",
|
|
body: formData
|
|
});
|
|
|
|
if (!fileInput.files.length) return;
|
|
|
|
const file = fileInput.files[0];
|
|
const text = await file.text();
|
|
|
|
const lines = text.split(/\r?\n/).filter(l => l.trim() !== "");
|
|
if (lines.length < 2) 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", () => {
|
|
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", () => {
|
|
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(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 () => {
|
|
|
|
const labelColumnName = headers[labelIndex];
|
|
let normalValue = normalBox.querySelector("input:checked").value;
|
|
|
|
if (!isNaN(normalValue)) {
|
|
normalValue = Number(normalValue);
|
|
}
|
|
|
|
const selectedFeatures = Array.from(
|
|
featureList.querySelectorAll("input:checked")
|
|
).map(cb => headers[cb.dataset.col]);
|
|
|
|
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: "isolation_forest"
|
|
}
|
|
};
|
|
|
|
await fetch("/save-config", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(config)
|
|
});
|
|
const selected = featureList.querySelectorAll("input:checked").length;
|
|
|
|
if (selected !== MAX_FEATURES) {
|
|
featureError.textContent = `You must select exactly ${MAX_FEATURES} numeric features.`;
|
|
show(featureError);
|
|
return;
|
|
}
|
|
|
|
hide(featureError);
|
|
|
|
show(step5);
|
|
});
|
|
|