/* ========================================================================= Bangla Text Analyzer — frontend logic ========================================================================= */ const $ = (sel) => document.querySelector(sel); const textInput = $("#text-input"); const dropZone = $("#drop-zone"); const fileInput = $("#file-input"); const fileBtn = $("#file-btn"); const fileName = $("#file-name"); const charCount = $("#char-count"); const taskSelect = $("#task-select"); const modelWrap = $("#model-wrap"); const modelSelect = $("#model-select"); const runBtn = $("#run-btn"); const outputBody = $("#output-body"); const outputDot = $(".output-head__dot"); // Holds a dropped/selected file so we can send it instead of textarea text. let pendingFile = null; /* ---------------- character counter ---------------- */ function updateCount() { const n = textInput.value.length; charCount.textContent = `${n.toLocaleString("bn-BD")} অক্ষর`; } textInput.addEventListener("input", () => { // If the user types, we abandon any previously attached file. if (pendingFile && textInput.value.trim()) clearFile(); updateCount(); }); /* ---------------- file handling ---------------- */ function clearFile() { pendingFile = null; fileName.textContent = ""; fileInput.value = ""; } function attachFile(file) { pendingFile = file; fileName.textContent = "📎 " + file.name; // Show a preview of the file's text in the textarea. const reader = new FileReader(); reader.onload = (e) => { textInput.value = e.target.result; updateCount(); }; reader.readAsText(file, "utf-8"); } fileBtn.addEventListener("click", () => fileInput.click()); fileInput.addEventListener("change", (e) => { if (e.target.files.length) attachFile(e.target.files[0]); }); // Drag & drop onto the text area. ["dragenter", "dragover"].forEach((evt) => dropZone.addEventListener(evt, (e) => { e.preventDefault(); dropZone.classList.add("dragover"); }) ); ["dragleave", "drop"].forEach((evt) => dropZone.addEventListener(evt, (e) => { e.preventDefault(); if (evt === "dragleave" && dropZone.contains(e.relatedTarget)) return; dropZone.classList.remove("dragover"); }) ); dropZone.addEventListener("drop", (e) => { const file = e.dataTransfer.files[0]; if (file) attachFile(file); }); /* ---------------- running analysis ---------------- */ async function analyze() { const task = taskSelect.value; const text = textInput.value.trim(); // Only the hate-speech task offers a model choice; default otherwise. const model = task === "hate_speech" ? modelSelect.value : "banglabert"; if (!text) { renderError("দয়া করে কিছু টেক্সট লিখুন বা একটি ফাইল দিন।"); return; } setLoading(true); try { let response; if (pendingFile) { // Send the raw file via multipart form-data. const fd = new FormData(); fd.append("file", pendingFile); fd.append("task", task); fd.append("model", model); response = await fetch("/analyze", { method: "POST", body: fd }); } else { // Send typed text as JSON. response = await fetch("/analyze", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text, task, model }), }); } const data = await response.json(); if (!response.ok) { renderError(data.error || "একটি ত্রুটি ঘটেছে।"); } else { renderResult(data); } } catch (err) { renderError("সার্ভারের সাথে সংযোগ ব্যর্থ হয়েছে: " + err.message); } finally { setLoading(false); } } function setLoading(on) { runBtn.disabled = on; outputDot.classList.toggle("live", on); if (on) { outputBody.className = "output-body"; outputBody.innerHTML = ' বিশ্লেষণ চলছে…'; } } /* ---------------- rendering ---------------- */ function renderError(msg) { outputBody.className = "output-body"; outputBody.innerHTML = `
${escapeHtml(msg)}
`; } function classifyLabel(label) { const l = (label || "").toLowerCase(); if (/(not|human|positive|neutral|নয়|মানব|ইতিবাচক)/.test(l)) return "is-good"; if (/(hate|offensive|ai|negative|generated|ঘৃণা|নেতিবাচক|কৃত্রিম)/.test(l)) return "is-bad"; return ""; } function renderResult(data) { const r = data.result || {}; const label = r.label ?? "—"; const score = typeof r.score === "number" ? r.score : null; const labelClass = classifyLabel(label); let html = `
`; html += `
${escapeHtml(label)} ${escapeHtml(data.task_name || "")}
`; if (score !== null) { const pct = Math.round(score * 100); html += `
আত্মবিশ্বাস · confidence: ${pct}%
`; } // Render any extra details the model returned. const details = r.details || {}; const keys = Object.keys(details); if (keys.length) { html += `
`; for (const k of keys) { let v = details[k]; if (typeof v === "number") v = (v * 100).toFixed(1) + "%"; html += `
${escapeHtml( k )}${escapeHtml(String(v))}
`; } html += `
`; } html += `
`; outputBody.className = "output-body"; outputBody.innerHTML = html; // Animate the confidence bar after it's in the DOM. if (score !== null) { requestAnimationFrame(() => { const fill = outputBody.querySelector(".confidence__fill"); if (fill) fill.style.width = Math.round(score * 100) + "%"; }); } } function escapeHtml(str) { return String(str) .replace(/&/g, "&") .replace(//g, ">"); } /* ---------------- events ---------------- */ runBtn.addEventListener("click", analyze); // Show the model picker only when the hate-speech task is selected. function updateModelVisibility() { modelWrap.hidden = taskSelect.value !== "hate_speech"; } taskSelect.addEventListener("change", updateModelVisibility); updateModelVisibility(); // set correct state on page load // Ctrl/Cmd + Enter (or plain Enter when not adding a newline) runs analysis. textInput.addEventListener("keydown", (e) => { if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); analyze(); } }); updateCount();