Spaces:
Running
Running
Download index.html from Ghada-99-Ragab/Islamic2: direct link, hf CLI and curl.
- Browser
- Download file 25.9 kB
-
https://huggingface.co/spaces/Ghada-99-Ragab/Islamic2/resolve/main/index.html
- Command line
-
hf download hf://spaces/Ghada-99-Ragab/Islamic2/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Ghada-99-Ragab/Islamic2/resolve/main/index.html
25.9 kB
| <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") ; font-family:'Cairo','Segoe UI',Tahoma,sans-serif; color:var(--ink); } | |
| .gradio-container { max-width:1060px ; --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 ; font-size:1.25rem ; line-height:2.1 ; background:#FFFFFF ; color:#1F2933 ; } | |
| .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);} .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> | |