dashhdata's picture
Upload folder using huggingface_hub
1e8bb26 verified
Raw
History Blame Contribute Delete
14.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DocChat · Document Intelligence</title>
<style>
:root {
--bg:#0c0e14; --panel:#151823; --panel2:#1c2030; --border:#2a3040;
--accent:#7c6cf0; --accent2:#5b8def; --text:#eef0f6; --muted:#98a1b5;
--green:#3ecf8e; --red:#f06c7c; --amber:#f0b84c; --radius:12px;
}
* { box-sizing:border-box; }
html,body { height:100%; }
body { margin:0; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
background:radial-gradient(1200px 600px at 80% -10%, #1a1f3a 0%, var(--bg) 55%); color:var(--text); }
header { display:flex; align-items:center; gap:12px; padding:14px 22px; border-bottom:1px solid var(--border);
backdrop-filter:blur(6px); position:sticky; top:0; z-index:5; }
.logo { width:30px;height:30px;border-radius:8px;
background:linear-gradient(135deg,var(--accent),var(--accent2)); display:grid;place-items:center;font-size:16px; }
header h1 { font-size:16px; font-weight:650; margin:0; letter-spacing:.2px; }
header .sub { color:var(--muted); font-size:12px; margin-left:2px; }
.health { margin-left:auto; display:flex; gap:14px; font-size:12px; color:var(--muted); align-items:center; }
.dot { display:inline-block; width:8px;height:8px;border-radius:50%; margin-right:5px; background:var(--muted); vertical-align:middle; }
.dot.ok{background:var(--green);} .dot.bad{background:var(--red);} .dot.warn{background:var(--amber);}
.wrap { display:grid; grid-template-columns:330px 1fr; height:calc(100vh - 59px); }
.side { border-right:1px solid var(--border); padding:18px; overflow-y:auto; display:flex; flex-direction:column; gap:18px; }
.card { background:var(--panel); border:1px solid var(--border); border-radius:var(--radius); padding:16px; }
h3 { margin:0 0 12px; font-size:12px; text-transform:uppercase; letter-spacing:.08em; color:var(--muted); }
.drop { border:1.5px dashed var(--border); border-radius:10px; padding:22px 14px; text-align:center; cursor:pointer;
transition:.15s; color:var(--muted); font-size:13px; }
.drop:hover,.drop.drag { border-color:var(--accent); color:var(--text); background:var(--panel2); }
.drop b { color:var(--accent); }
.btn { background:linear-gradient(135deg,var(--accent),var(--accent2)); color:#fff; border:0; border-radius:9px;
padding:10px 16px; cursor:pointer; font-weight:600; font-size:13px; width:100%; margin-top:10px; }
.btn:disabled { opacity:.5; cursor:default; }
.btn.ghost { background:transparent; border:1px solid var(--border); color:var(--muted); }
.status { font-size:12px; color:var(--muted); margin-top:8px; min-height:16px; }
.doc { background:var(--panel2); border:1px solid var(--border); border-radius:9px; padding:10px 11px; margin-bottom:8px;
display:flex; gap:9px; align-items:flex-start; }
.doc input { margin-top:3px; accent-color:var(--accent); }
.doc .meta { flex:1; min-width:0; }
.doc .name { font-size:13px; font-weight:550; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.doc small { color:var(--muted); font-size:11px; }
.doc .x { color:var(--muted); cursor:pointer; font-size:14px; padding:0 3px; }
.doc .x:hover { color:var(--red); }
.tag { display:inline-block; font-size:10px; padding:1px 6px; border-radius:6px; background:#2b3350; color:#aab4ff; margin-left:6px; }
.scope { font-size:11px; color:var(--muted); margin-top:6px; }
.main { display:flex; flex-direction:column; min-width:0; }
.chat { flex:1; overflow-y:auto; padding:26px 28px; display:flex; flex-direction:column; gap:20px; }
.empty { margin:auto; text-align:center; color:var(--muted); max-width:420px; }
.empty h2 { color:var(--text); font-weight:600; margin-bottom:8px; }
.suggest { display:flex; flex-wrap:wrap; gap:8px; justify-content:center; margin-top:16px; }
.chip { background:var(--panel); border:1px solid var(--border); border-radius:20px; padding:7px 13px; font-size:12px;
cursor:pointer; color:var(--muted); } .chip:hover{ border-color:var(--accent); color:var(--text); }
.msg { display:flex; gap:12px; max-width:860px; }
.msg.me { flex-direction:row-reverse; align-self:flex-end; }
.av { width:30px;height:30px;border-radius:8px; flex-shrink:0; display:grid;place-items:center; font-size:14px; }
.av.ai { background:linear-gradient(135deg,var(--accent),var(--accent2)); }
.av.me { background:var(--panel2); border:1px solid var(--border); }
.bubble { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:13px 15px; line-height:1.6;
font-size:14.5px; white-space:pre-wrap; word-wrap:break-word; }
.me .bubble { background:#222a4a; }
.bubble code { background:#0d1018; padding:1px 5px; border-radius:5px; font-size:13px; }
.bubble pre { background:#0d1018; padding:10px; border-radius:8px; overflow-x:auto; }
.sources { margin-top:10px; font-size:12px; }
.sources summary { cursor:pointer; color:var(--muted); }
.src { background:var(--panel2); border:1px solid var(--border); border-radius:8px; padding:8px 10px; margin-top:6px; }
.src .h { color:#aab4ff; font-size:11px; margin-bottom:3px; }
.src .t { color:var(--muted); font-size:12px; }
.typing span { display:inline-block; width:6px;height:6px;border-radius:50%; background:var(--muted); margin:0 2px;
animation:b 1.2s infinite; } .typing span:nth-child(2){animation-delay:.2s;} .typing span:nth-child(3){animation-delay:.4s;}
@keyframes b { 0%,60%,100%{opacity:.3;transform:translateY(0)} 30%{opacity:1;transform:translateY(-3px)} }
.composer { border-top:1px solid var(--border); padding:14px 22px; }
.composer .row { display:flex; gap:10px; align-items:flex-end; background:var(--panel); border:1px solid var(--border);
border-radius:12px; padding:8px 10px; }
textarea#q { flex:1; background:transparent; border:0; color:var(--text); font-size:14.5px; resize:none; outline:none;
max-height:140px; font-family:inherit; line-height:1.5; padding:6px; }
.send { background:linear-gradient(135deg,var(--accent),var(--accent2)); border:0; border-radius:9px; width:40px;height:40px;
cursor:pointer; color:#fff; font-size:17px; flex-shrink:0; } .send:disabled{opacity:.4;cursor:default;}
.hint { font-size:11px; color:var(--muted); margin-top:6px; text-align:center; }
@media (max-width:780px){ .wrap{grid-template-columns:1fr;} .side{display:none;} }
</style>
</head>
<body>
<header>
<div class="logo">📄</div>
<div>
<h1>DocChat <span class="sub">Document Intelligence &amp; Chat</span></h1>
</div>
<div class="health" id="health">
<span><span class="dot" id="d-qdrant"></span>Qdrant</span>
<span><span class="dot" id="d-mongo"></span>Mongo</span>
<span><span class="dot" id="d-llm"></span>LLM</span>
</div>
</header>
<div class="wrap">
<aside class="side">
<div class="card">
<h3>Upload a document</h3>
<div class="drop" id="drop">
<div>Drop a file here or <b>browse</b></div>
<div style="margin-top:6px;font-size:11px;">PDF · DOCX · images · TXT — OCR built in</div>
</div>
<input type="file" id="file" hidden />
<button class="btn" id="uploadBtn" onclick="upload()">Upload &amp; Index</button>
<div class="status" id="uploadStatus"></div>
</div>
<div class="card" style="flex:1;">
<h3 style="display:flex;justify-content:space-between;align-items:center;">
Your documents <span id="count" style="color:var(--accent);">0</span>
</h3>
<div id="docs"></div>
<div class="scope" id="scope">Chatting across all documents.</div>
</div>
</aside>
<main class="main">
<div class="chat" id="chat">
<div class="empty" id="empty">
<h2>Ask your documents anything</h2>
<p>Upload a file on the left, then ask a question. Answers are grounded in
your documents and cite their sources.</p>
<div class="suggest">
<div class="chip" onclick="useChip(this)">Summarize the key points</div>
<div class="chip" onclick="useChip(this)">What are the main risks?</div>
<div class="chip" onclick="useChip(this)">List the action items</div>
</div>
</div>
</div>
<div class="composer">
<div class="row">
<textarea id="q" rows="1" placeholder="Ask a question about your documents…"
oninput="autogrow(this)" onkeydown="onKey(event)"></textarea>
<button class="send" id="send" onclick="ask()"></button>
</div>
<div class="hint">Enter to send · Shift+Enter for newline</div>
</div>
</main>
</div>
<script>
let DOCS = [];
const $ = (id) => document.getElementById(id);
/* ---------- health ---------- */
async function health() {
try {
const r = await fetch('/health'); const h = await r.json();
setDot('d-qdrant', h.qdrant === 'ok');
setDot('d-mongo', h.mongodb === 'ok');
setDot('d-llm', h.llm_key_configured);
} catch { ['d-qdrant','d-mongo','d-llm'].forEach(d=>setDot(d,false)); }
}
function setDot(id, ok){ const e=$(id); e.className = 'dot ' + (ok?'ok':'bad'); }
/* ---------- documents ---------- */
async function loadDocs() {
try {
const r = await fetch('/documents'); DOCS = await r.json();
} catch { DOCS = []; }
$('count').textContent = DOCS.length;
const el = $('docs');
if (!DOCS.length) { el.innerHTML = '<small>No documents yet — upload one above.</small>'; updateScope(); return; }
el.innerHTML = DOCS.map(d => `
<div class="doc">
<input type="checkbox" class="docsel" value="${d.id}" onchange="updateScope()" checked />
<div class="meta">
<div class="name" title="${esc(d.filename)}">${esc(d.filename)}${d.ocr_used?'<span class="tag">OCR</span>':''}</div>
<small>${d.num_chunks} chunks · ${new Date(d.created_at).toLocaleDateString()}</small>
</div>
<span class="x" title="Delete" onclick="del('${d.id}')">✕</span>
</div>`).join('');
updateScope();
}
function selectedIds(){ return [...document.querySelectorAll('.docsel:checked')].map(c=>c.value); }
function updateScope(){
const sel = selectedIds(), total = DOCS.length;
const s = $('scope');
if (!total) s.textContent = '';
else if (sel.length === total) s.textContent = 'Chatting across all documents.';
else if (sel.length === 0) s.textContent = '⚠ No documents selected — select at least one.';
else s.textContent = `Chatting across ${sel.length} of ${total} documents.`;
}
async function del(id){
if (!confirm('Delete this document and its vectors?')) return;
await fetch('/documents/'+id, {method:'DELETE'}); await loadDocs();
}
/* ---------- upload ---------- */
const drop = $('drop'), fileInput = $('file');
drop.onclick = () => fileInput.click();
['dragover','dragenter'].forEach(e=>drop.addEventListener(e,ev=>{ev.preventDefault();drop.classList.add('drag');}));
['dragleave','drop'].forEach(e=>drop.addEventListener(e,ev=>{ev.preventDefault();drop.classList.remove('drag');}));
drop.addEventListener('drop', ev=>{ if(ev.dataTransfer.files[0]){ fileInput.files=ev.dataTransfer.files; reflectFile(); }});
fileInput.onchange = reflectFile;
function reflectFile(){ const f=fileInput.files[0]; if(f) drop.querySelector('div').innerHTML = '📎 '+esc(f.name); }
async function upload(){
const f = fileInput.files[0]; const st = $('uploadStatus');
if (!f){ st.textContent='Choose a file first.'; return; }
const btn = $('uploadBtn'); btn.disabled = true; st.textContent = '⏳ Extracting, embedding & indexing…';
const fd = new FormData(); fd.append('file', f);
try {
const r = await fetch('/documents/upload',{method:'POST',body:fd});
const d = await r.json();
st.textContent = r.ok ? ('✓ '+d.message) : ('⚠ '+(d.detail||'Upload failed'));
if (r.ok){ fileInput.value=''; drop.querySelector('div').innerHTML='Drop a file here or <b>browse</b>'; await loadDocs(); }
} catch(e){ st.textContent = '⚠ '+e; }
btn.disabled = false;
}
/* ---------- chat ---------- */
function autogrow(t){ t.style.height='auto'; t.style.height=Math.min(t.scrollHeight,140)+'px'; }
function onKey(e){ if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); ask(); } }
function useChip(el){ $('q').value = el.textContent; $('q').focus(); }
function esc(s){ return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function fmt(s){ // tiny markdown: code fences, inline code, bold
s = esc(s);
s = s.replace(/```([\s\S]*?)```/g,(m,c)=>'<pre>'+c+'</pre>');
s = s.replace(/`([^`]+)`/g,'<code>$1</code>');
s = s.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>');
return s;
}
function bubble(role, inner){
$('empty')?.remove();
const wrap = document.createElement('div'); wrap.className = 'msg '+(role==='me'?'me':'');
wrap.innerHTML = `<div class="av ${role==='me'?'me':'ai'}">${role==='me'?'🙂':'📄'}</div>
<div class="bubble">${inner}</div>`;
$('chat').appendChild(wrap); $('chat').scrollTop = $('chat').scrollHeight;
return wrap.querySelector('.bubble');
}
async function ask(){
const t = $('q'); const q = t.value.trim(); if(!q) return;
const ids = selectedIds();
if (DOCS.length && ids.length===0){ alert('Select at least one document to chat with.'); return; }
t.value=''; autogrow(t); bubble('me', esc(q));
const b = bubble('ai', '<div class="typing"><span></span><span></span><span></span></div>');
$('send').disabled = true;
try {
const body = { question:q };
if (DOCS.length && ids.length < DOCS.length) body.document_ids = ids;
const r = await fetch('/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const d = await r.json();
if (!r.ok){ b.innerHTML = '⚠ '+esc(d.detail||'Error'); $('send').disabled=false; return; }
let html = fmt(d.answer||'');
if (d.sources?.length){
const items = d.sources.map((s,i)=>`<div class="src"><div class="h">[${i+1}] ${esc(s.filename)} · chunk ${s.chunk_index} · ${(s.score*100).toFixed(0)}% match</div><div class="t">${esc(s.snippet)}</div></div>`).join('');
html += `<details class="sources"><summary>${d.sources.length} source${d.sources.length>1?'s':''}</summary>${items}</details>`;
}
b.innerHTML = html;
} catch(e){ b.innerHTML = '⚠ '+esc(''+e); }
$('send').disabled = false; $('chat').scrollTop = $('chat').scrollHeight;
}
health(); loadDocs(); setInterval(health, 30000);
</script>
</body>
</html>