687 lines
35 KiB
HTML
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 & 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,'"')}">⚠</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,'<').replace(/>/g,'>'), 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>
|