Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Scholar RAG Chat</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| *{ | |
| margin:0; | |
| padding:0; | |
| box-sizing:border-box; | |
| } | |
| body{ | |
| font-family:'Inter',sans-serif; | |
| background:#0f0f1a; | |
| color:#e0e0e0; | |
| height:100vh; | |
| overflow:hidden; | |
| display:flex; | |
| } | |
| /* sidebar */ | |
| .sidebar{ | |
| width:340px; | |
| min-width:340px; | |
| background:linear-gradient(180deg,#161628 0%,#1a1a2e 100%); | |
| border-right:1px solid rgba(255,255,255,0.06); | |
| display:flex; | |
| flex-direction:column; | |
| padding:24px 20px; | |
| gap:20px; | |
| height:100vh; | |
| } | |
| .logo{ | |
| display:flex; | |
| align-items:center; | |
| gap:12px; | |
| padding-bottom:16px; | |
| border-bottom:1px solid rgba(255,255,255,0.06); | |
| } | |
| .logo-icon{ | |
| width:42px; | |
| height:42px; | |
| background:linear-gradient(135deg,#6c5ce7,#a855f7); | |
| border-radius:12px; | |
| display:flex; | |
| align-items:center; | |
| justify-content:center; | |
| font-size:20px; | |
| } | |
| .logo h1{ | |
| font-size:18px; | |
| font-weight:700; | |
| color:#fff; | |
| letter-spacing:-0.3px; | |
| } | |
| .logo p{ | |
| font-size:11px; | |
| color:#888; | |
| margin-top:2px; | |
| } | |
| /* cards */ | |
| .card{ | |
| background:rgba(255,255,255,0.03); | |
| border:1px solid rgba(255,255,255,0.06); | |
| padding:18px; | |
| border-radius:14px; | |
| transition:border-color .3s; | |
| } | |
| .card:hover{ | |
| border-color:rgba(168,85,247,0.3); | |
| } | |
| .card-header{ | |
| display:flex; | |
| align-items:center; | |
| gap:10px; | |
| margin-bottom:14px; | |
| } | |
| .card-header .icon{ | |
| width:32px; | |
| height:32px; | |
| border-radius:8px; | |
| display:flex; | |
| align-items:center; | |
| justify-content:center; | |
| font-size:14px; | |
| } | |
| .card-header .icon.pdf{ | |
| background:rgba(239,68,68,0.15); | |
| } | |
| .card-header .icon.web{ | |
| background:rgba(59,130,246,0.15); | |
| } | |
| .card-header h3{ | |
| font-size:14px; | |
| font-weight:600; | |
| color:#fff; | |
| } | |
| /* file input */ | |
| .file-area{ | |
| position:relative; | |
| border:2px dashed rgba(255,255,255,0.1); | |
| border-radius:10px; | |
| padding:14px; | |
| text-align:center; | |
| cursor:pointer; | |
| transition:all .3s; | |
| margin-bottom:12px; | |
| } | |
| .file-area:hover{ | |
| border-color:rgba(168,85,247,0.4); | |
| background:rgba(168,85,247,0.03); | |
| } | |
| .file-area input[type=file]{ | |
| position:absolute; | |
| inset:0; | |
| opacity:0; | |
| cursor:pointer; | |
| } | |
| .file-area .label{ | |
| font-size:12px; | |
| color:#888; | |
| } | |
| .file-area .label span{ | |
| color:#a855f7; | |
| font-weight:600; | |
| } | |
| /* text input */ | |
| input[type=text]{ | |
| width:100%; | |
| padding:10px 14px; | |
| background:rgba(255,255,255,0.05); | |
| border:1px solid rgba(255,255,255,0.1); | |
| border-radius:10px; | |
| color:#fff; | |
| font-size:13px; | |
| font-family:'Inter',sans-serif; | |
| outline:none; | |
| transition:border-color .3s; | |
| margin-bottom:12px; | |
| } | |
| input[type=text]:focus{ | |
| border-color:#a855f7; | |
| } | |
| input[type=text]::placeholder{ | |
| color:#555; | |
| } | |
| /* buttons */ | |
| .btn{ | |
| width:100%; | |
| padding:10px; | |
| border:none; | |
| border-radius:10px; | |
| font-family:'Inter',sans-serif; | |
| font-size:13px; | |
| font-weight:600; | |
| cursor:pointer; | |
| transition:all .3s; | |
| display:flex; | |
| align-items:center; | |
| justify-content:center; | |
| gap:8px; | |
| } | |
| .btn-primary{ | |
| background:linear-gradient(135deg,#6c5ce7,#a855f7); | |
| color:#fff; | |
| } | |
| .btn-primary:hover{ | |
| transform:translateY(-1px); | |
| box-shadow:0 6px 20px rgba(168,85,247,0.35); | |
| } | |
| .btn-blue{ | |
| background:linear-gradient(135deg,#3b82f6,#60a5fa); | |
| color:#fff; | |
| } | |
| .btn-blue:hover{ | |
| transform:translateY(-1px); | |
| box-shadow:0 6px 20px rgba(59,130,246,0.35); | |
| } | |
| /* status */ | |
| .status{ | |
| font-size:11px; | |
| margin-top:8px; | |
| min-height:16px; | |
| color:#34d399; | |
| } | |
| /* sidebar bottom */ | |
| .sidebar-footer{ | |
| margin-top:auto; | |
| padding-top:16px; | |
| border-top:1px solid rgba(255,255,255,0.06); | |
| } | |
| .stats{ | |
| display:flex; | |
| gap:10px; | |
| } | |
| .stat{ | |
| flex:1; | |
| background:rgba(255,255,255,0.03); | |
| border:1px solid rgba(255,255,255,0.06); | |
| border-radius:10px; | |
| padding:12px; | |
| text-align:center; | |
| } | |
| .stat .num{ | |
| font-size:20px; | |
| font-weight:700; | |
| color:#fff; | |
| } | |
| .stat .lbl{ | |
| font-size:10px; | |
| color:#666; | |
| margin-top:2px; | |
| text-transform:uppercase; | |
| letter-spacing:.5px; | |
| } | |
| /* main area */ | |
| .main{ | |
| flex:1; | |
| display:flex; | |
| flex-direction:column; | |
| height:100vh; | |
| } | |
| /* header bar */ | |
| .topbar{ | |
| padding:18px 28px; | |
| border-bottom:1px solid rgba(255,255,255,0.06); | |
| display:flex; | |
| align-items:center; | |
| justify-content:space-between; | |
| background:rgba(22,22,40,0.6); | |
| backdrop-filter:blur(20px); | |
| } | |
| .topbar h2{ | |
| font-size:15px; | |
| font-weight:600; | |
| color:#fff; | |
| } | |
| .topbar .badge{ | |
| font-size:11px; | |
| padding:4px 12px; | |
| background:rgba(52,211,153,0.1); | |
| color:#34d399; | |
| border-radius:20px; | |
| border:1px solid rgba(52,211,153,0.2); | |
| } | |
| /* chat area */ | |
| .chatbox{ | |
| flex:1; | |
| overflow-y:auto; | |
| padding:24px 28px; | |
| display:flex; | |
| flex-direction:column; | |
| gap:16px; | |
| background: | |
| radial-gradient(ellipse at 20% 50%,rgba(168,85,247,0.03) 0%,transparent 50%), | |
| radial-gradient(ellipse at 80% 50%,rgba(59,130,246,0.03) 0%,transparent 50%); | |
| } | |
| .chatbox::-webkit-scrollbar{ | |
| width:6px; | |
| } | |
| .chatbox::-webkit-scrollbar-track{ | |
| background:transparent; | |
| } | |
| .chatbox::-webkit-scrollbar-thumb{ | |
| background:rgba(255,255,255,0.08); | |
| border-radius:3px; | |
| } | |
| /* welcome */ | |
| .welcome{ | |
| display:flex; | |
| flex-direction:column; | |
| align-items:center; | |
| justify-content:center; | |
| flex:1; | |
| text-align:center; | |
| gap:12px; | |
| opacity:0.6; | |
| } | |
| .welcome .big-icon{ | |
| font-size:48px; | |
| margin-bottom:4px; | |
| } | |
| .welcome h3{ | |
| color:#fff; | |
| font-size:18px; | |
| } | |
| .welcome p{ | |
| font-size:13px; | |
| color:#666; | |
| max-width:360px; | |
| line-height:1.6; | |
| } | |
| /* messages */ | |
| .message{ | |
| max-width:78%; | |
| padding:14px 18px; | |
| border-radius:16px; | |
| font-size:14px; | |
| line-height:1.65; | |
| animation:fadeIn .3s ease; | |
| } | |
| @keyframes fadeIn{ | |
| from{opacity:0;transform:translateY(8px)} | |
| to{opacity:1;transform:translateY(0)} | |
| } | |
| .message.user{ | |
| align-self:flex-end; | |
| background:linear-gradient(135deg,#6c5ce7,#a855f7); | |
| color:#fff; | |
| border-bottom-right-radius:4px; | |
| } | |
| .message.bot{ | |
| align-self:flex-start; | |
| background:rgba(255,255,255,0.05); | |
| border:1px solid rgba(255,255,255,0.08); | |
| color:#ddd; | |
| border-bottom-left-radius:4px; | |
| } | |
| .message .sender{ | |
| font-size:10px; | |
| text-transform:uppercase; | |
| letter-spacing:.8px; | |
| margin-bottom:6px; | |
| opacity:0.6; | |
| font-weight:600; | |
| } | |
| /* chunks dropdown */ | |
| details{ | |
| margin-top:12px; | |
| background:rgba(0,0,0,0.2); | |
| padding:10px 14px; | |
| border-radius:10px; | |
| border:1px solid rgba(255,255,255,0.05); | |
| } | |
| summary{ | |
| cursor:pointer; | |
| font-size:12px; | |
| font-weight:600; | |
| color:#a855f7; | |
| list-style:none; | |
| display:flex; | |
| align-items:center; | |
| gap:6px; | |
| } | |
| summary::before{ | |
| content:'▶'; | |
| font-size:9px; | |
| transition:transform .2s; | |
| } | |
| details[open] summary::before{ | |
| transform:rotate(90deg); | |
| } | |
| summary::-webkit-details-marker{ | |
| display:none; | |
| } | |
| .chunk{ | |
| font-size:12px; | |
| margin-top:10px; | |
| padding:10px; | |
| background:rgba(255,255,255,0.03); | |
| border-radius:8px; | |
| border-left:3px solid #a855f7; | |
| line-height:1.5; | |
| color:#aaa; | |
| } | |
| .chunk b{ | |
| color:#ccc; | |
| font-size:11px; | |
| } | |
| /* input bar */ | |
| .input-bar{ | |
| padding:16px 28px; | |
| border-top:1px solid rgba(255,255,255,0.06); | |
| background:rgba(22,22,40,0.8); | |
| backdrop-filter:blur(20px); | |
| } | |
| .input-row{ | |
| display:flex; | |
| gap:10px; | |
| align-items:center; | |
| background:rgba(255,255,255,0.04); | |
| border:1px solid rgba(255,255,255,0.08); | |
| border-radius:14px; | |
| padding:6px 6px 6px 18px; | |
| transition:border-color .3s; | |
| } | |
| .input-row:focus-within{ | |
| border-color:rgba(168,85,247,0.5); | |
| box-shadow:0 0 20px rgba(168,85,247,0.08); | |
| } | |
| .input-row input[type=text]{ | |
| flex:1; | |
| padding:10px 0; | |
| background:transparent; | |
| border:none; | |
| color:#fff; | |
| font-size:14px; | |
| margin-bottom:0; | |
| } | |
| .input-row input[type=text]:focus{ | |
| border-color:transparent; | |
| } | |
| .send-btn{ | |
| width:42px; | |
| height:42px; | |
| border:none; | |
| background:linear-gradient(135deg,#6c5ce7,#a855f7); | |
| border-radius:10px; | |
| cursor:pointer; | |
| display:flex; | |
| align-items:center; | |
| justify-content:center; | |
| transition:all .3s; | |
| flex-shrink:0; | |
| } | |
| .send-btn:hover{ | |
| transform:scale(1.05); | |
| box-shadow:0 4px 16px rgba(168,85,247,0.4); | |
| } | |
| .send-btn svg{ | |
| width:18px; | |
| height:18px; | |
| fill:none; | |
| stroke:#fff; | |
| stroke-width:2.5; | |
| stroke-linecap:round; | |
| stroke-linejoin:round; | |
| } | |
| /* loading */ | |
| .typing{ | |
| display:inline-flex; | |
| gap:4px; | |
| padding:4px 0; | |
| } | |
| .typing span{ | |
| width:7px; | |
| height:7px; | |
| background:#a855f7; | |
| border-radius:50%; | |
| animation:bounce .6s infinite alternate; | |
| } | |
| .typing span:nth-child(2){animation-delay:.2s} | |
| .typing span:nth-child(3){animation-delay:.4s} | |
| @keyframes bounce{ | |
| to{opacity:.3;transform:translateY(-4px)} | |
| } | |
| /* responsive */ | |
| @media(max-width:800px){ | |
| body{flex-direction:column} | |
| .sidebar{ | |
| width:100%; | |
| min-width:unset; | |
| height:auto; | |
| flex-direction:row; | |
| flex-wrap:wrap; | |
| padding:12px; | |
| gap:10px; | |
| } | |
| .logo{width:100%} | |
| .card{flex:1;min-width:200px} | |
| .sidebar-footer{display:none} | |
| .main{height:calc(100vh - 200px)} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Sidebar --> | |
| <aside class="sidebar"> | |
| <div class="logo"> | |
| <div class="logo-icon">📚</div> | |
| <div> | |
| <h1>Scholar RAG</h1> | |
| <p>Intelligent Document Chat</p> | |
| </div> | |
| </div> | |
| <!-- Upload PDF --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <div class="icon pdf">📄</div> | |
| <h3>Upload PDF</h3> | |
| </div> | |
| <div class="file-area"> | |
| <input type="file" id="pdf" accept=".pdf"> | |
| <div class="label">Drop file or <span>browse</span></div> | |
| </div> | |
| <button class="btn btn-primary" onclick="upload()"> | |
| ⬆ Upload & Index | |
| </button> | |
| <p class="status" id="uploadStatus"></p> | |
| </div> | |
| <!-- Scrape Website --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <div class="icon web">🌐</div> | |
| <h3>Scrape Website</h3> | |
| </div> | |
| <input type="text" id="url" placeholder="https://example.com/article"> | |
| <button class="btn btn-blue" onclick="scrape()"> | |
| 🔍 Scrape & Index | |
| </button> | |
| <p class="status" id="scrapeStatus"></p> | |
| </div> | |
| <!-- Footer stats --> | |
| <div class="sidebar-footer"> | |
| <div class="stats"> | |
| <div class="stat"> | |
| <div class="num" id="srcCount">0</div> | |
| <div class="lbl">Sources</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="num" id="msgCount">0</div> | |
| <div class="lbl">Messages</div> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- Main Chat --> | |
| <main class="main"> | |
| <div class="topbar"> | |
| <h2>💬 Chat</h2> | |
| <div class="badge">● RAG Active</div> | |
| </div> | |
| <div class="chatbox" id="chat"> | |
| <div class="welcome"> | |
| <div class="big-icon">🧠</div> | |
| <h3>Ask anything about your documents</h3> | |
| <p>Upload a PDF or scrape a website, then start asking questions. Answers are grounded in your sources.</p> | |
| </div> | |
| </div> | |
| <div class="input-bar"> | |
| <div class="input-row"> | |
| <input type="text" id="msg" placeholder="Ask a question about your documents…" onkeydown="if(event.key==='Enter')send()"> | |
| <button class="send-btn" onclick="send()"> | |
| <svg viewBox="0 0 24 24"><path d="M22 2L11 13"/><path d="M22 2L15 22L11 13L2 9L22 2Z"/></svg> | |
| </button> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| let sourceCount=0 | |
| let messageCount=0 | |
| function updateStats(){ | |
| document.getElementById("srcCount").innerText=sourceCount | |
| document.getElementById("msgCount").innerText=messageCount | |
| } | |
| function clearWelcome(){ | |
| let w=document.querySelector(".welcome") | |
| if(w) w.remove() | |
| } | |
| /* Upload PDF */ | |
| async function upload(){ | |
| let file=document.getElementById("pdf").files[0] | |
| if(!file){ | |
| alert("Select a PDF") | |
| return | |
| } | |
| let status=document.getElementById("uploadStatus") | |
| status.innerText="Uploading…" | |
| status.style.color="#fbbf24" | |
| let formData=new FormData() | |
| formData.append("file",file) | |
| let res=await fetch("/upload",{ | |
| method:"POST", | |
| body:formData | |
| }) | |
| let data=await res.json() | |
| status.innerText=data.status | |
| status.style.color="#34d399" | |
| sourceCount++ | |
| updateStats() | |
| } | |
| /* Scrape Website */ | |
| async function scrape(){ | |
| let url=document.getElementById("url").value | |
| if(!url){ | |
| alert("Enter URL") | |
| return | |
| } | |
| let status=document.getElementById("scrapeStatus") | |
| status.innerText="Scraping…" | |
| status.style.color="#fbbf24" | |
| let formData=new FormData() | |
| formData.append("url",url) | |
| let res=await fetch("/scrape",{ | |
| method:"POST", | |
| body:formData | |
| }) | |
| let data=await res.json() | |
| status.innerText=data.status | |
| status.style.color="#34d399" | |
| sourceCount++ | |
| updateStats() | |
| } | |
| /* Chat */ | |
| async function send(){ | |
| let msg=document.getElementById("msg").value | |
| if(!msg) return | |
| clearWelcome() | |
| let chat=document.getElementById("chat") | |
| chat.innerHTML+=`<div class="message user"><div class="sender">You</div>${msg}</div>` | |
| document.getElementById("msg").value="" | |
| messageCount++ | |
| updateStats() | |
| /* typing indicator */ | |
| let typingId="t"+Date.now() | |
| chat.innerHTML+=`<div class="message bot" id="${typingId}"><div class="typing"><span></span><span></span><span></span></div></div>` | |
| chat.scrollTop=chat.scrollHeight | |
| let formData=new FormData() | |
| formData.append("message",msg) | |
| let res=await fetch("/chat",{ | |
| method:"POST", | |
| body:formData | |
| }) | |
| let data=await res.json() | |
| /* remove typing */ | |
| let el=document.getElementById(typingId) | |
| if(el) el.remove() | |
| let answerHTML=`<div class="message bot"><div class="sender">Scholar AI</div>${data.answer}` | |
| /* Retrieved chunks dropdown */ | |
| if(data.chunks){ | |
| answerHTML+=`<details> | |
| <summary>Retrieved Sources (${data.chunks.length})</summary>` | |
| data.chunks.forEach((c,i)=>{ | |
| answerHTML+=` | |
| <div class="chunk"> | |
| <b>Source ${i+1}</b> — ${c.source}<br> | |
| ${c.text} | |
| </div> | |
| ` | |
| }) | |
| answerHTML+=`</details>` | |
| } | |
| answerHTML+=`</div>` | |
| chat.innerHTML+=answerHTML | |
| messageCount++ | |
| updateStats() | |
| chat.scrollTop=chat.scrollHeight | |
| } | |
| /* File label update */ | |
| document.getElementById("pdf").addEventListener("change",function(){ | |
| let label=this.parentElement.querySelector(".label") | |
| if(this.files[0]){ | |
| label.innerHTML=`<span>${this.files[0].name}</span>` | |
| } | |
| }) | |
| </script> | |
| </body> | |
| </html> |