Spaces:
Running
Running
File size: 11,439 Bytes
c08b36a | 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 | /* 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 ? '<span class="spinner"></span>' : "") + 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 = '<div class="panel sk-card"><div class="skeleton sk-title"></div><div class="skeleton sk-line w1"></div><div class="skeleton sk-line w2"></div><div class="skeleton sk-line w3"></div></div>';
$("results").innerHTML = '<div class="icv" aria-busy="true">' + card.repeat(n) + "</div>";
}
function notice(text, kind) { $("results").innerHTML = '<div class="icv"><div class="notice ' + kind + '">' + esc(text) + "</div></div>"; }
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 '<span class="chip ' + kind + '">' + esc(t.token) + "<small>" + esc(t.tag) + "</small></span>";
}).join("");
const badge = det.simulated ? '<span class="badge-sim">Simulation · no weights loaded</span>' : '<span class="badge-live">Live · Hugging Face</span>';
return '<div class="icv"><div class="panel model-card"><div class="head"><span class="title">CAMeLBERT-MSA · Token Classification (Subtask 1A)</span>' + badge +
'</div><div class="chips">' + chips + "</div></div></div>";
}
/* ---------- 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();
})();
|