BC_praca/Frontend/JS/upload.js
2026-05-06 17:00:41 +02:00

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("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
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);
});