Spaces:
Running
Running
File size: 25,906 Bytes
ebe6d14 6cb74c6 ebe6d14 | 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 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 | <!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>
|