textclsy / app /static /js /classify.js
Arafath10's picture
Upload 33 files
dad80ae verified
Raw
History Blame Contribute Delete
4.59 kB
/* Classify page: submit a complaint and render the result. */
document.addEventListener("DOMContentLoaded", () => {
const input = $("#complaint");
const btn = $("#btnClassify");
const spin = $("#spin");
const thr = $("#optThreshold");
// r=37 in the ring SVG -> circumference 2*pi*37
const RING_C = 2 * Math.PI * 37;
thr.addEventListener("input", () => { $("#thrOut").value = (+thr.value).toFixed(2); });
$$(".chip").forEach((chip) =>
chip.addEventListener("click", () => {
input.value = chip.dataset.sample;
input.focus();
})
);
$("#btnClear").addEventListener("click", () => {
input.value = "";
$("#result").classList.add("hidden");
$("#resultEmpty").classList.remove("hidden");
input.focus();
});
input.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") classify();
});
btn.addEventListener("click", classify);
async function classify() {
const text = input.value.trim();
if (!text) { toast("Type a complaint first.", true); input.focus(); return; }
btn.disabled = true;
spin.classList.remove("hidden");
try {
const data = await api("/api/classify", {
method: "POST",
body: {
text,
translate: $("#optTranslate").checked,
multi_label: $("#optMulti").checked,
save: $("#optSave").checked,
threshold: parseFloat(thr.value),
},
});
render(data);
toast(`Classified as "${data.predicted_label}" in ${data.took_ms} ms`);
} catch (err) {
toast(err.message, true);
} finally {
btn.disabled = false;
spin.classList.add("hidden");
}
}
function render(d) {
$("#resultEmpty").classList.add("hidden");
$("#result").classList.remove("hidden");
$("#rLabel").textContent = d.predicted_label;
$("#rTime").textContent = d.took_ms + " ms";
$("#rEngine").textContent = d.engine;
$("#rEngine").title = d.engine;
// Confidence ring -- reset to empty first so the sweep animates every time.
const ring = $("#rRing");
const arc = $(".ring-fg", ring);
ring.classList.toggle("low", !d.confident);
$("#rConf").textContent = Math.round(d.confidence * 100) + "%";
arc.style.strokeDasharray = RING_C;
arc.style.strokeDashoffset = RING_C;
requestAnimationFrame(() =>
requestAnimationFrame(() => {
arc.style.strokeDashoffset = RING_C * (1 - Math.min(d.confidence, 1));
})
);
const t = d.translation;
// Romanised text (Hindi typed in Latin letters, say) often detects with low
// confidence even when the translation itself is fine -- say so rather than
// presenting a shaky guess as fact.
const shaky = t.detection_confidence > 0 && t.detection_confidence < 0.6 ? " · uncertain" : "";
const lang = `${t.source_lang_name} (${t.source_lang})${shaky}`;
$("#rLang").textContent = lang + (t.was_translated ? " → EN" : "");
$("#rLang").title = t.was_translated ? `${lang}, translated to English` : lang;
const warn = $("#rWarn");
if (!d.confident) {
$("#rWarnText").textContent =
`Low confidence — the top score of ${(d.confidence * 100).toFixed(1)}% is below the ` +
`${(d.threshold * 100).toFixed(0)}% threshold. Treat this as unclassified, or add a ` +
`label that covers this kind of complaint.`;
warn.classList.remove("hidden");
} else {
warn.classList.add("hidden");
}
$("#rOriginal").textContent = t.original_text;
$("#rTranslated").textContent = t.translated_text;
const note = $("#rNote");
note.textContent = t.note || "";
note.classList.toggle("hidden", !t.note);
$("#rTransBox").open = t.was_translated;
// Bars are scaled to the top score so small differences stay readable.
const max = Math.max(...d.scores.map((s) => s.score), 0.0001);
$("#rScores").innerHTML = d.scores
.map((s, i) => `
<div class="score-row ${i === 0 ? "top" : ""}" style="--h: ${hueOf(s.label)}">
<span class="n" title="${escapeHtml(s.label)}">${escapeHtml(s.label)}</span>
<span class="bar"><span data-w="${(s.score / max * 100).toFixed(1)}%"></span></span>
<span class="v">${(s.score * 100).toFixed(1)}%</span>
</div>`)
.join("");
// Stagger the bar fills so the ranking reads left-to-right, top-down.
requestAnimationFrame(() =>
$$("#rScores .bar > span").forEach((el, i) => {
setTimeout(() => { el.style.width = el.dataset.w; }, 40 + i * 35);
})
);
}
});