Spaces:
Running
Running
| <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 & 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> |