Files
afakzaid-rag/static/reader.html
T

435 lines
16 KiB
HTML

<!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>