/* 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 = `
`; 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 = ``; 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 += ``; }); 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 = "غير متصل"; });