/* Main-thread UI. Heavy work (Python/Pyodide) lives in worker.js. */ (function () { const cfg = window.ICV_CONFIG, cache = window.icvCache, $ = (id) => document.getElementById(id); const PY_FILES = ["app.py", "verifier.py", "retrieval.py", "index_builder.py", "normalization.py", "alignment.py", "similarity.py", "detector.py", "scanner.py", "idgham.py", "ui.py", "llm_client.py", "benchmark.py", "index/quran.idx.gz", "demo/examples.json"]; const SYSTEM_PROMPT = "أنت مساعد معرفي في العلوم الإسلامية. أجب بالعربية بإيجاز ودقة. عند الاستشهاد بآية قرآنية أو حديث نبوي اكتب نصه كاملًا واذكر السورة ورقم الآية أو مصدر الحديث."; const HTTP_MESSAGES = { 400: "رفض الخادم الطلب.", 401: "مفتاح الواجهة البرمجية غير صالح.", 403: "المفتاح غير مصرّح له بهذا الطلب.", 429: "تم تجاوز حد الاستخدام أو الرصيد؛ حاول لاحقًا." }; const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); let worker = null, ready = false, nextId = 0, examples = [], exampleIndex = 0, lastText = ""; const pending = new Map(); /* ---------- toast + clipboard ---------- */ let toastTimer = 0; function toast(msg, error) { const t = $("toast"); t.textContent = msg; t.classList.toggle("err", !!error); t.classList.add("show"); clearTimeout(toastTimer); toastTimer = setTimeout(() => t.classList.remove("show"), 2200); } async function copyText(text) { try { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); return true; } } catch (e) { /* fall back */ } const area = document.createElement("textarea"); // works inside iframes / non-secure contexts area.value = text; area.setAttribute("readonly", ""); area.style.cssText = "position:fixed;top:-1000px;opacity:0"; document.body.appendChild(area); area.select(); area.setSelectionRange(0, text.length); let ok = false; try { ok = document.execCommand("copy"); } catch (e) { ok = false; } area.remove(); return ok; } window.icvCopy = async (button) => { const ok = await copyText(button.dataset.text || ""); toast(ok ? "تم نسخ النص بنجاح" : "تعذّر نسخ النص، حدّده يدويًا", !ok); }; /* ---------- status, boot progress, skeletons ---------- */ function setStatus(text, busy = true) { $("status").innerHTML = (busy && text ? '' : "") + esc(text); } function setStep(step, fraction) { document.querySelectorAll(".boot .step").forEach((el) => { const n = Number(el.dataset.step); el.classList.toggle("done", n < step); el.classList.toggle("on", n === step); }); if (fraction != null) { const bar = $("bootbar"); bar.classList.remove("indeterminate"); bar.firstElementChild.style.width = Math.round(fraction * 100) + "%"; } } function skeleton(n = 3) { const card = '
'; $("results").innerHTML = '
' + card.repeat(n) + "
"; } function notice(text, kind) { $("results").innerHTML = '
' + esc(text) + "
"; } function busy(flag) { for (const id of ["verify", "example", "ask"]) $(id).disabled = flag || !ready; } /* ---------- worker RPC ---------- */ function call(type, payload) { return new Promise((resolve) => { const id = ++nextId; pending.set(id, resolve); worker.postMessage({ type, id, ...payload }); }); } /* ---------- model card (Subtask 1A, CAMeLBERT-MSA) ---------- */ function modelCard(det) { const chips = det.tags.map((t) => { const kind = t.tag === "O" ? "O" : t.tag.slice(2); return '' + esc(t.token) + "" + esc(t.tag) + ""; }).join(""); const badge = det.simulated ? 'Simulation · no weights loaded' : 'Live · Hugging Face'; return '
CAMeLBERT-MSA · Token Classification (Subtask 1A)' + badge + '
' + chips + "
"; } /* ---------- direct verification ---------- */ async function runDirect() { const text = $("text").value; if (!text.trim()) return notice("الرجاء إدخال نص للتحقق منه.", "warn"); const engine = $("engine").value, key = "r:" + cache.hash(engine + "|" + text); busy(true); lastText = text; $("export").hidden = true; const hit = cache.get(key); if (hit) { $("results").innerHTML = hit; setStatus("تم عرض النتيجة من الذاكرة المحلية.", false); $("export").hidden = false; busy(false); return; } skeleton(); setStatus("جارٍ التحقق… (قد ينتظر إتمام تجهيز كتب الحديث)"); let html; if (engine === "camelbert") { setStatus("CAMeLBERT-MSA: جارٍ استخراج المقاطع…"); const det = await window.icvDetectors.detectSpans(text, async () => JSON.parse((await call("detect", { text })) || "[]")); setStatus("جارٍ التحقق من المقاطع المستخرجة…"); const report = await call("verify_spans", { text, spans: JSON.stringify(det.spans) }); html = report === null ? null : modelCard(det) + report; } else { html = await call("verify", { text, generated: false }); } if (html === null) notice("حدث خطأ غير متوقع أثناء التحقق.", "bad"); else { $("results").innerHTML = html; cache.set(key, html); $("export").hidden = false; } setStatus("", false); busy(false); } /* ---------- Ask then Verify: pre-configured ChatGPT client ---------- */ async function askChatGPT(prompt) { if (!prompt.trim()) throw new Error("اكتب سؤالًا أولًا."); if (!cfg.OPENAI_API_KEY) throw new Error("لم يتم ضبط مفتاح ChatGPT في ملف config.js."); let response; try { response = await fetch(cfg.OPENAI_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer " + cfg.OPENAI_API_KEY }, body: JSON.stringify({ model: cfg.OPENAI_MODEL, messages: [{ role: "system", content: SYSTEM_PROMPT }, { role: "user", content: prompt.trim() }] }) }); } catch (e) { throw new Error("تعذّر الاتصال بالخدمة. تحقق من الاتصال بالإنترنت."); } if (!response.ok) throw new Error(HTTP_MESSAGES[response.status] || "فشل الطلب (الرمز " + response.status + ")."); const data = await response.json(), answer = data.choices && data.choices[0] && data.choices[0].message.content; if (!answer || !answer.trim()) throw new Error("لم يُرجع النموذج أي نص."); return answer.trim(); } async function runAsk() { busy(true); $("export").hidden = true; try { skeleton(2); setStatus("جارٍ سؤال ChatGPT…"); const answer = await askChatGPT($("prompt").value); setStatus("جارٍ التحقق من إجابة النموذج…"); const html = await call("verify", { text: answer, generated: true }); if (html === null) notice("تعذّر التحقق من إجابة النموذج.", "bad"); else { $("results").innerHTML = html; lastText = answer; $("export").hidden = false; } } catch (e) { notice(e.message, "warn"); } setStatus("", false); busy(false); } /* ---------- benchmark export (IslamicEval-style 1A/1B/1C) ---------- */ async function exportBenchmark(kind) { if (!lastText) return; toast("Preparing export…"); const raw = await call("benchmark", { text: lastText }); if (!raw) return toast("تعذّر التصدير", true); const data = JSON.parse(raw), body = kind === "tsv" ? data.tsv : JSON.stringify(data, null, 2); const blob = new Blob([body], { type: kind === "tsv" ? "text/tab-separated-values" : "application/json" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "islamiceval_" + data.Response_ID + "." + kind; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 4000); if (!(await copyText(body))) return; toast("Exported · copied to clipboard"); } /* ---------- boot ---------- */ function start() { try { worker = new Worker("worker.js"); } catch (e) { return setStatus("تعذّر تشغيل النظام: " + e.message, false); } worker.onerror = (ev) => setStatus("تعذّر تشغيل النظام: " + (ev.message || ""), false); worker.onmessage = (event) => { const m = event.data; if (m.type === "status") { setStatus(m.text); setStep(m.step, m.step === 1 ? 0.1 : m.step === 2 ? 0.3 : null); } else if (m.type === "progress") setStep(3, 0.35 + 0.65 * (m.total ? m.got / m.total : 0.5)); else if (m.type === "error") setStatus("تعذّر تشغيل النظام: " + m.text + ". جرّب متصفح كمبيوتر حديثًا ثم أعد تحميل الصفحة.", false); else if (m.type === "result") { const r = pending.get(m.id); pending.delete(m.id); r(m.value); } else if (m.type === "ready") { ready = true; busy(false); setStep(3, 0.35); setStatus("النظام جاهز. جارٍ تجهيز كتب الحديث في الخلفية…"); } else if (m.type === "hadith") { setStep(4, 1); setStatus("جاهز بالكامل. يعمل الكود داخل متصفحك ولا يُرسل نصك إلى أي خادم.", false); setTimeout(() => $("boot").hidden = true, 1800); } }; worker.postMessage({ type: "boot", base: location.href, pyodideVersion: cfg.PYODIDE_VERSION, files: PY_FILES, lazy: ["index/hadith.idx.gz"] }); } document.querySelectorAll(".tab").forEach((tab) => tab.addEventListener("click", () => { document.querySelectorAll(".tab").forEach((t) => t.classList.toggle("active", t === tab)); for (const name of ["direct", "ask"]) $("tab-" + name).hidden = tab.dataset.tab !== name; })); $("engine").addEventListener("change", () => { $("engine-hint").textContent = $("engine").selectedOptions[0].dataset.hint; }); $("engine").dispatchEvent(new Event("change")); $("verify").addEventListener("click", runDirect); $("ask").addEventListener("click", runAsk); $("example").addEventListener("click", () => { if (!examples.length) return; $("text").value = examples[exampleIndex++ % examples.length].text; runDirect(); }); $("export-json").addEventListener("click", () => exportBenchmark("json")); $("export-tsv").addEventListener("click", () => exportBenchmark("tsv")); document.addEventListener("click", (e) => { const b = e.target.closest && e.target.closest(".copy-btn"); if (b && !b.getAttribute("onclick")) window.icvCopy(b); }); cache.json("demo/examples.json").then((d) => { examples = d; }).catch(() => cache.json("examples.json").then((d) => { examples = d; }).catch(() => {})); busy(true); setStep(1, 0.05); start(); })();