648 lines
32 KiB
HTML
648 lines
32 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 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",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"},fontFamily:{sans:["Manrope","sans-serif"]}}}}</script>
|
|
<style>
|
|
/* ── Emil design vars ─────────────────────────────────────────────────────── */
|
|
:root {
|
|
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
|
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
|
|
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
|
|
}
|
|
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{background:radial-gradient(circle at 50% -20%,#151b2d 0%,#0c1324 100%);font-family:'Manrope',sans-serif;color:#dce1fb;min-height:100vh;overflow-x:hidden}
|
|
.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)}
|
|
|
|
/* Exact properties — never transition:all */
|
|
.glass-hover {
|
|
transition:background 200ms ease, border-color 200ms ease;
|
|
}
|
|
@media (hover: hover) and (pointer: fine) {
|
|
.glass-hover:hover{background:rgba(78,222,163,0.06);border-color:rgba(78,222,163,0.25)}
|
|
}
|
|
|
|
.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}
|
|
|
|
/* Upload zone — specific props, no transform on touch */
|
|
.upload-zone{
|
|
border:2px dashed rgba(78,222,163,0.2);
|
|
transition:border-color 200ms ease, background 200ms ease;
|
|
}
|
|
.upload-zone.dragover{border-color:rgba(78,222,163,0.6);background:rgba(78,222,163,0.05)}
|
|
@media (hover: hover) and (pointer: fine) {
|
|
.upload-zone:hover{border-color:rgba(78,222,163,0.4);background:rgba(78,222,163,0.03)}
|
|
}
|
|
|
|
.scrollbar::-webkit-scrollbar{width:4px}
|
|
.scrollbar::-webkit-scrollbar-thumb{background:rgba(78,222,163,0.2);border-radius:4px}
|
|
|
|
/* ── Entry keyframes ──────────────────────────────────────────────────────── */
|
|
/* Displacement: 8px max — subtle, professional */
|
|
@keyframes fadeUp {
|
|
from { opacity:0; transform:translateY(8px); }
|
|
to { opacity:1; transform:translateY(0); }
|
|
}
|
|
@keyframes fadeIn {
|
|
from { opacity:0; }
|
|
to { opacity:1; }
|
|
}
|
|
@keyframes slideDown {
|
|
from { opacity:0; transform:translateY(-8px); }
|
|
to { opacity:1; transform:translateY(0); }
|
|
}
|
|
/* Panels: 16px — noticeable but not theatrical */
|
|
@keyframes slideRight {
|
|
from { opacity:0; transform:translateX(16px); }
|
|
to { opacity:1; transform:translateX(0); }
|
|
}
|
|
@keyframes slideLeft {
|
|
from { opacity:0; transform:translateX(-16px); }
|
|
to { opacity:1; transform:translateX(0); }
|
|
}
|
|
/* Scale from 0.95 not 0.88 — nothing appears from nothing */
|
|
@keyframes scalePop {
|
|
0% { opacity:0; transform:scale(0.95); }
|
|
100% { opacity:1; transform:scale(1); }
|
|
}
|
|
/* Answer box entry */
|
|
@keyframes fadeUpFast {
|
|
from { opacity:0; transform:translateY(6px); }
|
|
to { opacity:1; transform:translateY(0); }
|
|
}
|
|
|
|
/* ── Ambient orb — slow linear drift, off main thread ────────────────────── */
|
|
@keyframes orbDrift {
|
|
0%,100% { transform:translate(0,0) scale(1); }
|
|
50% { transform:translate(-8px,10px) scale(1.05); }
|
|
}
|
|
@keyframes orbDrift2 {
|
|
0%,100% { transform:translate(0,0) scale(1); }
|
|
50% { transform:translate(10px,-8px) scale(0.97); }
|
|
}
|
|
|
|
/* ── Scan line on brand icon — runs once on load, then stops ─────────────── */
|
|
@keyframes scanLine {
|
|
0% { top:-100%; opacity:.5; }
|
|
100% { top:200%; opacity:0; }
|
|
}
|
|
|
|
/* ── Scroll reveal ────────────────────────────────────────────────────────── */
|
|
.reveal {
|
|
opacity:0;
|
|
transform:translateY(8px);
|
|
/* ease-out: entering element, starts fast */
|
|
transition:opacity 400ms var(--ease-out), transform 400ms var(--ease-out);
|
|
}
|
|
.reveal.visible { opacity:1; transform:translateY(0); }
|
|
|
|
/* ── Entry sequencing ────────────────────────────────────────────────────── */
|
|
/* Header: instant, just a subtle drop */
|
|
header { animation:slideDown 350ms var(--ease-out) both; }
|
|
|
|
/* Stats: stagger 50ms — Emil says 30-80ms */
|
|
.stat-card { animation:scalePop 400ms var(--ease-out) both; }
|
|
.stat-card:nth-child(1) { animation-delay: 80ms; }
|
|
.stat-card:nth-child(2) { animation-delay:130ms; }
|
|
.stat-card:nth-child(3) { animation-delay:180ms; }
|
|
.stat-card:nth-child(4) { animation-delay:230ms; }
|
|
|
|
/* Panels enter slightly later */
|
|
.query-card { animation:slideLeft 450ms var(--ease-out) 300ms both; }
|
|
.upload-card-wrap { animation:slideRight 450ms var(--ease-out) 340ms both; }
|
|
.books-card { animation:fadeUp 400ms var(--ease-out) 440ms both; }
|
|
|
|
/* ── Stat card — hover only on pointer devices ───────────────────────────── */
|
|
.stat-card {
|
|
cursor:default;
|
|
will-change:transform;
|
|
}
|
|
.stat-num {
|
|
font-variant-numeric:tabular-nums;
|
|
transition:color 200ms ease;
|
|
}
|
|
@media (hover: hover) and (pointer: fine) {
|
|
.stat-card {
|
|
transition:transform 200ms var(--ease-out), box-shadow 200ms ease;
|
|
}
|
|
.stat-card:hover {
|
|
transform:translateY(-3px) scale(1.02);
|
|
box-shadow:0 8px 24px rgba(78,222,163,0.14);
|
|
}
|
|
.stat-card:hover .stat-num { color:#fff; }
|
|
}
|
|
/* Active state — works on all devices */
|
|
.stat-card:active { transform:scale(0.97); }
|
|
|
|
/* ── Book cards ──────────────────────────────────────────────────────────── */
|
|
.book-card {
|
|
will-change:transform;
|
|
}
|
|
@media (hover: hover) and (pointer: fine) {
|
|
.book-card {
|
|
transition:transform 200ms var(--ease-out), box-shadow 200ms ease, border-color 200ms ease;
|
|
}
|
|
.book-card:hover {
|
|
transform:translateY(-2px);
|
|
box-shadow:0 6px 20px rgba(78,222,163,0.12);
|
|
border-color:rgba(78,222,163,0.28) !important;
|
|
}
|
|
}
|
|
.book-card:active { transform:scale(0.98); }
|
|
|
|
/* ── Query button ────────────────────────────────────────────────────────── */
|
|
#query-btn {
|
|
/* No constant animation — button is used frequently */
|
|
transition:transform 150ms var(--ease-out), box-shadow 200ms ease, filter 200ms ease;
|
|
position:relative; overflow:hidden;
|
|
}
|
|
/* Glow only on focus/hover — not constantly */
|
|
@media (hover: hover) and (pointer: fine) {
|
|
#query-btn:hover {
|
|
transform:scale(1.015);
|
|
box-shadow:0 0 28px rgba(78,222,163,0.45);
|
|
filter:brightness(1.08);
|
|
}
|
|
}
|
|
#query-btn:focus-visible {
|
|
box-shadow:0 0 0 3px rgba(78,222,163,0.4);
|
|
outline:none;
|
|
}
|
|
/* Active: fast press feedback — Emil says 100-160ms */
|
|
#query-btn:active { transform:scale(0.97); transition-duration:100ms; }
|
|
|
|
/* Shimmer on hover */
|
|
#query-btn::after {
|
|
content:'';
|
|
position:absolute; inset:0;
|
|
background:linear-gradient(90deg, transparent 0%, rgba(255,255,255,.1) 50%, transparent 100%);
|
|
transform:translateX(-100%);
|
|
transition:transform 380ms var(--ease-in-out);
|
|
pointer-events:none;
|
|
}
|
|
@media (hover: hover) and (pointer: fine) {
|
|
#query-btn:hover::after { transform:translateX(100%); }
|
|
}
|
|
|
|
/* ── Brand icon scan line — fires once on load ───────────────────────────── */
|
|
.brand-icon-wrap { position:relative; overflow:hidden; }
|
|
.brand-icon-wrap::after {
|
|
content:'';
|
|
position:absolute; left:0; right:0; height:30%;
|
|
background:linear-gradient(to bottom, transparent, rgba(78,222,163,.25), transparent);
|
|
animation:scanLine 1.8s var(--ease-in-out) 0.6s 1 forwards;
|
|
opacity:0;
|
|
}
|
|
|
|
/* ── Answer reveal ───────────────────────────────────────────────────────── */
|
|
#answer-wrap { animation:fadeUpFast 280ms var(--ease-out) both; }
|
|
|
|
/* ── Textarea ────────────────────────────────────────────────────────────── */
|
|
#query-input {
|
|
transition:border-color 200ms ease, box-shadow 200ms ease;
|
|
}
|
|
#query-input:focus { box-shadow:0 0 0 2px rgba(78,222,163,.12); }
|
|
|
|
/* ── Orbs: slow linear, off main thread ─────────────────────────────────── */
|
|
.orb-1 { animation:orbDrift 18s linear infinite; }
|
|
.orb-2 { animation:orbDrift2 24s linear infinite 3s; }
|
|
|
|
/* ── prefers-reduced-motion — keep opacity fades, remove transforms ──────── */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
*,*::before,*::after {
|
|
animation-duration:.01ms !important;
|
|
animation-iteration-count:1 !important;
|
|
transition-duration:.01ms !important;
|
|
}
|
|
.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>
|
|
|
|
<!-- Ambient orbs -->
|
|
<div class="fixed top-0 right-0 -z-10 w-96 h-96 bg-primary/5 rounded-full blur-[120px] pointer-events-none orb-1"></div>
|
|
<div class="fixed bottom-0 left-0 -z-10 w-72 h-72 bg-tertiary/5 rounded-full blur-[100px] pointer-events-none orb-2"></div>
|
|
<div class="fixed top-1/2 left-1/2 -z-10 w-64 h-64 bg-secondary/3 rounded-full blur-[140px] pointer-events-none" style="transform:translate(-50%,-50%);animation:orbFloat 16s ease-in-out infinite 4s"></div>
|
|
|
|
<!-- Header -->
|
|
<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-3 flex items-center justify-between gap-3">
|
|
<div class="flex items-center gap-3 shrink-0">
|
|
<div class="w-8 h-8 bg-primary/15 rounded-xl flex items-center justify-center shrink-0 brand-icon-wrap">
|
|
<span class="msym text-primary text-base">auto_awesome</span>
|
|
</div>
|
|
<div class="min-w-0 hidden sm:block">
|
|
<h1 class="text-sm font-bold text-primary leading-tight truncate" 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>
|
|
<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-1 px-2.5 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 shrink-0">
|
|
<div id="nav-user" class="shrink-0"></div>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="max-w-6xl mx-auto px-4 lg:px-8 py-8">
|
|
|
|
<!-- Stats -->
|
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
|
<div class="glass glow rounded-2xl p-4 text-center stat-card">
|
|
<div class="text-2xl font-bold text-primary stat-num" id="s-books">0</div>
|
|
<div class="text-xs text-on-surface-variant mt-1">كتاب</div>
|
|
</div>
|
|
<div class="glass glow rounded-2xl p-4 text-center stat-card">
|
|
<div class="text-2xl font-bold text-primary stat-num" id="s-vectors">0</div>
|
|
<div class="text-xs text-on-surface-variant mt-1">متجه</div>
|
|
</div>
|
|
<div class="glass glow rounded-2xl p-4 text-center stat-card">
|
|
<div class="text-2xl font-bold text-secondary stat-num" id="s-convos">0</div>
|
|
<div class="text-xs text-on-surface-variant mt-1">محادثة</div>
|
|
</div>
|
|
<div class="glass glow rounded-2xl p-4 text-center stat-card">
|
|
<div class="text-2xl font-bold text-tertiary stat-num" id="s-memory">0</div>
|
|
<div class="text-xs text-on-surface-variant mt-1">ذاكرة</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
|
|
|
<!-- Query -->
|
|
<div class="glass rounded-3xl p-6 query-card">
|
|
<h2 class="text-primary text-xs font-bold uppercase tracking-widest mb-4 flex items-center gap-2">
|
|
<span class="msym text-base">search</span> اسأل الكتب
|
|
</h2>
|
|
<textarea id="query-input" rows="3" dir="rtl"
|
|
class="w-full bg-surface-high border border-outline-variant rounded-2xl px-4 py-3 text-on-surface text-sm resize-none outline-none focus:border-primary transition-colors mb-3"
|
|
placeholder="اكتب سؤالك هنا..."></textarea>
|
|
<button onclick="doQuery()" id="query-btn"
|
|
class="w-full bg-primary text-surface font-bold py-3 rounded-xl flex items-center justify-center gap-2">
|
|
<span class="msym text-lg">search</span> بحث
|
|
</button>
|
|
<div id="answer-wrap" class="hidden mt-4">
|
|
<div id="answer-box" class="bg-surface-high rounded-2xl p-4 text-sm leading-7 text-on-surface whitespace-pre-wrap max-h-64 overflow-y-auto scrollbar"></div>
|
|
<div id="answer-meta" class="flex flex-wrap gap-2 mt-3"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Upload -->
|
|
<div class="upload-card-wrap">
|
|
<div class="glass rounded-3xl p-6 h-full" id="upload-card">
|
|
<h2 class="text-primary text-xs font-bold uppercase tracking-widest mb-4 flex items-center gap-2">
|
|
<span class="msym text-base">upload_file</span> رفع كتاب PDF
|
|
</h2>
|
|
<div class="upload-zone rounded-2xl p-8 text-center cursor-pointer relative mb-4" id="drop-zone">
|
|
<input type="file" accept=".pdf" multiple class="absolute inset-0 opacity-0 cursor-pointer w-full h-full" onchange="handleFiles(this.files)"/>
|
|
<div class="w-14 h-14 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-3 glow" style="transition:transform .3s">
|
|
<span class="msym text-primary text-3xl">cloud_upload</span>
|
|
</div>
|
|
<p class="text-on-surface-variant text-sm">اسحب ملفات PDF وأفلتها هنا</p>
|
|
<p class="text-primary text-xs font-bold mt-1">أو انقر للاختيار</p>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<input type="text" id="url-input" dir="ltr" placeholder="https://example.com/book.pdf"
|
|
class="flex-1 bg-surface-high border border-outline-variant rounded-xl px-3 py-2 text-xs text-on-surface outline-none focus:border-primary transition-colors"/>
|
|
<button onclick="ingestURL()" class="bg-primary/10 border border-primary/30 text-primary px-3 py-2 rounded-xl text-xs font-bold hover:bg-primary/20 transition-colors flex items-center gap-1">
|
|
<span class="msym text-sm">add</span>
|
|
</button>
|
|
</div>
|
|
<div id="upload-progress" class="hidden mt-3">
|
|
<div class="bg-surface-high rounded-full h-1.5 overflow-hidden">
|
|
<div id="progress-fill" class="h-1.5 bg-primary rounded-full transition-all duration-500" style="width:0%"></div>
|
|
</div>
|
|
<p id="progress-text" class="text-xs text-on-surface-variant mt-2"></p>
|
|
</div>
|
|
<pre id="log-box" class="hidden mt-2 text-xs text-on-surface-variant bg-surface-high rounded-xl p-3 max-h-24 overflow-y-auto scrollbar font-mono"></pre>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Books -->
|
|
<div class="glass rounded-3xl p-6 books-card reveal" id="books-section">
|
|
<div class="flex items-center justify-between mb-5">
|
|
<h2 class="text-primary text-xs font-bold uppercase tracking-widest flex items-center gap-2">
|
|
<span class="msym text-base">library_books</span> الكتب المُدرجة
|
|
</h2>
|
|
<a href="/library" class="text-xs text-on-surface-variant hover:text-primary transition-colors flex items-center gap-1">
|
|
عرض الكل <span class="msym text-sm">arrow_back</span>
|
|
</a>
|
|
</div>
|
|
<div id="books-list" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"></div>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<script>
|
|
const API_KEY = 'zRf77E_brSEnA2BUi40uxLxT4uofAk661WMZb-cdv4M';
|
|
let userToken = localStorage.getItem('rag_token');
|
|
let currentUser = null;
|
|
|
|
// ── Branding ──────────────────────────────────────────────────────────────────
|
|
|
|
function applyBrandingColors(p, s) {
|
|
p = p || '#4edea3'; s = s || '#95d3ba';
|
|
const hr = (hex) => { const r=parseInt(hex.slice(1,3),16),g=parseInt(hex.slice(3,5),16),b=parseInt(hex.slice(5,7),16); return r+','+g+','+b; };
|
|
const pr = hr(p), sr = hr(s);
|
|
const old = document.getElementById('_bco'); if(old) old.remove();
|
|
const st = document.createElement('style'); st.id='_bco';
|
|
st.textContent=
|
|
'.text-primary{color:'+p+'!important}.bg-primary{background-color:'+p+'!important}.border-primary{border-color:'+p+'!important}'+
|
|
'.text-secondary{color:'+s+'!important}.bg-secondary{background-color:'+s+'!important}'+
|
|
'.glass{border-color:rgba('+pr+',0.12)!important}'+
|
|
'.glass-hover:hover{background:rgba('+pr+',0.06)!important;border-color:rgba('+pr+',0.25)!important}'+
|
|
'.glow{box-shadow:0 0 20px rgba('+pr+',0.15)!important}'+
|
|
'.glow-sm{box-shadow:0 0 10px rgba('+pr+',0.1)!important}'+
|
|
'.glass.glow{box-shadow:inset 0 1px 0 rgba(255,255,255,0.06),0 0 20px rgba('+pr+',0.18),0 4px 24px rgba(0,0,0,0.15)!important}'+
|
|
'.upload-zone{border-color:rgba('+pr+',0.2)!important}'+
|
|
'.upload-zone:hover{border-color:rgba('+pr+',0.4)!important}'+
|
|
'.scrollbar::-webkit-scrollbar-thumb{background:rgba('+pr+',0.2)!important}';
|
|
document.head.appendChild(st);
|
|
document.documentElement.style.setProperty('--primary',p);
|
|
document.documentElement.style.setProperty('--secondary',s);
|
|
}
|
|
fetch('/branding').then(r=>r.json()).then(b=>{
|
|
if(b.site_name){document.title=b.site_name;const bn=document.getElementById('brand-name');if(bn)bn.textContent=b.site_name;}
|
|
if(b.site_subtitle){const bs=document.getElementById('brand-sub');if(bs)bs.textContent=b.site_subtitle;}
|
|
if(b.primary_color||b.secondary_color){applyBrandingColors(b.primary_color,b.secondary_color);}
|
|
if(b.logo_url){
|
|
const logo=document.getElementById('brand-logo');
|
|
if(logo){logo.src=b.logo_url;logo.style.height=(b.logo_size||'40')+'px';logo.classList.remove('hidden');}
|
|
}
|
|
}).catch(()=>{});
|
|
|
|
// ── Counter animation ─────────────────────────────────────────────────────────
|
|
function animateCount(el, target, duration=900) {
|
|
const start = performance.now();
|
|
const from = parseInt(el.textContent.replace(/,/g,'')) || 0;
|
|
const isLarge = target > 999;
|
|
function tick(now) {
|
|
const elapsed = now - start;
|
|
const progress = Math.min(elapsed / duration, 1);
|
|
// ease out cubic
|
|
const eased = 1 - Math.pow(1 - progress, 3);
|
|
const current = Math.round(from + (target - from) * eased);
|
|
el.textContent = isLarge ? current.toLocaleString() : current;
|
|
if (progress < 1) requestAnimationFrame(tick);
|
|
else el.textContent = isLarge ? target.toLocaleString() : target;
|
|
}
|
|
requestAnimationFrame(tick);
|
|
}
|
|
|
|
// ── Scroll reveal ─────────────────────────────────────────────────────────────
|
|
const observer = new IntersectionObserver(entries => {
|
|
entries.forEach(e => { if(e.isIntersecting) { e.target.classList.add('visible'); observer.unobserve(e.target); } });
|
|
}, { threshold: 0.12 });
|
|
document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
|
|
|
|
// ── Auth ──────────────────────────────────────────────────────────────────────
|
|
async function initAuth() {
|
|
const nav = document.getElementById('nav-user');
|
|
if (!userToken) {
|
|
nav.innerHTML = `<a href="/login" class="bg-primary text-surface font-bold px-3 py-2 rounded-xl text-xs hover:shadow-[0_0_15px_rgba(78,222,163,0.4)] transition-all flex items-center gap-1.5 whitespace-nowrap" style="animation:scalePop .4s cubic-bezier(.22,1,.36,1) .8s both"><span class="msym text-base">login</span><span>دخول</span></a>`;
|
|
document.getElementById('upload-card').classList.add('hidden');
|
|
return;
|
|
}
|
|
try {
|
|
const r = await fetch('/auth/me', {headers:{'Authorization':'Bearer '+userToken}});
|
|
if (!r.ok) { localStorage.removeItem('rag_token'); initAuth(); return; }
|
|
currentUser = await r.json();
|
|
const perms = currentUser.permissions || [];
|
|
if (!perms.includes('upload')) document.getElementById('upload-card').classList.add('hidden');
|
|
nav.innerHTML = `
|
|
<div class="flex items-center gap-2" style="animation:fadeIn .4s both .6s">
|
|
<div class="text-right hidden sm:block">
|
|
<p class="text-xs font-bold">${currentUser.name}</p>
|
|
<p class="text-[10px] text-primary">${currentUser.role}</p>
|
|
</div>
|
|
${perms.includes('admin') ? '<a href="/admin" class="glass glass-hover p-2 rounded-xl transition-colors"><span class="msym text-base text-on-surface-variant">admin_panel_settings</span></a>' : ''}
|
|
<button onclick="logout()" class="glass glass-hover p-2 rounded-xl transition-colors"><span class="msym text-base text-on-surface-variant">logout</span></button>
|
|
</div>`;
|
|
} catch(e){}
|
|
}
|
|
|
|
function logout() {
|
|
if(userToken) fetch('/auth/logout',{method:'POST',headers:{'Authorization':'Bearer '+userToken}});
|
|
localStorage.removeItem('rag_token');
|
|
window.location.reload();
|
|
}
|
|
|
|
// ── Stats ─────────────────────────────────────────────────────────────────────
|
|
async function loadStats() {
|
|
try {
|
|
const d = await (await fetch('/stats')).json();
|
|
animateCount(document.getElementById('s-books'), d.books||0);
|
|
animateCount(document.getElementById('s-vectors'), d.vectors||0);
|
|
animateCount(document.getElementById('s-convos'), d.conversations||0);
|
|
animateCount(document.getElementById('s-memory'), d.memories||0);
|
|
} catch(e){}
|
|
}
|
|
|
|
// ── Books ─────────────────────────────────────────────────────────────────────
|
|
async function loadBooks() {
|
|
try {
|
|
const d = await (await fetch('/api/library?limit=6')).json();
|
|
const list = document.getElementById('books-list');
|
|
if (!d.books||!d.books.length) {
|
|
list.innerHTML='<p class="text-on-surface-variant text-sm">لا توجد كتب حتى الآن</p>'; return;
|
|
}
|
|
list.innerHTML = d.books.map((b,i)=>{
|
|
const name = b.title || b.filename.replace(/\.pdf$/i,'').replace(/^\d{4}-\d{2}-\d{2}\s*/,'');
|
|
return `<div class="glass glass-hover rounded-2xl p-4 flex items-center gap-3 cursor-pointer book-card"
|
|
style="animation:fadeUp .5s cubic-bezier(.22,1,.36,1) ${.85+i*.08}s both"
|
|
onclick="window.location='/library'">
|
|
<div class="w-9 h-9 bg-primary/10 rounded-xl flex items-center justify-center shrink-0" style="transition:transform .25s">
|
|
<span class="msym text-primary">menu_book</span>
|
|
</div>
|
|
<div class="min-w-0">
|
|
<p class="text-sm font-medium truncate">${name}</p>
|
|
${b.author?`<p class="text-xs text-on-surface-variant">${b.author}</p>`:''}
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
} catch(e){}
|
|
}
|
|
|
|
// ── Query ─────────────────────────────────────────────────────────────────────
|
|
async function doQuery() {
|
|
const q = document.getElementById('query-input').value.trim();
|
|
if(!q) return;
|
|
const btn = document.getElementById('query-btn');
|
|
btn.disabled = true;
|
|
btn.style.animation = 'none';
|
|
btn.innerHTML = '<span class="msym text-lg" style="animation:spin .8s linear infinite">autorenew</span> جاري البحث...';
|
|
const wrap = document.getElementById('answer-wrap');
|
|
const box = document.getElementById('answer-box');
|
|
const meta = document.getElementById('answer-meta');
|
|
wrap.classList.remove('hidden');
|
|
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/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> بحث';
|
|
}
|
|
|
|
// ── Upload ────────────────────────────────────────────────────────────────────
|
|
async function handleFiles(files){for(const f of files)await uploadFile(f);loadStats();loadBooks();}
|
|
|
|
async function uploadFile(file){
|
|
const prog=document.getElementById('upload-progress'),fill=document.getElementById('progress-fill');
|
|
const txt=document.getElementById('progress-text'),log=document.getElementById('log-box');
|
|
prog.classList.remove('hidden');log.classList.remove('hidden');
|
|
txt.textContent='رفع: '+file.name;fill.style.width='30%';
|
|
const fd=new FormData();fd.append('file',file);
|
|
try{
|
|
fill.style.width='60%';
|
|
const headers={'X-API-Key':API_KEY};
|
|
if(userToken) headers['Authorization']='Bearer '+userToken;
|
|
const r=await fetch('/ingest',{method:'POST',headers,body:fd});
|
|
fill.style.width='100%';
|
|
const d=await r.json();
|
|
txt.textContent='✅ تم: '+file.name;log.textContent=d.log||'Done';
|
|
}catch(e){txt.textContent='❌ خطأ: '+e.message;}
|
|
}
|
|
|
|
async function ingestURL(){
|
|
const url=document.getElementById('url-input').value.trim();if(!url)return;
|
|
const prog=document.getElementById('upload-progress'),fill=document.getElementById('progress-fill');
|
|
const txt=document.getElementById('progress-text'),log=document.getElementById('log-box');
|
|
prog.classList.remove('hidden');log.classList.remove('hidden');
|
|
txt.textContent='جلب الملف...';fill.style.width='40%';
|
|
try{
|
|
fill.style.width='70%';
|
|
const headers={'Content-Type':'application/json','X-API-Key':API_KEY};
|
|
if(userToken) headers['Authorization']='Bearer '+userToken;
|
|
const r=await fetch('/ingest-url',{method:'POST',headers,body:JSON.stringify({url})});
|
|
fill.style.width='100%';
|
|
const d=await r.json();
|
|
txt.textContent='✅ تم: '+d.file;log.textContent=d.log||'Done';
|
|
document.getElementById('url-input').value='';
|
|
loadStats();loadBooks();
|
|
}catch(e){txt.textContent='❌ خطأ: '+e.message;}
|
|
}
|
|
|
|
// ── Upload zone drag ─────────────────────────────────────────────────────────
|
|
const zone = document.getElementById('drop-zone');
|
|
const cloudIcon = zone.querySelector('.w-14');
|
|
zone.addEventListener('dragover',e=>{e.preventDefault();zone.classList.add('dragover');cloudIcon.style.transform='scale(1.15)';});
|
|
zone.addEventListener('dragleave',()=>{zone.classList.remove('dragover');cloudIcon.style.transform='';});
|
|
zone.addEventListener('drop',e=>{e.preventDefault();zone.classList.remove('dragover');cloudIcon.style.transform='';handleFiles(e.dataTransfer.files);});
|
|
|
|
// ── Keyboard shortcut ────────────────────────────────────────────────────────
|
|
document.getElementById('query-input').addEventListener('keydown',e=>{if(e.ctrlKey&&e.key==='Enter')doQuery();});
|
|
|
|
// ── URL pre-fill ─────────────────────────────────────────────────────────────
|
|
const urlQ = new URLSearchParams(location.search).get('q');
|
|
if(urlQ){document.getElementById('query-input').value=decodeURIComponent(urlQ);setTimeout(doQuery,600);}
|
|
|
|
// ── Spin keyframe for loading ────────────────────────────────────────────────
|
|
const spinStyle = document.createElement('style');
|
|
spinStyle.textContent = '@keyframes spin{to{transform:rotate(360deg)}}';
|
|
document.head.appendChild(spinStyle);
|
|
|
|
// ── Init ──────────────────────────────────────────────────────────────────────
|
|
initAuth();
|
|
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>
|