Ghada-99-Ragab's picture
Upload 8 files
1fe445a verified
Raw History Blame Contribute Delete
17.8 kB
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Islamic Content Verifier</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700&family=Amiri:wght@400;700&display=swap');
.gradio-container {
--body-background-fill:#0a1128; --block-background-fill:#101a38; --block-border-color:#1e2d57;
--input-background-fill:#0d1631; --body-text-color:#e8eefc; --block-label-text-color:#9fb3da;
--button-primary-background-fill:#14b8a6; --button-primary-background-fill-hover:#22d3ee; --button-primary-text-color:#04202a;
--button-secondary-background-fill:#16244a; --button-secondary-text-color:#e8eefc; --button-secondary-border-color:#2a3d73;
background:#0a1128 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='80' viewBox='0 0 80 80'%3E%3Cg fill='none' stroke='%2322d3ee' stroke-opacity='0.07' stroke-width='1'%3E%3Crect x='20' y='20' width='40' height='40'/%3E%3Crect x='20' y='20' width='40' height='40' transform='rotate(45 40 40)'/%3E%3C/g%3E%3C/svg%3E"); font-family:'Cairo','Segoe UI',Tahoma,sans-serif; max-width:1080px !important; color:#e8eefc;
}
.hero { text-align:center; padding:34px 12px 18px; direction:rtl; }
.hero .eyebrow { color:#22d3ee; letter-spacing:.14em; font-size:.8rem; text-transform:uppercase; }
.hero h1 { font-size:2.3rem; margin:.3rem 0 .1rem; color:#fff; }
.hero h2 { font-size:1.05rem; font-weight:400; color:#9fb3da; margin:0 0 .9rem; direction:ltr; }
.hero p { max-width:760px; margin:.3rem auto; color:#c7d4f0; line-height:1.9; }
.hero .en { direction:ltr; color:#8ea3cf; font-size:.92rem; }
.input-area textarea { direction:rtl; text-align:right; font-family:'Amiri','Cairo',serif !important; font-size:1.2rem !important; line-height:2 !important; }
.btn-row button { font-weight:700 !important; border-radius:12px !important; }
.disclaimer { font-size:.82rem; color:#8ea3cf; text-align:center; padding:12px 8px; direction:rtl; line-height:1.8; }
.disclaimer .en { display:block; direction:ltr; }
.results { direction:rtl; text-align:right; }
.results .en, .notice .en { display:block; direction:ltr; text-align:left; color:#8ea3cf; font-size:.88rem; font-weight:400; }
.summary { display:grid; grid-template-columns:repeat(auto-fit,minmax(130px,1fr)); gap:10px; margin:6px 0 14px; }
.tile { background:#101a38; border:1px solid #1e2d57; border-radius:14px; padding:12px; text-align:center; }
.tile b { display:block; font-size:1.8rem; color:#fff; } .tile span { display:block; font-size:.9rem; } .tile small { color:#8ea3cf; }
.tile.verified { border-color:#14b8a6; } .tile.verified b { color:#2dd4bf; }
.tile.mismatch { border-color:#f43f5e; } .tile.mismatch b { color:#fb7185; }
.tile.review { border-color:#f59e0b; } .tile.review b { color:#fbbf24; }
.highlight { background:#0d1631; border:1px solid #1e2d57; border-radius:14px; padding:14px 16px; margin-bottom:14px; }
.highlight label, .field label { display:block; color:#7e93c0; font-size:.78rem; margin-bottom:4px; }
.highlight p { font-family:'Amiri','Cairo',serif; font-size:1.15rem; line-height:2.1; margin:0; white-space:pre-wrap; }
mark { color:#fff; border-radius:6px; padding:1px 4px; }
mark.verified { background:rgba(20,184,166,.35); } mark.mismatch { background:rgba(244,63,94,.35); } mark.review { background:rgba(245,158,11,.35); }
.qcard { background:#101a38; border:1px solid #1e2d57; border-inline-start:5px solid #3b4b7a; border-radius:16px; padding:16px 18px; margin:12px 0; }
.qcard.verified { border-inline-start-color:#14b8a6; } .qcard.mismatch { border-inline-start-color:#f43f5e; } .qcard.review { border-inline-start-color:#f59e0b; }
.qhead { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-bottom:10px; }
.idx { background:#1b2a55; border-radius:50%; width:28px; height:28px; display:inline-flex; align-items:center; justify-content:center; font-weight:700; }
.badge { padding:3px 12px; border-radius:999px; font-size:.82rem; background:#16244a; border:1px solid #2a3d73; }
.badge.st.verified { background:rgba(20,184,166,.18); border-color:#14b8a6; color:#5eead4; }
.badge.st.mismatch { background:rgba(244,63,94,.16); border-color:#f43f5e; color:#fda4af; }
.badge.st.review { background:rgba(245,158,11,.16); border-color:#f59e0b; color:#fcd34d; }
.conf { margin-inline-start:auto; color:#9fb3da; font-size:.88rem; } .conf b { color:#22d3ee; }
.field { margin:8px 0; } .field p { margin:0; line-height:1.8; }
.quote { font-family:'Amiri','Cairo',serif; font-size:1.2rem; line-height:2.1; color:#fff; } .quote.small { font-size:1.05rem; color:#d6e0f7; }
.diff { font-family:'Amiri','Cairo',serif; font-size:1.1rem; line-height:2.1; }
.w-extra { background:rgba(244,63,94,.25); border-radius:4px; padding:0 3px; text-decoration:line-through; }
.w-missing { background:rgba(20,184,166,.28); border-radius:4px; padding:0 3px; }
.legend { color:#8ea3cf; font-size:.82rem; margin:4px 0 0; } .legend .w-extra { text-decoration:none; }
.tile.zero { opacity:.5; }
.det-chip { text-align:center; color:#8ea3cf; font-size:.82rem; margin:2px 0 8px; } .det-chip b { color:#22d3ee; font-weight:600; }
.hl-legend { display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; font-size:.78rem; } .hl-legend mark { padding:2px 10px; }
.flow { display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:8px; margin:16px 0 4px; direction:rtl; }
.flow span { background:#101a38; border:1px solid #1e2d57; border-radius:999px; padding:5px 14px; font-size:.85rem; color:#cfe0ff; }
.flow span small { color:#7e93c0; margin-inline-start:6px; direction:ltr; display:inline-block; }
.flow i { color:#22d3ee; font-style:normal; }
.hero .mark { width:52px; height:52px; margin:0 auto 6px; display:block; }
.hero h1 { background:linear-gradient(90deg,#fff,#a5f3fc); -webkit-background-clip:text; background-clip:text; color:transparent; }
.action { border-radius:12px; padding:10px 14px; margin-top:10px; line-height:1.8; }
.action.ok { background:rgba(20,184,166,.12); border:1px solid #14b8a6; }
.action.warn { background:rgba(245,158,11,.12); border:1px solid #f59e0b; }
.action.bad { background:rgba(244,63,94,.12); border:1px solid #f43f5e; }
.evidence { margin-top:12px; border-top:1px dashed #2a3d73; padding-top:8px; }
.evidence summary { cursor:pointer; color:#22d3ee; font-weight:700; }
.ev-row { margin:10px 0; } .ev-row b { color:#9fb3da; font-size:.85rem; } .ev-row p { margin:2px 0; }
.indicators { display:grid; gap:6px; margin-top:6px; }
.ind { display:grid; grid-template-columns:150px 1fr 48px; gap:10px; align-items:center; font-size:.85rem; }
.ind-name small { display:block; color:#7e93c0; font-size:.72rem; direction:ltr; text-align:right; }
.bar { background:#0d1631; border-radius:999px; height:8px; overflow:hidden; } .bar span { display:block; height:100%; background:linear-gradient(90deg,#14b8a6,#22d3ee); }
.ind-val { text-align:left; direction:ltr; color:#cfe0ff; } .ind-val.wide { grid-column:2 / span 2; text-align:right; }
.notice { background:#101a38; border:1px solid #1e2d57; border-radius:14px; padding:16px; direction:rtl; line-height:1.9; }
.notice.warn { border-color:#f59e0b; } .notice.bad { border-color:#f43f5e; }
@media (max-width:640px){ .hero h1{font-size:1.7rem;} .ind{grid-template-columns:110px 1fr 40px;} }
html,body{margin:0;background:#0a1128;}
.gradio-container{min-height:100vh;box-sizing:border-box;padding:8px 16px 24px;margin:0 auto;}
.input-area textarea{width:100%;box-sizing:border-box;min-height:220px;background:#0d1631;color:#e8eefc;border:1px solid #1e2d57;border-radius:14px;padding:14px;resize:vertical;outline:none;}
.input-area textarea:focus{border-color:#22d3ee;}
.input-area label{display:block;color:#9fb3da;font-size:.85rem;margin:0 0 6px;direction:rtl;text-align:right;}
.btn-row{display:flex;gap:12px;margin:12px 0 18px;flex-wrap:wrap;}
.btn-row button{flex:1 1 220px;border:0;border-radius:12px;padding:13px 18px;font-size:1rem;font-weight:700;cursor:pointer;font-family:inherit;}
#verify{background:#14b8a6;color:#04202a;} #verify:hover{background:#22d3ee;}
#example{background:#16244a;color:#e8eefc;border:1px solid #2a3d73;} #example:hover{border-color:#22d3ee;}
.btn-row button:disabled{opacity:.45;cursor:not-allowed;}
#status{direction:rtl;text-align:center;color:#9fb3da;font-size:.92rem;margin:0 0 10px;min-height:1.4em;}
#status .en{display:block;direction:ltr;color:#7e93c0;font-size:.82rem;}
.spinner{display:inline-block;width:14px;height:14px;border:2px solid #2a3d73;border-top-color:#22d3ee;border-radius:50%;animation:spin 1s linear infinite;vertical-align:middle;margin-inline-end:8px;}
@keyframes spin{to{transform:rotate(360deg)}}
</style>
</head>
<body>
<div class="gradio-container">
<div class="hero">
<svg class="mark" viewBox="0 0 52 52" fill="none" stroke="#22d3ee" stroke-width="1.6" aria-hidden="true">
<rect x="11" y="11" width="30" height="30"/><rect x="11" y="11" width="30" height="30" transform="rotate(45 26 26)"/>
<circle cx="26" cy="26" r="6" fill="#14b8a6" stroke="none"/></svg>
<div class="eyebrow">Islamic Content Verifier</div>
<h1>مُدقِّق المحتوى الإسلامي</h1>
<h2>Verify Quranic and Hadith quotations with evidence.</h2>
<p>الصق نصًا ولّده نموذج لغوي، وسيكتشف النظام الآيات والأحاديث الواردة فيه، ويسترجع نصوصها من المصادر، ويقارنها كلمةً بكلمة،
ثم يعرض الدليل. وإذا لم تكفِ الأدلة فلن يختلق تصحيحًا، بل يحيل الحالة إلى المراجعة البشرية.</p>
<p class="en">Paste text generated by a language model. The system detects Quran and Hadith quotations, retrieves the source texts,
compares them word by word and shows the evidence. When evidence is insufficient it never invents a correction: it recommends human review.</p>
<div class="flow"><span>كشف<small>Detect</small></span><i>←</i><span>استرجاع<small>Retrieve</small></span><i>←</i>
<span>تحقق<small>Verify</small></span><i>←</i><span>دليل<small>Evidence</small></span><i>←</i><span>قرار<small>Decide</small></span></div>
</div>
<div class="input-area">
<label for="text">النص المراد التحقق منه · Text to verify</label>
<textarea id="text" dir="rtl" placeholder="الصق هنا الرد الذي ولّده النموذج اللغوي، ويفضَّل أن تكون الاقتباسات بين علامات تنصيص بعد عبارة مثل «قال الله تعالى» أو «قال رسول الله ﷺ»…"></textarea>
</div>
<div class="btn-row">
<button id="verify" disabled>تحقق من النص · Verify Text</button>
<button id="example" disabled>جرّب مثالًا · Try an Example</button>
</div>
<div id="status"></div>
<div id="results" class="results"></div>
<div class="disclaimer">أداة مساعدة للتدقيق النصي وليست فتوى ولا بديلًا عن المراجعة المتخصصة. النتائج مبنية على مراجع القرآن والكتب الستة المضمّنة فقط.
<span class="en">A text-verification aid, not a religious ruling and not a substitute for expert review. Results rely only on the bundled Quran and Six Books corpora.</span></div>
</div>
<script type="text/plain" id="worker-src">
// Runs Pyodide and the verification code off the main thread so the page stays responsive while the index builds.
let verify = null;
const post = (message) => self.postMessage(message);
const status = (ar, en) => post({ type: "status", ar, en });
async function fetchBytes(url, onProgress) {
const response = await fetch(url);
if (!response.ok) throw new Error("Could not download " + url + " (" + response.status + ")");
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 data/ and demo/ folders, or uploaded flat next to index.html.
async function fetchAny(names, base, onProgress) {
let lastError = null;
for (const name of names) {
for (const path of [name, name.split("/").pop()]) {
try { return await fetchBytes(new URL(path, base).href, onProgress); } catch (error) { lastError = error; }
}
}
throw lastError;
}
async function boot(message) {
const url = (file) => new URL(file, message.base).href;
status("جارٍ تحميل بيئة بايثون…", "Loading the Python runtime…");
importScripts("https://cdn.jsdelivr.net/pyodide/v" + message.pyodideVersion + "/full/pyodide.js");
const pyodide = await loadPyodide();
status("جارٍ تحميل الكود والمراجع…", "Downloading code and corpora…");
for (const dir of ["/app", "/app/data", "/app/demo"]) pyodide.FS.mkdir(dir);
for (const file of message.files) pyodide.FS.writeFile("/app/" + file, await fetchAny([file], message.base));
const hadith = await fetchAny(["data/hadith.json.gz", "data/hadith.json"], message.base, (got, total) => {
const mb = (got / 1048576).toFixed(1), of = total ? " / " + (total / 1048576).toFixed(1) : "";
status("جارٍ تحميل كتب الحديث… " + mb + of + " MB", "Downloading the Hadith corpus… " + mb + of + " MB");
});
const isGzip = hadith[0] === 0x1f && hadith[1] === 0x8b; // the file may be the compressed or the plain JSON copy
pyodide.FS.writeFile(isGzip ? "/app/data/hadith.json.gz" : "/app/data/hadith.json", hadith);
const examples = JSON.parse(new TextDecoder().decode(pyodide.FS.readFile("/app/demo/examples.json")));
status("جارٍ بناء فهرس المصادر (مرة واحدة، قد يستغرق دقيقة)…", "Building the source index (once, may take up to a minute)…");
pyodide.runPython("import sys; sys.path.insert(0, '/app')");
pyodide.runPython("from app import get_pipeline, verify_text; get_pipeline()");
verify = pyodide.globals.get("verify_text");
post({ type: "ready", examples });
}
self.onmessage = async (event) => {
const message = event.data;
if (message.type === "boot") {
try { await boot(message); }
catch (error) { post({ type: "error", message: String((error && error.message) || error) }); }
} else if (message.type === "verify") {
try { post({ type: "result", id: message.id, html: verify(message.text) }); }
catch (error) { post({ type: "result", id: message.id, html: null }); }
}
};
</script>
<script>
const PYODIDE_VERSION = "0.26.4";
const PYTHON_FILES = ["app.py", "verifier.py", "retrieval.py"];
const $ = (id) => document.getElementById(id);
let examples = [], exampleIndex = 0, nextId = 0, worker = null;
const pending = new Map();
function setStatus(ar, en, busy = true) {
$("status").innerHTML = (busy ? '<span class="spinner"></span>' : "") + ar + '<span class="en">' + en + "</span>";
}
function fail(message) {
console.error(message);
setStatus("تعذّر تشغيل النظام. جرّب متصفح كمبيوتر حديثًا ثم أعد تحميل الصفحة.",
"The verifier could not start (" + message + "). Try a modern desktop browser and reload the page.", false);
}
function ask(text) {
return new Promise((resolve) => {
const id = ++nextId;
pending.set(id, resolve);
worker.postMessage({ type: "verify", id, text });
});
}
async function run() {
if (!worker) return;
$("verify").disabled = $("example").disabled = true;
setStatus("جارٍ التحقق…", "Verifying…");
const html = await ask($("text").value);
$("results").innerHTML = html !== null ? html
: '<div class="notice bad">حدث خطأ غير متوقع.<span class="en">An unexpected error occurred.</span></div>';
setStatus("", "", false);
$("verify").disabled = $("example").disabled = false;
}
function start() {
try {
const source = $("worker-src").textContent;
worker = new Worker(URL.createObjectURL(new Blob([source], { type: "text/javascript" })));
} catch (error) { return fail(error.message); }
worker.onerror = (event) => fail(event.message || "worker error");
worker.onmessage = (event) => {
const message = event.data;
if (message.type === "status") setStatus(message.ar, message.en);
else if (message.type === "error") fail(message.message);
else if (message.type === "result") { const resolve = pending.get(message.id); pending.delete(message.id); resolve(message.html); }
else if (message.type === "ready") {
examples = message.examples;
$("verify").disabled = $("example").disabled = false;
setStatus("جاهز. الكود يعمل داخل متصفحك ولا يُرسل النص إلى أي خادم.",
"Ready. The code runs in your browser; your text is not sent to any server.", false);
}
};
worker.postMessage({ type: "boot", base: location.href, pyodideVersion: PYODIDE_VERSION,
files: PYTHON_FILES.concat(["demo/examples.json", "data/quran.json"]) });
}
$("verify").addEventListener("click", run);
$("example").addEventListener("click", () => {
if (!examples.length) return;
$("text").value = examples[exampleIndex % examples.length].text;
exampleIndex += 1;
run();
});
start();
</script>
</body>
</html>