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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[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();
})();