Islamic2 / index.html
Ghada-99-Ragab's picture
Upload 21 files
6cb74c6 verified
Raw History Blame Contribute Delete
25.9 kB
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>مُدقِّق المحتوى الإسلامي</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700&family=Amiri:wght@400;700&display=swap');
:root { --green:#0F4C3A; --green-2:#17694F; --gold:#B8912F; --gold-2:#E2C06E; --cream:#FBF6EA; --paper:#FFFFFF;
--ink:#1F2933; --muted:#55626D; --line:#E6DCC3; --ok:#1B7A4B; --ok-bg:#EAF6EF; --ok-line:#BFE3CD;
--bad:#B3261E; --bad-bg:#FDECEA; --bad-line:#F4B8B3; --warn:#7A5B0C; --warn-bg:#FFF6DA; --warn-line:#EBD28A; }
.gradio-container, body.icv-page { background:var(--cream) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='88' height='88' viewBox='0 0 88 88'%3E%3Cg fill='none' stroke='%230F4C3A' stroke-opacity='0.07' stroke-width='1'%3E%3Crect x='22' y='22' width='44' height='44'/%3E%3Crect x='22' y='22' width='44' height='44' transform='rotate(45 44 44)'/%3E%3C/g%3E%3C/svg%3E") !important; font-family:'Cairo','Segoe UI',Tahoma,sans-serif; color:var(--ink); }
.gradio-container { max-width:1060px !important; --body-text-color:#1F2933; --block-background-fill:#FFFFFF; --block-border-color:#E6DCC3;
--input-background-fill:#FFFFFF; --block-label-text-color:#55626D; --button-primary-background-fill:#0F4C3A;
--button-primary-background-fill-hover:#17694F; --button-primary-text-color:#FFFFFF; --button-secondary-background-fill:#FFFFFF;
--button-secondary-text-color:#0F4C3A; --button-secondary-border-color:#B8912F; --color-accent:#B8912F; }
.icv { direction:rtl; text-align:right; color:var(--ink); line-height:1.9; }
.icv .hero { background:linear-gradient(135deg,#0F4C3A,#17694F); border-radius:20px; padding:30px 22px 26px; margin:10px 0 18px; text-align:center;
box-shadow:0 6px 22px rgba(15,76,58,.18); border-bottom:4px solid var(--gold); }
.icv .hero .mark { width:54px; height:54px; display:block; margin:0 auto 4px; }
.icv .hero h1 { font-size:2.3rem; margin:.1rem 0; color:#FFFFFF; font-weight:700; }
.icv .hero .tagline { color:var(--gold-2); font-size:1.2rem; font-weight:600; margin:.1rem 0 .6rem; }
.icv .hero .sub { max-width:720px; margin:0 auto; color:#E9F2EE; font-size:1rem; }
.icv .flow { display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:8px; margin-top:16px; }
.icv .flow span { background:rgba(255,255,255,.12); border:1px solid rgba(226,192,110,.55); color:#FFF; border-radius:999px; padding:3px 16px; font-size:.9rem; }
.icv .flow i { color:var(--gold-2); font-style:normal; font-size:1.2rem; }
.icv .disclaimer { font-size:.85rem; color:var(--muted); text-align:center; padding:14px 8px; }
.icv .en-free { direction:rtl; }
.input-area textarea { direction:rtl; text-align:right; font-family:'Amiri','Cairo',serif !important; font-size:1.25rem !important; line-height:2.1 !important; background:#FFFFFF !important; color:#1F2933 !important; }
.llm-row label, .gradio-container label span { font-family:'Cairo',sans-serif; }
.results { direction:rtl; }
.icv .summary { display:grid; grid-template-columns:repeat(auto-fit,minmax(130px,1fr)); gap:10px; margin:8px 0 14px; }
.icv .tile { background:var(--paper); border:1px solid var(--line); border-radius:14px; padding:12px 8px; text-align:center; }
.icv .tile b { display:block; font-size:1.8rem; color:var(--green); line-height:1.3; } .icv .tile span { font-size:.92rem; color:var(--muted); }
.icv .tile.verified { background:var(--ok-bg); border-color:var(--ok-line); } .icv .tile.verified b { color:var(--ok); }
.icv .tile.mismatch { background:var(--bad-bg); border-color:var(--bad-line); } .icv .tile.mismatch b { color:var(--bad); }
.icv .tile.review { background:var(--warn-bg); border-color:var(--warn-line); } .icv .tile.review b { color:var(--warn); }
.icv .tile.zero { background:var(--paper); border-color:var(--line); opacity:.6; } .icv .tile.zero b { color:var(--muted); }
.icv .highlight, .icv .generated, .icv .final { background:var(--paper); border:1px solid var(--line); border-radius:14px; padding:14px 18px; margin-bottom:14px; }
.icv label { display:block; color:var(--muted); font-size:.82rem; font-weight:600; margin-bottom:4px; }
.icv .highlight p, .icv .generated p, .icv .final-text { font-family:'Amiri','Cairo',serif; font-size:1.2rem; line-height:2.2; margin:0; white-space:pre-wrap; }
.icv mark { color:var(--ink); border-radius:6px; padding:1px 5px; }
.icv mark.verified { background:#D5EEDF; } .icv mark.mismatch { background:#F8CFCB; } .icv mark.review { background:#F7E3A6; }
.icv .hl-legend { display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; font-size:.78rem; }
.icv .qcard { background:var(--paper); border:1px solid var(--line); border-inline-start:6px solid var(--line); border-radius:16px; padding:16px 18px; margin:12px 0; }
.icv .qcard.verified { border-inline-start-color:var(--ok); } .icv .qcard.mismatch { border-inline-start-color:var(--bad); } .icv .qcard.review { border-inline-start-color:var(--gold); }
.icv .qhead { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-bottom:8px; }
.icv .idx { background:var(--green); color:#FFF; border-radius:50%; width:28px; height:28px; display:inline-flex; align-items:center; justify-content:center; font-weight:700; font-size:.9rem; }
.icv .badge { padding:2px 14px; border-radius:999px; font-size:.84rem; background:#F3EEDD; border:1px solid var(--line); color:var(--ink); }
.icv .badge.st.verified { background:var(--ok-bg); border-color:var(--ok-line); color:var(--ok); font-weight:600; }
.icv .badge.st.mismatch { background:var(--bad-bg); border-color:var(--bad-line); color:var(--bad); font-weight:600; }
.icv .badge.st.review { background:var(--warn-bg); border-color:var(--warn-line); color:var(--warn); font-weight:600; }
.icv .badge.scan { background:#EEF3FB; border-color:#C9D8EE; color:#2F4F7F; }
.icv .conf { margin-inline-start:auto; color:var(--muted); font-size:.88rem; } .icv .conf b { color:var(--green); }
.icv .field { margin:8px 0; } .icv .field p { margin:0; }
.icv .quote { font-family:'Amiri','Cairo',serif; font-size:1.25rem; line-height:2.2; color:var(--ink); } .icv .quote.small { font-size:1.05rem; color:#3a4651; }
.icv .diff { font-family:'Amiri','Cairo',serif; font-size:1.2rem; line-height:2.2; }
.icv .w-extra { background:var(--bad-bg); color:var(--bad); border-radius:4px; padding:0 4px; text-decoration:line-through; }
.icv .w-missing { background:var(--ok-bg); color:var(--ok); border-radius:4px; padding:0 4px; font-weight:700; }
.icv .legend { color:var(--muted); font-size:.82rem; margin:4px 0 0; } .icv .legend span { text-decoration:none; font-size:.8rem; }
.icv .reason { color:var(--muted); font-size:.92rem; margin:6px 0 0; }
.icv .note { color:var(--warn); background:var(--warn-bg); border:1px solid var(--warn-line); border-radius:10px; padding:5px 12px; font-size:.88rem; margin:8px 0 0; }
.icv .action { border-radius:12px; padding:10px 14px; margin-top:10px; }
.icv .action.ok { background:var(--ok-bg); border:1px solid var(--ok-line); } .icv .action.ok b { color:var(--ok); }
.icv .action.warn { background:var(--warn-bg); border:1px solid var(--warn-line); } .icv .action.warn b { color:var(--warn); }
.icv .action.bad { background:var(--bad-bg); border:1px solid var(--bad-line); } .icv .action.bad b { color:var(--bad); }
.icv .evidence { margin-top:12px; border-top:1px dashed var(--line); padding-top:8px; }
.icv .evidence summary { cursor:pointer; color:var(--green); font-weight:700; }
.icv .ev-row { margin:10px 0; } .icv .ev-row b { color:var(--gold); font-size:.88rem; } .icv .ev-row p { margin:2px 0; }
.icv .indicators { display:grid; gap:6px; margin-top:6px; }
.icv .ind { display:grid; grid-template-columns:150px 1fr 48px; gap:10px; align-items:center; font-size:.88rem; }
.icv .bar { background:#EFE8D3; border-radius:999px; height:9px; overflow:hidden; direction:rtl; } .icv .bar span { display:block; height:100%; background:linear-gradient(270deg,var(--green),var(--gold)); }
.icv .ind-val { text-align:left; direction:ltr; color:var(--ink); } .icv .ind-val.wide { grid-column:2 / span 2; text-align:right; direction:rtl; }
.icv .final { border-color:var(--gold); background:#FFFDF6; }
.icv .final-head { display:flex; justify-content:space-between; align-items:center; } .icv .final-head b { color:var(--green); font-size:1.05rem; }
.icv .copy { background:var(--green); color:#FFF; border:0; border-radius:10px; padding:5px 16px; font-family:inherit; cursor:pointer; }
.icv .notice { background:var(--paper); border:1px solid var(--line); border-radius:14px; padding:16px; }
.icv .notice.warn { background:var(--warn-bg); border-color:var(--warn-line); color:var(--warn); } .icv .notice.bad { background:var(--bad-bg); border-color:var(--bad-line); color:var(--bad); }
@media (max-width:640px){ .icv .hero h1{font-size:1.7rem;} .icv .ind{grid-template-columns:104px 1fr 40px;} }
body.icv-page{margin:0;min-height:100vh;} .page{max-width:1060px;margin:0 auto;padding:6px 16px 24px;box-sizing:border-box;}
.tabs{display:flex;gap:8px;margin:6px 0 14px;direction:rtl;}
.tab{flex:1;border:1px solid var(--line);background:#FFF;color:var(--green);border-radius:12px;padding:11px 14px;font:700 1rem 'Cairo',sans-serif;cursor:pointer;}
.tab.active{background:var(--green);color:#FFF;border-color:var(--green);box-shadow:0 3px 10px rgba(15,76,58,.2);}
section[hidden]{display:none;}
.form label{display:block;color:var(--muted);font-size:.88rem;font-weight:600;margin:10px 0 5px;direction:rtl;text-align:right;}
.form textarea,.form input,.form select{width:100%;box-sizing:border-box;background:#FFF;color:var(--ink);border:1px solid var(--line);border-radius:12px;padding:12px 14px;font:1rem 'Cairo',sans-serif;direction:rtl;text-align:right;outline:none;}
.form textarea{min-height:200px;resize:vertical;font-family:'Amiri','Cairo',serif;font-size:1.25rem;line-height:2.1;}
.form textarea.short{min-height:96px;}
.form textarea:focus,.form input:focus,.form select:focus{border-color:var(--gold);box-shadow:0 0 0 3px rgba(184,145,47,.18);}
.row{display:grid;grid-template-columns:1fr 1.4fr 1.6fr;gap:12px;} @media (max-width:760px){.row{grid-template-columns:1fr;}}
.btns{display:flex;gap:12px;margin:14px 0 6px;flex-wrap:wrap;}
.btns button{flex:1 1 200px;border:0;border-radius:12px;padding:13px 18px;font:700 1.02rem 'Cairo',sans-serif;cursor:pointer;}
.btn-main{background:var(--green);color:#FFF;} .btn-main:hover{background:var(--green-2);}
.btn-alt{background:#FFF;color:var(--green);border:1.5px solid var(--gold)!important;} .btn-alt:hover{background:#FFFBEF;}
.btns button:disabled{opacity:.45;cursor:not-allowed;}
.hint{font-size:.85rem;color:var(--muted);direction:rtl;margin:6px 0;}
#status{direction:rtl;text-align:center;color:var(--muted);font-size:.93rem;margin:8px 0;min-height:1.5em;}
.spinner{display:inline-block;width:14px;height:14px;border:2px solid var(--line);border-top-color:var(--green);border-radius:50%;animation:spin 1s linear infinite;vertical-align:middle;margin-inline-end:8px;}
@keyframes spin{to{transform:rotate(360deg)}}
</style>
</head>
<body class="icv-page">
<div class="page">
<div class="icv"><div class="hero">
<svg class="mark" viewBox="0 0 64 64" fill="none" stroke="#E2C06E" stroke-width="1.8" aria-hidden="true"><rect x="14" y="14" width="36" height="36"/><rect x="14" y="14" width="36" height="36" transform="rotate(45 32 32)"/><circle cx="32" cy="32" r="7" fill="#E2C06E" stroke="none"/></svg>
<h1>مُدقِّق المحتوى الإسلامي</h1>
<p class="tagline">تحقّق من آيات القرآن والأحاديث النبوية بالدليل</p>
<p class="sub">يكتشف النظام الاقتباسات داخل أي نص، ويسترجع نصوصها من المصادر، ويقارنها كلمةً بكلمة، ثم يعرض الدليل.
وإذا لم تكفِ الأدلة فلن يختلق تصحيحًا، بل يحيل الحالة إلى المراجعة البشرية.</p>
<div class="flow"><span>كشف</span><i>‹</i><span>استرجاع</span><i>‹</i><span>محاذاة</span><i>‹</i><span>دليل</span><i>‹</i><span>قرار</span></div>
</div></div>
<div class="tabs"><button class="tab active" data-tab="direct">تحقّق مباشر</button><button class="tab" data-tab="ask">اسأل ثم تحقّق</button></div>
<section id="tab-direct" class="form">
<label for="text">النص المراد التحقق منه</label>
<textarea id="text" placeholder="الصق هنا النص الذي ولّده نموذج لغوي»…"></textarea>
<div class="btns"><button id="verify" class="btn-main" disabled>تحقّق من النص</button><button id="example" class="btn-alt" disabled>جرّب مثالًا</button></div>
</section>
<section id="tab-ask" class="form" hidden>
<div class="icv"><div class="notice">اكتب سؤالًا، وسيجيب النموذج الذي تختاره، ثم يفحص النظام كل آية وحديث في إجابته ويعرض الأخطاء والتصحيحات.
يُرسل مفتاحك من متصفحك مباشرة إلى المزوّد ولا يُحفظ في أي مكان. استخدم مفتاحك الشخصي فقط.</div></div>
<div class="row">
<div><label for="provider">مزوّد النموذج</label><select id="provider"></select></div>
<div><label for="model">اسم النموذج (قابل للتعديل)</label><input id="model" type="text" dir="ltr"></div>
<div><label for="key">مفتاح الواجهة البرمجية</label><input id="key" type="password" dir="ltr" autocomplete="off"></div>
</div>
<label for="prompt">سؤالك</label>
<textarea id="prompt" class="short" placeholder="اكتب سؤالك، مثل: اشرح لي فضل الصبر في القرآن والسنة مع ذكر الأدلة."></textarea>
<div class="btns"><button id="ask" class="btn-main" disabled>اسأل ثم تحقّق</button></div>
</section>
<div id="status"></div>
<div id="results" class="results"></div>
<div class="icv"><div class="disclaimer">أداة مساعدة للتدقيق النصي وليست فتوى ولا بديلًا عن المراجعة المتخصصة.
النتائج مبنية على مراجع القرآن الكريم والكتب الستة المضمّنة فقط.</div></div>
</div>
<script type="text/plain" id="worker-src">
// Python (Pyodide) and the whole verification pipeline run here, off the main thread.
let py = null, hadithReady = null;
const post = (message) => self.postMessage(message);
const status = (text) => post({ type: "status", text });
async function fetchBytes(url, onProgress) {
let cache = null;
try { cache = await caches.open("icv-v1"); const hit = await cache.match(url); if (hit) return new Uint8Array(await hit.arrayBuffer()); } catch (e) { cache = null; }
const response = await fetch(url);
if (!response.ok) throw new Error("تعذّر تحميل " + url + " (" + response.status + ")");
if (cache) { try { await cache.put(url, response.clone()); } catch (e) { /* cache is an optimisation only */ } }
const total = Number(response.headers.get("Content-Length")) || 0;
if (!response.body || !onProgress) return new Uint8Array(await response.arrayBuffer());
const reader = response.body.getReader(), chunks = []; let received = 0;
for (;;) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); received += value.length; onProgress(received, total); }
const bytes = new Uint8Array(received); let offset = 0;
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.length; }
return bytes;
}
// Accept both layouts: files inside folders (index/, demo/) or uploaded flat next to index.html.
async function fetchAny(file, base, onProgress) {
let lastError = null;
for (const path of [file, file.split("/").pop()]) {
try { return await fetchBytes(new URL(path, base).href, onProgress); } catch (error) { lastError = error; }
}
throw lastError;
}
async function boot(message) {
status("جارٍ تحميل بيئة بايثون…");
importScripts("https://cdn.jsdelivr.net/pyodide/v" + message.pyodideVersion + "/full/pyodide.js");
py = await loadPyodide();
status("جارٍ تحميل الكود وفهرس القرآن…");
for (const dir of ["/app", "/app/index", "/app/demo", "/app/data"]) py.FS.mkdir(dir);
for (const file of message.files) py.FS.writeFile("/app/" + file, await fetchAny(file, message.base));
const examples = JSON.parse(new TextDecoder().decode(py.FS.readFile("/app/demo/examples.json")));
py.runPython("import sys; sys.path.insert(0, '/app')");
py.runPython("from app import get_pipeline, verify_text, verify_generated_answer; get_pipeline()");
post({ type: "ready", examples });
hadithReady = loadHadith(message); // runs in the background; verification waits for it
}
async function loadHadith(message) {
const file = message.lazy[0];
const bytes = await fetchAny(file, message.base, (got, total) => {
const mb = (got / 1048576).toFixed(1), of = total ? " / " + (total / 1048576).toFixed(1) : "";
status("جارٍ تحميل كتب الحديث… " + mb + of + " ميغابايت");
});
py.FS.writeFile("/app/" + file, bytes);
status("جارٍ تجهيز كتب الحديث…");
py.runPython("get_pipeline().retriever.warm()");
post({ type: "hadith" });
}
self.onmessage = async (event) => {
const message = event.data;
if (message.type === "boot") {
try { await boot(message); } catch (error) { post({ type: "error", text: String((error && error.message) || error) }); }
} else if (message.type === "verify") {
try {
await hadithReady;
const fn = py.globals.get(message.generated ? "verify_generated_answer" : "verify_text");
post({ type: "result", id: message.id, html: fn(message.text) });
} catch (error) { post({ type: "result", id: message.id, html: null }); }
}
};
</script>
<script>
const PROVIDERS = {"gemini": {"label": "Google Gemini", "model": "gemini-2.5-flash", "base_url": "https://generativelanguage.googleapis.com/v1beta", "label_ar": "جوجل جيميناي"}, "openai": {"label": "OpenAI", "model": "gpt-4o-mini", "base_url": "https://api.openai.com/v1", "label_ar": "أوبن إيه آي"}, "huggingface": {"label": "Hugging Face", "model": "Qwen/Qwen2.5-72B-Instruct", "base_url": "https://router.huggingface.co/v1", "label_ar": "هاغينغ فيس"}};
const SYSTEM_PROMPT = "أنت مساعد معرفي في العلوم الإسلامية. أجب بالعربية بإيجاز ودقة. عند الاستشهاد بآية قرآنية أو حديث نبوي اكتب نصه كاملًا بين علامتي تنصيص مزدوجتين \"...\" بعد عبارة تمهيدية مثل: قال الله تعالى: أو قال رسول الله ﷺ:. لا تضع بين علامات التنصيص إلا نص الآية أو الحديث، واذكر السورة ورقم الآية أو مصدر الحديث بعد الاقتباس.";
const HTTP_MESSAGES = {"400": "رفض المزوّد الطلب؛ تحقق من اسم النموذج.", "401": "مفتاح الواجهة البرمجية غير صالح.", "403": "المفتاح غير مصرّح له باستخدام هذا النموذج.", "404": "النموذج غير موجود عند المزوّد؛ تحقق من اسمه.", "429": "تجاوزت حد الاستخدام المسموح؛ حاول لاحقًا."};
const $ = (id) => document.getElementById(id);
let examples = [], exampleIndex = 0, nextId = 0, worker = null, ready = false;
const pending = new Map();
function setStatus(text, busy = true) { $("status").innerHTML = (busy && text ? '<span class="spinner"></span>' : "") + text; }
function busy(flag) { for (const id of ["verify", "example", "ask"]) $(id).disabled = flag || !ready; }
function notice(text, kind) { $("results").innerHTML = '<div class="icv"><div class="notice ' + kind + '">' + text + "</div></div>"; }
function verifyRemote(text, generated) {
return new Promise((resolve) => { const id = ++nextId; pending.set(id, resolve); worker.postMessage({ type: "verify", id, text, generated }); });
}
async function runDirect() {
if (!$("text").value.trim()) return notice("الرجاء إدخال نص للتحقق منه.", "warn");
busy(true); setStatus("جارٍ التحقق… (قد ينتظر إتمام تجهيز كتب الحديث)");
const html = await verifyRemote($("text").value, false);
$("results").innerHTML = html !== null ? html : '<div class="icv"><div class="notice bad">حدث خطأ غير متوقع أثناء التحقق.</div></div>';
setStatus("", false); busy(false);
}
// ---- mode B: the browser calls the chosen provider directly with the visitor's own key ------------------------------
function buildRequest(provider, model, key, prompt) {
const info = PROVIDERS[provider], base = info.base_url.replace(/\/$/, ""), name = (model || "").trim() || info.model;
if (provider === "gemini") {
return { url: base + "/models/" + name + ":generateContent",
headers: { "Content-Type": "application/json", "x-goog-api-key": key },
body: { systemInstruction: { parts: [{ text: SYSTEM_PROMPT }] }, contents: [{ role: "user", parts: [{ text: prompt }] }] } };
}
return { url: base + "/chat/completions", headers: { "Content-Type": "application/json", Authorization: "Bearer " + key },
body: { model: name, messages: [{ role: "system", content: SYSTEM_PROMPT }, { role: "user", content: prompt }] } };
}
function parseResponse(provider, data) {
const text = provider === "gemini" ? data.candidates[0].content.parts.map((p) => p.text || "").join("") : data.choices[0].message.content;
if (!text || !text.trim()) throw new Error("لم يُرجع النموذج أي نص.");
return text.trim();
}
async function generate(provider, model, key, prompt) {
if (!prompt.trim()) throw new Error("اكتب سؤالًا أولًا.");
if (!key.trim()) throw new Error("أدخل مفتاح الواجهة البرمجية للمزوّد.");
const request = buildRequest(provider, model, key.trim(), prompt.trim());
let response;
try { response = await fetch(request.url, { method: "POST", headers: request.headers, body: JSON.stringify(request.body) }); }
catch (error) { throw new Error("تعذّر الاتصال بالمزوّد (قد يمنع المزوّد الطلبات من المتصفح، أو لا يوجد اتصال)."); }
if (!response.ok) throw new Error(HTTP_MESSAGES[response.status] || "فشل الطلب (الرمز " + response.status + ").");
try { return parseResponse(provider, await response.json()); }
catch (error) { throw new Error(error.message || "وصلت استجابة غير متوقعة من النموذج."); }
}
async function runAsk() {
busy(true);
try {
setStatus("جارٍ سؤال النموذج…");
const answer = await generate($("provider").value, $("model").value, $("key").value, $("prompt").value);
setStatus("جارٍ التحقق من إجابة النموذج…");
const html = await verifyRemote(answer, true);
$("results").innerHTML = html !== null ? html : '<div class="icv"><div class="notice bad">تعذّر التحقق من إجابة النموذج.</div></div>';
} catch (error) { notice(error.message, "warn"); }
setStatus("", false); busy(false);
}
function start() {
try { worker = new Worker(URL.createObjectURL(new Blob([$("worker-src").textContent], { type: "text/javascript" }))); }
catch (error) { return setStatus("تعذّر تشغيل النظام: " + error.message, false); }
worker.onerror = (event) => setStatus("تعذّر تشغيل النظام: " + (event.message || ""), false);
worker.onmessage = (event) => {
const m = event.data;
if (m.type === "status") setStatus(m.text);
else if (m.type === "error") setStatus("تعذّر تشغيل النظام: " + m.text + ". جرّب متصفح كمبيوتر حديثًا ثم أعد تحميل الصفحة.", false);
else if (m.type === "result") { const resolve = pending.get(m.id); pending.delete(m.id); resolve(m.html); }
else if (m.type === "ready") { examples = m.examples; ready = true; busy(false); setStatus("النظام جاهز. جارٍ تجهيز كتب الحديث في الخلفية…"); }
else if (m.type === "hadith") setStatus("جاهز بالكامل. يعمل الكود داخل متصفحك ولا يُرسل نصك إلى أي خادم.", false);
};
worker.postMessage({ type: "boot", base: location.href, pyodideVersion: "0.26.4", 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", "index/quran.idx.gz", "demo/examples.json"], 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;
}));
for (const key of Object.keys(PROVIDERS)) { const option = document.createElement("option"); option.value = key; option.textContent = PROVIDERS[key].label_ar; $("provider").appendChild(option); }
$("provider").addEventListener("change", () => { $("model").value = PROVIDERS[$("provider").value].model; });
$("model").value = PROVIDERS[$("provider").value || "gemini"].model;
$("verify").addEventListener("click", runDirect);
$("ask").addEventListener("click", runAsk);
$("example").addEventListener("click", () => { if (!examples.length) return; $("text").value = examples[exampleIndex++ % examples.length].text; runDirect(); });
busy(true);
start();
</script>
</body>
</html>