feat: complete AfakZaid RAG system v1.7.4
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
<!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</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@400,0&display=swap" rel="stylesheet"/>
|
||||
<script>tailwind.config={darkMode:"class",theme:{extend:{colors:{"primary":"#4edea3","surface":"#0c1324","surface-high":"#23293c","on-surface":"#dce1fb","on-surface-variant":"#bbcabf","outline-variant":"#3c4a42"},fontFamily:{sans:["Manrope","sans-serif"]}}}}</script>
|
||||
<style>
|
||||
:root { --ease-out: cubic-bezier(0.23,1,0.32,1); }
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
html,body{height:100%;overflow:hidden}
|
||||
body{background:#0c1324;font-family:'Manrope',sans-serif;color:#dce1fb;display:flex;flex-direction:column}
|
||||
.glass{background:rgba(21,27,45,0.6);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)}
|
||||
.msym{font-family:'Material Symbols Outlined';font-variation-settings:'FILL' 0,'wght' 400,'GRAD' 0,'opsz' 24;vertical-align:middle;line-height:1}
|
||||
|
||||
/* Header buttons — specific transitions only */
|
||||
.hdr-btn {
|
||||
transition: background 150ms var(--ease-out), border-color 150ms var(--ease-out), color 150ms var(--ease-out);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.hdr-btn:hover { background: rgba(78,222,163,0.08); border-color: rgba(78,222,163,0.2); }
|
||||
}
|
||||
.hdr-btn:active { transform: scale(0.95); transition-duration: 100ms; }
|
||||
|
||||
/* AI btn glow */
|
||||
.ai-btn {
|
||||
background: rgba(78,222,163,0.12);
|
||||
border: 1px solid rgba(78,222,163,0.25);
|
||||
transition: background 150ms var(--ease-out), box-shadow 200ms ease;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.ai-btn:hover { background: rgba(78,222,163,0.2); box-shadow: 0 0 20px rgba(78,222,163,0.2); }
|
||||
}
|
||||
.ai-btn:active { transform: scale(0.96); transition-duration: 100ms; }
|
||||
|
||||
/* Fav btn — love animation */
|
||||
@keyframes heartBeat {
|
||||
0% { transform: scale(1); }
|
||||
30% { transform: scale(1.4); }
|
||||
60% { transform: scale(0.9); }
|
||||
100%{ transform: scale(1); }
|
||||
}
|
||||
#fav-btn { transition: color 200ms ease, transform 200ms var(--ease-out); }
|
||||
#fav-btn.faved { animation: heartBeat 400ms var(--ease-out); color: #f87171; }
|
||||
#fav-btn:active { transform: scale(0.9); }
|
||||
|
||||
/* PDF frame */
|
||||
#pdf-frame { flex:1; width:100%; height:100%; border:none; background:#1a1a2e; display:block; }
|
||||
|
||||
/* Loading state */
|
||||
#loading {
|
||||
position:absolute; inset:0;
|
||||
display:flex; align-items:center; justify-content:center; flex-direction:column;
|
||||
background:#0c1324; gap:16px; z-index:5;
|
||||
animation: fadeIn 200ms ease both;
|
||||
}
|
||||
@keyframes fadeIn { from{opacity:0} to{opacity:1} }
|
||||
|
||||
/* Loading spinner */
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.loading-ring {
|
||||
width: 40px; height: 40px;
|
||||
border: 2px solid rgba(78,222,163,0.15);
|
||||
border-top-color: #4edea3;
|
||||
border-radius: 50%;
|
||||
animation: spin 800ms linear infinite;
|
||||
}
|
||||
|
||||
/* AI panel slide */
|
||||
#ai-panel {
|
||||
transition: max-height 300ms var(--ease-out), opacity 250ms ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
#ai-panel.collapsed { max-height: 0; opacity: 0; }
|
||||
#ai-panel.open { max-height: 400px; opacity: 1; }
|
||||
|
||||
/* Ask input */
|
||||
#ask-input {
|
||||
transition: border-color 180ms ease, box-shadow 180ms ease;
|
||||
}
|
||||
#ask-input:focus { border-color: #4edea3; box-shadow: 0 0 0 2px rgba(78,222,163,0.1); outline: none; }
|
||||
|
||||
/* prefers-reduced-motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,*::before,*::after { animation-duration:.01ms !important; transition-duration:.01ms !important; }
|
||||
}
|
||||
|
||||
|
||||
/* ── 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; }
|
||||
}
|
||||
</style>
|
||||
<script src="/static/analytics.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Top bar -->
|
||||
<header class="glass border-b border-primary/20 px-4 py-3 flex items-center gap-3 shrink-0">
|
||||
<a href="/library" class="text-on-surface-variant hover:text-primary transition-colors">
|
||||
<span class="msym text-2xl">arrow_back</span>
|
||||
</a>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 id="book-title" class="text-sm font-bold truncate text-primary">جاري التحميل...</h1>
|
||||
<p id="book-author" class="text-xs text-on-surface-variant truncate"></p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="fav-btn" onclick="toggleFavorite()" class="glass flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all hover:border-primary/40">
|
||||
<span class="msym text-lg" id="fav-icon">favorite_border</span>
|
||||
<span id="fav-text" class="hidden sm:inline">إضافة</span>
|
||||
</button>
|
||||
<a id="download-btn" href="#" class="glass flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs text-on-surface-variant hover:text-primary transition-all" download>
|
||||
<span class="msym text-lg">download</span>
|
||||
<span class="hidden sm:inline">تحميل</span>
|
||||
</a>
|
||||
<button onclick="askAboutBook()" class="bg-primary text-surface font-bold px-3 py-2 rounded-xl text-xs flex items-center gap-1.5 hover:shadow-[0_0_15px_rgba(78,222,163,0.4)] transition-all">
|
||||
<span class="msym text-lg">auto_awesome</span>
|
||||
<span class="hidden sm:inline">اسأل</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- PDF Viewer -->
|
||||
<div class="flex-1 relative overflow-hidden" style="height:100%;min-height:0">
|
||||
<div id="loading">
|
||||
<div class="spinner mb-4"></div>
|
||||
<p class="text-on-surface-variant text-sm">جاري تحميل الكتاب...</p>
|
||||
</div>
|
||||
<iframe id="pdf-frame" title="PDF Reader"></iframe>
|
||||
</div>
|
||||
|
||||
<!-- Ask modal -->
|
||||
<div id="ask-modal" class="hidden fixed inset-0 z-50 flex items-end justify-center p-4 bg-black/60">
|
||||
<div class="glass rounded-3xl p-6 w-full max-w-lg">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold flex items-center gap-2"><span class="msym text-primary">auto_awesome</span>اسأل عن هذا الكتاب</h3>
|
||||
<button onclick="document.getElementById('ask-modal').classList.add('hidden')" class="text-on-surface-variant hover:text-primary"><span class="msym text-xl">close</span></button>
|
||||
</div>
|
||||
<textarea id="ask-input" rows="3" dir="rtl" placeholder="اكتب سؤالك..."
|
||||
class="w-full bg-surface-high border border-outline-variant rounded-xl px-4 py-3 text-sm text-on-surface outline-none focus:border-primary transition-colors resize-none mb-3"></textarea>
|
||||
<button onclick="doAsk()" class="w-full bg-primary text-surface font-bold py-3 rounded-xl text-sm hover:shadow-[0_0_20px_rgba(78,222,163,0.4)] transition-all">
|
||||
<span class="msym text-lg align-middle mr-1">search</span> بحث
|
||||
</button>
|
||||
<div id="ask-answer" class="hidden mt-4 bg-surface-high rounded-xl p-4 text-sm leading-7 max-h-48 overflow-y-auto text-on-surface"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const params = new URLSearchParams(location.search);
|
||||
const bookId = params.get('id');
|
||||
const bookTitle = decodeURIComponent(params.get('title') || '');
|
||||
const bookAuthor = decodeURIComponent(params.get('author') || '');
|
||||
const userToken = localStorage.getItem('rag_token');
|
||||
const API_KEY = 'zRf77E_brSEnA2BUi40uxLxT4uofAk661WMZb-cdv4M';
|
||||
let isFavorite = false;
|
||||
|
||||
// Set title
|
||||
document.getElementById('book-title').textContent = bookTitle || 'كتاب';
|
||||
document.getElementById('book-author').textContent = bookAuthor;
|
||||
document.title = (bookTitle || 'كتاب') + ' — AfakZaid';
|
||||
|
||||
// Load PDF — public, no auth required
|
||||
if (bookId) {
|
||||
const pdfUrl = `/api/library/${bookId}/pdf`;
|
||||
const titleEl = document.getElementById('book-title-loading');
|
||||
if (titleEl) titleEl.textContent = bookTitle || '';
|
||||
|
||||
// Reading progress — restore last page
|
||||
const savedPage = localStorage.getItem(`progress_${bookId}`);
|
||||
if (savedPage && parseInt(savedPage) > 1) {
|
||||
const resume = confirm(`استئناف القراءة من صفحة ${savedPage}؟`);
|
||||
if (resume) {
|
||||
window.history.replaceState({}, '', `?id=${bookId}&title=${encodeURIComponent(bookTitle||'')}&page=${savedPage}`);
|
||||
}
|
||||
}
|
||||
const downloadUrl = `/api/library/${bookId}/download`;
|
||||
const frame = document.getElementById('pdf-frame');
|
||||
const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
||||
|
||||
document.getElementById('download-btn').href = downloadUrl;
|
||||
document.getElementById('download-btn').removeAttribute('onclick');
|
||||
|
||||
function showPdfError() {
|
||||
document.getElementById('loading').style.display = 'flex';
|
||||
document.getElementById('loading').innerHTML = `
|
||||
<div class="text-center px-8">
|
||||
<span class="msym text-6xl text-on-surface-variant/30 block mb-4">picture_as_pdf</span>
|
||||
<p class="text-on-surface-variant font-semibold mb-2">PDF غير متاح حالياً</p>
|
||||
<p class="text-xs text-on-surface-variant/60 mb-6">يجب إعادة رفع الكتاب لتفعيل القراءة</p>
|
||||
<a href="${downloadUrl}" class="bg-primary/10 border border-primary/20 text-primary px-6 py-3 rounded-xl text-sm inline-block">
|
||||
تحميل الكتاب
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
// Mobile browsers can't render PDFs in iframes — use Google Docs viewer
|
||||
const fullUrl = window.location.origin + pdfUrl;
|
||||
frame.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;border:none';
|
||||
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);
|
||||
}
|
||||
|
||||
if (userToken) checkFavorite();
|
||||
}
|
||||
|
||||
async function checkFavorite() {
|
||||
try {
|
||||
const r = await fetch(`/api/favorites/check/${bookId}`, {headers:{'Authorization':'Bearer '+userToken}});
|
||||
const d = await r.json();
|
||||
isFavorite = d.is_favorite;
|
||||
updateFavBtn();
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function updateFavBtn() {
|
||||
document.getElementById('fav-icon').textContent = isFavorite ? 'favorite' : 'favorite_border';
|
||||
document.getElementById('fav-icon').style.color = isFavorite ? '#f87171' : '';
|
||||
document.getElementById('fav-text').textContent = isFavorite ? 'مفضلة' : 'إضافة';
|
||||
}
|
||||
|
||||
async function toggleFavorite() {
|
||||
if (!userToken) { window.location.href='/login'; return; }
|
||||
try {
|
||||
const method = isFavorite ? 'DELETE' : 'POST';
|
||||
await fetch(`/api/favorites/${bookId}`, {method, headers:{'Authorization':'Bearer '+userToken}});
|
||||
isFavorite = !isFavorite;
|
||||
updateFavBtn();
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function askAboutBook() {
|
||||
document.getElementById('ask-modal').classList.remove('hidden');
|
||||
document.getElementById('ask-input').focus();
|
||||
}
|
||||
|
||||
async function doAsk() {
|
||||
const q = document.getElementById('ask-input').value.trim();
|
||||
if (!q) return;
|
||||
const ansEl = document.getElementById('ask-answer');
|
||||
ansEl.classList.remove('hidden');
|
||||
ansEl.textContent = '...';
|
||||
try {
|
||||
const r = await fetch('/query', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json','X-API-Key':API_KEY},
|
||||
body: JSON.stringify({query: q + ' في كتاب: ' + bookTitle, session_id: 'reader-'+(bookId||'guest')})
|
||||
});
|
||||
const d = await r.json();
|
||||
ansEl.textContent = d.answer;
|
||||
} catch(e) { ansEl.textContent = 'خطأ: ' + e.message; }
|
||||
}
|
||||
|
||||
document.getElementById('ask-input').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); doAsk(); }
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user