oddadmix's picture
Upload static/app.js with huggingface_hub
8f21465 verified
Raw
History Blame Contribute Delete
11.6 kB
/* Nawah-Router demo — editable categories, live routing, connectors weighted by probability. */
/* Three backbones on the same head: a 6M bidirectional BERT encoder (Arabic), the 52M
Llama decoder (Arabic), and a 6M BERT encoder pretrained bilingually (Arabic+English).
Switching re-routes immediately so the comparison lands on the same text and the same
categories. Declared here because run() references it. */
var currentModel = "6M-BERT";
const $ = id => document.getElementById(id);
const PRESETS = [
{ name: "توجيه مساعد ذكي",
text: "عندي كود بايثون بيطلع خطأ عند قراءة ملف CSV كبير، ممكن تشوف السبب وتظبطه؟",
cats: ["مهمة برمجية بسيطة", "مهمة برمجية معقدة", "ترجمة", "كتابة إبداعية",
"بحث عن معلومة مباشرة", "محادثة عابرة"] },
{ name: "فرز خدمة العملاء",
text: "الطلب تأخر ساعة ونصف عن الموعد والسائق ما رد على الاتصال، وأبغى تعويض عن التأخير.",
cats: ["شكوى غاضبة تحتاج تصعيدًا فوريًا", "استفسار عادي غير مستعجل",
"طلب استرجاع مبلغ", "رسالة شكر وثناء", "رسالة مزعجة إعلانية"] },
{ name: "تصنيف المحتوى",
text: "بعد شهرين من الاستخدام اليومي، جودة الصوت ممتازة لكن البطارية لا تدوم أكثر من خمس ساعات.",
cats: ["مراجعة منتج", "خبر اقتصادي", "إعلان تجاري", "مقال رأي", "محتوى تقني متقدم"] },
{ name: "تعقيد الطلب",
text: "أحتاج خطة كاملة لترحيل قاعدة بيانات من MySQL إلى PostgreSQL مع الحفاظ على البيانات وبدون توقف الخدمة.",
cats: ["طلب بسيط ومباشر", "يحتاج خطوتين أو ثلاثًا", "طلب معقّد متعدد الخطوات",
"يحتاج نموذجًا أكبر"] },
{ name: "أي أداة نستدعي",
text: "كم صار سعر صرف الدولار مقابل الجنيه اليوم؟",
cats: ["بحث في الويب", "حاسبة", "تقويم ومواعيد", "قاعدة بيانات", "لا يحتاج أداة"] },
{ name: "دعم فني",
text: "ما أقدر أسجل دخول للحساب، يطلع لي أن كلمة المرور غلط مع إني متأكد منها.",
cats: ["مشكلة في الدخول إلى الحساب", "استفسار عن الفاتورة", "طلب ميزة جديدة",
"بلاغ عطل حرج يوقف الخدمة", "أخرى / لا ينطبق"] },
{ name: "English: assistant routing",
text: "My Python script throws an error reading a large CSV file, can you find the bug and fix it?",
cats: ["simple coding task", "complex coding task", "translation", "creative writing",
"direct factual lookup", "casual conversation"] },
{ name: "English: support triage",
text: "The order is an hour and a half late and the driver isn't answering my calls. I want a refund for the delay.",
cats: ["angry complaint needing immediate escalation", "routine non-urgent enquiry",
"refund request", "thank-you message", "spam / promotional message"] },
{ name: "English: content classification",
text: "After two months of daily use, the sound quality is excellent but the battery doesn't last more than five hours.",
cats: ["product review", "economic news", "advertisement", "opinion piece", "advanced technical content"] },
{ name: "English: task complexity",
text: "I need a full plan to migrate a database from MySQL to PostgreSQL, keeping the data intact with no service downtime.",
cats: ["simple, direct request", "needs two or three steps", "complex multi-step request",
"needs a larger model"] },
{ name: "English: tool routing",
text: "What's today's exchange rate between the dollar and the pound?",
cats: ["web search", "calculator", "calendar and scheduling", "database", "no tool needed"] },
{ name: "English: technical support",
text: "I can't log into my account, it says my password is wrong even though I'm sure it's correct.",
cats: ["account login issue", "billing enquiry", "feature request",
"critical outage report", "other / not applicable"] },
];
let rows = []; // [{el, input, fill, pct}]
let timer = null, seq = 0;
/* A half-typed category name is not a category, so edits to the lane list do not auto-route the
way prompt edits do. Any change here sets `dirty`, which suspends auto-routing until the user
submits — otherwise every keystroke would score a lane that does not exist yet. */
let dirty = false;
function setDirty(on) {
dirty = on;
$("go").classList.toggle("dirty", on);
$("go").disabled = !on && !$("text").value.trim();
$("dirtynote").textContent = on ? "تغيّرت الفئات — اضغط للتوجيه" : "";
}
function cats() {
return rows.map(r => r.input.value.trim());
}
function addRow(value = "", focus = false) {
const el = document.createElement("div");
el.className = "row";
el.innerHTML = `<div class="fill" style="width:0%"></div>
<input type="text" value="" placeholder="اسم الفئة…">
<span class="pct">—</span>
<button class="del" type="button" title="حذف">×</button>`;
const input = el.querySelector("input");
input.value = value;
input.dir = "auto"; // mixed Arabic/English category text, now that both route
input.addEventListener("input", () => setDirty(true));
input.addEventListener("keydown", ev => {
if (ev.key === "Enter") { ev.preventDefault(); submit(); }
});
el.querySelector(".del").addEventListener("click", () => {
rows = rows.filter(r => r.el !== el);
el.remove(); drawWires(); setDirty(true);
});
$("rows").appendChild(el);
rows.push({ el, input, fill: el.querySelector(".fill"), pct: el.querySelector(".pct") });
if (focus) input.focus();
drawWires();
}
function setPreset(p, btn) {
document.querySelectorAll(".preset").forEach(b => b.classList.remove("on"));
if (btn) btn.classList.add("on");
$("text").value = p.text;
$("rows").innerHTML = ""; rows = [];
p.cats.forEach(c => addRow(c));
updateCount(); setDirty(false); schedule(0);
}
/* Connectors: prompt -> model -> each category, thickness and opacity carrying the score.
Redrawn on resize and whenever rows change, since row heights move. */
function drawWires(scores) {
const svg = $("wires"), stage = svg.parentElement;
if (window.innerWidth <= 900) { svg.innerHTML = ""; return; }
const S = stage.getBoundingClientRect();
const P = $("promptcard").getBoundingClientRect();
const N = $("node").getBoundingClientRect();
const rtl = getComputedStyle(document.body).direction === "rtl";
// in RTL the prompt sits on the right, so wires run right-to-left
const px = (rtl ? P.left : P.right) - S.left, py = P.top + P.height / 2 - S.top;
const nIn = (rtl ? N.right : N.left) - S.left, nOut = (rtl ? N.left : N.right) - S.left;
const ny = N.top + N.height / 2 - S.top;
let d = `<path d="M ${px} ${py} C ${(px + nIn) / 2} ${py}, ${(px + nIn) / 2} ${ny}, ${nIn} ${ny}"
fill="none" stroke="var(--acc-line)" stroke-width="2" opacity=".7"/>`;
rows.forEach((r, i) => {
const R = r.el.getBoundingClientRect();
const cx = (rtl ? R.right : R.left) - S.left, cy = R.top + R.height / 2 - S.top;
const s = scores ? scores[i] : null;
const w = s == null ? 1.2 : 1 + s * 4.5;
const o = s == null ? .3 : .18 + s * .72;
d += `<path d="M ${nOut} ${ny} C ${(nOut + cx) / 2} ${ny}, ${(nOut + cx) / 2} ${cy}, ${cx} ${cy}"
fill="none" stroke="var(--acc-line)" stroke-width="${w.toFixed(2)}"
opacity="${o.toFixed(2)}" stroke-linecap="round"/>`;
});
svg.setAttribute("viewBox", `0 0 ${S.width} ${S.height}`);
svg.innerHTML = d;
}
function updateCount() { $("charcount").textContent = $("text").value.length; }
function schedule(delay = 260) {
updateCount();
if (dirty) return; // lane list is mid-edit; wait for an explicit submit
clearTimeout(timer);
timer = setTimeout(run, delay);
}
function submit() {
clearTimeout(timer);
setDirty(false);
run();
}
async function run() {
const text = $("text").value.trim(), cs = cats().filter(Boolean);
if (!text || !cs.length) {
rows.forEach(r => { r.fill.style.width = "0%"; r.pct.textContent = "—";
r.el.classList.remove("top"); });
$("stats").textContent = ""; drawWires(); return;
}
const my = ++seq;
$("orb").classList.add("busy");
try {
const res = await fetch("/api/route", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, cats: cats(), model: currentModel })
});
const data = await res.json();
if (my !== seq) return; // a newer keystroke already superseded this
const byName = new Map(data.results.map(r => [r.route, r.score]));
const scores = rows.map(r => byName.get(r.input.value.trim()) ?? 0);
const top = scores.indexOf(Math.max(...scores));
rows.forEach((r, i) => {
const s = scores[i];
r.fill.style.width = (s * 100).toFixed(1) + "%";
r.pct.textContent = (s * 100).toFixed(1) + "%";
r.el.classList.toggle("top", i === top && s > 0);
});
const langLbl = data.lang === "ar" ? "عربي" : "English";
$("stats").textContent = `${data.ms} ms · ${data.tokens} توكن · ${cs.length} فئة · ${langLbl}`;
drawWires(scores);
} catch (e) {
$("stats").textContent = "تعذّر الاتصال";
} finally {
if (my === seq) $("orb").classList.remove("busy");
}
}
$("text").addEventListener("input", () => { schedule(); if (dirty) setDirty(true); });
$("add").addEventListener("click", () => { addRow("", true); setDirty(true); });
$("go").addEventListener("click", submit);
window.addEventListener("resize", () => drawWires());
PRESETS.forEach((p, i) => {
const b = document.createElement("button");
b.className = "preset"; b.type = "button"; b.textContent = p.name;
b.addEventListener("click", () => setPreset(p, b));
$("presets").appendChild(b);
if (i === 0) setTimeout(() => setPreset(p, b), 0);
});
fetch("/api/ready").then(r => r.json()).then(d => {
$("dot").classList.add("on");
$("nodemeta").textContent = `CPU · مسار واحد · حتى ${d.max_routes} فئات`;
currentModel = d.default || currentModel;
const pick = $("modelpick");
const ICON_LABEL = {
"6M-BERT": "🔵 6M encoder (AR)",
"52M": "🧱 52M decoder (AR)",
"6M-BILINGUAL": "🌐 6M encoder (AR+EN)",
};
(d.models || []).forEach(m => {
const o = document.createElement("option");
o.value = m.key;
o.textContent = ICON_LABEL[m.key] || m.key;
o.title = m.label;
if (m.key === currentModel) o.selected = true;
pick.appendChild(o);
});
const sizeOf = k => {
const m = (d.models || []).find(x => x.key === k);
return m ? (m.params / 1e6).toFixed(1) + "M" : "";
};
$("modelsize").textContent = sizeOf(currentModel);
pick.addEventListener("change", () => {
currentModel = pick.value;
$("modelsize").textContent = sizeOf(currentModel);
run(); // same text, same categories, other backbone
});
}).catch(() => { $("modelsize").textContent = "غير متصل"; });