/* 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 = '"; }
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();
})();