v1.8.0: bge-m3 embeddings, qwen3 LLM, retrieval fixes, input validation, memory poisoning fix
This commit is contained in:
+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>
|
||||
Reference in New Issue
Block a user