Spaces:
Runtime error
Runtime error
| /* ========================================================================= | |
| 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 = | |
| '<span class="spinner"></span> বিশ্লেষণ চলছে…'; | |
| } | |
| } | |
| /* ---------------- rendering ---------------- */ | |
| function renderError(msg) { | |
| outputBody.className = "output-body"; | |
| outputBody.innerHTML = `<div class="error-box">${escapeHtml(msg)}</div>`; | |
| } | |
| 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 = `<div class="result-card">`; | |
| html += `<div class="result-card__top"> | |
| <span class="result-label ${labelClass}">${escapeHtml(label)}</span> | |
| <span class="result-task">${escapeHtml(data.task_name || "")}</span> | |
| </div>`; | |
| if (score !== null) { | |
| const pct = Math.round(score * 100); | |
| html += `<div class="confidence"> | |
| <div class="confidence__bar"><div class="confidence__fill" style="width:0%"></div></div> | |
| <div class="confidence__num">আত্মবিশ্বাস · confidence: ${pct}%</div> | |
| </div>`; | |
| } | |
| // Render any extra details the model returned. | |
| const details = r.details || {}; | |
| const keys = Object.keys(details); | |
| if (keys.length) { | |
| html += `<div class="details-grid">`; | |
| for (const k of keys) { | |
| let v = details[k]; | |
| if (typeof v === "number") v = (v * 100).toFixed(1) + "%"; | |
| html += `<div class="detail-row"><span class="k">${escapeHtml( | |
| k | |
| )}</span><span class="v">${escapeHtml(String(v))}</span></div>`; | |
| } | |
| html += `</div>`; | |
| } | |
| html += `</div>`; | |
| 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, "<") | |
| .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(); |