v1.8.0: bge-m3 embeddings, qwen3 LLM, retrieval fixes, input validation, memory poisoning fix
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>AfakZaid RAG — Scholarly Intelligence Platform</title>
|
||||
<style>
|
||||
body{margin:0;font-family:system-ui,sans-serif;background:#0c1324;color:#dce1fb;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;box-sizing:border-box}
|
||||
.card{max-width:600px;width:100%;background:rgba(21,27,45,0.8);border:1px solid rgba(78,222,163,0.2);border-radius:20px;padding:48px 40px;text-align:center}
|
||||
.icon{font-size:48px;margin-bottom:16px}
|
||||
h1{font-size:28px;font-weight:700;color:#4edea3;margin:0 0 8px}
|
||||
p.sub{color:#bbcabf;font-size:14px;margin:0 0 24px}
|
||||
p.desc{color:#dce1fb;font-size:15px;line-height:1.7;margin:0 0 32px}
|
||||
.btn{display:inline-block;background:#4edea3;color:#0c1324;font-weight:700;padding:12px 32px;border-radius:12px;text-decoration:none;font-size:14px;margin-bottom:32px}
|
||||
.links{border-top:1px solid rgba(78,222,163,0.1);padding-top:24px;font-size:12px;color:#888}
|
||||
.links a{color:#4edea3;text-decoration:none;margin:0 12px}
|
||||
.links a:hover{text-decoration:underline}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="icon">✦</div>
|
||||
<h1>AfakZaid RAG</h1>
|
||||
<p class="sub">Scholarly Intelligence Platform</p>
|
||||
<p class="desc">
|
||||
AfakZaid RAG is an AI-powered platform for searching, querying, and exploring
|
||||
Arabic scholarly books and documents. Users can import PDF books from Google Drive,
|
||||
index them, and ask questions using natural language. The platform uses Google Drive
|
||||
read-only access solely to download PDF files selected by the user for indexing.
|
||||
</p>
|
||||
<a href="https://rag.afaqzaid.com" class="btn">Open Platform</a>
|
||||
<div class="links">
|
||||
<a href="https://rag.afaqzaid.com/privacy" rel="privacy-policy">Privacy Policy</a>
|
||||
<a href="https://rag.afaqzaid.com/terms">Terms of Service</a>
|
||||
<span style="margin:0 12px">© 2026 AfakZaid</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+237
-5
@@ -242,6 +242,49 @@ a[role=button]:active {
|
||||
Sign In
|
||||
</button>
|
||||
<p id="login-err" class="text-error text-xs mt-3 hidden">Invalid credentials</p>
|
||||
|
||||
<section id="page-pages" class="page hidden">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Pages</h2>
|
||||
<p class="text-sm text-on-surface-variant mt-1">Edit site pages content</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page Tabs -->
|
||||
<div class="flex gap-2 mb-6">
|
||||
<button id="pages-tab-privacy" onclick="switchPageTab('privacy')"
|
||||
class="px-4 py-2 rounded-xl text-sm font-semibold bg-primary text-surface transition-all">
|
||||
سياسة الخصوصية
|
||||
</button>
|
||||
<button id="pages-tab-terms" onclick="switchPageTab('terms')"
|
||||
class="px-4 py-2 rounded-xl text-sm font-semibold glass text-on-surface-variant hover:text-primary transition-all">
|
||||
شروط الخدمة
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Editor -->
|
||||
<div class="glass rounded-2xl p-5 space-y-4" style="border:1px solid rgba(78,222,163,0.1)">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm text-on-surface-variant">تعديل محتوى HTML للصفحة</p>
|
||||
<div class="flex gap-2">
|
||||
<a id="pages-preview-btn" href="/privacy" target="_blank"
|
||||
class="glass px-3 py-1.5 rounded-lg text-xs text-on-surface-variant hover:text-primary transition-colors flex items-center gap-1">
|
||||
<span class="msym text-sm">open_in_new</span> معاينة
|
||||
</a>
|
||||
<button onclick="savePageContent()" id="pages-save-btn"
|
||||
class="bg-primary text-surface font-bold px-4 py-1.5 rounded-lg text-xs flex items-center gap-1 hover:shadow-[0_0_15px_rgba(78,222,163,0.4)] transition-all">
|
||||
<span class="msym text-sm">save</span> حفظ
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="pages-editor"
|
||||
class="w-full bg-surface/60 border border-white/10 rounded-xl p-4 text-xs font-mono text-on-surface focus:outline-none focus:border-primary/50 transition-colors resize-none"
|
||||
style="height:420px;direction:ltr"></textarea>
|
||||
<p id="pages-save-status" class="text-xs text-primary hidden">✅ تم الحفظ بنجاح</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -292,6 +335,9 @@ a[role=button]:active {
|
||||
<button class="nav-btn w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold transition-all" onclick="showPage('settings',this)">
|
||||
<span class="msym text-lg">settings</span><span>Settings</span>
|
||||
</button>
|
||||
<button class="nav-btn w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold transition-all" onclick="showPage('pages',this)">
|
||||
<span class="msym text-lg">description</span><span>Pages</span>
|
||||
</button>
|
||||
<button class="nav-btn w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold transition-all" onclick="showPage('branding',this)">
|
||||
<span class="msym text-lg">palette</span><span>Branding</span>
|
||||
</button>
|
||||
@@ -516,6 +562,7 @@ curl -X POST https://rag.afaqzaid.com/ingest-url \
|
||||
<div id="settings-ai" class="glass rounded-2xl p-6"></div>
|
||||
<div id="settings-system" class="glass rounded-2xl p-6"></div>
|
||||
</div>
|
||||
<div id="settings-llm" class="glass rounded-2xl p-6 mt-6"></div>
|
||||
<div id="settings-r2" class="glass rounded-2xl p-6 mt-6"></div>
|
||||
</section>
|
||||
|
||||
@@ -805,7 +852,7 @@ curl -X POST https://rag.afaqzaid.com/ingest-url \
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let ADMIN_KEY = 'afakadmin-Ihb7kflFA682Ax3EICnV2ymr4KuNZSo7';
|
||||
// ADMIN_KEY removed — JWT-only auth
|
||||
let JWT_TOKEN = localStorage.getItem('admin_jwt') || '';
|
||||
const RAG_KEY = 'zRf77E_brSEnA2BUi40uxLxT4uofAk661WMZb-cdv4M';
|
||||
let keyVisible = false;
|
||||
@@ -815,7 +862,7 @@ if (!JWT_TOKEN) document.getElementById('login').style.display = 'flex';
|
||||
|
||||
// ── Core API ──────────────────────────────────────────────────────────────────
|
||||
async function api(path, opts={}) {
|
||||
const headers = {'Content-Type':'application/json', 'X-API-Key': ADMIN_KEY};
|
||||
const headers = {'Content-Type':'application/json', 'Authorization':'Bearer '+JWT_TOKEN};
|
||||
if (JWT_TOKEN) headers['Authorization'] = 'Bearer ' + JWT_TOKEN;
|
||||
const r = await fetch(path, {...opts, headers:{...headers, ...(opts.headers||{})}});
|
||||
if (r.status === 401) {
|
||||
@@ -967,7 +1014,7 @@ async function deleteBook(id, filename, btn) {
|
||||
if (!confirm(`حذف الكتاب؟\n"${filename}"\n\nسيُحذف ملف PDF والمتجهات والسجل نهائياً.`)) return;
|
||||
btn.disabled = true; btn.innerHTML = '<span class="msym text-sm animate-spin">autorenew</span>';
|
||||
try {
|
||||
const r = await fetch(`/api/library/${id}`, {method:'DELETE', headers:{'X-API-Key':ADMIN_KEY,'Authorization':'Bearer '+JWT_TOKEN}});
|
||||
const r = await fetch(`/api/library/${id}`, {method:'DELETE', headers:{'Authorization':'Bearer '+JWT_TOKEN}});
|
||||
const d = await r.json();
|
||||
if (r.ok) { document.getElementById('book-row-'+id)?.remove(); loadOverview(); toast('Book deleted','success'); }
|
||||
else { alert('فشل الحذف: '+(d.detail||'خطأ')); btn.disabled=false; btn.innerHTML='<span class="msym text-sm">delete</span>'; }
|
||||
@@ -1078,6 +1125,7 @@ async function loadResources() {
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
loadLLMConfig();
|
||||
try {
|
||||
const d = await api('/admin/settings');
|
||||
document.getElementById('settings-ai').innerHTML = `
|
||||
@@ -1236,7 +1284,7 @@ async function loadBranding() {
|
||||
|
||||
async function saveBranding() {
|
||||
try {
|
||||
const r = await fetch('/admin/branding', {method:'POST', headers:{'Content-Type':'application/json','X-API-Key':ADMIN_KEY,'Authorization':'Bearer '+JWT_TOKEN}, body:JSON.stringify({site_name:document.getElementById('b-site-name').value, site_subtitle:document.getElementById('b-subtitle').value, logo_url:document.getElementById('b-logo').value, primary_color:document.getElementById('b-primary-hex').value, secondary_color:document.getElementById('b-secondary-hex').value})});
|
||||
const r = await fetch('/admin/branding', {method:'POST', headers:{'Content-Type':'application/json','Authorization':'Bearer '+JWT_TOKEN}, body:JSON.stringify({site_name:document.getElementById('b-site-name').value, site_subtitle:document.getElementById('b-subtitle').value, logo_url:document.getElementById('b-logo').value, primary_color:document.getElementById('b-primary-hex').value, secondary_color:document.getElementById('b-secondary-hex').value})});
|
||||
if (r.ok) toast('Branding saved!','success'); else alert('Save failed');
|
||||
} catch(e) { alert('Error: '+e.message); }
|
||||
}
|
||||
@@ -1338,7 +1386,7 @@ async function saveClaudeKey() {
|
||||
const key = document.getElementById('claude-key-input').value.trim();
|
||||
if (!key) return;
|
||||
try {
|
||||
const r = await fetch('/admin/claude/key', {method:'POST', headers:{'Content-Type':'application/json','X-API-Key':ADMIN_KEY,'Authorization':'Bearer '+JWT_TOKEN}, body:JSON.stringify({api_key:key})});
|
||||
const r = await fetch('/admin/claude/key', {method:'POST', headers:{'Content-Type':'application/json','Authorization':'Bearer '+JWT_TOKEN}, body:JSON.stringify({api_key:key})});
|
||||
if (r.ok) { toast('Claude key saved!','success'); loadClaude(); document.getElementById('claude-key-input').value=''; }
|
||||
else toast('Failed to save key');
|
||||
} catch(e) { toast('Error: '+e.message); }
|
||||
@@ -1777,6 +1825,122 @@ function applyScanResult(item) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function loadLLMConfig() {
|
||||
try {
|
||||
const d = await api('/admin/llm-config');
|
||||
const isLocal = d.provider !== 'cloudflare';
|
||||
const isCF = d.provider === 'cloudflare';
|
||||
window._llmProvider = d.provider || 'local';
|
||||
document.getElementById('settings-llm').innerHTML = `
|
||||
<h3 class="text-primary text-xs font-bold uppercase tracking-widest mb-5 flex items-center gap-2">
|
||||
<span class="msym text-base">hub</span>LLM Provider
|
||||
<span class="ml-auto text-[10px] font-normal glass px-2 py-0.5 rounded-full ${isCF ? 'text-blue-400' : 'text-emerald-400'}">
|
||||
${isCF ? '⚡ Cloudflare GPU' : '🖥 Local Ollama'}
|
||||
</span>
|
||||
</h3>
|
||||
<div class="flex gap-2 mb-5">
|
||||
<button onclick="llmSetProvider('local')" id="llm-btn-local"
|
||||
class="flex-1 py-2.5 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 ${isLocal ? 'bg-primary text-surface' : 'glass glass-hover text-on-surface-variant'}">
|
||||
<span class="msym text-base">computer</span> Local (Ollama)
|
||||
</button>
|
||||
<button onclick="llmSetProvider('cloudflare')" id="llm-btn-cf"
|
||||
class="flex-1 py-2.5 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 ${isCF ? 'bg-blue-500 text-white' : 'glass glass-hover text-on-surface-variant'}">
|
||||
<span class="msym text-base">cloud</span> Cloudflare GPU
|
||||
</button>
|
||||
</div>
|
||||
<div id="llm-local-info" class="${isLocal ? '' : 'hidden'} glass rounded-xl p-4 mb-4 text-xs text-on-surface-variant space-y-1">
|
||||
<p class="flex items-center gap-2"><span class="msym text-base text-emerald-400">check_circle</span>Running qwen2:7b on your VPS CPU</p>
|
||||
<p class="flex items-center gap-2"><span class="msym text-base text-yellow-400">schedule</span>~30-50s per query — streaming masks latency</p>
|
||||
<p class="flex items-center gap-2"><span class="msym text-base text-primary">lock</span>Fully private — no external requests</p>
|
||||
</div>
|
||||
<div id="llm-cf-fields" class="${isCF ? '' : 'hidden'} space-y-3 mb-4">
|
||||
<div class="glass rounded-xl p-4 text-xs text-on-surface-variant space-y-1 mb-3">
|
||||
<p class="flex items-center gap-2"><span class="msym text-base text-blue-400">bolt</span>GPU inference — 3-5s per query</p>
|
||||
<p class="flex items-center gap-2"><span class="msym text-base text-primary">all_inclusive</span>~900 free queries/day on free tier</p>
|
||||
<p class="flex items-center gap-2"><span class="msym text-base text-on-surface-variant">storage</span>Your PDFs stay local — only text prompt sent</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-on-surface-variant block mb-1.5">Account ID</label>
|
||||
<input id="llm-cf-account" type="text" dir="ltr" value="${d.cf_account_id||''}"
|
||||
placeholder="Find in dash.cloudflare.com URL bar"
|
||||
class="w-full bg-surface-high border border-outline-variant rounded-xl px-3 py-2 text-xs font-mono outline-none focus:border-primary transition-colors"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-on-surface-variant block mb-1.5">API Token
|
||||
${d.cf_api_token_configured ? '<span class="text-emerald-400 text-[10px] ml-2">✓ Saved ('+d.cf_api_token_masked+')</span>' : ''}
|
||||
</label>
|
||||
<input id="llm-cf-token" type="password" dir="ltr"
|
||||
placeholder="${d.cf_api_token_configured ? 'Leave blank to keep saved token' : 'Workers AI API token'}"
|
||||
class="w-full bg-surface-high border border-outline-variant rounded-xl px-3 py-2 text-xs font-mono outline-none focus:border-primary transition-colors"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-on-surface-variant block mb-1.5">Model</label>
|
||||
<select id="llm-cf-model" class="w-full bg-surface-high border border-outline-variant rounded-xl px-3 py-2 text-xs outline-none focus:border-primary transition-colors">
|
||||
<option value="@cf/meta/llama-3.1-8b-instruct-fp8-fast" ${(d.cf_model||'').includes('fp8-fast')?'selected':''}>Llama 3.1 8B FP8 Fast ⚡ (Recommended)</option>
|
||||
<option value="@cf/meta/llama-3.1-8b-instruct" ${d.cf_model==='@cf/meta/llama-3.1-8b-instruct'?'selected':''}>Llama 3.1 8B Standard</option>
|
||||
<option value="@cf/meta/llama-3.2-3b-instruct" ${(d.cf_model||'').includes('3.2-3b')?'selected':''}>Llama 3.2 3B (Faster)</option>
|
||||
<option value="@cf/zai-org/glm-4.7-flash" ${(d.cf_model||'').includes('glm')?'selected':''}>GLM-4.7 Flash (100+ languages)</option>
|
||||
<option value="@cf/qwen/qwen3-30b-a3b-fp8" ${(d.cf_model||'').includes('qwen3-30b')?'selected':''}>Qwen 3 30B ⭐ (Best Arabic)</option>
|
||||
<option value="@cf/qwen/qwq-32b" ${(d.cf_model||'').includes('qwq-32b')?'selected':''}>Qwen QwQ 32B (Reasoning)</option>
|
||||
<option value="@cf/deepseek-ai/deepseek-r1-distill-qwen-32b" ${(d.cf_model||'').includes('deepseek-r1')?'selected':''}>DeepSeek R1 32B (Reasoning)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="saveLLMConfig()" class="flex-1 bg-primary text-surface font-bold py-2.5 rounded-xl text-sm flex items-center justify-center gap-2 hover:shadow-[0_0_20px_rgba(78,222,163,0.4)] transition-all">
|
||||
<span class="msym text-base">save</span> Save
|
||||
</button>
|
||||
<button onclick="testLLMConfig()" class="glass glass-hover px-4 py-2.5 rounded-xl text-sm text-on-surface-variant flex items-center gap-2">
|
||||
<span class="msym text-base">wifi_tethering</span> Test
|
||||
</button>
|
||||
</div>
|
||||
<p id="llm-status" class="text-xs text-center mt-3 hidden"></p>`;
|
||||
} catch(e) { showErr('settings-llm', e.message); }
|
||||
}
|
||||
|
||||
function llmSetProvider(p) {
|
||||
window._llmProvider = p;
|
||||
const isLocal = p !== 'cloudflare', isCF = p === 'cloudflare';
|
||||
document.getElementById('llm-btn-local').className = `flex-1 py-2.5 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 ${isLocal?'bg-primary text-surface':'glass glass-hover text-on-surface-variant'}`;
|
||||
document.getElementById('llm-btn-cf').className = `flex-1 py-2.5 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 ${isCF?'bg-blue-500 text-white':'glass glass-hover text-on-surface-variant'}`;
|
||||
document.getElementById('llm-local-info').classList.toggle('hidden', isCF);
|
||||
document.getElementById('llm-cf-fields').classList.toggle('hidden', isLocal);
|
||||
}
|
||||
|
||||
async function saveLLMConfig() {
|
||||
const status = document.getElementById('llm-status');
|
||||
const payload = {
|
||||
provider: window._llmProvider || 'local',
|
||||
cf_account_id: (document.getElementById('llm-cf-account')?.value||'').trim(),
|
||||
cf_api_token: (document.getElementById('llm-cf-token')?.value||'').trim(),
|
||||
cf_model: document.getElementById('llm-cf-model')?.value || '@cf/meta/llama-3.1-8b-instruct-fp8-fast',
|
||||
};
|
||||
status.textContent = 'Saving...'; status.classList.remove('hidden'); status.style.color = '';
|
||||
try {
|
||||
await api('/admin/llm-config', {method:'POST', body:JSON.stringify(payload)});
|
||||
status.textContent = '✅ Saved — restart API container to apply';
|
||||
status.style.color = '#4edea3';
|
||||
setTimeout(loadLLMConfig, 1500);
|
||||
} catch(e) { status.textContent = '❌ '+e.message; status.style.color = '#f87171'; }
|
||||
}
|
||||
|
||||
async function testLLMConfig() {
|
||||
const status = document.getElementById('llm-status');
|
||||
const payload = {
|
||||
provider: window._llmProvider || 'local',
|
||||
cf_account_id: (document.getElementById('llm-cf-account')?.value||'').trim(),
|
||||
cf_api_token: (document.getElementById('llm-cf-token')?.value||'').trim(),
|
||||
cf_model: document.getElementById('llm-cf-model')?.value || '@cf/meta/llama-3.1-8b-instruct-fp8-fast',
|
||||
};
|
||||
status.textContent = 'Testing...'; status.classList.remove('hidden'); status.style.color = '';
|
||||
try {
|
||||
const r = await api('/admin/llm-test', {method:'POST', body:JSON.stringify(payload)});
|
||||
status.textContent = r.ok ? '✅ '+r.detail : '❌ '+r.detail;
|
||||
status.style.color = r.ok ? '#4edea3' : '#f87171';
|
||||
} catch(e) { status.textContent = '❌ '+e.message; status.style.color = '#f87171'; }
|
||||
}
|
||||
|
||||
// design-taste-frontend: Spotlight border cards
|
||||
document.querySelectorAll('.spotlight-card').forEach(card => {
|
||||
card.addEventListener('mousemove', e => {
|
||||
@@ -1917,5 +2081,73 @@ async function retryMetaExtraction() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
// ── Pages Editor ─────────────────────────────────────────────────────────────
|
||||
let _currentPage = 'privacy';
|
||||
|
||||
function switchPageTab(page) {
|
||||
_currentPage = page;
|
||||
['privacy','terms'].forEach(p => {
|
||||
const btn = document.getElementById('pages-tab-' + p);
|
||||
if (p === page) {
|
||||
btn.className = 'px-4 py-2 rounded-xl text-sm font-semibold bg-primary text-surface transition-all';
|
||||
} else {
|
||||
btn.className = 'px-4 py-2 rounded-xl text-sm font-semibold glass text-on-surface-variant hover:text-primary transition-all';
|
||||
}
|
||||
});
|
||||
document.getElementById('pages-preview-btn').href = '/' + page;
|
||||
document.getElementById('pages-save-status').classList.add('hidden');
|
||||
loadPageContent(page);
|
||||
}
|
||||
|
||||
async function loadPageContent(page) {
|
||||
const editor = document.getElementById('pages-editor');
|
||||
editor.value = 'جاري التحميل...';
|
||||
try {
|
||||
const res = await fetch('/admin/pages/' + page, {
|
||||
headers: { 'Authorization': 'Bearer ' + JWT_TOKEN }
|
||||
});
|
||||
const data = await res.json();
|
||||
editor.value = data.content || '';
|
||||
} catch(e) {
|
||||
editor.value = 'خطأ في التحميل: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function savePageContent() {
|
||||
const btn = document.getElementById('pages-save-btn');
|
||||
const status = document.getElementById('pages-save-status');
|
||||
const content = document.getElementById('pages-editor').value;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="msym text-sm">progress_activity</span> جاري الحفظ...';
|
||||
try {
|
||||
const res = await fetch('/admin/pages/' + _currentPage, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + JWT_TOKEN },
|
||||
body: JSON.stringify({ content })
|
||||
});
|
||||
if (res.ok) {
|
||||
status.classList.remove('hidden');
|
||||
setTimeout(() => status.classList.add('hidden'), 3000);
|
||||
}
|
||||
} catch(e) {
|
||||
alert('فشل الحفظ: ' + e.message);
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span class="msym text-sm">save</span> حفظ';
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const origShowPage = window.showPage;
|
||||
if (origShowPage) {
|
||||
window.showPage = function(page, btn) {
|
||||
origShowPage(page, btn);
|
||||
if (page === 'pages') loadPageContent(_currentPage);
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* AfakZaid Analytics Bridge — GA4 + Pixel Tracking
|
||||
* Loads full config from /analytics/full-config
|
||||
* Boots GA4, injects pixels, wires dataLayer event rules
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
async function init() {
|
||||
let config;
|
||||
try {
|
||||
const r = await fetch('/analytics/full-config');
|
||||
config = await r.json();
|
||||
} catch (e) { return; }
|
||||
|
||||
bootGA(config.ga);
|
||||
bootPixels(config.pixels || []);
|
||||
}
|
||||
|
||||
// ── GA4 ──────────────────────────────────────────────────────────────────
|
||||
function bootGA(ga) {
|
||||
if (!ga || !ga.enabled || !ga.measurement_id) return;
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
window.gtag = function () { window.dataLayer.push(arguments); };
|
||||
gtag('js', new Date());
|
||||
gtag('config', ga.measurement_id, { send_page_view: true });
|
||||
const s = document.createElement('script');
|
||||
s.async = true;
|
||||
s.src = `https://www.googletagmanager.com/gtag/js?id=${ga.measurement_id}`;
|
||||
document.head.appendChild(s);
|
||||
if (Array.isArray(ga.events)) {
|
||||
const page = detectPage();
|
||||
ga.events
|
||||
.filter(ev => ev.active !== false)
|
||||
.filter(ev => !ev.pages || ev.pages === 'all' || ev.pages === page)
|
||||
.forEach(wireEvent);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pixels ────────────────────────────────────────────────────────────────
|
||||
function bootPixels(pixels) {
|
||||
pixels.filter(p => p.enabled !== false).forEach(p => {
|
||||
try {
|
||||
switch (p.platform) {
|
||||
case 'meta': bootMetaPixel(p.pixel_id); break;
|
||||
case 'tiktok': bootTikTokPixel(p.pixel_id); break;
|
||||
case 'snapchat': bootSnapchatPixel(p.pixel_id); break;
|
||||
case 'pinterest': bootPinterestTag(p.pixel_id); break;
|
||||
case 'twitter': bootTwitterPixel(p.pixel_id); break;
|
||||
case 'custom': bootCustomPixel(p.code); break;
|
||||
}
|
||||
} catch (e) { /* silent fail per pixel */ }
|
||||
});
|
||||
}
|
||||
|
||||
function bootMetaPixel(id) {
|
||||
if (!id) return;
|
||||
!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||||
n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
|
||||
n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
|
||||
t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
|
||||
document,'script','https://connect.facebook.net/en_US/fbevents.js');
|
||||
window.fbq('init', id);
|
||||
window.fbq('track', 'PageView');
|
||||
}
|
||||
|
||||
function bootTikTokPixel(id) {
|
||||
if (!id) return;
|
||||
!function(w,d,t){w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];
|
||||
ttq.methods=['page','track','identify','instances','debug','on','off','once','ready','alias','group','enableCookie','disableCookie'];
|
||||
ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};
|
||||
for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);
|
||||
ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e};
|
||||
ttq.load=function(e,n){var i="https://analytics.tiktok.com/i18n/pixel/events.js";ttq._i=ttq._i||{};ttq._i[e]=[];
|
||||
ttq._i[e]._u=i;ttq._t=ttq._t||{};ttq._t[e]=+new Date;ttq._o=ttq._o||{};ttq._o[e]=n||{};
|
||||
var o=document.createElement("script");o.type="text/javascript";o.async=!0;o.src=i+"?sdkid="+e+"&lib="+t;
|
||||
var a=document.getElementsByTagName("script")[0];a.parentNode.insertBefore(o,a)};
|
||||
ttq.load(id); ttq.page();}(window, document, 'ttq');
|
||||
}
|
||||
|
||||
function bootSnapchatPixel(id) {
|
||||
if (!id) return;
|
||||
(function(e,t,n){if(e.snaptr)return;var a=e.snaptr=function(){a.handleRequest?
|
||||
a.handleRequest.apply(a,arguments):a.queue.push(arguments)};a.queue=[];
|
||||
var s='script';r=t.createElement(s);r.async=!0;r.src=n;
|
||||
var u=t.getElementsByTagName(s)[0];u.parentNode.insertBefore(r,u);
|
||||
}(window,document,'https://sc-static.net/scevent.min.js'));
|
||||
window.snaptr('init', id, {});
|
||||
window.snaptr('track', 'PAGE_VIEW');
|
||||
}
|
||||
|
||||
function bootPinterestTag(id) {
|
||||
if (!id) return;
|
||||
!function(e){if(!window.pintrk){window.pintrk=function(){window.pintrk.queue.push(Array.prototype.slice.call(arguments))};
|
||||
var n=window.pintrk;n.queue=[],n.version="3.0";var t=document.createElement("script");
|
||||
t.async=!0,t.src=e;var r=document.getElementsByTagName("script")[0];
|
||||
r.parentNode.insertBefore(t,r)}}("https://s.pinimg.com/ct/core.js");
|
||||
window.pintrk('load', id);
|
||||
window.pintrk('page');
|
||||
}
|
||||
|
||||
function bootTwitterPixel(id) {
|
||||
if (!id) return;
|
||||
!function(e,t,n,s,u,a){e.twq||(s=e.twq=function(){s.exe?s.exe.apply(s,arguments):s.queue.push(arguments)},
|
||||
s.version='1.1',s.queue=[],u=t.createElement(n),u.async=!0,u.src='https://static.ads-twitter.com/uwt.js',
|
||||
a=t.getElementsByTagName(n)[0],a.parentNode.insertBefore(u,a))}(window,document,'script');
|
||||
window.twq('config', id);
|
||||
}
|
||||
|
||||
function bootCustomPixel(code) {
|
||||
if (!code) return;
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = code;
|
||||
[...div.querySelectorAll('script')].forEach(orig => {
|
||||
const s = document.createElement('script');
|
||||
if (orig.src) { s.src = orig.src; s.async = true; }
|
||||
else s.textContent = orig.textContent;
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
[...div.querySelectorAll('noscript,img')].forEach(el => document.body.appendChild(el.cloneNode(true)));
|
||||
}
|
||||
|
||||
// ── GA4 Event Wiring ──────────────────────────────────────────────────────
|
||||
function detectPage() {
|
||||
const p = window.location.pathname;
|
||||
if (p.startsWith('/library')) return 'library';
|
||||
if (p.startsWith('/reader')) return 'reader';
|
||||
if (p.startsWith('/login')) return 'login';
|
||||
if (p.startsWith('/admin')) return 'admin';
|
||||
return 'home';
|
||||
}
|
||||
|
||||
function wireEvent(ev) {
|
||||
const trigger = ev.trigger || 'click';
|
||||
if (trigger === 'pageview') { pushEvent(ev, document.body); return; }
|
||||
if (trigger === 'scroll_depth') { wireScrollDepth(ev); return; }
|
||||
document.addEventListener(trigger, function (e) {
|
||||
const target = e.target.closest(ev.selector);
|
||||
if (target) pushEvent(ev, target);
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
function pushEvent(ev, el) {
|
||||
if (!window.gtag) return;
|
||||
const params = { event_category: ev.category || 'engagement' };
|
||||
(ev.params || []).forEach(p => {
|
||||
let val = '';
|
||||
if (p.source === 'static') val = p.value || '';
|
||||
if (p.source === 'attribute') val = el.getAttribute(p.attribute) || '';
|
||||
if (p.source === 'text') val = (el.querySelector(p.selector) || el).textContent.trim().slice(0, 100);
|
||||
if (p.source === 'url_param') val = new URLSearchParams(window.location.search).get(p.param) || '';
|
||||
if (p.source === 'page_url') val = window.location.href;
|
||||
if (p.source === 'page_path') val = window.location.pathname;
|
||||
params[p.key] = val;
|
||||
});
|
||||
gtag('event', ev.event_name, params);
|
||||
if (window.__afak_analytics_debug) console.log('[GA4]', ev.event_name, params);
|
||||
}
|
||||
|
||||
function wireScrollDepth(ev) {
|
||||
const reached = new Set();
|
||||
window.addEventListener('scroll', function () {
|
||||
const pct = Math.round((window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100);
|
||||
[25, 50, 75, 90].forEach(d => {
|
||||
if (pct >= d && !reached.has(d)) {
|
||||
reached.add(d);
|
||||
if (window.gtag) gtag('event', ev.event_name || 'scroll_depth', { scroll_depth: d, page: window.location.pathname });
|
||||
}
|
||||
});
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
||||
else init();
|
||||
})();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,147 @@
|
||||
|
||||
// ── Sticky footer ─────────────────────────────────────────────────────────
|
||||
(function() {
|
||||
const b = document.body;
|
||||
b.style.minHeight = '100vh';
|
||||
b.style.display = 'flex';
|
||||
b.style.flexDirection = 'column';
|
||||
const main = document.querySelector('main');
|
||||
if (main) main.style.flex = '1';
|
||||
})();
|
||||
|
||||
|
||||
// ── Favicon ───────────────────────────────────────────────────────────────
|
||||
(function() {
|
||||
if (!document.querySelector('link[rel="icon"]')) {
|
||||
const l = document.createElement('link');
|
||||
l.rel = 'icon'; l.type = 'image/x-icon'; l.href = '/favicon.ico';
|
||||
document.head.appendChild(l);
|
||||
}
|
||||
if (!document.querySelector('link[rel="apple-touch-icon"]')) {
|
||||
const l = document.createElement('link');
|
||||
l.rel = 'apple-touch-icon'; l.href = '/apple-touch-icon.png';
|
||||
document.head.appendChild(l);
|
||||
}
|
||||
})();
|
||||
|
||||
// ── Universal Header & Footer ─────────────────────────────────────────────
|
||||
// Include this script in every page. It injects the header and footer,
|
||||
// loads branding, and handles auth nav automatically.
|
||||
|
||||
(function() {
|
||||
|
||||
const HEADER_HTML = `
|
||||
<header class="sticky top-0 z-20 glass border-b border-primary/20">
|
||||
<div class="max-w-6xl mx-auto px-4 lg:px-8 py-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/" class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 bg-primary/15 rounded-xl flex items-center justify-center glow-sm">
|
||||
<span class="msym text-primary text-lg">auto_awesome</span>
|
||||
</div>
|
||||
<div class="hidden sm:block">
|
||||
<h1 class="text-base font-bold text-primary leading-tight" id="brand-name">AfakZaid RAG</h1>
|
||||
<p class="text-[10px] text-on-surface-variant/60 uppercase tracking-widest" id="brand-sub">Scholarly Intelligence</p>
|
||||
</div>
|
||||
</a>
|
||||
<div class="h-5 w-px bg-outline-variant/40 hidden sm:block"></div>
|
||||
<a href="/library" class="glass glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-xs text-on-surface-variant hover:text-primary transition-colors">
|
||||
<span class="msym text-base">library_books</span>
|
||||
<span class="hidden sm:inline">المكتبة</span>
|
||||
</a>
|
||||
</div>
|
||||
<nav class="flex items-center gap-2">
|
||||
<div id="nav-auth" class="flex items-center gap-2"></div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>`;
|
||||
|
||||
const FOOTER_HTML = `
|
||||
<footer class="text-center py-6 mt-8 text-xs text-on-surface-variant border-t border-white/5">
|
||||
<div class="flex items-center justify-center gap-4 flex-wrap">
|
||||
<a href="/privacy" class="hover:text-primary transition-colors">سياسة الخصوصية</a>
|
||||
<span class="opacity-30">•</span>
|
||||
<a href="/terms" class="hover:text-primary transition-colors">شروط الخدمة</a>
|
||||
<span class="opacity-30">•</span>
|
||||
<span id="footer-brand">© 2026 أفاق زيد</span>
|
||||
</div>
|
||||
</footer>`;
|
||||
|
||||
// Inject header
|
||||
const headerMount = document.getElementById('universal-header');
|
||||
if (headerMount) headerMount.outerHTML = HEADER_HTML;
|
||||
else document.body.insertAdjacentHTML('afterbegin', HEADER_HTML);
|
||||
|
||||
// Inject footer
|
||||
const footerMount = document.getElementById('universal-footer');
|
||||
if (footerMount) footerMount.outerHTML = FOOTER_HTML;
|
||||
else document.body.insertAdjacentHTML('beforeend', FOOTER_HTML);
|
||||
|
||||
// ── Branding ──────────────────────────────────────────────────────────────
|
||||
fetch('/branding').then(r => r.json()).then(b => {
|
||||
const n = document.getElementById('brand-name');
|
||||
const s = document.getElementById('brand-sub');
|
||||
const f = document.getElementById('footer-brand');
|
||||
if (n && b.app_name) n.textContent = b.app_name;
|
||||
if (s && b.tagline) s.textContent = b.tagline;
|
||||
if (f && b.app_name) f.textContent = '© 2026 ' + b.app_name;
|
||||
}).catch(() => {});
|
||||
|
||||
// ── Auth Nav ──────────────────────────────────────────────────────────────
|
||||
const userToken = localStorage.getItem('rag_token');
|
||||
|
||||
async function initHeaderAuth() {
|
||||
const nav = document.getElementById('nav-auth');
|
||||
if (!nav) return;
|
||||
|
||||
if (!userToken) {
|
||||
nav.innerHTML = `<a href="/login" class="glass glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-xs text-on-surface-variant hover:text-primary transition-colors">
|
||||
<span class="msym text-base">login</span>
|
||||
<span class="hidden sm:inline">دخول</span>
|
||||
</a>`;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const r = await fetch('/auth/me', { headers: { 'Authorization': 'Bearer ' + userToken } });
|
||||
if (!r.ok) { localStorage.removeItem('rag_token'); initHeaderAuth(); return; }
|
||||
const user = await r.json();
|
||||
const perms = user.permissions || [];
|
||||
const isAdmin = user.role === 'admin';
|
||||
|
||||
nav.innerHTML = `
|
||||
<div class="relative" id="uh-user-wrap">
|
||||
<button onclick="document.getElementById('uh-drop').classList.toggle('hidden')"
|
||||
class="glass flex items-center gap-2 px-3 py-2 rounded-xl text-xs hover:border-primary/40 transition-colors">
|
||||
<span class="msym text-base text-primary">person</span>
|
||||
<span class="hidden sm:inline max-w-[80px] truncate">${user.name}</span>
|
||||
<span class="msym text-sm text-on-surface-variant">expand_more</span>
|
||||
</button>
|
||||
<div id="uh-drop" class="hidden absolute right-0 top-full mt-1 glass rounded-xl p-2 min-w-[160px] z-50 shadow-xl">
|
||||
<div class="px-3 py-2 border-b border-outline-variant/30 mb-1">
|
||||
<p class="text-xs font-bold">${user.name}</p>
|
||||
<p class="text-[10px] text-primary">${user.role_display || user.role}</p>
|
||||
</div>
|
||||
${isAdmin ? '<a href="/admin" class="flex items-center gap-2 px-3 py-2 rounded-lg text-xs text-on-surface-variant hover:text-primary hover:bg-primary/5"><span class="msym text-sm">admin_panel_settings</span>Admin</a>' : ''}
|
||||
<button onclick="uhLogout()" class="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-xs text-error hover:bg-red-900/10">
|
||||
<span class="msym text-sm">logout</span>خروج
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
const w = document.getElementById('uh-user-wrap');
|
||||
if (w && !w.contains(e.target)) document.getElementById('uh-drop')?.classList.add('hidden');
|
||||
});
|
||||
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
window.uhLogout = function() {
|
||||
fetch('/auth/logout', { method: 'POST', headers: { 'Authorization': 'Bearer ' + userToken } });
|
||||
localStorage.removeItem('rag_token');
|
||||
window.location.href = '/';
|
||||
};
|
||||
|
||||
initHeaderAuth();
|
||||
|
||||
})();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1 @@
|
||||
google-site-verification: google1d7bb3283b75009e.html
|
||||
+82
-10
@@ -224,8 +224,25 @@ header { animation:slideDown 350ms var(--ease-out) both; }
|
||||
}
|
||||
.reveal { opacity:1; transform:none; }
|
||||
}
|
||||
|
||||
/* Sticky footer — no layout impact */
|
||||
#sticky-footer { position: sticky; top: 100vh; }
|
||||
|
||||
.stream-cursor {
|
||||
display: inline-block;
|
||||
width: 2px;
|
||||
background: var(--color-primary, #4eded3);
|
||||
margin-right: 1px;
|
||||
border-radius: 1px;
|
||||
animation: blink 0.75s step-end infinite;
|
||||
vertical-align: text-bottom;
|
||||
height: 1.1em;
|
||||
}
|
||||
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
|
||||
</style>
|
||||
<script src="/static/analytics.js" defer></script>
|
||||
<link rel="privacy-policy" href="https://rag.afaqzaid.com/privacy"/>
|
||||
<meta name="description" content="AfakZaid RAG — AI-powered scholarly research platform. Search and query Arabic books and documents."/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -462,19 +479,65 @@ async function doQuery() {
|
||||
const box = document.getElementById('answer-box');
|
||||
const meta = document.getElementById('answer-meta');
|
||||
wrap.classList.remove('hidden');
|
||||
box.textContent = '...'; meta.innerHTML = '';
|
||||
box.textContent = ''; meta.innerHTML = '';
|
||||
|
||||
// Blinking cursor while streaming
|
||||
let cursor = document.createElement('span');
|
||||
cursor.className = 'stream-cursor';
|
||||
cursor.textContent = '|';
|
||||
box.appendChild(cursor);
|
||||
|
||||
try {
|
||||
const headers = {'Content-Type':'application/json','X-API-Key':API_KEY};
|
||||
if(userToken) headers['Authorization'] = 'Bearer '+userToken;
|
||||
const r = await fetch('/query',{method:'POST',headers,body:JSON.stringify({query:q,session_id:'web-'+(currentUser?.id||'guest')})});
|
||||
const d = await r.json();
|
||||
box.textContent = d.answer;
|
||||
meta.innerHTML = [
|
||||
`<span class="glass px-3 py-1 rounded-full text-xs text-primary font-bold">${d.llm}</span>`,
|
||||
`<span class="glass px-3 py-1 rounded-full text-xs text-secondary">${((d.confidence||0)*100).toFixed(0)}% ثقة</span>`,
|
||||
...(d.books_used||[]).map(b=>`<span class="glass px-3 py-1 rounded-full text-xs text-on-surface-variant">${b}</span>`)
|
||||
].join('');
|
||||
} catch(e){ box.textContent='خطأ: '+e.message; }
|
||||
|
||||
const r = await fetch('/query/stream', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({query:q, session_id:'web-'+(currentUser?.id||'guest')})
|
||||
});
|
||||
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
|
||||
const reader = r.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buf = '';
|
||||
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) break;
|
||||
buf += decoder.decode(value, {stream: true});
|
||||
|
||||
// Process complete SSE lines
|
||||
const lines = buf.split('\n');
|
||||
buf = lines.pop(); // keep incomplete last line
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith('data: ')) continue;
|
||||
const raw = line.slice(6).trim();
|
||||
if (raw === '[DONE]') break;
|
||||
|
||||
try {
|
||||
const frame = JSON.parse(raw);
|
||||
if (frame.t === 'token') {
|
||||
// Insert token before cursor
|
||||
cursor.before(document.createTextNode(frame.v));
|
||||
box.scrollTop = box.scrollHeight;
|
||||
} else if (frame.t === 'meta') {
|
||||
const m = frame.v;
|
||||
meta.innerHTML = [
|
||||
`<span class="glass px-3 py-1 rounded-full text-xs text-primary font-bold">${m.llm||''}</span>`,
|
||||
`<span class="glass px-3 py-1 rounded-full text-xs text-secondary">${((m.confidence||0)*100).toFixed(0)}% ثقة</span>`,
|
||||
...(m.books_used||[]).map(b=>`<span class="glass px-3 py-1 rounded-full text-xs text-on-surface-variant">${b}</span>`)
|
||||
].join('');
|
||||
}
|
||||
} catch(_) {}
|
||||
}
|
||||
}
|
||||
} catch(e) { box.textContent = 'خطأ: ' + e.message; }
|
||||
|
||||
// Remove cursor
|
||||
cursor.remove();
|
||||
btn.disabled = false;
|
||||
btn.style.animation = '';
|
||||
btn.innerHTML = '<span class="msym text-lg">search</span> بحث';
|
||||
@@ -544,5 +607,14 @@ loadStats();
|
||||
loadBooks();
|
||||
setInterval(loadStats, 30000);
|
||||
</script>
|
||||
|
||||
<footer id="sticky-footer" style="text-align:center;padding:20px;margin-top:16px;font-size:12px;color:#888;border-top:1px solid rgba(255,255,255,0.06)">
|
||||
<a href="https://rag.afaqzaid.com/privacy" rel="privacy-policy" style="color:#888;text-decoration:none;margin:0 8px">سياسة الخصوصية</a>
|
||||
•
|
||||
<a href="https://rag.afaqzaid.com/terms" style="color:#888;text-decoration:none;margin:0 8px">شروط الخدمة</a>
|
||||
•
|
||||
<span style="margin:0 8px">© 2026 أفاق زيد</span>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+370
-10
@@ -329,6 +329,9 @@ main {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Sticky footer — no layout impact */
|
||||
#sticky-footer { position: sticky; top: 100vh; }
|
||||
</style>
|
||||
<script src="/static/analytics.js" defer></script>
|
||||
</head>
|
||||
@@ -366,7 +369,7 @@ main {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="max-w-6xl mx-auto px-4 lg:px-8 py-8">
|
||||
<main class="max-w-6xl" mx-auto px-4 lg:px-8 py-8">
|
||||
|
||||
<!-- Search + Filters -->
|
||||
<div class="glass rounded-3xl p-6 mb-6">
|
||||
@@ -387,6 +390,7 @@ main {
|
||||
class="glass border border-primary/30 text-primary font-bold px-4 py-2.5 rounded-xl text-sm flex items-center gap-2 hover:bg-primary/10 transition-all whitespace-nowrap">
|
||||
<span class="msym text-lg">library_add</span> رفع مجموعة
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -425,6 +429,20 @@ main {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active import banner -->
|
||||
<div id="import-banner" class="hidden mb-4 glass rounded-2xl p-4 border border-primary/20">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2 text-sm font-medium">
|
||||
<span class="msym text-primary animate-spin text-base">progress_activity</span>
|
||||
<span id="import-banner-txt">جاري استيراد الكتب...</span>
|
||||
</div>
|
||||
<span id="import-banner-count" class="text-xs text-primary font-bold"></span>
|
||||
</div>
|
||||
<div class="w-full bg-white/5 rounded-full h-1.5 overflow-hidden">
|
||||
<div id="import-banner-bar" class="h-full bg-primary rounded-full transition-all duration-700" style="width:0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Books -->
|
||||
<div id="books-container"></div>
|
||||
|
||||
@@ -500,13 +518,55 @@ main {
|
||||
<button onclick="closeBulkModal()" class="text-on-surface-variant hover:text-primary"><span class="msym text-xl">close</span></button>
|
||||
</div>
|
||||
|
||||
<!-- Drop Zone -->
|
||||
<div id="bulk-drop" onclick="document.getElementById('bulk-file-input').click()"
|
||||
class="border-2 border-dashed border-primary/20 rounded-2xl p-8 text-center cursor-pointer hover:border-primary/50 hover:bg-primary/5 transition-all">
|
||||
<span class="msym text-primary text-4xl block mb-3">cloud_upload</span>
|
||||
<p class="text-sm font-semibold mb-1">اسحب ملفات PDF هنا أو اضغط للاختيار</p>
|
||||
<p class="text-xs text-on-surface-variant">يمكنك اختيار عدة ملفات في آن واحد</p>
|
||||
<input type="file" id="bulk-file-input" accept=".pdf" multiple class="hidden" onchange="onBulkFilesSelected(this.files)"/>
|
||||
<!-- Tabs -->
|
||||
<div class="flex gap-2 p-1 bg-surface-high rounded-xl">
|
||||
<button id="bulk-tab-local" onclick="switchBulkTab('local')"
|
||||
class="flex-1 py-2 rounded-lg text-sm font-bold flex items-center justify-center gap-2 transition-all bg-primary text-surface">
|
||||
<span class="msym text-base">upload_file</span> من الجهاز
|
||||
</button>
|
||||
<button id="bulk-tab-drive" onclick="switchBulkTab('drive')"
|
||||
class="flex-1 py-2 rounded-lg text-sm font-bold flex items-center justify-center gap-2 transition-all text-on-surface-variant hover:text-primary">
|
||||
<svg class="w-4 h-4" viewBox="0 0 87.3 78" xmlns="http://www.w3.org/2000/svg"><path d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z" fill="#0066da"/><path d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z" fill="#00ac47"/><path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z" fill="#ea4335"/><path d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z" fill="#00832d"/><path d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z" fill="#2684fc"/><path d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z" fill="#ffba00"/></svg>
|
||||
Google Drive
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Local Upload -->
|
||||
<div id="bulk-panel-local">
|
||||
<!-- Drop Zone -->
|
||||
<div id="bulk-drop" onclick="document.getElementById('bulk-file-input').click()"
|
||||
class="border-2 border-dashed border-primary/20 rounded-2xl p-8 text-center cursor-pointer hover:border-primary/50 hover:bg-primary/5 transition-all">
|
||||
<span class="msym text-primary text-4xl block mb-3">cloud_upload</span>
|
||||
<p class="text-sm font-semibold mb-1">اسحب ملفات PDF هنا أو اضغط للاختيار</p>
|
||||
<p class="text-xs text-on-surface-variant">يمكنك اختيار عدة ملفات في آن واحد</p>
|
||||
<input type="file" id="bulk-file-input" accept=".pdf" multiple class="hidden" onchange="onBulkFilesSelected(this.files)"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Google Drive -->
|
||||
<div id="bulk-panel-drive" class="hidden space-y-4">
|
||||
<p class="text-sm text-on-surface-variant">الصق رابط مجلد Google Drive ثم اربط حسابك — يعمل مع أي مجلد مشترك.</p>
|
||||
|
||||
<input id="gdrive-url-input" type="url" dir="ltr"
|
||||
placeholder="https://drive.google.com/drive/folders/..."
|
||||
class="w-full bg-surface/60 border border-white/10 rounded-xl px-4 py-3 text-sm focus:outline-none focus:border-blue-400/60 transition-colors"/>
|
||||
|
||||
<div id="gdrive-status" class="hidden space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span id="gdrive-status-text" class="text-on-surface-variant">جاري المعالجة...</span>
|
||||
<span id="gdrive-count" class="text-primary font-mono font-bold"></span>
|
||||
</div>
|
||||
<div class="h-2 bg-surface rounded-full overflow-hidden">
|
||||
<div id="gdrive-bar" class="h-full bg-blue-400 rounded-full transition-all duration-500" style="width:0%"></div>
|
||||
</div>
|
||||
<div id="gdrive-skipped-list" class="hidden text-xs text-on-surface-variant space-y-1 max-h-32 overflow-y-auto"></div>
|
||||
</div>
|
||||
|
||||
<button onclick="startGdriveOAuth()" id="gdrive-import-btn"
|
||||
class="w-full bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 rounded-xl text-sm flex items-center justify-center gap-2 transition-all">
|
||||
<svg class="w-4 h-4" viewBox="0 0 87.3 78" xmlns="http://www.w3.org/2000/svg"><path d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z" fill="#fff"/><path d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z" fill="#fff"/><path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z" fill="#fff"/><path d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z" fill="#fff"/><path d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z" fill="#fff"/><path d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z" fill="#fff"/></svg>
|
||||
ربط Google Drive واستيراد
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Selected Files Preview -->
|
||||
@@ -605,6 +665,7 @@ async function initAuth() {
|
||||
const r = await fetch('/auth/me', {headers:{'Authorization':'Bearer '+userToken}});
|
||||
if (!r.ok) { localStorage.removeItem('rag_token'); initAuth(); return; }
|
||||
currentUser = await r.json();
|
||||
checkActiveImport();
|
||||
nav.innerHTML = `<div class="relative" id="user-menu-wrap">
|
||||
<button onclick="document.getElementById('udrop').classList.toggle('hidden')" class="glass flex items-center gap-2 px-3 py-2 rounded-xl text-xs hover:border-primary/40 transition-colors">
|
||||
<span class="msym text-base text-primary">person</span>
|
||||
@@ -685,6 +746,77 @@ function debounceSearch(val) {
|
||||
}
|
||||
|
||||
// ── Load Books ─────────────────────────────────────────────────────────────
|
||||
// ── Persistent import banner ──────────────────────────────────────────────
|
||||
let _bannerPoll = null;
|
||||
let _activeBannerJobId = null;
|
||||
|
||||
async function checkActiveImport() {
|
||||
if (!currentUser || !['admin','librarian'].includes(currentUser.role)) return;
|
||||
try {
|
||||
const r = await fetch('/api/library/bulk-jobs/active', {
|
||||
headers: { 'Authorization': 'Bearer ' + userToken }
|
||||
});
|
||||
if (!r.ok) return;
|
||||
const job = await r.json();
|
||||
if (!job || !job.job_id) { hideBanner(); return; }
|
||||
|
||||
_activeBannerJobId = job.job_id;
|
||||
const total = job.total || 0;
|
||||
const done = job.done || 0;
|
||||
const status = job.status || '';
|
||||
|
||||
const banner = document.getElementById('import-banner');
|
||||
const txt = document.getElementById('import-banner-txt');
|
||||
const count = document.getElementById('import-banner-count');
|
||||
const bar = document.getElementById('import-banner-bar');
|
||||
|
||||
banner.classList.remove('hidden');
|
||||
|
||||
const downloaded = job.downloaded || 0;
|
||||
const skipped = job.skipped || 0;
|
||||
const processed = done + skipped;
|
||||
|
||||
if (status === 'downloading' && total === 0) {
|
||||
txt.textContent = '🔍 جاري جلب قائمة الملفات من Google Drive...';
|
||||
count.textContent = '';
|
||||
bar.style.width = '5%';
|
||||
} else if (status === 'downloading') {
|
||||
txt.textContent = '⬇️ جاري تنزيل الكتب...';
|
||||
count.textContent = downloaded + ' / ' + total;
|
||||
bar.style.width = Math.min(40, total > 0 ? Math.round((downloaded/total)*40) : 5) + '%';
|
||||
} else if (status === 'processing' || status === 'pending') {
|
||||
txt.textContent = '⚙️ جاري فهرسة الكتب...';
|
||||
count.textContent = processed + ' / ' + total;
|
||||
const pct = total > 0 ? Math.min(95, 40 + Math.round((processed/total)*55)) : 40;
|
||||
bar.style.width = pct + '%';
|
||||
} else if (status === 'done') {
|
||||
txt.textContent = '✅ اكتمل الاستيراد';
|
||||
count.textContent = done + ' كتاب';
|
||||
bar.style.width = '100%';
|
||||
bar.className = 'h-full bg-green-400 rounded-full transition-all duration-700';
|
||||
setTimeout(() => { hideBanner(); loadBooks(); }, 4000);
|
||||
stopBannerPoll();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_bannerPoll) startBannerPoll();
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function startBannerPoll() {
|
||||
if (_bannerPoll) return;
|
||||
_bannerPoll = setInterval(() => { checkActiveImport(); loadBooks(); }, 8000);
|
||||
}
|
||||
|
||||
function stopBannerPoll() {
|
||||
if (_bannerPoll) { clearInterval(_bannerPoll); _bannerPoll = null; }
|
||||
}
|
||||
|
||||
function hideBanner() {
|
||||
document.getElementById('import-banner').classList.add('hidden');
|
||||
stopBannerPoll();
|
||||
}
|
||||
|
||||
async function loadBooks() {
|
||||
let url = `/api/library?page=${currentPage}&limit=24`;
|
||||
if (filters.search) url += '&search='+encodeURIComponent(filters.search);
|
||||
@@ -711,7 +843,21 @@ async function loadFilters() {
|
||||
// Extract unique authors from books
|
||||
try {
|
||||
const d = await (await fetch('/api/library?limit=200')).json();
|
||||
const authors = [...new Set((d.books||[]).map(b=>b.author).filter(Boolean))];
|
||||
// Normalize Arabic chars before dedup (ي vs ی, ك vs ک, alef variants)
|
||||
function normalizeAr(s) {
|
||||
return (s||'')
|
||||
.replace(/\u06cc/g,'\u064a').replace(/\u06a9/g,'\u0643')
|
||||
.replace(/[\u0622\u0623\u0625]/g,'\u0627')
|
||||
.replace(/\u0629/g,'\u0647')
|
||||
.replace(/\s+/g,' ').trim();
|
||||
}
|
||||
const authorMap = {};
|
||||
(d.books||[]).forEach(b => {
|
||||
if (!b.author) return;
|
||||
const key = normalizeAr(b.author);
|
||||
if (!authorMap[key]) authorMap[key] = b.author;
|
||||
});
|
||||
const authors = Object.values(authorMap);
|
||||
if (authors.length) {
|
||||
document.getElementById('author-section').classList.remove('hidden');
|
||||
document.getElementById('author-chips').innerHTML =
|
||||
@@ -1195,7 +1341,7 @@ async function pollBulkStatus() {
|
||||
const d = await r.json();
|
||||
if (!r.ok) return;
|
||||
|
||||
const processed = (d.done||0) + (d.failed||0);
|
||||
const processed = (d.done||0) + (d.failed||0) + (d.skipped||0);
|
||||
const pct = d.total > 0 ? Math.round(processed / d.total * 100) : 0;
|
||||
document.getElementById('bulk-progress-bar').style.width = pct + '%';
|
||||
document.getElementById('bulk-progress-count').textContent = `${processed} / ${d.total}`;
|
||||
@@ -1216,7 +1362,35 @@ async function pollBulkStatus() {
|
||||
} catch(e) { /* keep polling */ }
|
||||
}
|
||||
|
||||
|
||||
function switchBulkTab(tab) {
|
||||
const isLocal = tab === 'local';
|
||||
// Panels
|
||||
document.getElementById('bulk-panel-local').classList.toggle('hidden', !isLocal);
|
||||
document.getElementById('bulk-panel-drive').classList.toggle('hidden', isLocal);
|
||||
// Tab styles
|
||||
const activeClass = ['bg-primary','text-surface'];
|
||||
const inactiveClass = ['text-on-surface-variant','hover:text-primary'];
|
||||
const localBtn = document.getElementById('bulk-tab-local');
|
||||
const driveBtn = document.getElementById('bulk-tab-drive');
|
||||
if (isLocal) {
|
||||
localBtn.classList.add(...activeClass); localBtn.classList.remove(...inactiveClass);
|
||||
driveBtn.classList.remove(...activeClass); driveBtn.classList.add(...inactiveClass);
|
||||
} else {
|
||||
driveBtn.classList.add(...activeClass); driveBtn.classList.remove(...inactiveClass);
|
||||
localBtn.classList.remove(...activeClass); localBtn.classList.add(...inactiveClass);
|
||||
// reset drive state
|
||||
document.getElementById('gdrive-status').classList.add('hidden');
|
||||
document.getElementById('gdrive-bar').style.width = '0%';
|
||||
document.getElementById('gdrive-bar').className = 'h-full bg-blue-400 rounded-full transition-all duration-500';
|
||||
document.getElementById('gdrive-import-btn').disabled = false;
|
||||
document.getElementById('gdrive-import-btn').innerHTML = '<svg class="w-4 h-4" viewBox="0 0 87.3 78" xmlns="http://www.w3.org/2000/svg"><path d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z" fill="#fff"/><path d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z" fill="#fff"/><path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z" fill="#fff"/><path d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z" fill="#fff"/><path d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z" fill="#fff"/><path d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z" fill="#fff"/></svg> استيراد من Google Drive';
|
||||
}
|
||||
}
|
||||
|
||||
function closeBulkModal() {
|
||||
const hadActiveJob = !!bulkJobId;
|
||||
switchBulkTab('local');
|
||||
document.getElementById('bulk-modal').classList.add('hidden');
|
||||
if (bulkPollTimer) clearInterval(bulkPollTimer);
|
||||
// Reset state for next use
|
||||
@@ -1232,9 +1406,182 @@ function closeBulkModal() {
|
||||
if (warn) warn.remove();
|
||||
const btn = document.getElementById('bulk-upload-btn');
|
||||
if (btn) { btn.disabled=false; btn.innerHTML='<span class="msym text-lg">rocket_launch</span> رفع ومعالجة الكل'; }
|
||||
// Show banner immediately if a job was running
|
||||
if (hadActiveJob) { setTimeout(checkActiveImport, 100); }
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ── Google Drive OAuth Import ────────────────────────────────────────────────
|
||||
let _gdriveState = null;
|
||||
let _gdriveFolderUrl = null;
|
||||
|
||||
function startGdriveOAuth() {
|
||||
const url = (document.getElementById('gdrive-url-input').value || '').trim();
|
||||
if (!url) { alert('الرجاء إدخال رابط المجلد أولاً'); return; }
|
||||
|
||||
_gdriveFolderUrl = url;
|
||||
_gdriveState = null;
|
||||
|
||||
const btn = document.getElementById('gdrive-import-btn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="msym text-lg" style="display:inline-block;animation:spin 1s linear infinite">progress_activity</span> جاري فتح Google...';
|
||||
|
||||
// Open OAuth popup
|
||||
const state = Math.random().toString(36).slice(2);
|
||||
const authUrl = `/auth/google/drive?folder_url=${encodeURIComponent(url)}&state=${state}`;
|
||||
const popup = window.open(authUrl, 'gdrive_oauth',
|
||||
'width=500,height=650,scrollbars=yes,resizable=yes');
|
||||
|
||||
// Listen for postMessage from callback page
|
||||
function onMessage(e) {
|
||||
if (!e.data) return;
|
||||
let data = e.data;
|
||||
if (typeof data === 'string') { try { data = JSON.parse(data); } catch(ex) { return; } }
|
||||
if (!data || !('ok' in data)) return;
|
||||
window.removeEventListener('message', onMessage);
|
||||
if (data.ok) {
|
||||
_gdriveState = data.state;
|
||||
_gdriveFolderUrl = data.folder_url || url;
|
||||
_startGdriveImportOAuth();
|
||||
} else {
|
||||
_gdriveError(data.error || 'فشل التفويض');
|
||||
}
|
||||
}
|
||||
window.addEventListener('message', onMessage);
|
||||
|
||||
// Detect if popup was closed without completing
|
||||
const check = setInterval(() => {
|
||||
if (popup && popup.closed) {
|
||||
clearInterval(check);
|
||||
window.removeEventListener('message', onMessage);
|
||||
if (!_gdriveState) {
|
||||
_gdriveError('تم إغلاق نافذة Google قبل إتمام التفويض');
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function _gdriveError(msg) {
|
||||
const btn = document.getElementById('gdrive-import-btn');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<svg class="w-4 h-4" viewBox="0 0 87.3 78" xmlns="http://www.w3.org/2000/svg"><path d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z" fill="#fff"/><path d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z" fill="#fff"/><path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z" fill="#fff"/><path d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z" fill="#fff"/><path d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z" fill="#fff"/><path d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z" fill="#fff"/></svg> ربط Google Drive واستيراد';
|
||||
const statusBox = document.getElementById('gdrive-status');
|
||||
const statusTxt = document.getElementById('gdrive-status-text');
|
||||
const bar = document.getElementById('gdrive-bar');
|
||||
statusBox.classList.remove('hidden');
|
||||
statusTxt.textContent = '❌ ' + msg;
|
||||
bar.style.width = '100%';
|
||||
bar.className = 'h-full bg-red-500 rounded-full transition-all duration-500';
|
||||
}
|
||||
|
||||
async function _startGdriveImportOAuth() {
|
||||
const btn = document.getElementById('gdrive-import-btn');
|
||||
const statusBox = document.getElementById('gdrive-status');
|
||||
const statusTxt = document.getElementById('gdrive-status-text');
|
||||
const countEl = document.getElementById('gdrive-count');
|
||||
const bar = document.getElementById('gdrive-bar');
|
||||
const skipList = document.getElementById('gdrive-skipped-list');
|
||||
|
||||
btn.innerHTML = '<span class="msym text-lg" style="display:inline-block;animation:spin 1s linear infinite">progress_activity</span> جاري الاستيراد...';
|
||||
statusBox.classList.remove('hidden');
|
||||
statusTxt.textContent = '⬇️ جاري جلب الملفات من Google Drive...';
|
||||
bar.style.width = '10%';
|
||||
bar.className = 'h-full bg-blue-400 rounded-full transition-all duration-500';
|
||||
skipList.classList.add('hidden');
|
||||
|
||||
let data;
|
||||
try {
|
||||
const res = await fetch('/api/library/gdrive-import-oauth', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + userToken },
|
||||
body: JSON.stringify({ state: _gdriveState, folder_url: _gdriveFolderUrl })
|
||||
});
|
||||
data = await res.json();
|
||||
if (!res.ok) {
|
||||
_gdriveError((typeof data.detail === 'object' ? data.detail.message : data.detail) || 'فشل الاستيراد');
|
||||
return;
|
||||
}
|
||||
} catch(e) {
|
||||
_gdriveError(e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const jobId = data.job_id;
|
||||
bar.style.width = '20%';
|
||||
|
||||
// Poll progress
|
||||
const poll = setInterval(async () => {
|
||||
try {
|
||||
const pr = await fetch('/api/library/bulk-status/' + jobId, {
|
||||
headers: { 'Authorization': 'Bearer ' + userToken }
|
||||
});
|
||||
const pd = await pr.json();
|
||||
const status = pd.status || '';
|
||||
const done = (pd.files || []).filter(f => f.status === 'done').length;
|
||||
const errFile = (pd.files || []).find(f => f.status === 'error' && f.filename === '_');
|
||||
|
||||
if (errFile) {
|
||||
clearInterval(poll);
|
||||
_gdriveError(errFile.error || 'فشل الاستيراد');
|
||||
return;
|
||||
}
|
||||
|
||||
const total = pd.total || 0;
|
||||
const dlDone = (pd.files || []).filter(f => f.status !== 'pending' && f.filename !== '_').length;
|
||||
|
||||
if (status === 'downloading') {
|
||||
if (total === 0) {
|
||||
bar.style.width = '8%';
|
||||
bar.className = 'h-full bg-blue-400 rounded-full transition-all duration-500';
|
||||
statusTxt.textContent = '🔍 جاري جلب قائمة الملفات...';
|
||||
countEl.textContent = '';
|
||||
} else {
|
||||
const pct = Math.min(45, 10 + Math.round((dlDone / total) * 35));
|
||||
bar.style.width = pct + '%';
|
||||
bar.className = 'h-full bg-blue-400 rounded-full transition-all duration-500';
|
||||
statusTxt.textContent = '⬇️ جاري التنزيل...';
|
||||
countEl.textContent = dlDone + ' / ' + total;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const safetotal = total || 1;
|
||||
const inProgress = (pd.files || []).filter(f => ['queued','processing','ingesting','compressing','extracting','downloading'].includes(f.status)).length;
|
||||
const downloaded = done + inProgress;
|
||||
const pct = Math.min(95, 10 + Math.round((downloaded / safetotal) * 85));
|
||||
bar.style.width = pct + '%';
|
||||
bar.className = 'h-full bg-primary rounded-full transition-all duration-500';
|
||||
countEl.textContent = done + ' / ' + safetotal;
|
||||
if (inProgress > 0)
|
||||
statusTxt.textContent = '⚙️ تم تحميل ' + downloaded + ' — فهرسة ' + done + ' من ' + safetotal;
|
||||
else
|
||||
statusTxt.textContent = '⚙️ جاري الفهرسة... ' + done + ' من ' + safetotal;
|
||||
|
||||
const skipped = (pd.files || []).filter(f => f.status === 'skipped');
|
||||
if (skipped.length) {
|
||||
skipList.classList.remove('hidden');
|
||||
skipList.innerHTML = '<p class="font-medium mb-1">تم تخطي (' + skipped.length + '):</p>' +
|
||||
skipped.slice(0,5).map(s => '<p>• ' + s.filename + ' — ' + (s.error||'') + '</p>').join('');
|
||||
}
|
||||
|
||||
if (status === 'done' || (status !== 'downloading' && done >= total && total > 0)) {
|
||||
clearInterval(poll);
|
||||
bar.style.width = '100%';
|
||||
bar.className = 'h-full bg-green-400 rounded-full transition-all duration-500';
|
||||
statusTxt.textContent = '✅ اكتمل — تمت فهرسة ' + done + ' كتاب';
|
||||
countEl.textContent = done + ' / ' + total;
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span class="msym text-lg">check_circle</span> تم';
|
||||
setTimeout(() => { loadBooks(); loadFilters(); }, 1500);
|
||||
}
|
||||
} catch(pe) { console.warn('poll error', pe); }
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Keep old startGdriveImport as no-op (unused now)
|
||||
function startGdriveImport() { startGdriveOAuth(); }
|
||||
|
||||
// design-taste-frontend: Spotlight border cards
|
||||
document.querySelectorAll('.spotlight-card').forEach(card => {
|
||||
card.addEventListener('mousemove', e => {
|
||||
@@ -1245,5 +1592,18 @@ document.querySelectorAll('.spotlight-card').forEach(card => {
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<footer id="sticky-footer" class="text-center py-6 mt-8 text-xs text-on-surface-variant border-t border-white/5">
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<a href="/privacy" target="_blank" class="hover:text-primary transition-colors">سياسة الخصوصية</a>
|
||||
<span class="opacity-30">•</span>
|
||||
<a href="/terms" target="_blank" class="hover:text-primary transition-colors">شروط الخدمة</a>
|
||||
<span class="opacity-30">•</span>
|
||||
<span>© 2026 أفاق زيد</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -497,5 +497,14 @@ async function detectLocation() {
|
||||
}, ()=>{ txt.textContent='Location denied'; btn.disabled=false; });
|
||||
}
|
||||
</script>
|
||||
|
||||
<footer style="text-align:center;padding:20px;margin-top:16px;font-size:12px;color:#888;border-top:1px solid rgba(255,255,255,0.06)">
|
||||
<a href="/privacy" target="_blank" style="color:#888;text-decoration:none;margin:0 8px">سياسة الخصوصية</a>
|
||||
•
|
||||
<a href="/terms" target="_blank" style="color:#888;text-decoration:none;margin:0 8px">شروط الخدمة</a>
|
||||
•
|
||||
<span style="margin:0 8px">© 2026 أفاق زيد</span>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="dark" lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>المكتبة — AfakZaid RAG</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script>tailwind.config={darkMode:"class",safelist:["grid","grid-cols-2","grid-cols-3","grid-cols-4","md:grid-cols-3","lg:grid-cols-4","gap-4","space-y-3","col-span-4"],theme:{extend:{colors:{"primary":"#4edea3","surface":"#0c1324","surface-low":"#151b2d","surface-container":"#191f31","surface-high":"#23293c","on-surface":"#dce1fb","on-surface-variant":"#bbcabf","outline-variant":"#3c4a42","secondary":"#95d3ba","tertiary":"#45dfa4","error":"#f87171"},fontFamily:{sans:["Manrope","sans-serif"]}}}}</script>
|
||||
<style>
|
||||
:root {
|
||||
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
||||
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{background:radial-gradient(circle at 50% -20%,#151b2d 0%,#0c1324 100%);font-family:'Manrope',sans-serif;color:#dce1fb;min-height:100vh}
|
||||
|
||||
/* Glass: purposeful, not default — liquid glass per design-taste */
|
||||
.glass{
|
||||
background:rgba(21,27,45,0.5);
|
||||
backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
|
||||
border:1px solid rgba(78,222,163,0.12);
|
||||
box-shadow:inset 0 1px 0 rgba(255,255,255,0.04);
|
||||
}
|
||||
.glass-hover { transition: background 180ms ease, border-color 180ms ease; }
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.glass-hover:hover { background:rgba(78,222,163,0.06); border-color:rgba(78,222,163,0.22); }
|
||||
}
|
||||
|
||||
.glow{box-shadow:0 0 20px rgba(78,222,163,0.15)}
|
||||
.glow-sm{box-shadow:0 0 10px rgba(78,222,163,0.1)}
|
||||
.msym{font-family:'Material Symbols Outlined';font-variation-settings:'FILL' 0,'wght' 400,'GRAD' 0,'opsz' 24;vertical-align:middle;line-height:1}
|
||||
|
||||
/* Filter chips — specific transitions */
|
||||
.filter-chip {
|
||||
padding:6px 14px; border-radius:999px; font-size:12px; font-weight:600; cursor:pointer;
|
||||
border:1px solid rgba(78,222,163,0.15); background:rgba(21,27,45,0.5); color:#bbcabf;
|
||||
transition: background 150ms var(--ease-out), border-color 150ms var(--ease-out), color 150ms var(--ease-out);
|
||||
will-change: transform;
|
||||
}
|
||||
.filter-chip.active { background:rgba(78,222,163,0.12); border-color:rgba(78,222,163,0.4); color:#4edea3; }
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.filter-chip:hover { background:rgba(78,222,163,0.08); border-color:rgba(78,222,163,0.3); color:#4edea3; }
|
||||
}
|
||||
.filter-chip:active { transform: scale(0.97); }
|
||||
|
||||
.scrollbar::-webkit-scrollbar{width:4px}
|
||||
.scrollbar::-webkit-scrollbar-thumb{background:rgba(78,222,163,0.2);border-radius:4px}
|
||||
|
||||
.inp{
|
||||
background:#23293c; border:1px solid #3c4a42; border-radius:12px;
|
||||
padding:10px 14px; color:#dce1fb; font-size:13px; outline:none;
|
||||
transition: border-color 180ms ease, box-shadow 180ms ease;
|
||||
font-family:'Manrope',sans-serif; width:100%;
|
||||
}
|
||||
.inp:focus { border-color:#4edea3; box-shadow:0 0 0 2px rgba(78,222,163,0.1); }
|
||||
|
||||
/* Book card — purposeful hover, only pointer devices */
|
||||
.book-card {
|
||||
transition: transform 200ms var(--ease-out), box-shadow 200ms ease, border-color 200ms ease;
|
||||
will-change: transform;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.book-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(78,222,163,0.1);
|
||||
border-color: rgba(78,222,163,0.28) !important;
|
||||
}
|
||||
}
|
||||
.book-card:active { transform: scale(0.98); }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* Skeleton loading — design-taste: layout-matching shimmer */
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -400px 0; }
|
||||
100% { background-position: 400px 0; }
|
||||
}
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #1a2035 25%, #23293c 50%, #1a2035 75%);
|
||||
background-size: 800px 100%;
|
||||
animation: shimmer 1.4s ease-in-out infinite;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Upload zone */
|
||||
.upload-zone {
|
||||
border: 2px dashed rgba(78,222,163,0.2);
|
||||
transition: border-color 200ms ease, background 200ms ease;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.upload-zone:hover { border-color: rgba(78,222,163,0.45); background: rgba(78,222,163,0.03); }
|
||||
}
|
||||
.upload-zone.dragover { border-color:rgba(78,222,163,0.6); background:rgba(78,222,163,0.05); }
|
||||
|
||||
/* Fav button pulse on add */
|
||||
@keyframes favPulse {
|
||||
0% { transform: scale(1); }
|
||||
40% { transform: scale(1.35); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
.fav-btn.added { animation: favPulse 350ms var(--ease-out); color: #f87171 !important; }
|
||||
|
||||
/* Page entry */
|
||||
@keyframes fadeUp {
|
||||
from { opacity:0; transform:translateY(8px); }
|
||||
to { opacity:1; transform:translateY(0); }
|
||||
}
|
||||
header { animation: fadeUp 350ms var(--ease-out) both; }
|
||||
.search-section { animation: fadeUp 400ms var(--ease-out) 80ms both; }
|
||||
.books-section { animation: fadeUp 400ms var(--ease-out) 160ms both; }
|
||||
|
||||
/* Reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: .01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: .01ms !important;
|
||||
}
|
||||
.skeleton { animation: none; background: #1a2035; }
|
||||
}
|
||||
|
||||
|
||||
/* ── Mobile-visible improvements ─────────────────────────────────────────── */
|
||||
|
||||
/* Larger touch targets — 44px minimum per Apple HIG */
|
||||
button, a, .filter-chip, .book-card, .stat-card, .nav-btn {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Tap feedback — visible on ALL devices including mobile */
|
||||
.book-card:active,
|
||||
.stat-card:active,
|
||||
.filter-chip:active {
|
||||
transform: scale(0.97) !important;
|
||||
transition: transform 100ms ease !important;
|
||||
}
|
||||
|
||||
/* Buttons get a visible press state on tap */
|
||||
button:active:not(:disabled) {
|
||||
filter: brightness(0.9);
|
||||
transform: scale(0.97);
|
||||
transition: transform 100ms ease, filter 100ms ease !important;
|
||||
}
|
||||
|
||||
/* Restore .glow — ensure it stacks correctly */
|
||||
.glow {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.06),
|
||||
0 0 20px rgba(78,222,163,0.15) !important;
|
||||
}
|
||||
.glow-sm {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.04),
|
||||
0 0 10px rgba(78,222,163,0.1) !important;
|
||||
}
|
||||
|
||||
/* Mobile: book cards get a more visible pressed state */
|
||||
@media (max-width: 768px) {
|
||||
.book-card {
|
||||
transition: transform 150ms ease, opacity 150ms ease !important;
|
||||
}
|
||||
.book-card:active {
|
||||
transform: scale(0.97) !important;
|
||||
opacity: 0.85 !important;
|
||||
}
|
||||
|
||||
/* Ensure skeleton is visible on mobile */
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #1a2035 25%, #2a3348 50%, #1a2035 75%) !important;
|
||||
background-size: 800px 100% !important;
|
||||
animation: shimmer 1.2s ease-in-out infinite !important;
|
||||
}
|
||||
|
||||
/* Better spacing on small screens */
|
||||
.filter-chip {
|
||||
padding: 8px 16px !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
}
|
||||
/* ── design-taste-frontend: Liquid Glass + Spotlight ─────────────────────── */
|
||||
|
||||
/* True liquid glass — inner refraction border */
|
||||
/* NOTE: does NOT define outer shadow — .glow still wins */
|
||||
.glass {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.06),
|
||||
inset 0 -1px 0 rgba(0,0,0,0.2);
|
||||
}
|
||||
/* Combined: liquid glass + green glow */
|
||||
.glass.glow {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.06),
|
||||
inset 0 -1px 0 rgba(0,0,0,0.2),
|
||||
0 0 20px rgba(78,222,163,0.18),
|
||||
0 4px 24px rgba(0,0,0,0.15);
|
||||
}
|
||||
.glass.glow-sm {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.06),
|
||||
inset 0 -1px 0 rgba(0,0,0,0.2),
|
||||
0 0 10px rgba(78,222,163,0.12);
|
||||
}
|
||||
/* .glow overrides naturally on top — no !important needed */
|
||||
|
||||
/* Spotlight border card — JS-driven cursor tracking */
|
||||
.spotlight-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.spotlight-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(
|
||||
400px circle at var(--mx, 50%) var(--my, 50%),
|
||||
rgba(78,222,163,0.06),
|
||||
transparent 60%
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 300ms ease;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
z-index: 0;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.spotlight-card:hover::before { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Ensure content above spotlight overlay */
|
||||
.spotlight-card > * { position: relative; z-index: 1; }
|
||||
|
||||
/* Tactile button press — design-taste rule */
|
||||
button:not(:disabled):active,
|
||||
a[role=button]:active {
|
||||
transform: translateY(-1px) scale(0.98) !important;
|
||||
transition-duration: 80ms !important;
|
||||
}
|
||||
|
||||
/* Hardware acceleration on animated elements */
|
||||
.stat-card, .book-card, .query-card, .books-card, .upload-card-wrap {
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
/* Directional hover-aware CTA — shimmer from left on hover */
|
||||
.cta-primary {
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.cta-primary::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: -100%; bottom: 0; width: 50%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
|
||||
transition: left 400ms cubic-bezier(0.77,0,0.175,1);
|
||||
pointer-events: none;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.cta-primary:hover::before { left: 200%; }
|
||||
}
|
||||
|
||||
/* ── Mobile improvements (actually visible on mobile) ────────────────────── */
|
||||
|
||||
/* Larger touch targets — 44px minimum per Apple HIG */
|
||||
@media (max-width: 768px) {
|
||||
/* Bigger tap areas on buttons */
|
||||
button, a { min-height: 44px; }
|
||||
.filter-chip { padding: 8px 16px; font-size: 13px; }
|
||||
|
||||
/* Book cards: more breathing room on mobile */
|
||||
.book-card { padding: 14px !important; }
|
||||
|
||||
/* Stats cards: larger numbers on mobile */
|
||||
.stat-card .stat-num, #s-books, #s-vectors, #s-convos, #s-memory {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
/* Header: tighter on mobile */
|
||||
header { padding-top: 10px; padding-bottom: 10px; }
|
||||
|
||||
/* Search input: larger font so iOS doesn't zoom */
|
||||
#search-input, .inp, textarea, input[type="text"], input[type="email"], input[type="password"] {
|
||||
font-size: 16px !important; /* prevents iOS zoom on focus */
|
||||
}
|
||||
|
||||
/* Bottom action bar spacing */
|
||||
main { padding-bottom: env(safe-area-inset-bottom, 16px); }
|
||||
}
|
||||
|
||||
/* Reader: full height on mobile */
|
||||
@media (max-width: 768px) {
|
||||
#pdf-frame { position: absolute; inset: 0; }
|
||||
.reader-header { padding: 8px 12px; }
|
||||
}
|
||||
|
||||
/* FORCE LAYOUT — overrides everything */
|
||||
main {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 72rem !important;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* Grid view */
|
||||
#books-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
#books-container { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
#books-container { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* List view — overrides grid completely */
|
||||
#books-container.books-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Sticky footer — no layout impact */
|
||||
#sticky-footer { position: sticky; top: 100vh; }
|
||||
</style>
|
||||
<script src="/static/analytics.js" defer></script>
|
||||
<link rel="privacy-policy" href="https://rag.afaqzaid.com/privacy"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="universal-header"></div>
|
||||
|
||||
<main class="max-w-3xl" mx-auto px-4 lg:px-8 py-10">
|
||||
<div class="glass rounded-2xl p-8" style="border:1px solid rgba(78,222,163,0.12)">
|
||||
<div class="flex items-center gap-3 mb-8 pb-6 border-b border-white/5">
|
||||
<div class="w-10 h-10 bg-primary/15 rounded-xl flex items-center justify-center shrink-0">
|
||||
<span class="msym text-primary text-xl">shield</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-xl font-bold">سياسة الخصوصية</h1>
|
||||
<p class="text-xs text-on-surface-variant mt-1">آخر تحديث: مايو 2026</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6 text-on-surface-variant leading-relaxed text-sm">
|
||||
<div><h2 class="text-primary font-bold mb-2">١. المعلومات التي نجمعها</h2>
|
||||
<p>عند استخدامك ميزة استيراد Google Drive، نطلب إذن القراءة فقط للوصول إلى الملفات التي تحددها. لا نخزن رمز الوصول بعد اكتمال عملية الاستيراد.</p></div>
|
||||
<div><h2 class="text-primary font-bold mb-2">٢. كيف نستخدم المعلومات</h2>
|
||||
<p>نستخدم الوصول إلى Google Drive فقط لتنزيل ملفات PDF التي تختارها وفهرستها في مكتبتك. لا نشارك بياناتك مع أطراف ثالثة ولا نستخدمها لأي غرض آخر.</p></div>
|
||||
<div><h2 class="text-primary font-bold mb-2">٣. تخزين البيانات</h2>
|
||||
<p>يتم تخزين الملفات التي ترفعها على خوادمنا الخاصة. رموز OAuth مؤقتة ولا تُحفظ بعد انتهاء جلسة الاستيراد.</p></div>
|
||||
<div><h2 class="text-primary font-bold mb-2">٤. حقوقك</h2>
|
||||
<p>يمكنك في أي وقت حذف ملفاتك من المكتبة أو التواصل معنا لحذف بياناتك بالكامل.</p></div>
|
||||
<div><h2 class="text-primary font-bold mb-2">٥. التواصل</h2>
|
||||
<p>لأي استفسار: <a href="mailto:admin@afaqzaid.com" class="text-primary hover:underline">admin@afaqzaid.com</a></p></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="universal-footer"></div>
|
||||
|
||||
<script src="/components.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+24
-5
@@ -331,7 +331,7 @@ document.title = (bookTitle || 'كتاب') + ' — AfakZaid';
|
||||
|
||||
// Load PDF — public, no auth required
|
||||
if (bookId) {
|
||||
const pdfUrl = `/api/library/${bookId}/pdf`;
|
||||
const pdfUrl = `/api/library/${bookId}/read`;
|
||||
const titleEl = document.getElementById('book-title-loading');
|
||||
if (titleEl) titleEl.textContent = bookTitle || '';
|
||||
|
||||
@@ -370,10 +370,29 @@ if (bookId) {
|
||||
frame.src = `https://docs.google.com/viewer?url=${encodeURIComponent(fullUrl)}&embedded=true`;
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
} else {
|
||||
// Desktop: direct iframe src
|
||||
frame.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;border:none';
|
||||
frame.src = pdfUrl;
|
||||
setTimeout(() => document.getElementById('loading').style.display = 'none', 1000);
|
||||
// Desktop: check book is accessible first then load
|
||||
fetch(pdfUrl, {method:'HEAD'}).then(r => {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
if (r.ok) {
|
||||
frame.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;border:none';
|
||||
frame.src = pdfUrl;
|
||||
} else {
|
||||
frame.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;border:none;display:flex;align-items:center;justify-content:center';
|
||||
frame.srcdoc = `<html dir="rtl"><body style="font-family:sans-serif;display:flex;height:100vh;align-items:center;justify-content:center;background:#0c1324;color:#dce1fb;flex-direction:column;gap:12px">
|
||||
<span style="font-size:48px">📚</span>
|
||||
<p style="font-size:18px;font-weight:bold">الكتاب قيد المعالجة</p>
|
||||
<p style="font-size:14px;color:#888">يتم فهرسة هذا الكتاب حالياً، يرجى المحاولة بعد قليل</p>
|
||||
<a href="/library" style="color:#4edea3;font-size:14px">← العودة للمكتبة</a>
|
||||
</body></html>`;
|
||||
}
|
||||
}).catch(() => {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
frame.srcdoc = `<html dir="rtl"><body style="font-family:sans-serif;display:flex;height:100vh;align-items:center;justify-content:center;background:#0c1324;color:#dce1fb;flex-direction:column;gap:12px">
|
||||
<span style="font-size:48px">⚠️</span>
|
||||
<p style="font-size:18px;font-weight:bold">تعذّر تحميل الكتاب</p>
|
||||
<a href="/library" style="color:#4edea3;font-size:14px">← العودة للمكتبة</a>
|
||||
</body></html>`;
|
||||
});
|
||||
}
|
||||
|
||||
if (userToken) checkFavorite();
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="dark" lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>المكتبة — AfakZaid RAG</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script>tailwind.config={darkMode:"class",safelist:["grid","grid-cols-2","grid-cols-3","grid-cols-4","md:grid-cols-3","lg:grid-cols-4","gap-4","space-y-3","col-span-4"],theme:{extend:{colors:{"primary":"#4edea3","surface":"#0c1324","surface-low":"#151b2d","surface-container":"#191f31","surface-high":"#23293c","on-surface":"#dce1fb","on-surface-variant":"#bbcabf","outline-variant":"#3c4a42","secondary":"#95d3ba","tertiary":"#45dfa4","error":"#f87171"},fontFamily:{sans:["Manrope","sans-serif"]}}}}</script>
|
||||
<style>
|
||||
:root {
|
||||
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
||||
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{background:radial-gradient(circle at 50% -20%,#151b2d 0%,#0c1324 100%);font-family:'Manrope',sans-serif;color:#dce1fb;min-height:100vh}
|
||||
|
||||
/* Glass: purposeful, not default — liquid glass per design-taste */
|
||||
.glass{
|
||||
background:rgba(21,27,45,0.5);
|
||||
backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
|
||||
border:1px solid rgba(78,222,163,0.12);
|
||||
box-shadow:inset 0 1px 0 rgba(255,255,255,0.04);
|
||||
}
|
||||
.glass-hover { transition: background 180ms ease, border-color 180ms ease; }
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.glass-hover:hover { background:rgba(78,222,163,0.06); border-color:rgba(78,222,163,0.22); }
|
||||
}
|
||||
|
||||
.glow{box-shadow:0 0 20px rgba(78,222,163,0.15)}
|
||||
.glow-sm{box-shadow:0 0 10px rgba(78,222,163,0.1)}
|
||||
.msym{font-family:'Material Symbols Outlined';font-variation-settings:'FILL' 0,'wght' 400,'GRAD' 0,'opsz' 24;vertical-align:middle;line-height:1}
|
||||
|
||||
/* Filter chips — specific transitions */
|
||||
.filter-chip {
|
||||
padding:6px 14px; border-radius:999px; font-size:12px; font-weight:600; cursor:pointer;
|
||||
border:1px solid rgba(78,222,163,0.15); background:rgba(21,27,45,0.5); color:#bbcabf;
|
||||
transition: background 150ms var(--ease-out), border-color 150ms var(--ease-out), color 150ms var(--ease-out);
|
||||
will-change: transform;
|
||||
}
|
||||
.filter-chip.active { background:rgba(78,222,163,0.12); border-color:rgba(78,222,163,0.4); color:#4edea3; }
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.filter-chip:hover { background:rgba(78,222,163,0.08); border-color:rgba(78,222,163,0.3); color:#4edea3; }
|
||||
}
|
||||
.filter-chip:active { transform: scale(0.97); }
|
||||
|
||||
.scrollbar::-webkit-scrollbar{width:4px}
|
||||
.scrollbar::-webkit-scrollbar-thumb{background:rgba(78,222,163,0.2);border-radius:4px}
|
||||
|
||||
.inp{
|
||||
background:#23293c; border:1px solid #3c4a42; border-radius:12px;
|
||||
padding:10px 14px; color:#dce1fb; font-size:13px; outline:none;
|
||||
transition: border-color 180ms ease, box-shadow 180ms ease;
|
||||
font-family:'Manrope',sans-serif; width:100%;
|
||||
}
|
||||
.inp:focus { border-color:#4edea3; box-shadow:0 0 0 2px rgba(78,222,163,0.1); }
|
||||
|
||||
/* Book card — purposeful hover, only pointer devices */
|
||||
.book-card {
|
||||
transition: transform 200ms var(--ease-out), box-shadow 200ms ease, border-color 200ms ease;
|
||||
will-change: transform;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.book-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(78,222,163,0.1);
|
||||
border-color: rgba(78,222,163,0.28) !important;
|
||||
}
|
||||
}
|
||||
.book-card:active { transform: scale(0.98); }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* Skeleton loading — design-taste: layout-matching shimmer */
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -400px 0; }
|
||||
100% { background-position: 400px 0; }
|
||||
}
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #1a2035 25%, #23293c 50%, #1a2035 75%);
|
||||
background-size: 800px 100%;
|
||||
animation: shimmer 1.4s ease-in-out infinite;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Upload zone */
|
||||
.upload-zone {
|
||||
border: 2px dashed rgba(78,222,163,0.2);
|
||||
transition: border-color 200ms ease, background 200ms ease;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.upload-zone:hover { border-color: rgba(78,222,163,0.45); background: rgba(78,222,163,0.03); }
|
||||
}
|
||||
.upload-zone.dragover { border-color:rgba(78,222,163,0.6); background:rgba(78,222,163,0.05); }
|
||||
|
||||
/* Fav button pulse on add */
|
||||
@keyframes favPulse {
|
||||
0% { transform: scale(1); }
|
||||
40% { transform: scale(1.35); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
.fav-btn.added { animation: favPulse 350ms var(--ease-out); color: #f87171 !important; }
|
||||
|
||||
/* Page entry */
|
||||
@keyframes fadeUp {
|
||||
from { opacity:0; transform:translateY(8px); }
|
||||
to { opacity:1; transform:translateY(0); }
|
||||
}
|
||||
header { animation: fadeUp 350ms var(--ease-out) both; }
|
||||
.search-section { animation: fadeUp 400ms var(--ease-out) 80ms both; }
|
||||
.books-section { animation: fadeUp 400ms var(--ease-out) 160ms both; }
|
||||
|
||||
/* Reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: .01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: .01ms !important;
|
||||
}
|
||||
.skeleton { animation: none; background: #1a2035; }
|
||||
}
|
||||
|
||||
|
||||
/* ── Mobile-visible improvements ─────────────────────────────────────────── */
|
||||
|
||||
/* Larger touch targets — 44px minimum per Apple HIG */
|
||||
button, a, .filter-chip, .book-card, .stat-card, .nav-btn {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Tap feedback — visible on ALL devices including mobile */
|
||||
.book-card:active,
|
||||
.stat-card:active,
|
||||
.filter-chip:active {
|
||||
transform: scale(0.97) !important;
|
||||
transition: transform 100ms ease !important;
|
||||
}
|
||||
|
||||
/* Buttons get a visible press state on tap */
|
||||
button:active:not(:disabled) {
|
||||
filter: brightness(0.9);
|
||||
transform: scale(0.97);
|
||||
transition: transform 100ms ease, filter 100ms ease !important;
|
||||
}
|
||||
|
||||
/* Restore .glow — ensure it stacks correctly */
|
||||
.glow {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.06),
|
||||
0 0 20px rgba(78,222,163,0.15) !important;
|
||||
}
|
||||
.glow-sm {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.04),
|
||||
0 0 10px rgba(78,222,163,0.1) !important;
|
||||
}
|
||||
|
||||
/* Mobile: book cards get a more visible pressed state */
|
||||
@media (max-width: 768px) {
|
||||
.book-card {
|
||||
transition: transform 150ms ease, opacity 150ms ease !important;
|
||||
}
|
||||
.book-card:active {
|
||||
transform: scale(0.97) !important;
|
||||
opacity: 0.85 !important;
|
||||
}
|
||||
|
||||
/* Ensure skeleton is visible on mobile */
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #1a2035 25%, #2a3348 50%, #1a2035 75%) !important;
|
||||
background-size: 800px 100% !important;
|
||||
animation: shimmer 1.2s ease-in-out infinite !important;
|
||||
}
|
||||
|
||||
/* Better spacing on small screens */
|
||||
.filter-chip {
|
||||
padding: 8px 16px !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
}
|
||||
/* ── design-taste-frontend: Liquid Glass + Spotlight ─────────────────────── */
|
||||
|
||||
/* True liquid glass — inner refraction border */
|
||||
/* NOTE: does NOT define outer shadow — .glow still wins */
|
||||
.glass {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.06),
|
||||
inset 0 -1px 0 rgba(0,0,0,0.2);
|
||||
}
|
||||
/* Combined: liquid glass + green glow */
|
||||
.glass.glow {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.06),
|
||||
inset 0 -1px 0 rgba(0,0,0,0.2),
|
||||
0 0 20px rgba(78,222,163,0.18),
|
||||
0 4px 24px rgba(0,0,0,0.15);
|
||||
}
|
||||
.glass.glow-sm {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.06),
|
||||
inset 0 -1px 0 rgba(0,0,0,0.2),
|
||||
0 0 10px rgba(78,222,163,0.12);
|
||||
}
|
||||
/* .glow overrides naturally on top — no !important needed */
|
||||
|
||||
/* Spotlight border card — JS-driven cursor tracking */
|
||||
.spotlight-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.spotlight-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(
|
||||
400px circle at var(--mx, 50%) var(--my, 50%),
|
||||
rgba(78,222,163,0.06),
|
||||
transparent 60%
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 300ms ease;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
z-index: 0;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.spotlight-card:hover::before { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Ensure content above spotlight overlay */
|
||||
.spotlight-card > * { position: relative; z-index: 1; }
|
||||
|
||||
/* Tactile button press — design-taste rule */
|
||||
button:not(:disabled):active,
|
||||
a[role=button]:active {
|
||||
transform: translateY(-1px) scale(0.98) !important;
|
||||
transition-duration: 80ms !important;
|
||||
}
|
||||
|
||||
/* Hardware acceleration on animated elements */
|
||||
.stat-card, .book-card, .query-card, .books-card, .upload-card-wrap {
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
/* Directional hover-aware CTA — shimmer from left on hover */
|
||||
.cta-primary {
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.cta-primary::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: -100%; bottom: 0; width: 50%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
|
||||
transition: left 400ms cubic-bezier(0.77,0,0.175,1);
|
||||
pointer-events: none;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.cta-primary:hover::before { left: 200%; }
|
||||
}
|
||||
|
||||
/* ── Mobile improvements (actually visible on mobile) ────────────────────── */
|
||||
|
||||
/* Larger touch targets — 44px minimum per Apple HIG */
|
||||
@media (max-width: 768px) {
|
||||
/* Bigger tap areas on buttons */
|
||||
button, a { min-height: 44px; }
|
||||
.filter-chip { padding: 8px 16px; font-size: 13px; }
|
||||
|
||||
/* Book cards: more breathing room on mobile */
|
||||
.book-card { padding: 14px !important; }
|
||||
|
||||
/* Stats cards: larger numbers on mobile */
|
||||
.stat-card .stat-num, #s-books, #s-vectors, #s-convos, #s-memory {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
/* Header: tighter on mobile */
|
||||
header { padding-top: 10px; padding-bottom: 10px; }
|
||||
|
||||
/* Search input: larger font so iOS doesn't zoom */
|
||||
#search-input, .inp, textarea, input[type="text"], input[type="email"], input[type="password"] {
|
||||
font-size: 16px !important; /* prevents iOS zoom on focus */
|
||||
}
|
||||
|
||||
/* Bottom action bar spacing */
|
||||
main { padding-bottom: env(safe-area-inset-bottom, 16px); }
|
||||
}
|
||||
|
||||
/* Reader: full height on mobile */
|
||||
@media (max-width: 768px) {
|
||||
#pdf-frame { position: absolute; inset: 0; }
|
||||
.reader-header { padding: 8px 12px; }
|
||||
}
|
||||
|
||||
/* FORCE LAYOUT — overrides everything */
|
||||
main {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 72rem !important;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* Grid view */
|
||||
#books-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
#books-container { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
#books-container { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* List view — overrides grid completely */
|
||||
#books-container.books-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Sticky footer — no layout impact */
|
||||
#sticky-footer { position: sticky; top: 100vh; }
|
||||
</style>
|
||||
<script src="/static/analytics.js" defer></script>
|
||||
<link rel="privacy-policy" href="https://rag.afaqzaid.com/privacy"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="universal-header"></div>
|
||||
|
||||
<main class="max-w-3xl" mx-auto px-4 lg:px-8 py-10">
|
||||
<div class="glass rounded-2xl p-8" style="border:1px solid rgba(78,222,163,0.12)">
|
||||
<div class="flex items-center gap-3 mb-8 pb-6 border-b border-white/5">
|
||||
<div class="w-10 h-10 bg-primary/15 rounded-xl flex items-center justify-center shrink-0">
|
||||
<span class="msym text-primary text-xl">gavel</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-xl font-bold">شروط الخدمة</h1>
|
||||
<p class="text-xs text-on-surface-variant mt-1">آخر تحديث: مايو 2026</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6 text-on-surface-variant leading-relaxed text-sm">
|
||||
<div><h2 class="text-primary font-bold mb-2">١. الاستخدام المقبول</h2>
|
||||
<p>يُسمح باستخدام المنصة لأغراض تعليمية وبحثية مشروعة. يُمنع رفع محتوى مخالف للحقوق أو القوانين المعمول بها.</p></div>
|
||||
<div><h2 class="text-primary font-bold mb-2">٢. ربط Google Drive</h2>
|
||||
<p>عند ربط حسابك بـ Google Drive، فإنك تمنح المنصة صلاحية القراءة فقط للملفات التي تختار استيرادها. يمكنك إلغاء هذا الإذن في أي وقت من إعدادات حسابك في Google.</p></div>
|
||||
<div><h2 class="text-primary font-bold mb-2">٣. المسؤولية</h2>
|
||||
<p>أنت مسؤول عن المحتوى الذي ترفعه والتأكد من امتلاك حقوق استخدامه.</p></div>
|
||||
<div><h2 class="text-primary font-bold mb-2">٤. تعديل الشروط</h2>
|
||||
<p>نحتفظ بحق تعديل هذه الشروط في أي وقت مع إشعار المستخدمين.</p></div>
|
||||
<div><h2 class="text-primary font-bold mb-2">٥. التواصل</h2>
|
||||
<p>لأي استفسار: <a href="mailto:admin@afaqzaid.com" class="text-primary hover:underline">admin@afaqzaid.com</a></p></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="universal-footer"></div>
|
||||
|
||||
<script src="/components.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user