SakibAhmed's picture
Upload 14 files
ca6e669 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RAG Admin Console</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Sora:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0b0d11;
--surface: #12151c;
--border: rgba(255,255,255,0.07);
--border-hi: rgba(99,210,255,0.35);
--text: #e8ecf4;
--muted: #5a6070;
--accent: #63d2ff;
--accent-dk: #3ab8e8;
--success: #3dffa0;
--danger: #ff5c72;
--warn: #ffc94a;
--glow: 0 0 24px rgba(99,210,255,0.12);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Sora', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background-image:
linear-gradient(rgba(99,210,255,0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(99,210,255,0.025) 1px, transparent 1px);
background-size: 48px 48px;
}
/* Card */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
width: 100%;
max-width: 420px;
overflow: hidden;
box-shadow: 0 32px 64px rgba(0,0,0,0.5), var(--glow);
animation: rise 0.5s cubic-bezier(0.22,1,0.36,1) both;
}
@keyframes rise {
from { opacity:0; transform: translateY(20px); }
to { opacity:1; transform: translateY(0); }
}
/* Header */
.card-header {
padding: 26px 30px 22px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 14px;
}
.logo-mark {
width: 38px; height: 38px;
border-radius: 10px;
background: linear-gradient(135deg, #1a3a50, #0d2236);
border: 1px solid var(--border-hi);
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.logo-mark svg { width: 20px; height: 20px; }
.header-text h1 {
font-size: 15px;
font-weight: 600;
letter-spacing: 0.01em;
color: var(--text);
}
.header-text p {
font-size: 12px;
color: var(--muted);
margin-top: 2px;
font-weight: 300;
}
/* Status Row */
.status-row {
padding: 14px 30px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
}
.status-label { font-size: 11px; color: var(--muted); font-family: 'DM Mono', monospace; letter-spacing: 0.06em; text-transform: uppercase; }
.pill {
display: inline-flex; align-items: center; gap: 7px;
padding: 5px 11px;
border-radius: 100px;
font-size: 11px; font-weight: 500;
font-family: 'DM Mono', monospace;
letter-spacing: 0.04em;
background: rgba(61,255,160,0.08);
color: var(--success);
border: 1px solid rgba(61,255,160,0.2);
transition: all 0.3s;
}
.pill.error {
background: rgba(255,92,114,0.08);
color: var(--danger);
border-color: rgba(255,92,114,0.2);
}
.pill.loading {
background: rgba(255,201,74,0.08);
color: var(--warn);
border-color: rgba(255,201,74,0.2);
}
.pill-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.pill-dot.pulse { animation: pulse 1.8s ease-in-out infinite; }
@keyframes pulse {
0%,100% { opacity:1; transform: scale(1); }
50% { opacity:0.4; transform: scale(0.7); }
}
/* Body */
.card-body { padding: 26px 30px 30px; }
/* Fields */
.field { margin-bottom: 14px; }
.field label {
display: block;
font-size: 10px; font-weight: 500;
color: var(--muted);
margin-bottom: 7px;
letter-spacing: 0.09em;
text-transform: uppercase;
font-family: 'DM Mono', monospace;
}
.field input {
width: 100%;
padding: 10px 14px;
background: rgba(255,255,255,0.04);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 14px;
font-family: 'Sora', sans-serif;
transition: border-color 0.2s, box-shadow 0.2s;
outline: none;
}
.field input::placeholder { color: var(--muted); opacity: 0.55; }
.field input:focus {
border-color: var(--border-hi);
box-shadow: 0 0 0 3px rgba(99,210,255,0.07);
}
/* Buttons */
.btn {
width: 100%;
padding: 11px 16px;
border: none;
border-radius: 9px;
font-family: 'Sora', sans-serif;
font-size: 13px; font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex; align-items: center; justify-content: center; gap: 8px;
letter-spacing: 0.02em;
margin-top: 8px;
}
.btn:active { transform: scale(0.98); }
.btn svg { flex-shrink: 0; }
.btn-primary { background: var(--accent); color: #06111a; }
.btn-primary:hover { background: var(--accent-dk); box-shadow: 0 0 20px rgba(99,210,255,0.22); }
.btn-ghost {
background: rgba(255,255,255,0.04);
color: var(--text);
border: 1px solid var(--border);
}
.btn-ghost:hover { background: rgba(255,255,255,0.07); border-color: rgba(255,255,255,0.12); }
.btn-danger-soft {
background: rgba(255,92,114,0.07);
color: var(--danger);
border: 1px solid rgba(255,92,114,0.18);
}
.btn-danger-soft:hover { background: rgba(255,92,114,0.13); }
/* Section labels */
.section-label {
font-size: 10px; font-weight: 500;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.1em;
font-family: 'DM Mono', monospace;
margin-top: 20px;
margin-bottom: 9px;
}
.section-label:first-child { margin-top: 0; }
/* Log */
.log-wrap { display: none; margin-top: 16px; }
.log-header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 6px;
}
.log-header span {
font-size: 10px;
font-family: 'DM Mono', monospace;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.08em;
}
#log-status { transition: color 0.3s; }
.log-box {
background: #080a0e;
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px;
font-family: 'DM Mono', monospace;
font-size: 11px;
line-height: 1.75;
color: #7ee8a2;
height: 130px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
.log-box::-webkit-scrollbar { width: 4px; }
.log-box::-webkit-scrollbar-thumb { background: #2a2f3a; border-radius: 2px; }
.log-error { color: var(--danger); }
/* Logout */
.logout-row {
margin-top: 20px;
padding-top: 18px;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
}
.logout-row span { font-size: 12px; color: var(--muted); }
.logout-btn {
font-family: 'DM Mono', monospace;
font-size: 11px;
background: none;
border: 1px solid var(--border);
color: var(--muted);
padding: 6px 13px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
letter-spacing: 0.05em;
}
.logout-btn:hover { color: var(--danger); border-color: rgba(255,92,114,0.3); background: rgba(255,92,114,0.05); }
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button { opacity: 0.4; }
</style>
</head>
<body>
<div class="card">
<!-- Header -->
<div class="card-header">
<div class="logo-mark">
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 5h14M3 10h9M3 15h5" stroke="#63d2ff" stroke-width="1.8" stroke-linecap="round"/>
<circle cx="16" cy="14" r="3" stroke="#63d2ff" stroke-width="1.5"/>
<path d="M18.5 16.5L20 18" stroke="#63d2ff" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<div class="header-text">
<h1>RAG Admin Console</h1>
<p>Knowledge base management</p>
</div>
</div>
<!-- Status -->
<div class="status-row">
<span class="status-label">System Status</span>
<span id="status-pill" class="pill loading">
<span class="pill-dot pulse"></span>
<span id="status-text">Checking...</span>
</span>
</div>
<!-- Body -->
<div class="card-body">
<!-- Login -->
<div id="login-section">
<div class="field">
<label>Username</label>
<input type="text" id="username" placeholder="admin@example.com" autocomplete="username">
</div>
<div class="field">
<label>Password</label>
<input type="password" id="password" placeholder="••••••••" autocomplete="current-password"
onkeydown="if(event.key==='Enter') login()">
</div>
<button class="btn btn-primary" onclick="login()">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
<path d="M6 2H3a1 1 0 00-1 1v10a1 1 0 001 1h3M10 5l3 3-3 3M13 8H6"
stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Sign In
</button>
</div>
<!-- Admin Panel -->
<div id="admin-section" style="display:none;">
<p class="section-label">URL Sources</p>
<button class="btn btn-ghost" onclick="performAction('/admin/fetch_rentry')">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
<path d="M13 8A5 5 0 112.5 6.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
<path d="M2 3.5L2.5 6.5 5.5 6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Fetch &amp; Update from URL
</button>
<p class="section-label" style="margin-top:20px;">Local Index</p>
<div class="field">
<label>Max files (incremental)</label>
<input type="number" id="max-files" value="50" min="1" max="500">
</div>
<button class="btn btn-ghost" onclick="performAction('/admin/update_faiss_index')">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
<path d="M8 2v6m0 0l-2.5-2.5M8 8l2.5-2.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 11v1a2 2 0 002 2h8a2 2 0 002-2v-1" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
</svg>
Update Index — New Files Only
</button>
<button class="btn btn-danger-soft" onclick="confirmRebuild()">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
<path d="M3 8a5 5 0 0110 0" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
<path d="M13 5.5V8h-2.5M3 10.5V8h2.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Rebuild Full Index
</button>
<!-- Log -->
<div class="log-wrap" id="log-wrap">
<div class="log-header">
<span>Operation Log</span>
<span id="log-status" style="color:var(--warn)">running…</span>
</div>
<div class="log-box" id="log-box"></div>
</div>
<!-- Logout -->
<div class="logout-row">
<span id="signed-in-label">Signed in as admin</span>
<button class="logout-btn" onclick="logout()">Sign out</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
let authHeader = null;
window.onload = async () => {
await checkStatus();
const savedUser = localStorage.getItem('rag_admin_user');
const savedPass = localStorage.getItem('rag_admin_pass');
if (savedUser && savedPass) {
document.getElementById('username').value = savedUser;
document.getElementById('password').value = savedPass;
await login(true);
}
};
async function checkStatus() {
const pill = document.getElementById('status-pill');
const text = document.getElementById('status-text');
const dot = pill.querySelector('.pill-dot');
try {
const res = await axios.get('/status');
dot.classList.remove('pulse');
if (res.data.rag_initialized) {
pill.className = 'pill';
text.textContent = 'Online';
} else {
pill.className = 'pill loading';
dot.classList.add('pulse');
text.textContent = 'Not Initialized';
}
} catch {
dot.classList.remove('pulse');
pill.className = 'pill error';
text.textContent = 'Offline';
}
}
async function login(isSilent = false) {
const u = document.getElementById('username').value.trim();
const p = document.getElementById('password').value;
try {
await axios.post('/admin/login', {}, { auth: { username: u, password: p } });
authHeader = { username: u, password: p };
localStorage.setItem('rag_admin_user', u);
localStorage.setItem('rag_admin_pass', p);
document.getElementById('login-section').style.display = 'none';
document.getElementById('admin-section').style.display = 'block';
document.getElementById('signed-in-label').textContent = `Signed in as ${u}`;
document.getElementById('password').value = '';
} catch {
if (!isSilent) flashError();
logout();
}
}
function logout() {
authHeader = null;
localStorage.removeItem('rag_admin_user');
localStorage.removeItem('rag_admin_pass');
document.getElementById('username').value = '';
document.getElementById('password').value = '';
document.getElementById('login-section').style.display = 'block';
document.getElementById('admin-section').style.display = 'none';
document.getElementById('log-wrap').style.display = 'none';
}
function flashError() {
const el = document.getElementById('password');
el.style.borderColor = 'var(--danger)';
el.style.boxShadow = '0 0 0 3px rgba(255,92,114,0.1)';
setTimeout(() => { el.style.borderColor = ''; el.style.boxShadow = ''; }, 2500);
}
function confirmRebuild() {
if (confirm('Rebuild the full index? This will re-process all documents and may take several minutes.')) {
performAction('/admin/rebuild_index');
}
}
async function performAction(url) {
const wrap = document.getElementById('log-wrap');
const logBox = document.getElementById('log-box');
const logSt = document.getElementById('log-status');
wrap.style.display = 'block';
logBox.textContent = 'Processing — this may take a minute…\n';
logSt.textContent = 'running…';
logSt.style.color = 'var(--warn)';
const payload = {};
if (url.includes('update')) {
payload.max_new_files = parseInt(document.getElementById('max-files').value, 10) || 50;
}
try {
const res = await axios.post(url, payload, { auth: authHeader });
logBox.textContent += '\n[SUCCESS]\n' + JSON.stringify(res.data, null, 2);
logSt.textContent = 'done';
logSt.style.color = 'var(--success)';
checkStatus();
} catch (e) {
const msg = e.response ? JSON.stringify(e.response.data, null, 2) : e.message;
logBox.innerHTML += `\n<span class="log-error">[ERROR]\n${msg}</span>`;
logSt.textContent = 'failed';
logSt.style.color = 'var(--danger)';
if (e.response?.status === 401) {
alert('Session expired. Please sign in again.');
logout();
}
} finally {
logBox.scrollTop = logBox.scrollHeight;
}
}
</script>
</body>
</html>