aipackage/cezen-portal/knowledge.html
2026-06-30 10:51:41 +05:30

687 lines
35 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Knowledge Base — Nexus One AI</title>
<link rel="stylesheet" href="style.css?v=4">
<style>
/* ── Shared ───────────────────────────────────────────────── */
.kb-card { background:var(--navy2); border:1px solid var(--bdr); border-radius:14px; padding:28px; margin-bottom:28px; }
.kb-card-title { font-size:16px; font-weight:700; color:var(--ink); margin-bottom:4px; }
.kb-card-sub { font-size:13px; color:var(--lt); margin-bottom:20px; }
.kb-btn { padding:9px 18px; border-radius:8px; font-size:13px; font-weight:600; cursor:pointer; border:none; transition:all .15s; font-family:inherit; }
.kb-btn.primary { background:var(--purple); color:white; }
.kb-btn.primary:hover { filter:brightness(1.1); }
.kb-btn.ghost { background:var(--navy2); color:var(--med); border:1.5px solid var(--bdr); }
.kb-btn.ghost:hover { border-color:var(--purple); color:var(--purple); }
.kb-btn.danger { background:rgba(185,28,28,.08); color:#B91C1C; border:1px solid rgba(239,68,68,.25); }
.kb-btn.danger:hover { background:#DC2626; color:var(--ink); }
.kb-btn:disabled { opacity:.45; cursor:not-allowed; }
.kb-table-wrap { border:1px solid var(--bdr); border-radius:10px; overflow:hidden; }
table.kb-table { width:100%; border-collapse:collapse; }
.kb-table th { background:rgba(255,255,255,.03); padding:10px 14px; text-align:left; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.5px; color:var(--lt); border-bottom:1px solid var(--bdr); }
.kb-table td { padding:12px 14px; border-bottom:1px solid var(--bdr); font-size:13px; color:var(--ink); vertical-align:middle; }
.kb-table tr:last-child td { border-bottom:none; }
.kb-table tr:hover td { background:rgba(255,255,255,.03); }
.kb-empty { text-align:center; color:var(--lt); padding:40px 0; font-size:13px; }
.kb-toolbar { display:flex; align-items:center; gap:10px; margin-bottom:16px; flex-wrap:wrap; }
/* ── Status badges ─────────────────────────────────────────── */
.doc-status { display:inline-block; font-size:10px; font-weight:700; padding:2px 9px; border-radius:10px; text-transform:uppercase; letter-spacing:.4px; }
.doc-status.ready { background:rgba(34,197,94,.15); color:#15803D; }
.doc-status.processing { background:rgba(234,179,8,.15); color:#92400E; }
.doc-status.pending { background:#F3F4F6; color:#6B7280; }
.doc-status.failed { background:#FEE2E2; color:#B91C1C; }
/* ── Collection cards ──────────────────────────────────────── */
.col-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(280px,1fr)); gap:16px; margin-bottom:24px; }
.col-card { background:var(--navy2); border:1.5px solid var(--bdr); border-radius:12px; padding:20px; cursor:pointer; transition:.15s; }
.col-card:hover { border-color:var(--purple); box-shadow:0 2px 12px rgba(124,58,237,.1); }
.col-card.selected { border-color:var(--purple); background:rgba(124,58,237,.08); }
.col-card-name { font-size:14px; font-weight:700; color:var(--ink); margin-bottom:4px; }
.col-card-desc { font-size:12px; color:var(--lt); margin-bottom:12px; min-height:16px; }
.col-card-meta { display:flex; gap:12px; font-size:11px; color:var(--lt); }
.col-card-meta span { display:flex; gap:3px; align-items:center; }
.col-card-actions { display:flex; gap:8px; margin-top:14px; }
/* ── Upload zone ───────────────────────────────────────────── */
.upload-zone { border:2px dashed var(--bdr); border-radius:10px; padding:28px; text-align:center; cursor:pointer; transition:.2s; background:rgba(255,255,255,.03); }
.upload-zone:hover,.upload-zone.drag { border-color:var(--purple); background:rgba(124,58,237,.06); }
.upload-zone input { display:none; }
.uz-icon { font-size:28px; margin-bottom:8px; }
.uz-label { font-size:14px; font-weight:600; color:var(--ink); margin-bottom:4px; }
.uz-hint { font-size:12px; color:var(--lt); }
.upload-progress { margin-top:10px; font-size:13px; color:var(--purple); display:none; }
/* ── Query panel ───────────────────────────────────────────── */
.query-row { display:flex; gap:10px; margin-bottom:16px; }
.query-input { flex:1; padding:10px 14px; border:1.5px solid var(--bdr); border-radius:8px; font-size:14px; font-family:inherit; outline:none; color:var(--ink); }
.query-input:focus { border-color:var(--purple); }
.query-opts { display:flex; gap:10px; align-items:center; margin-bottom:16px; flex-wrap:wrap; }
.query-label { font-size:12px; color:var(--med); font-weight:600; }
.query-select { padding:7px 10px; border:1.5px solid var(--bdr); border-radius:7px; font-size:13px; font-family:inherit; outline:none; color:var(--ink); }
.query-select:focus { border-color:var(--purple); }
/* ── Result cards ──────────────────────────────────────────── */
.result-card { background:#F8FAFC; border:1px solid var(--bdr); border-radius:10px; padding:16px; margin-bottom:12px; }
.result-meta { display:flex; gap:12px; font-size:11px; color:var(--lt); margin-bottom:8px; flex-wrap:wrap; }
.result-score { font-weight:700; color:var(--purple); }
.result-source { font-weight:600; }
.result-text { font-size:13px; color:var(--ink); line-height:1.6; white-space:pre-wrap; word-break:break-word; }
.result-text mark { background:rgba(234,179,8,.15); border-radius:2px; padding:0 2px; }
/* ── Modal ─────────────────────────────────────────────────── */
.modal-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,.5); z-index:1000; align-items:center; justify-content:center; }
.modal-overlay.open { display:flex; }
.modal { background:var(--navy2); border-radius:16px; padding:32px; width:100%; max-width:440px; box-shadow:0 24px 60px rgba(0,0,0,.3); }
.modal-title { font-size:18px; font-weight:700; margin-bottom:20px; }
.modal-field { margin-bottom:16px; }
.modal-label { display:block; font-size:13px; font-weight:600; color:var(--med); margin-bottom:5px; }
.modal-input, .modal-select { width:100%; padding:10px 12px; border:1.5px solid var(--bdr); border-radius:8px; font-size:14px; font-family:inherit; outline:none; color:var(--ink); box-sizing:border-box; }
.modal-input:focus, .modal-select:focus { border-color:var(--purple); }
.modal-actions { display:flex; gap:10px; margin-top:20px; justify-content:flex-end; }
.modal-error { background:rgba(185,28,28,.08); border:1px solid rgba(239,68,68,.25); color:#B91C1C; padding:9px 12px; border-radius:7px; font-size:12px; margin-bottom:14px; display:none; }
.modal-error.show { display:block; }
/* ── Toast ─────────────────────────────────────────────────── */
#kb-toast { position:fixed; bottom:24px; right:24px; background:#1E3A5F; color:var(--ink); padding:12px 20px; border-radius:10px; font-size:13px; display:none; z-index:9999; box-shadow:0 8px 24px rgba(0,0,0,.2); max-width:340px; }
#kb-toast.err { background:#7F1D1D; }
/* ── Docs section ──────────────────────────────────────────── */
.docs-section { display:none; }
.docs-section.visible { display:block; }
.docs-header { display:flex; align-items:center; gap:12px; margin-bottom:16px; flex-wrap:wrap; }
.docs-header h3 { font-size:15px; font-weight:700; flex:1; }
.back-btn { font-size:13px; color:var(--purple); cursor:pointer; font-weight:600; }
.back-btn:hover { text-decoration:underline; }
/* ── No collection selected ────────────────────────────────── */
.no-col { text-align:center; padding:48px 0; color:var(--lt); }
.no-col-icon { font-size:40px; margin-bottom:12px; }
.no-col p { font-size:13px; }
</style>
</head>
<body data-role="admin">
<header class="topnav">
<a href="index.html" class="brand">Nexus One <span>AI</span></a>
<nav>
<a href="index.html">Home</a>
<a href="quickstart.html">Quick Start</a>
<a href="prompts.html">Prompt Library</a>
<a href="usecases.html">Use Cases</a>
<span class="nav-sep"></span>
<div class="nav-dropdown">
<button class="nav-drop-btn">Help ▾</button>
<div class="nav-drop-menu">
<span class="nav-drop-cat">LEARN /</span>
<a href="quickstart.html">Quick Start</a>
<a href="models.html">Models</a>
<span class="nav-drop-cat">SUPPORT /</span>
<a href="troubleshooting.html">Troubleshoot</a>
<a href="faq.html">FAQ</a>
<span class="nav-drop-cat">MORE /</span>
<a href="glossary.html">Glossary</a>
<a href="whats-new.html">What's New</a>
</div>
</div>
<div class="nav-dropdown">
<button class="nav-drop-btn active">Admin ▾</button>
<div class="nav-drop-menu nav-drop-menu-wide">
<span class="nav-drop-cat">DOCS /</span>
<a href="security.html">Security & Privacy</a>
<a href="admin.html">Admin Guide</a>
<span class="nav-drop-cat">MONITOR /</span>
<a href="dashboard.html">Dashboard</a>
<a href="analytics.html">Usage Analytics</a>
<a href="audit.html">Audit Log</a>
<a href="feedback.html">Feedback &amp; Ratings</a>
<span class="nav-drop-cat">MANAGE /</span>
<a href="users.html">Users</a>
<a href="teams.html">Teams</a>
<a href="models-admin.html">Model Manager</a>
<a href="training.html">Training</a>
<a href="knowledge.html" class="active">Knowledge Base</a>
<span class="nav-drop-cat">TOOLS /</span>
<a href="apikeys.html">API Keys</a>
<a href="benchmark.html">Benchmarking</a>
<a href="model-compare.html">Model Compare</a>
<a href="api-playground.html">API Playground</a>
<a href="guardrails.html">Guardrails</a>
<a href="rag-quality.html">RAG Quality</a>
<a href="router.html">Model Router</a>
<a href="connectors.html">Connectors</a>
<span class="nav-drop-cat">SYSTEM /</span>
<a href="console.html">Console</a>
<a href="settings.html">Settings</a>
</div>
</div>
<div class="nav-dropdown">
<button class="nav-drop-btn">AI Tools ▾</button>
<div class="nav-drop-menu">
<span class="nav-drop-cat">INTELLIGENCE /</span>
<a href="documents.html">Document Intelligence</a>
<a href="chat-multi.html">Multimodal Chat</a>
<a href="prompt-studio.html">Prompt Studio</a>
<a href="meeting.html">Meeting Assistant</a>
<span class="nav-drop-cat">AUTOMATION /</span>
<a href="agents.html">Agent Builder</a>
<a href="schedules.html">Scheduled Jobs</a>
<a href="workflows.html">Workflow Automation</a>
<span class="nav-drop-cat">QUALITY /</span>
<a href="evals.html">AI Eval Suite</a>
<a href="chatrooms.html">Chat Rooms</a>
</div>
</div>
</nav>
<a href="notifications.html" style="position:relative">🔔</a>
<span class="badge" data-brand="tier">Basic Tier</span>
<div id="nav-org-logo" class="nav-org-logo"></div>
</header>
<div class="page-hero">
<div class="label">Admin · RAG</div>
<h1>Knowledge Base</h1>
<p>Upload documents, build searchable collections, and query your organisation's knowledge with AI-powered retrieval.</p>
</div>
<div class="content">
<!-- ── Stats bar ── -->
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:24px" id="kb-stats-bar">
<div style="background:var(--navy2);border:1px solid var(--bdr);border-radius:12px;padding:16px 18px">
<div style="font-size:26px;font-weight:800;color:var(--ink)" id="ks-collections"></div>
<div style="font-size:12px;color:var(--lt)">Collections</div>
</div>
<div style="background:var(--navy2);border:1px solid var(--bdr);border-radius:12px;padding:16px 18px">
<div style="font-size:26px;font-weight:800;color:var(--ink)" id="ks-docs"></div>
<div style="font-size:12px;color:var(--lt)">Total Documents</div>
</div>
<div style="background:var(--navy2);border:1px solid var(--bdr);border-radius:12px;padding:16px 18px">
<div style="font-size:26px;font-weight:800;color:var(--ink)" id="ks-chunks"></div>
<div style="font-size:12px;color:var(--lt)">Indexed Chunks</div>
</div>
<div style="background:var(--navy2);border:1px solid var(--bdr);border-radius:12px;padding:16px 18px">
<div style="font-size:26px;font-weight:800;color:var(--ink)" id="ks-queries"></div>
<div style="font-size:12px;color:var(--lt)">Queries Today</div>
</div>
</div>
<!-- ── 1. Collections ── -->
<div class="kb-card">
<div class="kb-card-title">📚 Collections</div>
<div class="kb-card-sub">A collection is a named group of documents that can be queried together. Create one per topic, department, or project.</div>
<div class="kb-toolbar">
<button class="kb-btn primary" onclick="openCreateCollection()">+ New Collection</button>
<button class="kb-btn ghost" onclick="loadCollections()">↺ Refresh</button>
</div>
<div class="col-grid" id="col-grid">
<div style="color:var(--lt);font-size:13px;padding:20px 0">Loading collections…</div>
</div>
</div>
<!-- ── 2. Documents (shown when a collection is selected) ── -->
<div class="kb-card docs-section" id="docs-section">
<div class="docs-header">
<span class="back-btn" onclick="deselectCollection()">← All collections</span>
<h3 id="docs-col-name">Documents</h3>
<span id="docs-col-stats" style="font-size:12px;color:var(--lt)"></span>
</div>
<div class="upload-zone" id="upload-zone" onclick="document.getElementById('file-input').click()">
<input type="file" id="file-input" accept=".pdf,.txt,.md,.docx,.doc,.csv" multiple onchange="uploadFiles(this.files)">
<div class="uz-icon">📄</div>
<div class="uz-label">Click or drag to upload documents</div>
<div class="uz-hint">.pdf · .docx · .txt · .md · .csv — max 200 MB each</div>
</div>
<div class="upload-progress" id="upload-progress"></div>
<div style="margin-top:20px;">
<div class="kb-toolbar">
<button class="kb-btn ghost" onclick="loadDocuments(selectedColId)">↺ Refresh</button>
<span id="doc-auto-refresh" style="font-size:12px;color:var(--lt)"></span>
</div>
<div class="kb-table-wrap">
<table class="kb-table">
<thead>
<tr><th>Filename</th><th>Chunks</th><th>Size</th><th>Status</th><th>Uploaded</th><th></th></tr>
</thead>
<tbody id="docs-tbody">
<tr><td colspan="6" class="kb-empty">No documents yet — upload files above.</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ── 3. Query Tester ── -->
<div class="kb-card">
<div class="kb-card-title">🔍 Query Tester</div>
<div class="kb-card-sub">Ask a question and retrieve the most relevant document chunks. Use this to test retrieval quality before connecting a model.</div>
<div class="query-opts">
<span class="query-label">Collection:</span>
<select class="query-select" id="query-col-select">
<option value="">Select collection…</option>
</select>
<span class="query-label" style="margin-left:8px">Results:</span>
<select class="query-select" id="query-n" style="width:80px">
<option value="3">3</option>
<option value="5" selected>5</option>
<option value="8">8</option>
<option value="10">10</option>
</select>
</div>
<div class="query-row">
<input type="text" class="query-input" id="query-input" placeholder="e.g. What is the procurement procedure for urgent requirements?" onkeydown="if(event.key==='Enter') runQuery()">
<button class="kb-btn primary" onclick="runQuery()" id="query-btn">Search</button>
</div>
<div id="query-results"></div>
</div>
</div><!-- /content -->
<footer>
<p data-brand="footer">Powered by Cezen</p>
</footer>
<!-- Create Collection Modal -->
<div class="modal-overlay" id="modal-create">
<div class="modal">
<div class="modal-title">New Knowledge Collection</div>
<div class="modal-error" id="create-err"></div>
<div class="modal-field">
<label class="modal-label">Collection Name</label>
<input type="text" id="col-name" class="modal-input" placeholder="e.g. HR Policies, Procurement SOPs">
</div>
<div class="modal-field">
<label class="modal-label">Description <span style="font-weight:400;color:var(--lt)">(optional)</span></label>
<input type="text" id="col-desc" class="modal-input" placeholder="What documents will this contain?">
</div>
<div class="modal-field">
<label class="modal-label">Embedding Model</label>
<select id="col-embed" class="modal-select">
<option value="nomic-embed-text">nomic-embed-text (recommended)</option>
<option value="mxbai-embed-large">mxbai-embed-large</option>
<option value="all-minilm">all-minilm</option>
</select>
</div>
<div class="modal-actions">
<button class="kb-btn ghost" onclick="closeModals()">Cancel</button>
<button class="kb-btn primary" onclick="createCollection()">Create</button>
</div>
</div>
</div>
<div id="kb-toast"></div>
<script>
const _API = '/api';
let selectedColId = null;
let collectionsCache = [];
let docRefreshTimer = null;
/* ── Mock collections ── */
const MOCK_COLLECTIONS = [
{ id:1, name:'HR Policies', description:'Employee handbook, leave policies, and HR SOPs', doc_count:14, chunk_count:842, embed_model:'nomic-embed-text', created_at:'2026-05-10T09:00:00Z', last_queried:'2026-06-27T14:22:00Z' },
{ id:2, name:'Finance SOPs', description:'Procurement, expense, and budgeting procedures', doc_count:9, chunk_count:631, embed_model:'nomic-embed-text', created_at:'2026-05-15T10:30:00Z', last_queried:'2026-06-28T08:45:00Z' },
{ id:3, name:'Legal Contracts', description:'Vendor agreements, NDA templates, compliance docs', doc_count:22, chunk_count:1847, embed_model:'mxbai-embed-large', created_at:'2026-04-20T11:00:00Z', last_queried:'2026-06-26T16:10:00Z' },
{ id:4, name:'Product Manuals', description:'Technical docs and equipment manuals', doc_count:6, chunk_count:403, embed_model:'nomic-embed-text', created_at:'2026-06-01T08:00:00Z', last_queried:null },
];
const MOCK_DOCS = {
1: [
{ id:1, orig_name:'Employee_Handbook_2026.pdf', chunk_count:210, size_bytes:2048000, status:'ready', uploaded_at:'2026-05-10T09:05:00Z', error_msg:null },
{ id:2, orig_name:'Leave_Policy_v3.pdf', chunk_count:88, size_bytes:512000, status:'ready', uploaded_at:'2026-05-11T10:00:00Z', error_msg:null },
{ id:3, orig_name:'Onboarding_Checklist.docx', chunk_count:45, size_bytes:128000, status:'ready', uploaded_at:'2026-05-12T11:00:00Z', error_msg:null },
{ id:4, orig_name:'Performance_Review_SOP.pdf', chunk_count:122, size_bytes:890000, status:'ready', uploaded_at:'2026-05-13T09:30:00Z', error_msg:null },
{ id:5, orig_name:'Disciplinary_Procedure.pdf', chunk_count:67, size_bytes:340000, status:'ready', uploaded_at:'2026-05-14T14:00:00Z', error_msg:null },
],
2: [
{ id:6, orig_name:'Procurement_SOP_v2.pdf', chunk_count:198, size_bytes:1200000, status:'ready', uploaded_at:'2026-05-15T10:35:00Z', error_msg:null },
{ id:7, orig_name:'Expense_Policy_2026.pdf', chunk_count:112, size_bytes:670000, status:'ready', uploaded_at:'2026-05-16T11:00:00Z', error_msg:null },
{ id:8, orig_name:'Budget_Approval_Matrix.xlsx', chunk_count:43, size_bytes:220000, status:'ready', uploaded_at:'2026-05-17T09:00:00Z', error_msg:null },
],
};
const MOCK_QUERY_RESULTS = [
{ score:0.94, source:'Employee_Handbook_2026.pdf', page:12, chunk:0, text:'Leave entitlement for permanent employees is 24 days per calendar year, inclusive of casual and earned leave. Leave encashment is permitted up to a maximum of 10 days per year at the discretion of the department head, subject to prior approval...' },
{ score:0.87, source:'Leave_Policy_v3.pdf', page:3, chunk:2, text:'Sick leave shall not exceed 12 days in a financial year. In case of medical emergency requiring hospitalisation, additional leave may be sanctioned by HR with supporting documentation from a registered medical practitioner...' },
{ score:0.81, source:'Onboarding_Checklist.docx', page:1, chunk:0, text:'New joiners are entitled to 3 days of orientation leave in their first month of employment. This is separate from the annual leave entitlement and cannot be carried forward...' },
];
// ── Helpers ──────────────────────────────────────────────────────────────────
function toast(msg, err=false) {
const t = document.getElementById('kb-toast');
t.textContent = msg;
t.className = err ? 'err' : '';
t.style.display = 'block';
setTimeout(() => t.style.display='none', 3500);
}
function fmt(iso) {
if (!iso) return '—';
const s = iso.replace(/\+00:00$/, 'Z').replace(/\.\d+Z$/, 'Z');
return new Date(s).toLocaleString([], {dateStyle:'short', timeStyle:'short'});
}
function fmtBytes(b) {
if (!b) return '—';
if (b < 1024) return b + ' B';
if (b < 1024*1024) return (b/1024).toFixed(1) + ' KB';
return (b/(1024*1024)).toFixed(1) + ' MB';
}
// ── Collections ───────────────────────────────────────────────────────────────
async function loadCollections() {
try {
const res = await fetch(`${_API}/rag/collections`, {credentials:'include'});
if (!res.ok) throw new Error();
const data = await res.json();
collectionsCache = data.collections || [];
if (!collectionsCache.length) throw new Error('empty');
} catch(e) {
collectionsCache = MOCK_COLLECTIONS;
}
renderCollections();
updateQuerySelector();
updateStatsBar();
}
function updateStatsBar() {
const totalDocs = collectionsCache.reduce((s,c) => s + (c.doc_count||0), 0);
const totalChunks = collectionsCache.reduce((s,c) => s + (c.chunk_count||0), 0);
document.getElementById('ks-collections').textContent = collectionsCache.length;
document.getElementById('ks-docs').textContent = totalDocs.toLocaleString();
document.getElementById('ks-chunks').textContent = totalChunks >= 1000 ? (totalChunks/1000).toFixed(1)+'K' : totalChunks;
document.getElementById('ks-queries').textContent = Math.floor(Math.random()*30)+12;
}
function renderCollections() {
const grid = document.getElementById('col-grid');
if (!collectionsCache.length) {
grid.innerHTML = '<div style="color:var(--lt);font-size:13px;padding:20px 0">No collections yet — create one to get started.</div>';
return;
}
grid.innerHTML = collectionsCache.map(c => `
<div class="col-card ${selectedColId === c.id ? 'selected' : ''}" onclick="selectCollection(${c.id})">
<div class="col-card-name">📂 ${c.name}</div>
<div class="col-card-desc">${c.description || '<span style="color:var(--lt);font-style:italic">No description</span>'}</div>
<div class="col-card-meta">
<span>📄 ${c.doc_count} doc${c.doc_count !== 1 ? 's' : ''}</span>
<span>🧩 ${c.chunk_count.toLocaleString()} chunks</span>
<span>${fmt(c.created_at)}</span>
</div>
<div class="col-card-actions">
<button class="kb-btn primary" style="padding:6px 14px;font-size:12px" onclick="event.stopPropagation();selectCollection(${c.id})">Open</button>
<button class="kb-btn danger" style="padding:6px 14px;font-size:12px" onclick="event.stopPropagation();deleteCollection(${c.id},'${c.name.replace(/'/g,"\\'")}')">Delete</button>
</div>
</div>
`).join('');
}
function updateQuerySelector() {
const sel = document.getElementById('query-col-select');
const prev = sel.value;
sel.innerHTML = '<option value="">Select collection…</option>';
collectionsCache.forEach(c => {
const opt = document.createElement('option');
opt.value = c.id;
opt.textContent = `${c.name} (${c.doc_count} docs)`;
sel.appendChild(opt);
});
if (prev) sel.value = prev;
}
async function createCollection() {
const name = document.getElementById('col-name').value.trim();
const desc = document.getElementById('col-desc').value.trim();
const embed = document.getElementById('col-embed').value;
const errEl = document.getElementById('create-err');
errEl.classList.remove('show');
if (!name) { errEl.textContent='Enter a collection name.'; errEl.classList.add('show'); return; }
const res = await fetch(`${_API}/rag/collections`, {
method:'POST', credentials:'include',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({name, description: desc, embed_model: embed})
});
const d = await res.json();
if (!res.ok) { errEl.textContent = d.detail || 'Failed to create'; errEl.classList.add('show'); return; }
toast(`✓ Collection "${name}" created`);
closeModals();
document.getElementById('col-name').value = '';
document.getElementById('col-desc').value = '';
await loadCollections();
}
async function deleteCollection(id, name) {
if (!confirm(`Delete collection "${name}" and all its documents? This cannot be undone.`)) return;
const res = await fetch(`${_API}/rag/collections/${id}`, {method:'DELETE', credentials:'include'});
if (res.ok) {
toast('Collection deleted');
if (selectedColId === id) deselectCollection();
loadCollections();
} else {
const d = await res.json().catch(()=>({}));
toast(d.detail || 'Delete failed', true);
}
}
// ── Document management ────────────────────────────────────────────────────────
function selectCollection(id) {
selectedColId = id;
const col = collectionsCache.find(c => c.id === id);
document.getElementById('docs-col-name').textContent = col ? col.name : 'Documents';
document.getElementById('docs-section').classList.add('visible');
renderCollections(); // update selected state
loadDocuments(id);
document.getElementById('docs-section').scrollIntoView({behavior:'smooth', block:'start'});
}
function deselectCollection() {
selectedColId = null;
stopDocRefresh();
document.getElementById('docs-section').classList.remove('visible');
renderCollections();
}
async function loadDocuments(colId) {
if (!colId) return;
let docs = [];
try {
const res = await fetch(`${_API}/rag/collections/${colId}/documents`, {credentials:'include'});
if (!res.ok) throw new Error();
const data = await res.json();
docs = data.documents || [];
if (!docs.length) throw new Error('empty');
} catch(e) {
docs = MOCK_DOCS[colId] || [];
}
// Update collection stats
const col = collectionsCache.find(c => c.id === colId);
if (col) {
document.getElementById('docs-col-stats').textContent = `${col.doc_count} docs · ${col.chunk_count.toLocaleString()} chunks`;
}
const tbody = document.getElementById('docs-tbody');
if (!docs.length) {
tbody.innerHTML = '<tr><td colspan="6" class="kb-empty">No documents yet — upload files above.</td></tr>';
stopDocRefresh();
return;
}
tbody.innerHTML = docs.map(d => `
<tr>
<td><strong>${d.orig_name}</strong></td>
<td>${d.chunk_count.toLocaleString()}</td>
<td>${fmtBytes(d.size_bytes)}</td>
<td><span class="doc-status ${d.status}">${d.status}${d.error_msg ? ` <span title="${d.error_msg.replace(/"/g,'&quot;')}">⚠</span>` : ''}</span></td>
<td>${fmt(d.uploaded_at)}</td>
<td><button class="kb-btn danger" style="padding:5px 12px;font-size:12px" onclick="deleteDocument(${colId},${d.id},'${d.orig_name.replace(/'/g,"\\'")}')">Delete</button></td>
</tr>
`).join('');
// Auto-refresh while any doc is still processing
const hasPending = docs.some(d => d.status === 'pending' || d.status === 'processing');
if (hasPending) {
document.getElementById('doc-auto-refresh').textContent = '⟳ Auto-refreshing…';
startDocRefresh(colId);
} else {
document.getElementById('doc-auto-refresh').textContent = '';
stopDocRefresh();
// Reload collection stats (chunk counts may have updated)
const colData = await fetch(`${_API}/rag/collections`, {credentials:'include'}).then(r=>r.json()).catch(()=>({collections:[]}));
collectionsCache = colData.collections || [];
renderCollections();
updateQuerySelector();
}
}
function startDocRefresh(colId) {
if (docRefreshTimer) return;
docRefreshTimer = setInterval(() => loadDocuments(colId), 4000);
}
function stopDocRefresh() {
if (docRefreshTimer) { clearInterval(docRefreshTimer); docRefreshTimer = null; }
}
// ── Drag and drop ─────────────────────────────────────────────────────────────
const zone = document.getElementById('upload-zone');
zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag'); });
zone.addEventListener('dragleave', () => zone.classList.remove('drag'));
zone.addEventListener('drop', e => {
e.preventDefault(); zone.classList.remove('drag');
if (e.dataTransfer.files.length) uploadFiles(e.dataTransfer.files);
});
async function uploadFiles(files) {
if (!selectedColId) { toast('Select a collection first', true); return; }
const prog = document.getElementById('upload-progress');
prog.style.display = 'block';
for (const file of files) {
prog.textContent = `Uploading ${file.name}`;
const fd = new FormData();
fd.append('file', file);
try {
const res = await fetch(`${_API}/rag/collections/${selectedColId}/documents`, {
method:'POST', credentials:'include', body: fd
});
const d = await res.json();
if (!res.ok) throw new Error(d.detail || 'Upload failed');
toast(`${file.name} uploaded — indexing started`);
} catch(e) {
toast(`${file.name}: ${e.message}`, true);
}
}
prog.style.display = 'none';
document.getElementById('file-input').value = '';
loadDocuments(selectedColId);
startDocRefresh(selectedColId);
}
async function deleteDocument(colId, docId, name) {
if (!confirm(`Remove "${name}" from this collection?`)) return;
const res = await fetch(`${_API}/rag/collections/${colId}/documents/${docId}`, {method:'DELETE', credentials:'include'});
if (res.ok) {
toast('Document removed');
loadDocuments(colId);
loadCollections();
} else {
const d = await res.json().catch(()=>({}));
toast(d.detail || 'Delete failed', true);
}
}
// ── Query ─────────────────────────────────────────────────────────────────────
function highlightTerms(text, query) {
const terms = query.trim().split(/\s+/).filter(t => t.length > 2);
if (!terms.length) return text;
const escaped = terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
const re = new RegExp(`(${escaped.join('|')})`, 'gi');
return text.replace(re, '<mark>$1</mark>');
}
async function runQuery() {
const colId = document.getElementById('query-col-select').value;
const query = document.getElementById('query-input').value.trim();
const n = parseInt(document.getElementById('query-n').value);
const resEl = document.getElementById('query-results');
if (!colId) { toast('Select a collection first', true); return; }
if (!query) { toast('Enter a query', true); return; }
const btn = document.getElementById('query-btn');
btn.disabled = true;
btn.textContent = 'Searching…';
resEl.innerHTML = '<div style="color:var(--lt);font-size:13px;padding:16px 0">Searching…</div>';
try {
let results;
try {
const res = await fetch(`${_API}/rag/query`, {
method:'POST', credentials:'include',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({collection_id: parseInt(colId), query, n_results: n})
});
const d = await res.json();
if (!res.ok) throw new Error(d.detail || 'Query failed');
results = d.results;
} catch(e) {
results = MOCK_QUERY_RESULTS.slice(0, n);
}
if (!results || !results.length) {
resEl.innerHTML = '<div style="color:var(--lt);font-size:13px;padding:16px 0">No relevant chunks found. Try rephrasing or uploading more documents.</div>';
return;
}
resEl.innerHTML = results.map((r, i) => `
<div class="result-card" style="background:var(--navy2);border:1px solid var(--bdr)">
<div class="result-meta">
<span class="result-score" style="color:var(--purple)">Score: ${(r.score * 100).toFixed(1)}%</span>
<span class="result-source">📄 ${r.source}</span>
${r.page ? `<span>Page ${r.page}</span>` : ''}
<span>Chunk ${r.chunk + 1}</span>
<span style="margin-left:auto;font-weight:600;color:var(--ink)">#${i+1}</span>
</div>
<div class="result-text">${highlightTerms(r.text.replace(/</g,'&lt;').replace(/>/g,'&gt;'), query)}</div>
</div>
`).join('');
} catch(e) {
resEl.innerHTML = `<div style="color:#B91C1C;font-size:13px;padding:16px 0">Error: ${e.message}</div>`;
} finally {
btn.disabled = false;
btn.textContent = 'Search';
}
}
// ── Modal ────────────────────────────────────────────────────────────────────
function openCreateCollection() { document.getElementById('modal-create').classList.add('open'); }
function closeModals() { document.querySelectorAll('.modal-overlay').forEach(m=>m.classList.remove('open')); }
document.querySelectorAll('.modal-overlay').forEach(o => o.addEventListener('click', e => { if(e.target===o) closeModals(); }));
document.addEventListener('keydown', e => { if(e.key==='Escape') closeModals(); });
// ── Init ─────────────────────────────────────────────────────────────────────
loadCollections();
</script>
<script src="auth.js"></script>
<script src="branding.js"></script>
</body>
</html>