Spaces:
Running
Running
Upload 2 files
Browse files- index.html +514 -0
- ui.py +544 -0
index.html
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="ar" dir="rtl">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>Amanah AI · نظام ذكي للتحقق من الاقتباسات القرآنية والحديثية</title>
|
| 7 |
+
<meta name="description" content="نظام ذكي للتحقق من الاقتباسات القرآنية والحديثية">
|
| 8 |
+
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
|
| 9 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
+
<style>
|
| 12 |
+
@import url('https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700&family=Amiri:wght@400;700&display=swap');
|
| 13 |
+
:root { --green:#0F4C3A; --green-2:#17694F; --gold:#B8912F; --gold-2:#E2C06E; --cream:#FBF6EA; --paper:#FFFFFF;
|
| 14 |
+
--ink:#1F2933; --muted:#55626D; --line:#E6DCC3; --ok:#1B7A4B; --ok-bg:#EAF6EF; --ok-line:#BFE3CD;
|
| 15 |
+
--bad:#B3261E; --bad-bg:#FDECEA; --bad-line:#F4B8B3; --warn:#7A5B0C; --warn-bg:#FFF6DA; --warn-line:#EBD28A; }
|
| 16 |
+
.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); }
|
| 17 |
+
.gradio-container { max-width:1060px !important; --body-text-color:#1F2933; --block-background-fill:#FFFFFF; --block-border-color:#E6DCC3;
|
| 18 |
+
--input-background-fill:#FFFFFF; --block-label-text-color:#55626D; --button-primary-background-fill:#0F4C3A;
|
| 19 |
+
--button-primary-background-fill-hover:#17694F; --button-primary-text-color:#FFFFFF; --button-secondary-background-fill:#FFFFFF;
|
| 20 |
+
--button-secondary-text-color:#0F4C3A; --button-secondary-border-color:#B8912F; --color-accent:#B8912F; }
|
| 21 |
+
.icv { direction:rtl; text-align:right; color:var(--ink); line-height:1.9; }
|
| 22 |
+
.icv .hero { background:linear-gradient(135deg,#0F4C3A,#17694F); border-radius:20px; padding:30px 22px 26px; margin:10px 0 18px; text-align:center;
|
| 23 |
+
box-shadow:0 6px 22px rgba(15,76,58,.18); border-bottom:4px solid var(--gold); }
|
| 24 |
+
.icv .hero .mark { width:54px; height:54px; display:block; margin:0 auto 4px; }
|
| 25 |
+
.icv .hero h1 { font-size:2.3rem; margin:.1rem 0; color:#FFFFFF; font-weight:700; }
|
| 26 |
+
.icv .hero .tagline { color:var(--gold-2); font-size:1.2rem; font-weight:600; margin:.1rem 0 .6rem; }
|
| 27 |
+
.icv .hero .sub { max-width:720px; margin:0 auto; color:#E9F2EE; font-size:1rem; }
|
| 28 |
+
.icv .flow { display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:8px; margin-top:16px; }
|
| 29 |
+
.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; }
|
| 30 |
+
.icv .flow i { color:var(--gold-2); font-style:normal; font-size:1.2rem; }
|
| 31 |
+
.icv .disclaimer { font-size:.85rem; color:var(--muted); text-align:center; padding:14px 8px; }
|
| 32 |
+
.icv .en-free { direction:rtl; }
|
| 33 |
+
.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; }
|
| 34 |
+
.llm-row label, .gradio-container label span { font-family:'Cairo',sans-serif; }
|
| 35 |
+
.results { direction:rtl; }
|
| 36 |
+
.icv .summary { display:grid; grid-template-columns:repeat(auto-fit,minmax(130px,1fr)); gap:10px; margin:8px 0 14px; }
|
| 37 |
+
.icv .tile { background:var(--paper); border:1px solid var(--line); border-radius:14px; padding:12px 8px; text-align:center; }
|
| 38 |
+
.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); }
|
| 39 |
+
.icv .tile.verified { background:var(--ok-bg); border-color:var(--ok-line); } .icv .tile.verified b { color:var(--ok); }
|
| 40 |
+
.icv .tile.mismatch { background:var(--bad-bg); border-color:var(--bad-line); } .icv .tile.mismatch b { color:var(--bad); }
|
| 41 |
+
.icv .tile.review { background:var(--warn-bg); border-color:var(--warn-line); } .icv .tile.review b { color:var(--warn); }
|
| 42 |
+
.icv .tile.zero { background:var(--paper); border-color:var(--line); opacity:.6; } .icv .tile.zero b { color:var(--muted); }
|
| 43 |
+
.icv .highlight, .icv .generated, .icv .final { background:var(--paper); border:1px solid var(--line); border-radius:14px; padding:14px 18px; margin-bottom:14px; }
|
| 44 |
+
.icv label { display:block; color:var(--muted); font-size:.82rem; font-weight:600; margin-bottom:4px; }
|
| 45 |
+
.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; }
|
| 46 |
+
.icv mark { color:var(--ink); border-radius:6px; padding:1px 5px; }
|
| 47 |
+
.icv mark.verified { background:#D5EEDF; } .icv mark.mismatch { background:#F8CFCB; } .icv mark.review { background:#F7E3A6; }
|
| 48 |
+
.icv .hl-legend { display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; font-size:.78rem; }
|
| 49 |
+
.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; }
|
| 50 |
+
.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); }
|
| 51 |
+
.icv .qhead { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-bottom:8px; }
|
| 52 |
+
.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; }
|
| 53 |
+
.icv .badge { padding:2px 14px; border-radius:999px; font-size:.84rem; background:#F3EEDD; border:1px solid var(--line); color:var(--ink); }
|
| 54 |
+
.icv .badge.st.verified { background:var(--ok-bg); border-color:var(--ok-line); color:var(--ok); font-weight:600; }
|
| 55 |
+
.icv .badge.st.mismatch { background:var(--bad-bg); border-color:var(--bad-line); color:var(--bad); font-weight:600; }
|
| 56 |
+
.icv .badge.st.review { background:var(--warn-bg); border-color:var(--warn-line); color:var(--warn); font-weight:600; }
|
| 57 |
+
.icv .badge.scan { background:#EEF3FB; border-color:#C9D8EE; color:#2F4F7F; }
|
| 58 |
+
.icv .conf { margin-inline-start:auto; color:var(--muted); font-size:.88rem; } .icv .conf b { color:var(--green); }
|
| 59 |
+
.icv .field { margin:8px 0; } .icv .field p { margin:0; }
|
| 60 |
+
.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; }
|
| 61 |
+
.icv .diff { font-family:'Amiri','Cairo',serif; font-size:1.2rem; line-height:2.2; }
|
| 62 |
+
.icv .w-extra { background:var(--bad-bg); color:var(--bad); border-radius:4px; padding:0 4px; text-decoration:line-through; }
|
| 63 |
+
.icv .w-missing { background:var(--ok-bg); color:var(--ok); border-radius:4px; padding:0 4px; font-weight:700; }
|
| 64 |
+
.icv .legend { color:var(--muted); font-size:.82rem; margin:4px 0 0; } .icv .legend span { text-decoration:none; font-size:.8rem; }
|
| 65 |
+
.icv .reason { color:var(--muted); font-size:.92rem; margin:6px 0 0; }
|
| 66 |
+
.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; }
|
| 67 |
+
.icv .action { border-radius:12px; padding:10px 14px; margin-top:10px; }
|
| 68 |
+
.icv .action.ok { background:var(--ok-bg); border:1px solid var(--ok-line); } .icv .action.ok b { color:var(--ok); }
|
| 69 |
+
.icv .action.warn { background:var(--warn-bg); border:1px solid var(--warn-line); } .icv .action.warn b { color:var(--warn); }
|
| 70 |
+
.icv .action.bad { background:var(--bad-bg); border:1px solid var(--bad-line); } .icv .action.bad b { color:var(--bad); }
|
| 71 |
+
.icv .evidence { margin-top:12px; border-top:1px dashed var(--line); padding-top:8px; }
|
| 72 |
+
.icv .evidence summary { cursor:pointer; color:var(--green); font-weight:700; }
|
| 73 |
+
.icv .ev-row { margin:10px 0; } .icv .ev-row b { color:var(--gold); font-size:.88rem; } .icv .ev-row p { margin:2px 0; }
|
| 74 |
+
.icv .indicators { display:grid; gap:6px; margin-top:6px; }
|
| 75 |
+
.icv .ind { display:grid; grid-template-columns:150px 1fr 48px; gap:10px; align-items:center; font-size:.88rem; }
|
| 76 |
+
.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)); }
|
| 77 |
+
.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; }
|
| 78 |
+
.icv .final { border-color:var(--gold); background:#FFFDF6; }
|
| 79 |
+
.icv .final-head { display:flex; justify-content:space-between; align-items:center; } .icv .final-head b { color:var(--green); font-size:1.05rem; }
|
| 80 |
+
.icv .copy { background:var(--green); color:#FFF; border:0; border-radius:10px; padding:5px 16px; font-family:inherit; cursor:pointer; }
|
| 81 |
+
.icv .notice { background:var(--paper); border:1px solid var(--line); border-radius:14px; padding:16px; }
|
| 82 |
+
.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); }
|
| 83 |
+
@media (max-width:640px){ .icv .hero h1{font-size:1.7rem;} .icv .ind{grid-template-columns:104px 1fr 40px;} }
|
| 84 |
+
|
| 85 |
+
:root { --glass:rgba(255,255,255,.62); --glass-strong:rgba(255,255,255,.82); --glass-line:rgba(255,255,255,.75);
|
| 86 |
+
--shadow-1:0 1px 2px rgba(15,76,58,.06), 0 8px 24px rgba(15,76,58,.08); --shadow-2:0 2px 4px rgba(15,76,58,.08), 0 18px 44px rgba(15,76,58,.16); }
|
| 87 |
+
.gradio-container, body.icv-page { background:
|
| 88 |
+
radial-gradient(900px 520px at 88% -8%, rgba(226,192,110,.34), transparent 60%),
|
| 89 |
+
radial-gradient(760px 520px at 6% 4%, rgba(23,105,79,.20), transparent 62%),
|
| 90 |
+
linear-gradient(180deg,#FBF6EA 0%,#F3EBD3 100%) !important; background-attachment:fixed !important; }
|
| 91 |
+
|
| 92 |
+
.icv, .icv * { box-sizing:border-box; min-width:0; }
|
| 93 |
+
.icv p, .icv span, .icv b, .icv label, .icv summary, .icv button, .icv .badge, .icv .tile { overflow-wrap:anywhere; }
|
| 94 |
+
.icv .quote, .icv .diff, .icv .final-text, .icv .highlight p, .icv .generated p { unicode-bidi:plaintext; text-align:start; }
|
| 95 |
+
.icv .hero { position:relative; overflow:hidden; border-bottom:0; border:1px solid rgba(226,192,110,.45);
|
| 96 |
+
background:linear-gradient(120deg,#0B3B2D,#17694F 45%,#0F4C3A 70%,#1d7a5c); background-size:240% 240%; animation:icv-flow 16s ease-in-out infinite;
|
| 97 |
+
box-shadow:var(--shadow-2); }
|
| 98 |
+
.icv .hero::before { content:""; position:absolute; inset:-40% -10% auto auto; width:60%; aspect-ratio:1; border-radius:50%;
|
| 99 |
+
background:radial-gradient(circle, rgba(226,192,110,.38), transparent 65%); pointer-events:none; }
|
| 100 |
+
.icv .hero::after { content:""; position:absolute; inset:auto auto 0 0; width:100%; height:4px; background:linear-gradient(90deg,transparent,var(--gold-2),transparent); }
|
| 101 |
+
.icv .hero h1 { font-size:clamp(1.45rem,4.2vw,2.3rem); line-height:1.5; text-wrap:balance; position:relative; }
|
| 102 |
+
.icv .hero .tagline, .icv .hero .sub, .icv .flow { position:relative; }
|
| 103 |
+
.icv .flow span { backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px); }
|
| 104 |
+
@keyframes icv-flow { 0%,100%{background-position:0% 50%} 50%{background-position:100% 50%} }
|
| 105 |
+
.icv .tile, .icv .highlight, .icv .generated, .icv .final, .icv .qcard, .icv .notice, .icv .export {
|
| 106 |
+
background:var(--glass); border:1px solid var(--glass-line); box-shadow:var(--shadow-1);
|
| 107 |
+
backdrop-filter:blur(14px) saturate(150%); -webkit-backdrop-filter:blur(14px) saturate(150%); transition:transform .25s ease, box-shadow .25s ease; }
|
| 108 |
+
.icv .qcard { border-inline-start:6px solid var(--line); }
|
| 109 |
+
.icv .qcard:hover, .icv .tile:hover { transform:translateY(-2px); box-shadow:var(--shadow-2); }
|
| 110 |
+
.icv .tile.verified { background:linear-gradient(160deg,rgba(234,246,239,.92),rgba(255,255,255,.6)); }
|
| 111 |
+
.icv .tile.mismatch { background:linear-gradient(160deg,rgba(253,236,234,.92),rgba(255,255,255,.6)); }
|
| 112 |
+
.icv .tile.review { background:linear-gradient(160deg,rgba(255,246,218,.95),rgba(255,255,255,.6)); }
|
| 113 |
+
.icv .final { background:linear-gradient(160deg,rgba(255,253,246,.92),rgba(250,240,208,.55)); border-color:rgba(184,145,47,.55); }
|
| 114 |
+
.icv .final-head { flex-wrap:wrap; gap:10px; }
|
| 115 |
+
.icv .qhead .badge { max-width:100%; white-space:normal; }
|
| 116 |
+
.icv .ind { grid-template-columns:minmax(96px,150px) minmax(0,1fr) 48px; }
|
| 117 |
+
.icv .bar span { background:linear-gradient(270deg,var(--green),var(--gold-2)); transition:width .6s ease; }
|
| 118 |
+
.icv .copy, .icv .dl { background:linear-gradient(135deg,var(--green),var(--green-2)); color:#FFF; border:0; border-radius:12px; padding:7px 18px;
|
| 119 |
+
font-family:inherit; font-weight:600; cursor:pointer; box-shadow:0 4px 12px rgba(15,76,58,.25); transition:transform .15s ease, box-shadow .15s ease, filter .15s ease; }
|
| 120 |
+
.icv .copy:hover, .icv .dl:hover { transform:translateY(-1px); filter:brightness(1.08); box-shadow:0 8px 18px rgba(15,76,58,.3); }
|
| 121 |
+
.icv .copy:active, .icv .dl:active { transform:translateY(0); }
|
| 122 |
+
.icv .copy:focus-visible, .icv .dl:focus-visible { outline:3px solid rgba(184,145,47,.55); outline-offset:2px; }
|
| 123 |
+
.icv .copy.done { background:linear-gradient(135deg,#1B7A4B,#2a9d66); }
|
| 124 |
+
.icv .export { border-radius:16px; padding:12px 18px; margin:12px 0; }
|
| 125 |
+
.icv .export summary { cursor:pointer; color:var(--green); font-weight:700; }
|
| 126 |
+
.icv .dls { display:flex; flex-wrap:wrap; gap:10px; margin-top:10px; }
|
| 127 |
+
|
| 128 |
+
#icv-toast { position:fixed; inset-inline:0; bottom:28px; margin-inline:auto; width:max-content; max-width:calc(100vw - 32px); z-index:99999;
|
| 129 |
+
direction:rtl; text-align:center; font:600 1rem 'Cairo','Segoe UI',Tahoma,sans-serif; color:#FFF; padding:12px 24px; border-radius:999px;
|
| 130 |
+
background:linear-gradient(135deg,rgba(15,76,58,.94),rgba(23,105,79,.94)); border:1px solid rgba(226,192,110,.6);
|
| 131 |
+
box-shadow:0 14px 40px rgba(15,76,58,.35); backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px);
|
| 132 |
+
opacity:0; transform:translateY(18px) scale(.97); pointer-events:none; transition:opacity .28s ease, transform .28s cubic-bezier(.2,.9,.3,1.2); }
|
| 133 |
+
#icv-toast.show { opacity:1; transform:none; } #icv-toast.bad { background:linear-gradient(135deg,rgba(179,38,30,.95),rgba(214,69,58,.95)); }
|
| 134 |
+
|
| 135 |
+
.skel { position:relative; overflow:hidden; background:rgba(15,76,58,.08); border-radius:10px; }
|
| 136 |
+
.skel::after { content:""; position:absolute; inset:0; transform:translateX(100%); animation:icv-shimmer 1.4s infinite;
|
| 137 |
+
background:linear-gradient(90deg,transparent,rgba(255,255,255,.75),transparent); }
|
| 138 |
+
@keyframes icv-shimmer { 100% { transform:translateX(-100%); } }
|
| 139 |
+
.skel-card { background:var(--glass); border:1px solid var(--glass-line); border-radius:16px; padding:16px 18px; margin:12px 0; box-shadow:var(--shadow-1); }
|
| 140 |
+
.skel-line { height:14px; margin:10px 0; } .skel-line.w60 { width:60%; } .skel-line.w85 { width:85%; } .skel-line.w40 { width:40%; }
|
| 141 |
+
.skel-tiles { display:grid; grid-template-columns:repeat(auto-fit,minmax(120px,1fr)); gap:10px; margin:8px 0 14px; } .skel-tile { height:78px; border-radius:14px; }
|
| 142 |
+
.icv-spinner { width:18px; height:18px; border-radius:50%; border:3px solid rgba(15,76,58,.18); border-top-color:var(--gold); display:inline-block;
|
| 143 |
+
vertical-align:middle; margin-inline-end:10px; animation:icv-spin .8s linear infinite; }
|
| 144 |
+
@keyframes icv-spin { to { transform:rotate(360deg); } }
|
| 145 |
+
@media (prefers-reduced-motion: reduce) { .icv .hero, .skel::after, .icv-spinner { animation:none; } .icv .qcard, .icv .tile, #icv-toast { transition:none; } }
|
| 146 |
+
@media (max-width:640px){ .icv .ind{grid-template-columns:minmax(84px,104px) minmax(0,1fr) 40px;} .icv .final-head{flex-direction:column; align-items:stretch;} .icv .copy{width:100%;} }
|
| 147 |
+
|
| 148 |
+
.icv .notice, .icv .disclaimer, .icv .export { text-align:center; }
|
| 149 |
+
.icv .export .legend { max-width:640px; margin:8px auto 0; font-size:.9rem; line-height:1.9; color:var(--muted); }
|
| 150 |
+
.icv .dls { justify-content:center; }
|
| 151 |
+
.icv .dl { display:inline-flex; flex-direction:column; align-items:center; gap:2px; min-width:170px; }
|
| 152 |
+
.icv .dl .t { font-weight:700; } .icv .dl .s { font-size:.78rem; font-weight:500; opacity:.85; }
|
| 153 |
+
.icv .hero .flow { display:flex; justify-content:center; flex-wrap:wrap; gap:8px; }
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
.icv .hero { padding:38px 24px 30px; border-radius:26px; background:
|
| 157 |
+
radial-gradient(900px 300px at 85% -10%, rgba(226,192,110,.28), transparent 60%),
|
| 158 |
+
radial-gradient(700px 320px at 0% 110%, rgba(23,105,79,.9), transparent 70%),
|
| 159 |
+
linear-gradient(135deg,#0A3B2C 0%,#0F4C3A 45%,#17694F 100%); box-shadow:0 18px 50px rgba(10,59,44,.28); }
|
| 160 |
+
.icv .hero .mark { width:60px; height:60px; margin-bottom:6px; filter:drop-shadow(0 4px 10px rgba(226,192,110,.45)); }
|
| 161 |
+
.icv .hero h1.brand { font-family:'Cairo','Segoe UI',sans-serif; font-weight:800; font-size:clamp(2.4rem,7vw,3.9rem); letter-spacing:.02em; direction:ltr;
|
| 162 |
+
line-height:1.15; margin:.1rem 0 .2rem; background:linear-gradient(180deg,#FFFFFF 30%,#F3DFA6 100%); -webkit-background-clip:text; background-clip:text; color:transparent; }
|
| 163 |
+
.icv .hero .tagline { font-size:clamp(1.05rem,2.6vw,1.45rem); font-weight:700; color:var(--gold-2); margin:.2rem 0 .8rem; }
|
| 164 |
+
.icv .hero .sub { max-width:760px; font-size:1.02rem; line-height:2; color:#E4EFEA; }
|
| 165 |
+
.icv .trust { list-style:none; display:flex; flex-wrap:wrap; justify-content:center; gap:10px; margin:20px 0 0; padding:0; position:relative; }
|
| 166 |
+
.icv .trust li { background:rgba(255,255,255,.1); border:1px solid rgba(226,192,110,.5); border-radius:14px; padding:8px 16px; color:#FFF; font-size:.92rem; backdrop-filter:blur(6px); }
|
| 167 |
+
.icv .trust li b { color:var(--gold-2); font-size:1.05rem; margin-inline-end:6px; }
|
| 168 |
+
.icv .tile { box-shadow:0 6px 18px rgba(15,76,58,.07); transition:transform .2s; } .icv .tile:hover { transform:translateY(-2px); }
|
| 169 |
+
.icv .qcard { box-shadow:0 8px 26px rgba(15,76,58,.09); }
|
| 170 |
+
|
| 171 |
+
body.icv-page{margin:0;min-height:100vh;background-attachment:fixed;} .page{max-width:980px;margin:0 auto;padding:10px 16px 28px;box-sizing:border-box;}
|
| 172 |
+
.glass{background:var(--glass);border:1px solid var(--glass-line);border-radius:20px;box-shadow:var(--shadow-1);
|
| 173 |
+
backdrop-filter:blur(16px) saturate(150%);-webkit-backdrop-filter:blur(16px) saturate(150%);}
|
| 174 |
+
.tabs{display:flex;gap:6px;margin:6px auto 14px;direction:rtl;padding:6px;border-radius:18px;max-width:560px;}
|
| 175 |
+
.tab{flex:1;border:0;background:transparent;color:var(--green);border-radius:13px;padding:11px 14px;font:700 1rem 'Cairo',sans-serif;cursor:pointer;transition:background .2s,color .2s,box-shadow .2s;}
|
| 176 |
+
.tab:hover{background:rgba(15,76,58,.07);}
|
| 177 |
+
.tab.active{background:linear-gradient(135deg,var(--green),var(--green-2));color:#FFF;box-shadow:0 6px 16px rgba(15,76,58,.28);}
|
| 178 |
+
.tab:focus-visible,.btns button:focus-visible{outline:3px solid rgba(184,145,47,.55);outline-offset:2px;}
|
| 179 |
+
section[hidden]{display:none;}
|
| 180 |
+
.form{padding:18px 20px 14px;margin-bottom:14px;} .form .icv{margin-bottom:6px;}
|
| 181 |
+
.form label{display:block;color:var(--muted);font-size:.88rem;font-weight:600;margin:10px 0 5px;direction:rtl;text-align:right;}
|
| 182 |
+
.form textarea,.form select{width:100%;box-sizing:border-box;background:rgba(255,255,255,.88);color:var(--ink);border:1px solid var(--line);border-radius:14px;padding:12px 14px;font:1rem 'Cairo',sans-serif;direction:rtl;text-align:right;outline:none;transition:border-color .2s,box-shadow .2s;}
|
| 183 |
+
.form textarea{min-height:200px;resize:vertical;font-family:'Amiri','Cairo',serif;font-size:1.25rem;line-height:2.1;}
|
| 184 |
+
.form textarea.short{min-height:96px;}
|
| 185 |
+
.form textarea:focus,.form select:focus{border-color:var(--gold);box-shadow:0 0 0 4px rgba(184,145,47,.18);}
|
| 186 |
+
.hint{font-size:.84rem;color:var(--muted);direction:rtl;margin:2px 0 0;}
|
| 187 |
+
.btns{display:flex;gap:12px;margin:14px 0 6px;flex-wrap:wrap;justify-content:center;}
|
| 188 |
+
.btns button{flex:1 1 200px;border:0;border-radius:14px;padding:13px 18px;font:700 1.02rem 'Cairo',sans-serif;cursor:pointer;transition:transform .15s,box-shadow .15s,filter .15s;}
|
| 189 |
+
.btn-main{background:linear-gradient(135deg,var(--green),var(--green-2));color:#FFF;box-shadow:0 8px 20px rgba(15,76,58,.28);}
|
| 190 |
+
.btn-main:hover{transform:translateY(-1px);filter:brightness(1.07);}
|
| 191 |
+
.btn-alt{background:rgba(255,255,255,.85);color:var(--green);border:1.5px solid var(--gold)!important;} .btn-alt:hover{background:#FFFBEF;transform:translateY(-1px);}
|
| 192 |
+
.btns button:disabled{opacity:.5;cursor:not-allowed;transform:none;}
|
| 193 |
+
#status{direction:rtl;text-align:center;color:var(--muted);font-size:.93rem;margin:8px 0;} #status:empty{display:none;}
|
| 194 |
+
.boot{display:flex;align-items:center;justify-content:center;gap:10px;margin:2px 0 12px;direction:rtl;color:var(--muted);font-size:.9rem;min-height:26px;transition:opacity .35s ease;}
|
| 195 |
+
.boot .dot{width:18px;height:18px;border-radius:50%;border:2.5px solid rgba(15,76,58,.16);border-top-color:var(--gold);animation:icv-spin .8s linear infinite;flex:none;}
|
| 196 |
+
.boot.ready{opacity:0;pointer-events:none;height:0;min-height:0;margin:0;overflow:hidden;}
|
| 197 |
+
@media (prefers-reduced-motion: reduce){.boot .dot{animation-duration:2s;}}
|
| 198 |
+
.banner{margin:0 0 12px;}
|
| 199 |
+
</style>
|
| 200 |
+
</head>
|
| 201 |
+
<body class="icv-page">
|
| 202 |
+
<div class="page">
|
| 203 |
+
|
| 204 |
+
<div class="icv"><header class="hero">
|
| 205 |
+
<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>
|
| 206 |
+
<h1 class="brand">Amanah AI</h1>
|
| 207 |
+
<p class="tagline">نظام ذكي للتحقق من الاقتباسات القرآنية والحديثية</p>
|
| 208 |
+
<p class="sub">يكتشف الاقتباسات القرآنية والحديثية داخل أي نص، ويطابقها كلمةً بكلمة مع نصوص المراجع، ويصحّح الخطأ من المصدر نفسه.
|
| 209 |
+
وحين لا يجد دليلًا كافيًا لا يخمّن أبدًا، بل يعلن أن الاقتباس غير مطابق أو يحيله إلى المراجعة البشرية.</p>
|
| 210 |
+
<ul class="trust"><li><b>من المصدر</b> كل مرجع وتصحيح من نص المراجع حرفيًا</li><li><b>٣ قرارات</b> موثّق · غير مطابق · مراجعة بشرية</li><li><b>داخل المتصفح</b> التحقق لا يرسل نصك إلى أي خادم</li></ul>
|
| 211 |
+
</header></div>
|
| 212 |
+
|
| 213 |
+
<div class="tabs glass" role="tablist"><button class="tab active" role="tab" aria-selected="true" data-tab="ask">اسأل ثم تحقّق</button><button class="tab" role="tab" aria-selected="false" data-tab="direct">تحقّق مباشر</button></div>
|
| 214 |
+
<section id="tab-ask" class="form glass">
|
| 215 |
+
<div class="icv"><div class="notice">اكتب سؤالًا، وسيجيب عنه النظام مباشرةً، ثم يفحص كل آية وحديث في الإجابة ويعرض الأخطاء والتصحيحات.</div></div>
|
| 216 |
+
<label for="prompt">سؤالك</label>
|
| 217 |
+
<textarea id="prompt" class="short" maxlength="1500" placeholder="اكتب سؤالك، مثل: اشرح لي فضل الصبر في القرآن والسنة مع ذكر الأدلة."></textarea>
|
| 218 |
+
<div class="btns"><button id="ask" class="btn-main">اسأل ثم تحقّق</button><button id="ask-example" class="btn-alt">جرّب مثالًا</button></div>
|
| 219 |
+
</section>
|
| 220 |
+
<section id="tab-direct" class="form glass" hidden>
|
| 221 |
+
<label for="text">النص المراد التحقق منه</label>
|
| 222 |
+
<textarea id="text" placeholder="الصق هنا النص الذي ولّده نموذج لغوي. يكتشف النظام الآيات والأحاديث الواردة فيه، بعلامات تنصيص أو بدونها…"></textarea>
|
| 223 |
+
<div class="btns"><button id="verify" class="btn-main">تحقّق من النص</button><button id="example" class="btn-alt">جرّب مثالًا</button></div>
|
| 224 |
+
</section>
|
| 225 |
+
<div class="boot" id="boot" aria-live="polite"><i class="dot"></i><span id="boot-text">جارٍ تحميل البيانات…</span></div>
|
| 226 |
+
<div id="status" role="status"></div>
|
| 227 |
+
<div id="results" class="results"></div>
|
| 228 |
+
<div class="icv"><div class="disclaimer">أداة مساعدة للتدقيق النصي وليست فتوى ولا بديلًا عن المراجعة المتخصصة.
|
| 229 |
+
النتائج مبنية على مراجع القرآن الكريم والكتب الستة المضمّنة فقط.
|
| 230 |
+
<br>نص القرآن: <a href="https://tanzil.net" target="_blank" rel="noopener">مشروع <bdi>Tanzil</bdi></a> <bdi>(CC BY 3.0)</bdi> · بيانات <bdi>IslamicEval 2025</bdi></div></div>
|
| 231 |
+
</div>
|
| 232 |
+
<script>
|
| 233 |
+
(function () {
|
| 234 |
+
if (window.icvCopy) return;
|
| 235 |
+
function toast(message, bad) {
|
| 236 |
+
var t = document.getElementById('icv-toast');
|
| 237 |
+
if (!t) { t = document.createElement('div'); t.id = 'icv-toast'; t.setAttribute('role', 'status'); t.setAttribute('aria-live', 'polite'); document.body.appendChild(t); }
|
| 238 |
+
t.textContent = message; t.className = 'show' + (bad ? ' bad' : '');
|
| 239 |
+
clearTimeout(window.__icvToast); window.__icvToast = setTimeout(function () { t.className = bad ? 'bad' : ''; }, 2400);
|
| 240 |
+
}
|
| 241 |
+
function legacyCopy(text) {
|
| 242 |
+
var area = document.createElement('textarea'); area.value = text; area.setAttribute('readonly', '');
|
| 243 |
+
area.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none'; document.body.appendChild(area);
|
| 244 |
+
area.select(); area.setSelectionRange(0, text.length); var ok = false;
|
| 245 |
+
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
|
| 246 |
+
document.body.removeChild(area); return ok;
|
| 247 |
+
}
|
| 248 |
+
window.icvToast = toast;
|
| 249 |
+
window.icvCopy = function (button) {
|
| 250 |
+
var text = button.getAttribute('data-text') || '';
|
| 251 |
+
function finish(ok) {
|
| 252 |
+
toast(ok ? 'تم نسخ النص بنجاح' : 'تعذّر النسخ، حدّد النص وانسخه يدويًا', !ok);
|
| 253 |
+
if (ok) { button.classList.add('done'); setTimeout(function () { button.classList.remove('done'); }, 1600); }
|
| 254 |
+
}
|
| 255 |
+
if (navigator.clipboard && window.isSecureContext) {
|
| 256 |
+
navigator.clipboard.writeText(text).then(function () { finish(true); }, function () { finish(legacyCopy(text)); });
|
| 257 |
+
} else { finish(legacyCopy(text)); }
|
| 258 |
+
};
|
| 259 |
+
window.icvDownload = function (button) {
|
| 260 |
+
var blob = new Blob([button.getAttribute('data-text') || ''], { type: 'text/tab-separated-values;charset=utf-8' });
|
| 261 |
+
var link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = button.getAttribute('data-name') || 'results.tsv';
|
| 262 |
+
document.body.appendChild(link); link.click(); document.body.removeChild(link); setTimeout(function () { URL.revokeObjectURL(link.href); }, 1000);
|
| 263 |
+
};
|
| 264 |
+
})();
|
| 265 |
+
</script>
|
| 266 |
+
<script type="text/plain" id="worker-src">
|
| 267 |
+
// Python (Pyodide) and the whole verification pipeline run here, off the main thread.
|
| 268 |
+
let py = null, booted = null, hadithReady = null;
|
| 269 |
+
const post = (message) => self.postMessage(message);
|
| 270 |
+
const step = (name, state, extra) => post(Object.assign({ type: "step", name, state }, extra || {}));
|
| 271 |
+
|
| 272 |
+
async function openCache(buildId) {
|
| 273 |
+
try {
|
| 274 |
+
const names = await caches.keys();
|
| 275 |
+
await Promise.all(names.filter((n) => n.startsWith("icv-") && n !== "icv-" + buildId).map((n) => caches.delete(n))); // purge old builds
|
| 276 |
+
return await caches.open("icv-" + buildId);
|
| 277 |
+
} catch (e) { return null; }
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
async function fetchBytes(url, cache, onProgress) {
|
| 281 |
+
if (cache) { try { const hit = await cache.match(url); if (hit) return { bytes: new Uint8Array(await hit.arrayBuffer()), cached: true }; } catch (e) { /* fall through */ } }
|
| 282 |
+
const response = await fetch(url);
|
| 283 |
+
if (!response.ok) throw new Error("تعذّر تحميل " + url + " (" + response.status + ")");
|
| 284 |
+
if (cache) { try { await cache.put(url, response.clone()); } catch (e) { /* the cache is an optimisation only */ } }
|
| 285 |
+
const total = Number(response.headers.get("Content-Length")) || 0;
|
| 286 |
+
if (!response.body || !onProgress) return { bytes: new Uint8Array(await response.arrayBuffer()), cached: false };
|
| 287 |
+
const reader = response.body.getReader(), chunks = []; let received = 0;
|
| 288 |
+
for (;;) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); received += value.length; onProgress(received, total); }
|
| 289 |
+
const bytes = new Uint8Array(received); let offset = 0;
|
| 290 |
+
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.length; }
|
| 291 |
+
return { bytes, cached: false };
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
// Accept both layouts: files inside folders (index/, demo/) or uploaded flat next to index.html.
|
| 295 |
+
async function fetchAny(file, base, cache, onProgress) {
|
| 296 |
+
let lastError = null;
|
| 297 |
+
for (const path of [file, file.split("/").pop()]) {
|
| 298 |
+
try { return await fetchBytes(new URL(path, base).href, cache, onProgress); } catch (error) { lastError = error; }
|
| 299 |
+
}
|
| 300 |
+
throw lastError;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
// A server that sets Content-Encoding: gzip on .gz files makes the browser inflate them; Python expects gzip, so re-pack.
|
| 304 |
+
async function ensureGzip(file, bytes) {
|
| 305 |
+
if (!file.endsWith(".gz") || (bytes[0] === 0x1f && bytes[1] === 0x8b)) return bytes;
|
| 306 |
+
const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"));
|
| 307 |
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
async function boot(message) {
|
| 311 |
+
const cache = await openCache(message.buildId);
|
| 312 |
+
step("python", "active");
|
| 313 |
+
importScripts("https://cdn.jsdelivr.net/pyodide/v" + message.pyodideVersion + "/full/pyodide.js");
|
| 314 |
+
const hadithFile = message.lazy[0];
|
| 315 |
+
// the large Hadith index starts downloading now, in parallel with the Python runtime
|
| 316 |
+
const hadithDownload = fetchAny(hadithFile, message.base, cache, (got, total) => step("hadith", "active", { pct: total ? Math.round((got / total) * 100) : null, mb: (got / 1048576).toFixed(1) }));
|
| 317 |
+
hadithDownload.catch(() => {});
|
| 318 |
+
py = await loadPyodide();
|
| 319 |
+
step("python", "done");
|
| 320 |
+
step("code", "active", { pct: 0 });
|
| 321 |
+
for (const dir of ["/app", "/app/index", "/app/demo", "/app/data"]) py.FS.mkdir(dir);
|
| 322 |
+
let cached = true, n = 0;
|
| 323 |
+
for (const file of message.files) {
|
| 324 |
+
const result = await fetchAny(file, message.base, cache);
|
| 325 |
+
cached = cached && result.cached;
|
| 326 |
+
py.FS.writeFile("/app/" + file, await ensureGzip(file, result.bytes));
|
| 327 |
+
step("code", "active", { pct: Math.round((++n / message.files.length) * 100) });
|
| 328 |
+
}
|
| 329 |
+
const examplesText = new TextDecoder().decode(py.FS.readFile("/app/demo/examples.json"));
|
| 330 |
+
py.runPython("import sys; sys.path.insert(0, '/app')");
|
| 331 |
+
py.runPython("from app import get_pipeline, verify_text, verify_generated_answer; get_pipeline()");
|
| 332 |
+
step("code", "done", { cached });
|
| 333 |
+
post({ type: "ready", examples: JSON.parse(examplesText) });
|
| 334 |
+
hadithReady = (async () => {
|
| 335 |
+
const result = await hadithDownload;
|
| 336 |
+
step("hadith", "active", { pct: 100, prepare: true });
|
| 337 |
+
py.FS.writeFile("/app/" + hadithFile, await ensureGzip(hadithFile, result.bytes));
|
| 338 |
+
py.runPython("get_pipeline().retriever.warm()");
|
| 339 |
+
step("hadith", "done", { cached: result.cached });
|
| 340 |
+
post({ type: "hadith" });
|
| 341 |
+
})();
|
| 342 |
+
await hadithReady;
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
self.onmessage = async (event) => {
|
| 346 |
+
const message = event.data;
|
| 347 |
+
if (message.type === "boot") {
|
| 348 |
+
booted = boot(message);
|
| 349 |
+
try { await booted; } catch (error) { post({ type: "error", text: String((error && error.message) || error) }); }
|
| 350 |
+
} else if (message.type === "verify") {
|
| 351 |
+
try {
|
| 352 |
+
await booted;
|
| 353 |
+
const html = py.globals.get(message.generated ? "verify_generated_answer" : "verify_text")(message.text, message.entities || "");
|
| 354 |
+
post({ type: "result", id: message.id, html });
|
| 355 |
+
} catch (error) { post({ type: "result", id: message.id, html: null }); }
|
| 356 |
+
}
|
| 357 |
+
};
|
| 358 |
+
</script>
|
| 359 |
+
<script>
|
| 360 |
+
const DEFAULT_CONFIG = {"askEndpoint": "https://icv-ask-proxy.ghada-islamic-verifier-2026.workers.dev", "hf": {"model": "", "endpoint": "https://router.huggingface.co/hf-inference/models/{model}"}};
|
| 361 |
+
const BUILD_ID = "5582448610bd";
|
| 362 |
+
const SKELETON = "<div class=\"icv\" aria-busy=\"true\" aria-label=\"جارٍ التحقق\"><div class=\"skel-tiles\"><div class=\"skel skel-tile\"></div><div class=\"skel skel-tile\"></div><div class=\"skel skel-tile\"></div><div class=\"skel skel-tile\"></div></div><div class=\"skel-card\"><div class=\"skel skel-line w40\"></div><div class=\"skel skel-line w85\"></div><div class=\"skel skel-line\"></div><div class=\"skel skel-line w60\"></div></div><div class=\"skel-card\"><div class=\"skel skel-line w40\"></div><div class=\"skel skel-line w85\"></div><div class=\"skel skel-line\"></div><div class=\"skel skel-line w60\"></div></div></div>";
|
| 363 |
+
const MAX_PROMPT = 1500;
|
| 364 |
+
const $ = (id) => document.getElementById(id);
|
| 365 |
+
let cfg = DEFAULT_CONFIG, examples = [], exampleIndex = 0, askExampleIndex = 0, nextId = 0, worker = null, ready = false;
|
| 366 |
+
const pending = new Map();
|
| 367 |
+
|
| 368 |
+
const store = {
|
| 369 |
+
get(key) { try { return localStorage.getItem(key); } catch (e) { return null; } },
|
| 370 |
+
set(key, value) { try { localStorage.setItem(key, value); } catch (e) { /* private mode / quota: caching is optional */ } },
|
| 371 |
+
};
|
| 372 |
+
function purgeOldLocalCache() {
|
| 373 |
+
try { for (const key of Object.keys(localStorage)) if (key.startsWith("icv:examples:") && key !== "icv:examples:" + BUILD_ID) localStorage.removeItem(key); } catch (e) { /* ignore */ }
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
| 377 |
+
function setStatus(text, busy = true) { $("status").innerHTML = (busy && text ? '<span class="icv-spinner"></span>' : "") + esc(text); }
|
| 378 |
+
function setBusy(flag) { for (const id of ["verify", "example", "ask", "ask-example"]) $(id).disabled = flag; }
|
| 379 |
+
function notice(text, kind) { return '<div class="icv banner"><div class="notice ' + kind + '">' + esc(text) + "</div></div>"; }
|
| 380 |
+
function show(html, banner) { $("results").innerHTML = (banner || "") + html; }
|
| 381 |
+
function showSkeleton() { $("results").innerHTML = SKELETON; }
|
| 382 |
+
|
| 383 |
+
function verifyRemote(payload) {
|
| 384 |
+
return new Promise((resolve) => { const id = ++nextId; pending.set(id, resolve); worker.postMessage(Object.assign({ type: "verify", id }, payload)); });
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
async function hostedEntities(text) {
|
| 389 |
+
const model = (cfg.hf && cfg.hf.model || "").trim();
|
| 390 |
+
if (!model) return "";
|
| 391 |
+
const controller = new AbortController(), timer = setTimeout(() => controller.abort(), 6000);
|
| 392 |
+
try {
|
| 393 |
+
const response = await fetch(cfg.hf.endpoint.replace("{model}", encodeURIComponent(model)), {
|
| 394 |
+
method: "POST", headers: { "Content-Type": "application/json" }, signal: controller.signal,
|
| 395 |
+
body: JSON.stringify({ inputs: text, parameters: { aggregation_strategy: "simple" } }),
|
| 396 |
+
});
|
| 397 |
+
if (!response.ok) return "";
|
| 398 |
+
const data = await response.json();
|
| 399 |
+
return Array.isArray(data) ? JSON.stringify(data) : "";
|
| 400 |
+
} catch (e) { return ""; } finally { clearTimeout(timer); }
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
async function verifyAuto(text, generated) {
|
| 404 |
+
const entities = await hostedEntities(text);
|
| 405 |
+
return verifyRemote({ text, generated, entities });
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
const FOREIGN = /[\u0400-\u04ff\u0900-\u097f\u0e00-\u0e7f\u1100-\u11ff\u3000-\u303f\u3040-\u30ff\u3130-\u318f\u3400-\u4dbf\u4e00-\u9fff\uac00-\ud7af\uf900-\ufaff\uff00-\uffef]+/g;
|
| 410 |
+
const cleanAnswer = (t) => String(t).replace(FOREIGN, " ").replace(/([،,؛.])\s*[،,؛]+/g, "$1").replace(/[ \t]{2,}/g, " ").replace(/ +([،؛.:])/g, "$1").trim();
|
| 411 |
+
|
| 412 |
+
async function runDirect() {
|
| 413 |
+
if (!$("text").value.trim()) return show(notice("الرجاء إدخال نص للتحقق منه.", "warn"));
|
| 414 |
+
setBusy(true); showSkeleton(); setStatus(ready ? "جارٍ التحقق…" : "جارٍ تجهيز النظام ثم التحقق…");
|
| 415 |
+
const html = await verifyAuto($("text").value, false);
|
| 416 |
+
show(html !== null ? html : notice("حدث خطأ غير متوقع أثناء التحقق.", "bad"));
|
| 417 |
+
setStatus("", false); setBusy(false);
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
async function askServer(prompt) {
|
| 422 |
+
const response = await fetch(cfg.askEndpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt }) });
|
| 423 |
+
if (!response.ok) {
|
| 424 |
+
const error = new Error("http_" + response.status); error.status = response.status;
|
| 425 |
+
try { error.code = (await response.json()).error; } catch (e) { error.code = ""; }
|
| 426 |
+
throw error;
|
| 427 |
+
}
|
| 428 |
+
const data = await response.json();
|
| 429 |
+
if (!data.answer) throw new Error("empty");
|
| 430 |
+
return cleanAnswer(data.answer);
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
async function runAsk() {
|
| 434 |
+
const prompt = $("prompt").value.trim();
|
| 435 |
+
if (!prompt) return show(notice("اكتب سؤالًا أولًا.", "warn"));
|
| 436 |
+
if (prompt.length > MAX_PROMPT) return show(notice("السؤال طويل جدًا (الحد الأقصى " + MAX_PROMPT + " حرف).", "warn"));
|
| 437 |
+
setBusy(true); showSkeleton(); setStatus("جارٍ إعداد الإجابة…");
|
| 438 |
+
let answer = null, banner = "";
|
| 439 |
+
try { answer = await askServer(prompt); }
|
| 440 |
+
catch (error) {
|
| 441 |
+
const stop = (message) => { show(notice(message, "warn")); setStatus("", false); setBusy(false); };
|
| 442 |
+
if (error.status === 402) return stop("رصيد خدمة الإجابة غير كافٍ حاليًا؛ على صاحب الحساب مراجعة الحصة المتاحة ثم المحاولة مجددًا.");
|
| 443 |
+
if (error.status === 429) return stop("الطلبات كثيرة الآن؛ انتظر قليلًا ثم أعد المحاولة.");
|
| 444 |
+
if (error.status === 413) return stop("السؤال طويل جدًا.");
|
| 445 |
+
if (error.code === "upstream_auth") return stop("مفتاح الخدمة على الخادم غير صالح؛ يلزم تحديثه من صاحب الحساب.");
|
| 446 |
+
return stop("تعذّر الوصول إلى خدمة الإجابة الآن؛ يمكنك تجربة أحد الأمثلة المحفوظة بزر «جرّب مثالًا» أو إعادة المحاولة بعد قليل.");
|
| 447 |
+
}
|
| 448 |
+
if (answer === null) { show(notice("تعذّر الحصول على إجابة الآن؛ حاول لاحقًا.", "warn")); setStatus("", false); return setBusy(false); }
|
| 449 |
+
setStatus("جارٍ التحقق من الإجابة…");
|
| 450 |
+
const html = await verifyAuto(answer, true);
|
| 451 |
+
show(html !== null ? html : notice("تعذّر التحقق من الإجابة.", "bad"), banner);
|
| 452 |
+
setStatus("", false); setBusy(false);
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
async function runAskExample() {
|
| 457 |
+
const samples = examples.filter((e) => e.question);
|
| 458 |
+
if (!samples.length) return show(notice("الأمثلة لم تُحمَّل بعد؛ انتظر لحظة ثم أعد المحاولة.", "warn"));
|
| 459 |
+
const sample = samples[askExampleIndex++ % samples.length];
|
| 460 |
+
$("prompt").value = sample.question;
|
| 461 |
+
setBusy(true); showSkeleton(); setStatus("جارٍ التحقق من الإجابة…");
|
| 462 |
+
const html = await verifyAuto(sample.text, true);
|
| 463 |
+
show(html !== null ? html : notice("تعذّر التحقق من الإجابة.", "bad"),
|
| 464 |
+
notice("إجابة تجريبية محفوظة لحالة «" + sample.title + "»، لم تُرسل إلى أي خدمة. لتجربة إجابة حيّة اكتب سؤالك واضغط «اسأل ثم تحقّق».", ""));
|
| 465 |
+
setStatus("", false); setBusy(false);
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
const STEP_LABELS = { python: "جارٍ تحميل البيانات…", code: "جارٍ تحميل البيانات…", hadith: "جارٍ تحميل كتب الحديث…" };
|
| 469 |
+
const stepsDone = new Set();
|
| 470 |
+
function applyStep(m) {
|
| 471 |
+
if (m.state === "done") stepsDone.add(m.name);
|
| 472 |
+
const text = $("boot-text");
|
| 473 |
+
if (m.state === "active" && STEP_LABELS[m.name]) text.textContent = STEP_LABELS[m.name] + (m.name === "hadith" && m.pct != null && m.pct < 100 ? " " + m.pct + "%" : "");
|
| 474 |
+
if (stepsDone.size >= 3) $("boot").classList.add("ready");
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
function start() {
|
| 478 |
+
try { worker = new Worker(URL.createObjectURL(new Blob([$("worker-src").textContent], { type: "text/javascript" }))); }
|
| 479 |
+
catch (error) { return setStatus("تعذّر تشغيل النظام: " + error.message, false); }
|
| 480 |
+
worker.onerror = (event) => setStatus("تعذّر تشغيل النظام: " + (event.message || ""), false);
|
| 481 |
+
worker.onmessage = (event) => {
|
| 482 |
+
const m = event.data;
|
| 483 |
+
if (m.type === "step") applyStep(m);
|
| 484 |
+
else if (m.type === "error") setStatus("تعذّر تشغيل النظام: " + m.text + ". جرّب متصفح كمبيوتر حديثًا ثم أعد تحميل الصفحة.", false);
|
| 485 |
+
else if (m.type === "result") { const resolve = pending.get(m.id); pending.delete(m.id); resolve(m.html); }
|
| 486 |
+
else if (m.type === "ready") { examples = m.examples; ready = true; store.set("icv:examples:" + BUILD_ID, JSON.stringify(m.examples)); }
|
| 487 |
+
};
|
| 488 |
+
worker.postMessage({ type: "boot", base: location.href, buildId: BUILD_ID, 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", "benchmark_format.py", "camelbert_adapter.py", "index/quran.idx.gz", "demo/examples.json"], lazy: ["index/hadith.idx.gz"] });
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
async function loadConfig() {
|
| 492 |
+
try {
|
| 493 |
+
const response = await fetch("config.json", { cache: "no-store" });
|
| 494 |
+
if (response.ok) { cfg = Object.assign({}, DEFAULT_CONFIG, await response.json()); store.set("icv:config", JSON.stringify(cfg)); return; }
|
| 495 |
+
} catch (e) { }
|
| 496 |
+
try { const saved = store.get("icv:config"); if (saved) cfg = Object.assign({}, DEFAULT_CONFIG, JSON.parse(saved)); } catch (e) { /* ignore */ }
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
document.querySelectorAll(".tab").forEach((tab) => tab.addEventListener("click", () => {
|
| 500 |
+
document.querySelectorAll(".tab").forEach((t) => { const on = t === tab; t.classList.toggle("active", on); t.setAttribute("aria-selected", on); });
|
| 501 |
+
for (const name of ["direct", "ask"]) $("tab-" + name).hidden = tab.dataset.tab !== name;
|
| 502 |
+
}));
|
| 503 |
+
$("verify").addEventListener("click", runDirect);
|
| 504 |
+
$("ask").addEventListener("click", runAsk);
|
| 505 |
+
$("ask-example").addEventListener("click", runAskExample);
|
| 506 |
+
$("example").addEventListener("click", () => { if (!examples.length) return; $("text").value = examples[exampleIndex++ % examples.length].text; runDirect(); });
|
| 507 |
+
|
| 508 |
+
purgeOldLocalCache();
|
| 509 |
+
try { const saved = store.get("icv:examples:" + BUILD_ID); if (saved) examples = JSON.parse(saved); } catch (e) { examples = []; }
|
| 510 |
+
loadConfig();
|
| 511 |
+
start();
|
| 512 |
+
</script>
|
| 513 |
+
</body>
|
| 514 |
+
</html>
|
ui.py
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import html
|
| 4 |
+
from typing import List, Optional
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
_e = html.escape
|
| 8 |
+
|
| 9 |
+
APP_TITLE = "Amanah AI"
|
| 10 |
+
APP_TAGLINE = "نظام ذكي للتحقق من الاقتباسات القرآنية والحديثية"
|
| 11 |
+
ENGLISH_TITLE = "AI-Powered Verification and Correction of Quranic and Prophetic Quotations"
|
| 12 |
+
COPIED_MESSAGE = "تم نسخ النص بنجاح"
|
| 13 |
+
|
| 14 |
+
GROUP_LABEL = {"verified": "موثّق", "mismatch": "غير مطابق", "review": "تحتاج مراجعة بشرية"}
|
| 15 |
+
TYPE_LABEL = {"Ayah": "آية قرآنية", "Hadith": "حديث نبوي"}
|
| 16 |
+
INDICATORS = [
|
| 17 |
+
("composite", "الدرجة المركبة"),
|
| 18 |
+
("coverage", "تغطية الكلمات"),
|
| 19 |
+
("lcs_ratio", "التسلسل النصي"),
|
| 20 |
+
("token_overlap", "تداخل الكلمات"),
|
| 21 |
+
("edit_sim", "تشابه الحروف"),
|
| 22 |
+
("diacritic_sim", "التشكيل"),
|
| 23 |
+
]
|
| 24 |
+
METHODS = {
|
| 25 |
+
"substring_match": "الاقتباس واردٌ كاملًا في المصدر",
|
| 26 |
+
"threshold_pass": "تجاوز عتبة التطابق",
|
| 27 |
+
"threshold_fail": "دون عتبة التطابق",
|
| 28 |
+
"borderline_multi_cov": "حالة حدّية: عدة مصادر متقاربة",
|
| 29 |
+
"borderline_default": "حالة حدّية",
|
| 30 |
+
"borderline_low_retrieval": "حالة حدّية: استرجاع ضعيف",
|
| 31 |
+
"no_candidates": "لا توجد مصادر مرشحة",
|
| 32 |
+
"empty_span": "نص فارغ",
|
| 33 |
+
"error": "تعذّرت المعالجة",
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _pct(value: float) -> str:
|
| 38 |
+
return f"{round(value * 100)}%"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def source_label(source: dict) -> str:
|
| 42 |
+
if source["type"] == "Quran":
|
| 43 |
+
start, end = source["ayah_start"], source["ayah_end"]
|
| 44 |
+
verses = f"{start}" if start == end else f"{start}–{end}"
|
| 45 |
+
return f"سورة {source['surah_name']} — الآية {verses}"
|
| 46 |
+
return f"حديث رقم {source['hadithID']} — {source['title']}"
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def reason_text(reason: dict) -> str:
|
| 50 |
+
"""Arabic explanation of a decision code produced by the pipeline."""
|
| 51 |
+
code = reason["code"]
|
| 52 |
+
src = reason.get("source", "")
|
| 53 |
+
if code == "exact_match":
|
| 54 |
+
return f"تطابق تام مع {src} بعد تجاهل اختلافات الرسم والتشكيل."
|
| 55 |
+
if code == "close_match":
|
| 56 |
+
return f"تطابق شبه كامل مع {src}."
|
| 57 |
+
if code == "altered_passage":
|
| 58 |
+
n = reason.get("n", 0)
|
| 59 |
+
how = "تغيّر ترتيب الكلمات" if reason.get("reordered") else f"{n} موضع مختلف"
|
| 60 |
+
return f"النص يخالف {src} ({how}). التصحيح المقترح هو نص المصدر حرفيًا ولم يُولَّد."
|
| 61 |
+
if code == "weak_match":
|
| 62 |
+
return "وُجد مصدر قريب لكن التطابق غير كافٍ للحكم الآلي."
|
| 63 |
+
if code == "ungrounded":
|
| 64 |
+
return "تعذّر التثبّت من المرجع المقترح داخل المراجع المضمّنة، فأُسقط الاقتراح ولم يُعرض أي تصحيح؛ يلزم الرجوع إلى مختص."
|
| 65 |
+
if code == "too_short":
|
| 66 |
+
return "الاقتباس قصير جدًا (كلمتان فقط) فلا يمكن تحديد مصدره بيقين؛ يلزم الرجوع إلى مختص."
|
| 67 |
+
if code == "no_source":
|
| 68 |
+
return "لا يوجد في المراجع المضمّنة نصٌّ يشبه هذا الاقتباس، وقد يكون مختلَقًا."
|
| 69 |
+
if code == "insufficient_evidence":
|
| 70 |
+
return "الأدلة غير كافية: لا مصدر واضح، ودرجة اليقين منخفضة."
|
| 71 |
+
if code == "candidate_not_strong":
|
| 72 |
+
return f"وُجد مرشح ({src}، قوة المطابقة {round(reason.get('strength', 0) * 100)}%) لكن الدليل لا يكفي للتصحيح الآلي."
|
| 73 |
+
if code == "hadith_altered":
|
| 74 |
+
return (f"النص قريب جدًا من {src} لكن يختلف عنه في {reason.get('n', 1)} موضع (زيادة أو نقص أو استبدال كلمة)، وقد يغيّر ذلك المعنى؛ "
|
| 75 |
+
"لا يُوثَّق الحديث إلا إذا طابق النص حرفيًا، ولا يُصحَّح آليًا فتلزم مراجعة مختص.")
|
| 76 |
+
if code == "hadith_candidate":
|
| 77 |
+
return f"وُجدت رواية مشابهة ({src}) لكن لا يُصحَّح الحديث آليًا لاختلاف الروايات؛ يلزم الرجوع إلى مختص."
|
| 78 |
+
return "تعذّرت معالجة هذا الاقتباس آليًا."
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def note_text(note: dict) -> str:
|
| 82 |
+
n = note.get("n", 0)
|
| 83 |
+
code = note["code"]
|
| 84 |
+
if code == "diacritic_conflict":
|
| 85 |
+
return f"تنبيه: تشكيل {n} كلمة يخالف المصحف (الكلمات صحيحة لكن الحركات مختلفة)."
|
| 86 |
+
if code == "orthographic_variant":
|
| 87 |
+
return f"ملاحظة: {n} اختلاف إملائي (رسم أو مسافات) لا يغيّر الكلمة."
|
| 88 |
+
if note.get("misattributed"):
|
| 89 |
+
if code == "is_ayah":
|
| 90 |
+
return f"تنبيه: النص قرآني ({note['source']}) لكن عبارة التقديم تنس��ه إلى الحديث."
|
| 91 |
+
return f"تنبيه: النص حديث ({note['source']}) لكن عبارة التقديم تنسبه إلى القرآن."
|
| 92 |
+
if code == "is_hadith":
|
| 93 |
+
return f"تنبيه: هذا النص وارد في الحديث ({note['source']}) وليس في القرآن؛ ربما نُسب إلى الله تعالى خطأً."
|
| 94 |
+
if code == "is_ayah":
|
| 95 |
+
return f"تنبيه: هذا النص وارد في القرآن ({note['source']}) وليس في الحديث؛ ربما نُسب إلى النبي ﷺ خطأً."
|
| 96 |
+
if code == "hadith_minor_diffs":
|
| 97 |
+
return f"ملاحظة: {n} اختلاف طفيف عن أقرب رواية في المراجع."
|
| 98 |
+
return ""
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _diff_html(comparison: dict) -> str:
|
| 102 |
+
"""Red = words only in the quotation; green = the source words that should be there."""
|
| 103 |
+
parts = []
|
| 104 |
+
for op in comparison["word_diff"]:
|
| 105 |
+
if op["op"] == "equal":
|
| 106 |
+
parts.append(f'<span class="w-eq">{_e(op["span"])}</span>')
|
| 107 |
+
continue
|
| 108 |
+
if op["span"]:
|
| 109 |
+
parts.append(f'<span class="w-extra">{_e(op["span"])}</span>')
|
| 110 |
+
if op["source"]:
|
| 111 |
+
parts.append(f'<span class="w-missing">{_e(op["source"])}</span>')
|
| 112 |
+
return " ".join(parts)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def _legend(group: str, comparison: Optional[dict]) -> str:
|
| 116 |
+
if group == "verified" or not comparison or all(op["op"] == "equal" for op in comparison["word_diff"]):
|
| 117 |
+
return ""
|
| 118 |
+
return '<p class="legend"><span class="w-extra">كلمات في الاقتباس تخالف المصدر</span> <span class="w-missing">الصواب من المصدر</span></p>'
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def _indicator_table(signals: Optional[dict]) -> str:
|
| 122 |
+
if not signals:
|
| 123 |
+
return ""
|
| 124 |
+
rows = []
|
| 125 |
+
for key, label in INDICATORS:
|
| 126 |
+
value = float(signals.get(key, 0.0))
|
| 127 |
+
rows.append(
|
| 128 |
+
f'<div class="ind"><div class="ind-name">{label}</div>'
|
| 129 |
+
f'<div class="bar"><span style="width:{_pct(value)}"></span></div><div class="ind-val">{_pct(value)}</div></div>'
|
| 130 |
+
)
|
| 131 |
+
rows.append(f'<div class="ind"><div class="ind-name">وروده كاملًا في المصدر</div><div class="ind-val wide">{"نعم" if signals.get("is_substring") else "لا"}</div></div>')
|
| 132 |
+
return '<div class="indicators">' + "".join(rows) + "</div>"
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def _match_percent(span: dict) -> int:
|
| 136 |
+
evidence = span["evidence"]
|
| 137 |
+
if evidence and evidence.get("comparison"):
|
| 138 |
+
return round(evidence["comparison"]["word_similarity"] * 100)
|
| 139 |
+
if evidence and evidence.get("signals"):
|
| 140 |
+
return round(evidence["signals"]["composite"] * 100)
|
| 141 |
+
return 0
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def _evidence_panel(span: dict) -> str:
|
| 145 |
+
evidence, verification = span["evidence"], span["verification"]
|
| 146 |
+
rows = [
|
| 147 |
+
f'<div class="ev-row"><b>الاقتباس المكتشف</b><p class="quote">{_e(span["text"])}</p></div>',
|
| 148 |
+
f'<div class="ev-row"><b>النوع</b><p>{TYPE_LABEL[span["type"]]}</p></div>',
|
| 149 |
+
]
|
| 150 |
+
if evidence:
|
| 151 |
+
comparison = evidence["comparison"]
|
| 152 |
+
rows += [
|
| 153 |
+
f'<div class="ev-row"><b>المصدر المرشح</b><p>{_e(source_label(evidence["source"]))}</p></div>',
|
| 154 |
+
f'<div class="ev-row"><b>نص المصدر الأصلي</b><p class="quote">{_e(comparison["source_excerpt"])}</p></div>',
|
| 155 |
+
f'<div class="ev-row"><b>المقارنة كلمةً بكلمة</b><p class="diff">{_diff_html(comparison)}</p>{_legend(span["group"], comparison)}</div>',
|
| 156 |
+
f'<div class="ev-row"><b>مؤشرات التحقق</b>{_indicator_table(evidence.get("signals"))}</div>',
|
| 157 |
+
]
|
| 158 |
+
if comparison["diacritic_notes"]:
|
| 159 |
+
items = "، ".join(f"{_e(n['word'])} ← {_e(n['source_word'])}" for n in comparison["diacritic_notes"])
|
| 160 |
+
rows.append(f'<div class="ev-row"><b>اختلافات التشكيل</b><p>{items}</p></div>')
|
| 161 |
+
else:
|
| 162 |
+
rows.append('<div class="ev-row"><b>المصدر المرشح</b><p>لم يُسترجع أي مصدر.</p></div>')
|
| 163 |
+
rows += [
|
| 164 |
+
f'<div class="ev-row"><b>درجة اليقين في الحكم</b><p>{_pct(verification["confidence"])} · {METHODS.get(verification["method"], "")}</p></div>',
|
| 165 |
+
f'<div class="ev-row"><b>القرار</b><p>{_e(span["status_ar"])}</p></div>',
|
| 166 |
+
f'<div class="ev-row"><b>سبب القرار</b><p>{_e(reason_text(span["reason"]))}</p></div>',
|
| 167 |
+
]
|
| 168 |
+
return "".join(rows)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def _card(span: dict) -> str:
|
| 172 |
+
group, evidence = span["group"], span["evidence"]
|
| 173 |
+
label = GROUP_LABEL[group]
|
| 174 |
+
chip = f'<span class="conf">نسبة المطابقة <b>{_match_percent(span)}%</b></span>'
|
| 175 |
+
scan_badge = ('<span class="badge scan" title="اكتُشف بمطابقة النص مع المراجع دون علامات تنصيص">اقتباس غير معلَن</span>'
|
| 176 |
+
if span["detection"]["backend"] == "scan" else "")
|
| 177 |
+
|
| 178 |
+
fields = [f'<div class="field"><label>الاقتباس المكتشف</label><p class="quote">{_e(span["text"])}</p></div>']
|
| 179 |
+
if evidence:
|
| 180 |
+
comparison = evidence["comparison"]
|
| 181 |
+
fields.append(f'<div class="field"><label>المصدر المرشح</label><p>{_e(source_label(evidence["source"]))}</p></div>')
|
| 182 |
+
if group != "verified":
|
| 183 |
+
fields.append(
|
| 184 |
+
f'<div class="field"><label>المقارنة بالمصدر</label><p class="diff">{_diff_html(comparison)}</p>{_legend(group, comparison)}</div>'
|
| 185 |
+
)
|
| 186 |
+
else:
|
| 187 |
+
fields.append('<div class="field"><label>المصدر المرشح</label><p>لا يوجد</p></div>')
|
| 188 |
+
fields.append(f'<p class="reason">{_e(reason_text(span["reason"]))}</p>')
|
| 189 |
+
|
| 190 |
+
notes = "".join(f'<p class="note">{_e(note_text(n))}</p>' for n in span["notes"] if note_text(n))
|
| 191 |
+
action = ""
|
| 192 |
+
correction, suggestion = span["correction"], span["suggestion"]
|
| 193 |
+
if correction:
|
| 194 |
+
action = (
|
| 195 |
+
'<div class="action ok"><b>التصحيح المقترح من المصدر</b>'
|
| 196 |
+
f'<p class="quote">{_e(correction["display_text"])}</p>'
|
| 197 |
+
f'<p class="legend">{_e(source_label(correction["source"]))}</p></div>'
|
| 198 |
+
)
|
| 199 |
+
elif group == "review":
|
| 200 |
+
closest = ""
|
| 201 |
+
if suggestion:
|
| 202 |
+
closest = (f'<p class="legend">أقرب مصدر وُجد للمراجِع، وليس تصحيحًا آليًا: {_e(source_label(suggestion["source"]))}</p>'
|
| 203 |
+
f'<p class="quote small">{_e(suggestion["display_text"][:420])}</p>')
|
| 204 |
+
action = ('<div class="action warn"><b>الأدلة غير كافية للتصحيح الآلي.</b> يُوصى بالمراجعة البشرية.' + closest + "</div>")
|
| 205 |
+
elif span["status"] == "UNSUPPORTED":
|
| 206 |
+
action = '<div class="action bad"><b>لا يوجد مصدر مطابق في المراجع المتاحة.</b> لم يُقترح أي نص بديل.</div>'
|
| 207 |
+
|
| 208 |
+
return f"""
|
| 209 |
+
<div class="qcard {group}">
|
| 210 |
+
<div class="qhead">
|
| 211 |
+
<span class="idx">{span["id"]}</span>
|
| 212 |
+
<span class="badge type">{TYPE_LABEL[span["type"]]}</span>{scan_badge}
|
| 213 |
+
<span class="badge st {group}">{label}</span>
|
| 214 |
+
{chip}
|
| 215 |
+
</div>
|
| 216 |
+
{"".join(fields)}
|
| 217 |
+
{notes}
|
| 218 |
+
{action}
|
| 219 |
+
<details class="evidence"><summary>عرض الدليل</summary><div class="ev-body">{_evidence_panel(span)}</div></details>
|
| 220 |
+
</div>"""
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def _summary(summary: dict) -> str:
|
| 224 |
+
tiles = [
|
| 225 |
+
(summary["n_spans"], "إجمالي الاقتباسات", ""),
|
| 226 |
+
(summary["n_ayah"], "آيات قرآنية", ""),
|
| 227 |
+
(summary["n_hadith"], "أحاديث نبوية", ""),
|
| 228 |
+
(summary["VERIFIED"], "موثّق", "verified"),
|
| 229 |
+
(summary["CORRECTED"] + summary["UNSUPPORTED"], "غير مطابق", "mismatch"),
|
| 230 |
+
(summary["HUMAN_REVIEW"], "تحتاج مراجعة", "review"),
|
| 231 |
+
]
|
| 232 |
+
return '<div class="summary">' + "".join(
|
| 233 |
+
f'<div class="tile {cls}{" zero" if value == 0 and cls else ""}"><b>{value}</b><span>{label}</span></div>'
|
| 234 |
+
for value, label, cls in tiles
|
| 235 |
+
) + "</div>"
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def _highlighted_text(result: dict, title: str) -> str:
|
| 239 |
+
text, pieces, cursor = result["input_text"], [], 0
|
| 240 |
+
for span in result["spans"]:
|
| 241 |
+
pieces.append(_e(text[cursor:span["start"]]))
|
| 242 |
+
pieces.append(f'<mark class="{span["group"]}">{_e(text[span["start"]:span["end"]])}</mark>')
|
| 243 |
+
cursor = span["end"]
|
| 244 |
+
pieces.append(_e(text[cursor:]))
|
| 245 |
+
legend = ('<div class="hl-legend"><mark class="verified">موثّق</mark><mark class="mismatch">غير مطابق</mark>'
|
| 246 |
+
'<mark class="review">مراجعة بشرية</mark></div>')
|
| 247 |
+
return f'<div class="highlight"><label>{title}</label><p>' + "".join(pieces) + "</p>" + legend + "</div>"
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def final_text(result: dict) -> str:
|
| 251 |
+
"""Corrected text with an inline Arabic flag after every quotation that still needs attention."""
|
| 252 |
+
text, reports = result["input_text"], result["spans"]
|
| 253 |
+
for report in sorted(reports, key=lambda r: r["start"], reverse=True):
|
| 254 |
+
if report["status"] == "CORRECTED" and report["correction"]:
|
| 255 |
+
text = text[: report["start"]] + report["correction"]["display_text"] + text[report["end"]:]
|
| 256 |
+
elif report["status"] == "UNSUPPORTED":
|
| 257 |
+
text = text[: report["end"]] + " [⚠ لا يوجد مصدر مطابق]" + text[report["end"]:]
|
| 258 |
+
elif report["status"] == "HUMAN_REVIEW":
|
| 259 |
+
text = text[: report["end"]] + " [⚠ يحتاج مراجعة بشرية]" + text[report["end"]:]
|
| 260 |
+
return text
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def _final_block(result: dict) -> str:
|
| 264 |
+
summary = result["summary"]
|
| 265 |
+
fixed = summary["CORRECTED"]
|
| 266 |
+
open_items = summary["UNSUPPORTED"] + summary["HUMAN_REVIEW"]
|
| 267 |
+
if fixed == 0 and open_items == 0:
|
| 268 |
+
headline = "كل الاقتباسات المكتشفة مطابقة للمصادر."
|
| 269 |
+
else:
|
| 270 |
+
headline = f"صُحِّح {fixed} اقتباس من نص المصدر، وبقي {open_items} اقتباس يحتاج مراجعة بشرية ومعلَّم بعلامة تنبيه."
|
| 271 |
+
text = final_text(result)
|
| 272 |
+
return (
|
| 273 |
+
'<div class="final"><div class="final-head"><b>النسخة المصحّحة</b>'
|
| 274 |
+
f'<button class="copy" type="button" data-text="{_e(text, quote=True)}" '
|
| 275 |
+
'onclick="window.icvCopy&&window.icvCopy(this)">نسخ النص</button></div>'
|
| 276 |
+
f'<p class="legend">{headline}</p><p class="final-text">{_e(text)}</p></div>'
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def render_results(result: dict, generated_answer: Optional[str] = None) -> str:
|
| 281 |
+
"""HTML report for a pipeline result. Pass the model's answer (mode B) to show it first and label the text as generated."""
|
| 282 |
+
header = render_generated_header(generated_answer) if generated_answer is not None else ""
|
| 283 |
+
if not result["spans"]:
|
| 284 |
+
body = (
|
| 285 |
+
'<div class="notice">لم يُعثر على اقتباسات قرآنية أو حديثية في هذا النص. يتعرّف النظام على الاقتباسات بمطابقتها مع المراجع، '
|
| 286 |
+
'سواء وُضعت بين علامات تنصيص أو أقواس أو وردت داخل الكلام دون أي عبارة تمهيدية.</div>'
|
| 287 |
+
)
|
| 288 |
+
return f'<div class="icv">{header}{body}</div>'
|
| 289 |
+
generated = generated_answer is not None
|
| 290 |
+
title = "إجابة النموذج مع الاقتباسات المكتشفة" if generated else "النص مع الاقتباسات المكتشفة"
|
| 291 |
+
parts = [_summary(result["summary"]), _highlighted_text(result, title), "".join(_card(s) for s in result["spans"])]
|
| 292 |
+
summary = result["summary"]
|
| 293 |
+
if generated or summary["CORRECTED"] or summary["UNSUPPORTED"] or summary["HUMAN_REVIEW"]:
|
| 294 |
+
parts.append(_final_block(result))
|
| 295 |
+
return f'<div class="icv">{header}{"".join(parts)}</div>'
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def render_generated_header(answer: str) -> str:
|
| 299 |
+
return f'<div class="generated"><label>إجابة النموذج اللغوي كما وصلت</label><p>{_e(answer)}</p></div>'
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def render_message(message: str, kind: str = "info") -> str:
|
| 303 |
+
return f'<div class="icv"><div class="notice {kind}">{_e(message)}</div></div>'
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
STAR = (
|
| 307 |
+
'<svg class="mark" viewBox="0 0 64 64" fill="none" stroke="#E2C06E" stroke-width="1.8" aria-hidden="true">'
|
| 308 |
+
'<rect x="14" y="14" width="36" height="36"/><rect x="14" y="14" width="36" height="36" transform="rotate(45 32 32)"/>'
|
| 309 |
+
'<circle cx="32" cy="32" r="7" fill="#E2C06E" stroke="none"/></svg>'
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
HERO = f"""
|
| 313 |
+
<div class="icv"><header class="hero">
|
| 314 |
+
{STAR}
|
| 315 |
+
<h1 class="brand">{APP_TITLE}</h1>
|
| 316 |
+
<p class="tagline">{APP_TAGLINE}</p>
|
| 317 |
+
<p class="sub">يكتشف الاقتباسات القرآنية والحديثية داخل أي نص، ويطابقها كلمةً بكلمة مع نصوص المراجع، ويصحّح الخطأ من المصدر نفسه.
|
| 318 |
+
وحين لا يجد دليلًا كافيًا لا يخمّن أبدًا، بل يعلن أن الاقتباس غير مطابق أو يحيله إلى المراجعة البشرية.</p>
|
| 319 |
+
<ul class="trust"><li><b>من المصدر</b> كل مرجع وتصحيح من نص المراجع حرفيًا</li><li><b>٣ قرارات</b> موثّق · غير مطابق · مراجعة بشرية</li><li><b>داخل المتصفح</b> التحقق لا يرسل نصك إلى أي خادم</li></ul>
|
| 320 |
+
</header></div>
|
| 321 |
+
"""
|
| 322 |
+
|
| 323 |
+
DISCLAIMER = """<div class="icv"><div class="disclaimer">أداة مساعدة للتدقيق النصي وليست فتوى ولا بديلًا عن المراجعة المتخصصة.
|
| 324 |
+
النتائج مبنية على مراجع القرآن الكريم والكتب الستة المضمّنة فقط.
|
| 325 |
+
<br>نص القرآن: <a href="https://tanzil.net" target="_blank" rel="noopener">مشروع <bdi>Tanzil</bdi></a> <bdi>(CC BY 3.0)</bdi> · بيانات <bdi>IslamicEval 2025</bdi></div></div>"""
|
| 326 |
+
|
| 327 |
+
PLACEHOLDER = "الصق هنا النص الذي ولّده نموذج لغوي. يكتشف النظام الآيات والأحاديث الواردة فيه، بعلامات تنصيص أو بدونها…"
|
| 328 |
+
PROMPT_PLACEHOLDER = "اكتب سؤالك، مثل: اشرح لي فضل الصبر في القرآن والسنة مع ذكر الأدلة."
|
| 329 |
+
|
| 330 |
+
_PATTERN = (
|
| 331 |
+
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='88' height='88' viewBox='0 0 88 88'%3E"
|
| 332 |
+
"%3Cg fill='none' stroke='%230F4C3A' stroke-opacity='0.07' stroke-width='1'%3E"
|
| 333 |
+
"%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"
|
| 334 |
+
"%3C/g%3E%3C/svg%3E\")"
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
CSS = """
|
| 338 |
+
@import url('https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700&family=Amiri:wght@400;700&display=swap');
|
| 339 |
+
:root { --green:#0F4C3A; --green-2:#17694F; --gold:#B8912F; --gold-2:#E2C06E; --cream:#FBF6EA; --paper:#FFFFFF;
|
| 340 |
+
--ink:#1F2933; --muted:#55626D; --line:#E6DCC3; --ok:#1B7A4B; --ok-bg:#EAF6EF; --ok-line:#BFE3CD;
|
| 341 |
+
--bad:#B3261E; --bad-bg:#FDECEA; --bad-line:#F4B8B3; --warn:#7A5B0C; --warn-bg:#FFF6DA; --warn-line:#EBD28A; }
|
| 342 |
+
.gradio-container, body.icv-page { background:var(--cream) PATTERN !important; font-family:'Cairo','Segoe UI',Tahoma,sans-serif; color:var(--ink); }
|
| 343 |
+
.gradio-container { max-width:1060px !important; --body-text-color:#1F2933; --block-background-fill:#FFFFFF; --block-border-color:#E6DCC3;
|
| 344 |
+
--input-background-fill:#FFFFFF; --block-label-text-color:#55626D; --button-primary-background-fill:#0F4C3A;
|
| 345 |
+
--button-primary-background-fill-hover:#17694F; --button-primary-text-color:#FFFFFF; --button-secondary-background-fill:#FFFFFF;
|
| 346 |
+
--button-secondary-text-color:#0F4C3A; --button-secondary-border-color:#B8912F; --color-accent:#B8912F; }
|
| 347 |
+
.icv { direction:rtl; text-align:right; color:var(--ink); line-height:1.9; }
|
| 348 |
+
.icv .hero { background:linear-gradient(135deg,#0F4C3A,#17694F); border-radius:20px; padding:30px 22px 26px; margin:10px 0 18px; text-align:center;
|
| 349 |
+
box-shadow:0 6px 22px rgba(15,76,58,.18); border-bottom:4px solid var(--gold); }
|
| 350 |
+
.icv .hero .mark { width:54px; height:54px; display:block; margin:0 auto 4px; }
|
| 351 |
+
.icv .hero h1 { font-size:2.3rem; margin:.1rem 0; color:#FFFFFF; font-weight:700; }
|
| 352 |
+
.icv .hero .tagline { color:var(--gold-2); font-size:1.2rem; font-weight:600; margin:.1rem 0 .6rem; }
|
| 353 |
+
.icv .hero .sub { max-width:720px; margin:0 auto; color:#E9F2EE; font-size:1rem; }
|
| 354 |
+
.icv .flow { display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:8px; margin-top:16px; }
|
| 355 |
+
.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; }
|
| 356 |
+
.icv .flow i { color:var(--gold-2); font-style:normal; font-size:1.2rem; }
|
| 357 |
+
.icv .disclaimer { font-size:.85rem; color:var(--muted); text-align:center; padding:14px 8px; }
|
| 358 |
+
.icv .en-free { direction:rtl; }
|
| 359 |
+
.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; }
|
| 360 |
+
.llm-row label, .gradio-container label span { font-family:'Cairo',sans-serif; }
|
| 361 |
+
.results { direction:rtl; }
|
| 362 |
+
.icv .summary { display:grid; grid-template-columns:repeat(auto-fit,minmax(130px,1fr)); gap:10px; margin:8px 0 14px; }
|
| 363 |
+
.icv .tile { background:var(--paper); border:1px solid var(--line); border-radius:14px; padding:12px 8px; text-align:center; }
|
| 364 |
+
.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); }
|
| 365 |
+
.icv .tile.verified { background:var(--ok-bg); border-color:var(--ok-line); } .icv .tile.verified b { color:var(--ok); }
|
| 366 |
+
.icv .tile.mismatch { background:var(--bad-bg); border-color:var(--bad-line); } .icv .tile.mismatch b { color:var(--bad); }
|
| 367 |
+
.icv .tile.review { background:var(--warn-bg); border-color:var(--warn-line); } .icv .tile.review b { color:var(--warn); }
|
| 368 |
+
.icv .tile.zero { background:var(--paper); border-color:var(--line); opacity:.6; } .icv .tile.zero b { color:var(--muted); }
|
| 369 |
+
.icv .highlight, .icv .generated, .icv .final { background:var(--paper); border:1px solid var(--line); border-radius:14px; padding:14px 18px; margin-bottom:14px; }
|
| 370 |
+
.icv label { display:block; color:var(--muted); font-size:.82rem; font-weight:600; margin-bottom:4px; }
|
| 371 |
+
.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; }
|
| 372 |
+
.icv mark { color:var(--ink); border-radius:6px; padding:1px 5px; }
|
| 373 |
+
.icv mark.verified { background:#D5EEDF; } .icv mark.mismatch { background:#F8CFCB; } .icv mark.review { background:#F7E3A6; }
|
| 374 |
+
.icv .hl-legend { display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; font-size:.78rem; }
|
| 375 |
+
.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; }
|
| 376 |
+
.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); }
|
| 377 |
+
.icv .qhead { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-bottom:8px; }
|
| 378 |
+
.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; }
|
| 379 |
+
.icv .badge { padding:2px 14px; border-radius:999px; font-size:.84rem; background:#F3EEDD; border:1px solid var(--line); color:var(--ink); }
|
| 380 |
+
.icv .badge.st.verified { background:var(--ok-bg); border-color:var(--ok-line); color:var(--ok); font-weight:600; }
|
| 381 |
+
.icv .badge.st.mismatch { background:var(--bad-bg); border-color:var(--bad-line); color:var(--bad); font-weight:600; }
|
| 382 |
+
.icv .badge.st.review { background:var(--warn-bg); border-color:var(--warn-line); color:var(--warn); font-weight:600; }
|
| 383 |
+
.icv .badge.scan { background:#EEF3FB; border-color:#C9D8EE; color:#2F4F7F; }
|
| 384 |
+
.icv .conf { margin-inline-start:auto; color:var(--muted); font-size:.88rem; } .icv .conf b { color:var(--green); }
|
| 385 |
+
.icv .field { margin:8px 0; } .icv .field p { margin:0; }
|
| 386 |
+
.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; }
|
| 387 |
+
.icv .diff { font-family:'Amiri','Cairo',serif; font-size:1.2rem; line-height:2.2; }
|
| 388 |
+
.icv .w-extra { background:var(--bad-bg); color:var(--bad); border-radius:4px; padding:0 4px; text-decoration:line-through; }
|
| 389 |
+
.icv .w-missing { background:var(--ok-bg); color:var(--ok); border-radius:4px; padding:0 4px; font-weight:700; }
|
| 390 |
+
.icv .legend { color:var(--muted); font-size:.82rem; margin:4px 0 0; } .icv .legend span { text-decoration:none; font-size:.8rem; }
|
| 391 |
+
.icv .reason { color:var(--muted); font-size:.92rem; margin:6px 0 0; }
|
| 392 |
+
.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; }
|
| 393 |
+
.icv .action { border-radius:12px; padding:10px 14px; margin-top:10px; }
|
| 394 |
+
.icv .action.ok { background:var(--ok-bg); border:1px solid var(--ok-line); } .icv .action.ok b { color:var(--ok); }
|
| 395 |
+
.icv .action.warn { background:var(--warn-bg); border:1px solid var(--warn-line); } .icv .action.warn b { color:var(--warn); }
|
| 396 |
+
.icv .action.bad { background:var(--bad-bg); border:1px solid var(--bad-line); } .icv .action.bad b { color:var(--bad); }
|
| 397 |
+
.icv .evidence { margin-top:12px; border-top:1px dashed var(--line); padding-top:8px; }
|
| 398 |
+
.icv .evidence summary { cursor:pointer; color:var(--green); font-weight:700; }
|
| 399 |
+
.icv .ev-row { margin:10px 0; } .icv .ev-row b { color:var(--gold); font-size:.88rem; } .icv .ev-row p { margin:2px 0; }
|
| 400 |
+
.icv .indicators { display:grid; gap:6px; margin-top:6px; }
|
| 401 |
+
.icv .ind { display:grid; grid-template-columns:150px 1fr 48px; gap:10px; align-items:center; font-size:.88rem; }
|
| 402 |
+
.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)); }
|
| 403 |
+
.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; }
|
| 404 |
+
.icv .final { border-color:var(--gold); background:#FFFDF6; }
|
| 405 |
+
.icv .final-head { display:flex; justify-content:space-between; align-items:center; } .icv .final-head b { color:var(--green); font-size:1.05rem; }
|
| 406 |
+
.icv .copy { background:var(--green); color:#FFF; border:0; border-radius:10px; padding:5px 16px; font-family:inherit; cursor:pointer; }
|
| 407 |
+
.icv .notice { background:var(--paper); border:1px solid var(--line); border-radius:14px; padding:16px; }
|
| 408 |
+
.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); }
|
| 409 |
+
@media (max-width:640px){ .icv .hero h1{font-size:1.7rem;} .icv .ind{grid-template-columns:104px 1fr 40px;} }
|
| 410 |
+
""".replace("PATTERN", _PATTERN)
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
GLASS_CSS = """
|
| 415 |
+
:root { --glass:rgba(255,255,255,.62); --glass-strong:rgba(255,255,255,.82); --glass-line:rgba(255,255,255,.75);
|
| 416 |
+
--shadow-1:0 1px 2px rgba(15,76,58,.06), 0 8px 24px rgba(15,76,58,.08); --shadow-2:0 2px 4px rgba(15,76,58,.08), 0 18px 44px rgba(15,76,58,.16); }
|
| 417 |
+
.gradio-container, body.icv-page { background:
|
| 418 |
+
radial-gradient(900px 520px at 88% -8%, rgba(226,192,110,.34), transparent 60%),
|
| 419 |
+
radial-gradient(760px 520px at 6% 4%, rgba(23,105,79,.20), transparent 62%),
|
| 420 |
+
linear-gradient(180deg,#FBF6EA 0%,#F3EBD3 100%) !important; background-attachment:fixed !important; }
|
| 421 |
+
/* RTL safety: long words, URLs and mixed Latin/digit runs wrap inside their box instead of spilling out */
|
| 422 |
+
.icv, .icv * { box-sizing:border-box; min-width:0; }
|
| 423 |
+
.icv p, .icv span, .icv b, .icv label, .icv summary, .icv button, .icv .badge, .icv .tile { overflow-wrap:anywhere; }
|
| 424 |
+
.icv .quote, .icv .diff, .icv .final-text, .icv .highlight p, .icv .generated p { unicode-bidi:plaintext; text-align:start; }
|
| 425 |
+
.icv .hero { position:relative; overflow:hidden; border-bottom:0; border:1px solid rgba(226,192,110,.45);
|
| 426 |
+
background:linear-gradient(120deg,#0B3B2D,#17694F 45%,#0F4C3A 70%,#1d7a5c); background-size:240% 240%; animation:icv-flow 16s ease-in-out infinite;
|
| 427 |
+
box-shadow:var(--shadow-2); }
|
| 428 |
+
.icv .hero::before { content:""; position:absolute; inset:-40% -10% auto auto; width:60%; aspect-ratio:1; border-radius:50%;
|
| 429 |
+
background:radial-gradient(circle, rgba(226,192,110,.38), transparent 65%); pointer-events:none; }
|
| 430 |
+
.icv .hero::after { content:""; position:absolute; inset:auto auto 0 0; width:100%; height:4px; background:linear-gradient(90deg,transparent,var(--gold-2),transparent); }
|
| 431 |
+
.icv .hero h1 { font-size:clamp(1.45rem,4.2vw,2.3rem); line-height:1.5; text-wrap:balance; position:relative; }
|
| 432 |
+
.icv .hero .tagline, .icv .hero .sub, .icv .flow { position:relative; }
|
| 433 |
+
.icv .flow span { backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px); }
|
| 434 |
+
@keyframes icv-flow { 0%,100%{background-position:0% 50%} 50%{background-position:100% 50%} }
|
| 435 |
+
.icv .tile, .icv .highlight, .icv .generated, .icv .final, .icv .qcard, .icv .notice, .icv .export {
|
| 436 |
+
background:var(--glass); border:1px solid var(--glass-line); box-shadow:var(--shadow-1);
|
| 437 |
+
backdrop-filter:blur(14px) saturate(150%); -webkit-backdrop-filter:blur(14px) saturate(150%); transition:transform .25s ease, box-shadow .25s ease; }
|
| 438 |
+
.icv .qcard { border-inline-start:6px solid var(--line); }
|
| 439 |
+
.icv .qcard:hover, .icv .tile:hover { transform:translateY(-2px); box-shadow:var(--shadow-2); }
|
| 440 |
+
.icv .tile.verified { background:linear-gradient(160deg,rgba(234,246,239,.92),rgba(255,255,255,.6)); }
|
| 441 |
+
.icv .tile.mismatch { background:linear-gradient(160deg,rgba(253,236,234,.92),rgba(255,255,255,.6)); }
|
| 442 |
+
.icv .tile.review { background:linear-gradient(160deg,rgba(255,246,218,.95),rgba(255,255,255,.6)); }
|
| 443 |
+
.icv .final { background:linear-gradient(160deg,rgba(255,253,246,.92),rgba(250,240,208,.55)); border-color:rgba(184,145,47,.55); }
|
| 444 |
+
.icv .final-head { flex-wrap:wrap; gap:10px; }
|
| 445 |
+
.icv .qhead .badge { max-width:100%; white-space:normal; }
|
| 446 |
+
.icv .ind { grid-template-columns:minmax(96px,150px) minmax(0,1fr) 48px; }
|
| 447 |
+
.icv .bar span { background:linear-gradient(270deg,var(--green),var(--gold-2)); transition:width .6s ease; }
|
| 448 |
+
.icv .copy, .icv .dl { background:linear-gradient(135deg,var(--green),var(--green-2)); color:#FFF; border:0; border-radius:12px; padding:7px 18px;
|
| 449 |
+
font-family:inherit; font-weight:600; cursor:pointer; box-shadow:0 4px 12px rgba(15,76,58,.25); transition:transform .15s ease, box-shadow .15s ease, filter .15s ease; }
|
| 450 |
+
.icv .copy:hover, .icv .dl:hover { transform:translateY(-1px); filter:brightness(1.08); box-shadow:0 8px 18px rgba(15,76,58,.3); }
|
| 451 |
+
.icv .copy:active, .icv .dl:active { transform:translateY(0); }
|
| 452 |
+
.icv .copy:focus-visible, .icv .dl:focus-visible { outline:3px solid rgba(184,145,47,.55); outline-offset:2px; }
|
| 453 |
+
.icv .copy.done { background:linear-gradient(135deg,#1B7A4B,#2a9d66); }
|
| 454 |
+
.icv .export { border-radius:16px; padding:12px 18px; margin:12px 0; }
|
| 455 |
+
.icv .export summary { cursor:pointer; color:var(--green); font-weight:700; }
|
| 456 |
+
.icv .dls { display:flex; flex-wrap:wrap; gap:10px; margin-top:10px; }
|
| 457 |
+
/* toast */
|
| 458 |
+
#icv-toast { position:fixed; inset-inline:0; bottom:28px; margin-inline:auto; width:max-content; max-width:calc(100vw - 32px); z-index:99999;
|
| 459 |
+
direction:rtl; text-align:center; font:600 1rem 'Cairo','Segoe UI',Tahoma,sans-serif; color:#FFF; padding:12px 24px; border-radius:999px;
|
| 460 |
+
background:linear-gradient(135deg,rgba(15,76,58,.94),rgba(23,105,79,.94)); border:1px solid rgba(226,192,110,.6);
|
| 461 |
+
box-shadow:0 14px 40px rgba(15,76,58,.35); backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px);
|
| 462 |
+
opacity:0; transform:translateY(18px) scale(.97); pointer-events:none; transition:opacity .28s ease, transform .28s cubic-bezier(.2,.9,.3,1.2); }
|
| 463 |
+
#icv-toast.show { opacity:1; transform:none; } #icv-toast.bad { background:linear-gradient(135deg,rgba(179,38,30,.95),rgba(214,69,58,.95)); }
|
| 464 |
+
/* skeleton loaders */
|
| 465 |
+
.skel { position:relative; overflow:hidden; background:rgba(15,76,58,.08); border-radius:10px; }
|
| 466 |
+
.skel::after { content:""; position:absolute; inset:0; transform:translateX(100%); animation:icv-shimmer 1.4s infinite;
|
| 467 |
+
background:linear-gradient(90deg,transparent,rgba(255,255,255,.75),transparent); }
|
| 468 |
+
@keyframes icv-shimmer { 100% { transform:translateX(-100%); } }
|
| 469 |
+
.skel-card { background:var(--glass); border:1px solid var(--glass-line); border-radius:16px; padding:16px 18px; margin:12px 0; box-shadow:var(--shadow-1); }
|
| 470 |
+
.skel-line { height:14px; margin:10px 0; } .skel-line.w60 { width:60%; } .skel-line.w85 { width:85%; } .skel-line.w40 { width:40%; }
|
| 471 |
+
.skel-tiles { display:grid; grid-template-columns:repeat(auto-fit,minmax(120px,1fr)); gap:10px; margin:8px 0 14px; } .skel-tile { height:78px; border-radius:14px; }
|
| 472 |
+
.icv-spinner { width:18px; height:18px; border-radius:50%; border:3px solid rgba(15,76,58,.18); border-top-color:var(--gold); display:inline-block;
|
| 473 |
+
vertical-align:middle; margin-inline-end:10px; animation:icv-spin .8s linear infinite; }
|
| 474 |
+
@keyframes icv-spin { to { transform:rotate(360deg); } }
|
| 475 |
+
@media (prefers-reduced-motion: reduce) { .icv .hero, .skel::after, .icv-spinner { animation:none; } .icv .qcard, .icv .tile, #icv-toast { transition:none; } }
|
| 476 |
+
@media (max-width:640px){ .icv .ind{grid-template-columns:minmax(84px,104px) minmax(0,1fr) 40px;} .icv .final-head{flex-direction:column; align-items:stretch;} .icv .copy{width:100%;} }
|
| 477 |
+
/* layout polish: centred, compact, professional */
|
| 478 |
+
.icv .notice, .icv .disclaimer, .icv .export { text-align:center; }
|
| 479 |
+
.icv .export .legend { max-width:640px; margin:8px auto 0; font-size:.9rem; line-height:1.9; color:var(--muted); }
|
| 480 |
+
.icv .dls { justify-content:center; }
|
| 481 |
+
.icv .dl { display:inline-flex; flex-direction:column; align-items:center; gap:2px; min-width:170px; }
|
| 482 |
+
.icv .dl .t { font-weight:700; } .icv .dl .s { font-size:.78rem; font-weight:500; opacity:.85; }
|
| 483 |
+
.icv .hero .flow { display:flex; justify-content:center; flex-wrap:wrap; gap:8px; }
|
| 484 |
+
|
| 485 |
+
/* ---- Amanah AI visual identity ---- */
|
| 486 |
+
.icv .hero { padding:38px 24px 30px; border-radius:26px; background:
|
| 487 |
+
radial-gradient(900px 300px at 85% -10%, rgba(226,192,110,.28), transparent 60%),
|
| 488 |
+
radial-gradient(700px 320px at 0% 110%, rgba(23,105,79,.9), transparent 70%),
|
| 489 |
+
linear-gradient(135deg,#0A3B2C 0%,#0F4C3A 45%,#17694F 100%); box-shadow:0 18px 50px rgba(10,59,44,.28); }
|
| 490 |
+
.icv .hero .mark { width:60px; height:60px; margin-bottom:6px; filter:drop-shadow(0 4px 10px rgba(226,192,110,.45)); }
|
| 491 |
+
.icv .hero h1.brand { font-family:'Cairo','Segoe UI',sans-serif; font-weight:800; font-size:clamp(2.4rem,7vw,3.9rem); letter-spacing:.02em; direction:ltr;
|
| 492 |
+
line-height:1.15; margin:.1rem 0 .2rem; background:linear-gradient(180deg,#FFFFFF 30%,#F3DFA6 100%); -webkit-background-clip:text; background-clip:text; color:transparent; }
|
| 493 |
+
.icv .hero .tagline { font-size:clamp(1.05rem,2.6vw,1.45rem); font-weight:700; color:var(--gold-2); margin:.2rem 0 .8rem; }
|
| 494 |
+
.icv .hero .sub { max-width:760px; font-size:1.02rem; line-height:2; color:#E4EFEA; }
|
| 495 |
+
.icv .trust { list-style:none; display:flex; flex-wrap:wrap; justify-content:center; gap:10px; margin:20px 0 0; padding:0; position:relative; }
|
| 496 |
+
.icv .trust li { background:rgba(255,255,255,.1); border:1px solid rgba(226,192,110,.5); border-radius:14px; padding:8px 16px; color:#FFF; font-size:.92rem; backdrop-filter:blur(6px); }
|
| 497 |
+
.icv .trust li b { color:var(--gold-2); font-size:1.05rem; margin-inline-end:6px; }
|
| 498 |
+
.icv .tile { box-shadow:0 6px 18px rgba(15,76,58,.07); transition:transform .2s; } .icv .tile:hover { transform:translateY(-2px); }
|
| 499 |
+
.icv .qcard { box-shadow:0 8px 26px rgba(15,76,58,.09); }
|
| 500 |
+
"""
|
| 501 |
+
CSS += GLASS_CSS
|
| 502 |
+
|
| 503 |
+
SKELETON = (
|
| 504 |
+
'<div class="icv" aria-busy="true" aria-label="جارٍ التحقق">'
|
| 505 |
+
'<div class="skel-tiles">' + '<div class="skel skel-tile"></div>' * 4 + '</div>'
|
| 506 |
+
+ ('<div class="skel-card"><div class="skel skel-line w40"></div><div class="skel skel-line w85"></div>'
|
| 507 |
+
'<div class="skel skel-line"></div><div class="skel skel-line w60"></div></div>') * 2
|
| 508 |
+
+ '</div>'
|
| 509 |
+
)
|
| 510 |
+
|
| 511 |
+
COPY_JS = r"""
|
| 512 |
+
(function () {
|
| 513 |
+
if (window.icvCopy) return;
|
| 514 |
+
function toast(message, bad) {
|
| 515 |
+
var t = document.getElementById('icv-toast');
|
| 516 |
+
if (!t) { t = document.createElement('div'); t.id = 'icv-toast'; t.setAttribute('role', 'status'); t.setAttribute('aria-live', 'polite'); document.body.appendChild(t); }
|
| 517 |
+
t.textContent = message; t.className = 'show' + (bad ? ' bad' : '');
|
| 518 |
+
clearTimeout(window.__icvToast); window.__icvToast = setTimeout(function () { t.className = bad ? 'bad' : ''; }, 2400);
|
| 519 |
+
}
|
| 520 |
+
function legacyCopy(text) {
|
| 521 |
+
var area = document.createElement('textarea'); area.value = text; area.setAttribute('readonly', '');
|
| 522 |
+
area.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none'; document.body.appendChild(area);
|
| 523 |
+
area.select(); area.setSelectionRange(0, text.length); var ok = false;
|
| 524 |
+
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
|
| 525 |
+
document.body.removeChild(area); return ok;
|
| 526 |
+
}
|
| 527 |
+
window.icvToast = toast;
|
| 528 |
+
window.icvCopy = function (button) {
|
| 529 |
+
var text = button.getAttribute('data-text') || '';
|
| 530 |
+
function finish(ok) {
|
| 531 |
+
toast(ok ? '__COPIED__' : 'تعذّر النسخ، حدّد النص وانسخه يدويًا', !ok);
|
| 532 |
+
if (ok) { button.classList.add('done'); setTimeout(function () { button.classList.remove('done'); }, 1600); }
|
| 533 |
+
}
|
| 534 |
+
if (navigator.clipboard && window.isSecureContext) {
|
| 535 |
+
navigator.clipboard.writeText(text).then(function () { finish(true); }, function () { finish(legacyCopy(text)); });
|
| 536 |
+
} else { finish(legacyCopy(text)); }
|
| 537 |
+
};
|
| 538 |
+
window.icvDownload = function (button) {
|
| 539 |
+
var blob = new Blob([button.getAttribute('data-text') || ''], { type: 'text/tab-separated-values;charset=utf-8' });
|
| 540 |
+
var link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = button.getAttribute('data-name') || 'results.tsv';
|
| 541 |
+
document.body.appendChild(link); link.click(); document.body.removeChild(link); setTimeout(function () { URL.revokeObjectURL(link.href); }, 1000);
|
| 542 |
+
};
|
| 543 |
+
})();
|
| 544 |
+
""".replace("__COPIED__", COPIED_MESSAGE)
|