Spaces:
Sleeping
Sleeping
File size: 11,593 Bytes
532cbb3 8f21465 73caa3e 532cbb3 be53b32 8f21465 532cbb3 96242ce 532cbb3 be53b32 96242ce 532cbb3 96242ce 532cbb3 96242ce 532cbb3 96242ce 532cbb3 96242ce 532cbb3 73caa3e 532cbb3 be53b32 532cbb3 96242ce 532cbb3 73caa3e be53b32 73caa3e be53b32 73caa3e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | /* 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 = "غير متصل"; });
|