Spaces:
Running
Running
File size: 16,105 Bytes
0390c03 | 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 | /* 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 OPENAI_ERRORS = {
insufficient_quota: "رصيد حساب OpenAI المرتبط بالمفتاح نفد أو لم يُفعَّل. أضِف رصيدًا من صفحة Billing في حسابك أو ضع مفتاحًا آخر في config.js.",
billing_not_active: "الفوترة غير مفعّلة في حساب OpenAI. فعّلها وأضف رصيدًا ثم أعد المحاولة.",
rate_limit_exceeded: "كثرت الطلبات في وقت قصير. انتظر نصف دقيقة وأعد المحاولة.",
invalid_api_key: "مفتاح الواجهة البرمجية غير صالح أو أُلغي. أنشئ مفتاحًا جديدًا وضعه في config.js.",
model_not_found: "النموذج المحدد غير متاح لهذا الحساب. غيّر OPENAI_MODEL في config.js."
};
const DEMO_ANSWER = "فضل الصبر عظيم في القرآن والسنة. قال الله تعالى: \"إن الله مع الصابرين والمحسنين في كل حين\". وقال سبحانه: \"إِنَّمَا يُوَفَّى الصَّابِرُونَ أَجْرَهُم بِغَيْرِ حِسَابٍ\". وقال رسول الله ﷺ: \"من صبر على بلائه سبعين يوما دخل الجنة بغير حساب ولا عقاب\".";
const SAMPLES_DIRECT = [
["آية صحيحة", 'قال الله تعالى: "فَاسْتَقِمْ كَمَا أُمِرْتَ وَمَنْ تَابَ مَعَكَ".'],
["آية بكلمة مبدّلة", 'قال الله تعالى: "إن الله لا يغفر أن يشرك به ويغفر ما دون ذلك لمن يريد".'],
["آية بدون تمهيد وتنصيص", "من أعظم ما يثبّت القلب قوله إِنَّ مَعَ الْعُسْرِ يُسْرًا فلا تيأس."],
["حديث صحيح بدون تمهيد", "إنما الأعمال بالنيات وإنما لكل امرئ ما نوى."],
["حديث مختلق", 'قال رسول الله ﷺ: "من قرأ سورة الإخلاص ألف مرة دخل الجنة بغير حساب ولا عقاب".'],
["آية مختلقة", 'قال الله تعالى: "إن الله مع الصابرين والمحسنين في كل حين".'],
["فقرة مختلطة", 'الصبر مفتاح الفرج. قال الله تعالى: "إِنَّ مَعَ الْعُسْرِ يُسْرًا". وإنما الأعمال بالنيات وإنما لكل امرئ ما نوى. وقال تعالى: "إن الله يحب الصابرين دائما وأبدا".']
];
const SAMPLES_ASK = ["اشرح لي فضل الصبر في القرآن والسنة مع ذكر الأدلة.", "ما فضل سورة الإخلاص؟ اذكر الآيات والأحاديث الواردة.", "ما أركان الإسلام؟ اذكر الدليل من القرآن والسنة.", "اذكر أحاديث عن بر الوالدين مع تخريجها."];
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) {
let code = ""; try { const body = await response.json(); code = (body.error && (body.error.code || body.error.type)) || ""; } catch (e) { /* no body */ }
const err = new Error(OPENAI_ERRORS[code] || HTTP_MESSAGES[response.status] || "فشل الطلب (الرمز " + response.status + ").");
err.quota = code === "insufficient_quota" || code === "billing_not_active"; throw err;
}
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");
if (e.quota) { // keep the demo usable: verify a built-in sample answer instead
const box = document.createElement("div"); box.className = "icv"; box.style.marginTop = "10px";
box.innerHTML = '<button id="demo-answer" class="btn-main" style="border:0;border-radius:12px;padding:11px 18px;font:700 1rem Cairo,sans-serif;color:#fff;cursor:pointer">جرّب بإجابة تجريبية مضمّنة</button>';
$("results").appendChild(box);
$("demo-answer").addEventListener("click", async () => {
busy(true); skeleton(2); setStatus("جارٍ التحقق من الإجابة التجريبية…");
const html = await call("verify", { text: DEMO_ANSWER, generated: true });
$("results").innerHTML = html === null ? "" : html; lastText = DEMO_ANSWER; $("export").hidden = html === null;
setStatus("", false); busy(false);
});
}
}
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"] });
}
SAMPLES_DIRECT.forEach(([label, text]) => {
const b = document.createElement("button"); b.type = "button"; b.textContent = label;
b.addEventListener("click", () => { $("text").value = text; if (ready) runDirect(); });
$("samples-direct").appendChild(b);
});
SAMPLES_ASK.forEach((q) => {
const b = document.createElement("button"); b.type = "button"; b.textContent = q;
b.addEventListener("click", () => { $("prompt").value = q; });
$("samples-ask").appendChild(b);
});
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();
})();
|