snakeeee's picture
update chat UI with source dropdown and chatbot memory
bf4791f
Raw
History Blame Contribute Delete
15.3 kB
<!DOCTYPE html>
<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>