Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>RAG Admin Panel - Document Management</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary-color: #6d5ce7; | |
| --primary-hover: #4a3db0; | |
| --primary-light: #a29bfe; | |
| --primary-glow: rgba(109, 92, 231, 0.35); | |
| --accent: #5f72f3; | |
| --accent-light: #7c8cf8; | |
| --success-color: #00cec9; | |
| --danger-color: #ff6b6b; | |
| --warning-color: #feca57; | |
| --bg-dark: #080816; | |
| --bg-card: rgba(15, 15, 35, 0.65); | |
| --text-primary: #eef0ff; | |
| --text-secondary: #a0a8c8; | |
| --border-color: rgba(109, 92, 231, 0.12); | |
| --border-hover: rgba(109, 92, 231, 0.35); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background: var(--bg-dark); | |
| min-height: 100vh; | |
| color: var(--text-primary); | |
| padding: 20px; | |
| overflow-x: hidden; | |
| } | |
| #bgCanvas { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 0; | |
| pointer-events: none; | |
| } | |
| .container { | |
| max-width: 900px; | |
| margin: 0 auto; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| .header { | |
| text-align: center; | |
| margin-bottom: 40px; | |
| padding: 30px; | |
| background: var(--bg-card); | |
| border-radius: 16px; | |
| border: 1px solid var(--border-color); | |
| backdrop-filter: blur(20px); | |
| } | |
| .header h1 { | |
| font-size: 2rem; | |
| margin-bottom: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 12px; | |
| } | |
| .header h1 i { | |
| color: var(--primary-color); | |
| } | |
| .header p { | |
| color: var(--text-secondary); | |
| } | |
| .status-card { | |
| background: var(--bg-card); | |
| border-radius: 12px; | |
| padding: 20px; | |
| margin-bottom: 20px; | |
| border: 1px solid var(--border-color); | |
| backdrop-filter: blur(20px); | |
| } | |
| .status-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 15px; | |
| } | |
| .status-header h3 { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .status-indicator { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 6px 12px; | |
| border-radius: 20px; | |
| font-size: 0.85rem; | |
| } | |
| .status-indicator.ready { | |
| background: rgba(0, 206, 201, 0.2); | |
| color: var(--success-color); | |
| } | |
| .status-indicator.empty { | |
| background: rgba(254, 202, 87, 0.2); | |
| color: var(--warning-color); | |
| } | |
| .status-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: currentColor; | |
| } | |
| .upload-section { | |
| background: var(--bg-card); | |
| border-radius: 12px; | |
| padding: 30px; | |
| margin-bottom: 20px; | |
| border: 1px solid var(--border-color); | |
| backdrop-filter: blur(20px); | |
| } | |
| .upload-box { | |
| border: 2px dashed var(--border-color); | |
| border-radius: 12px; | |
| padding: 50px 20px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .upload-box:hover { | |
| border-color: var(--primary-color); | |
| background: rgba(109, 92, 231, 0.1); | |
| } | |
| .upload-box.dragover { | |
| border-color: var(--primary-color); | |
| background: rgba(109, 92, 231, 0.2); | |
| } | |
| .upload-box i { | |
| font-size: 3rem; | |
| color: var(--primary-color); | |
| margin-bottom: 15px; | |
| } | |
| .upload-box h3 { | |
| margin-bottom: 8px; | |
| } | |
| .upload-box p { | |
| color: var(--text-secondary); | |
| font-size: 0.9rem; | |
| } | |
| .documents-section { | |
| background: var(--bg-card); | |
| border-radius: 12px; | |
| padding: 20px; | |
| border: 1px solid var(--border-color); | |
| backdrop-filter: blur(20px); | |
| } | |
| .documents-section h3 { | |
| margin-bottom: 15px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .document-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .document-item { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 15px; | |
| background: rgba(8, 8, 22, 0.5); | |
| border-radius: 8px; | |
| border: 1px solid var(--border-color); | |
| } | |
| .document-info { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .document-info i { | |
| font-size: 1.5rem; | |
| color: var(--danger-color); | |
| } | |
| .document-name { | |
| font-weight: 500; | |
| } | |
| .delete-btn { | |
| background: rgba(255, 107, 107, 0.2); | |
| border: 1px solid var(--danger-color); | |
| color: var(--danger-color); | |
| padding: 8px 16px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .delete-btn:hover { | |
| background: var(--danger-color); | |
| color: white; | |
| } | |
| .clear-all-btn { | |
| background: var(--danger-color); | |
| border: none; | |
| color: white; | |
| padding: 10px 20px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-weight: 500; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| transition: all 0.3s ease; | |
| } | |
| .clear-all-btn:hover { | |
| background: #e55050; | |
| } | |
| .rebuild-btn { | |
| background: var(--primary-color); | |
| border: none; | |
| color: white; | |
| padding: 10px 20px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-weight: 500; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| transition: all 0.3s ease; | |
| } | |
| .rebuild-btn:hover { | |
| background: var(--primary-hover); | |
| } | |
| .empty-state { | |
| text-align: center; | |
| padding: 40px; | |
| color: var(--text-secondary); | |
| } | |
| .empty-state i { | |
| font-size: 3rem; | |
| margin-bottom: 15px; | |
| opacity: 0.5; | |
| } | |
| .toast { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| padding: 15px 25px; | |
| border-radius: 8px; | |
| color: white; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| z-index: 1000; | |
| animation: slideIn 0.3s ease; | |
| } | |
| .toast.success { | |
| background: var(--success-color); | |
| } | |
| .toast.error { | |
| background: var(--danger-color); | |
| } | |
| @keyframes slideIn { | |
| from { | |
| transform: translateX(100%); | |
| opacity: 0; | |
| } | |
| to { | |
| transform: translateX(0); | |
| opacity: 1; | |
| } | |
| } | |
| .loading-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(8, 8, 22, 0.88); | |
| display: none; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 999; | |
| backdrop-filter: blur(12px); | |
| } | |
| .loading-content { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 28px; | |
| } | |
| .loader-visual { | |
| position: relative; | |
| width: 100px; | |
| height: 100px; | |
| } | |
| .loader-ring { | |
| position: absolute; | |
| border-radius: 50%; | |
| border: 2px solid transparent; | |
| } | |
| .loader-ring:nth-child(1) { | |
| width: 100px; | |
| height: 100px; | |
| top: 0; left: 0; | |
| border-top-color: var(--primary-color); | |
| border-right-color: var(--primary-color); | |
| animation: lspin 1.2s cubic-bezier(0.5,0,0.5,1) infinite; | |
| filter: drop-shadow(0 0 6px var(--primary-glow)); | |
| } | |
| .loader-ring:nth-child(2) { | |
| width: 76px; | |
| height: 76px; | |
| top: 12px; left: 12px; | |
| border-bottom-color: var(--accent); | |
| border-left-color: var(--accent); | |
| animation: lspin-r 1s cubic-bezier(0.5,0,0.5,1) infinite; | |
| filter: drop-shadow(0 0 6px rgba(95,114,243,0.35)); | |
| } | |
| .loader-ring:nth-child(3) { | |
| width: 52px; | |
| height: 52px; | |
| top: 24px; left: 24px; | |
| border-top-color: var(--primary-light); | |
| border-right-color: var(--accent-light); | |
| animation: lspin 0.8s cubic-bezier(0.5,0,0.5,1) infinite; | |
| filter: drop-shadow(0 0 4px rgba(162,155,254,0.3)); | |
| } | |
| .loader-core { | |
| position: absolute; | |
| width: 36px; height: 36px; | |
| top: 32px; left: 32px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.1rem; | |
| color: var(--primary-light); | |
| animation: lpulse 1.5s ease-in-out infinite; | |
| } | |
| .loader-text-area { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .loader-text-area p { | |
| color: var(--text-primary); | |
| font-size: 1.05rem; | |
| font-weight: 600; | |
| } | |
| .loader-dots { | |
| display: flex; | |
| gap: 6px; | |
| } | |
| .loader-dots span { | |
| width: 6px; height: 6px; | |
| border-radius: 50%; | |
| background: var(--primary-light); | |
| animation: ldot 1.2s ease-in-out infinite; | |
| } | |
| .loader-dots span:nth-child(2) { animation-delay: 0.15s; } | |
| .loader-dots span:nth-child(3) { animation-delay: 0.3s; } | |
| @keyframes lspin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| @keyframes lspin-r { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(-360deg); } | |
| } | |
| @keyframes lpulse { | |
| 0%,100% { opacity: 0.6; transform: scale(1); } | |
| 50% { opacity: 1; transform: scale(1.15); } | |
| } | |
| @keyframes ldot { | |
| 0%,80%,100% { opacity: 0.3; transform: scale(0.8); } | |
| 40% { opacity: 1; transform: scale(1.2); } | |
| } | |
| .spinner { | |
| width: 50px; | |
| height: 50px; | |
| border: 4px solid var(--border-color); | |
| border-top-color: var(--primary-color); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 15px; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .back-link { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| margin-bottom: 20px; | |
| transition: color 0.3s; | |
| } | |
| .back-link:hover { | |
| color: var(--primary-color); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Three.js Background Canvas --> | |
| <canvas id="bgCanvas"></canvas> | |
| <div class="container"> | |
| <a href="http://localhost:8000" class="back-link" target="_blank"> | |
| <i class="fas fa-arrow-left"></i> Open Chatbot (Port 8000) | |
| </a> | |
| <div class="header"> | |
| <h1><i class="fas fa-database"></i> RAG Admin Panel</h1> | |
| <p>Upload and manage PDF documents for the RAG knowledge base</p> | |
| </div> | |
| <div class="status-card"> | |
| <div class="status-header"> | |
| <h3><i class="fas fa-chart-bar"></i> System Status</h3> | |
| <div class="status-indicator empty" id="statusIndicator"> | |
| <span class="status-dot"></span> | |
| <span id="statusText">No documents</span> | |
| </div> | |
| </div> | |
| <div id="statsInfo"> | |
| <p style="color: var(--text-secondary);">Documents: <span id="docCount">0</span></p> | |
| </div> | |
| </div> | |
| <div class="upload-section"> | |
| <div class="upload-box" id="uploadBox"> | |
| <i class="fas fa-cloud-upload-alt"></i> | |
| <h3>Upload PDF Document</h3> | |
| <p>Drag & drop your PDF here or click to browse</p> | |
| <input type="file" id="fileInput" accept=".pdf" hidden> | |
| </div> | |
| </div> | |
| <div class="documents-section"> | |
| <div class="status-header"> | |
| <h3><i class="fas fa-folder-open"></i> Uploaded Documents</h3> | |
| <div style="display: flex; gap: 10px;"> | |
| <button class="rebuild-btn" id="rebuildBtn" style="display: none;"> | |
| <i class="fas fa-gears"></i> Rebuild RAG | |
| </button> | |
| <button class="clear-all-btn" id="clearAllBtn" style="display: none;"> | |
| <i class="fas fa-trash-alt"></i> Clear All | |
| </button> | |
| </div> | |
| </div> | |
| <div class="document-list" id="documentList"> | |
| <div class="empty-state"> | |
| <i class="fas fa-file-pdf"></i> | |
| <p>No documents uploaded yet</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="loading-overlay" id="loadingOverlay"> | |
| <div class="loading-content"> | |
| <div class="loader-visual"> | |
| <div class="loader-ring"></div> | |
| <div class="loader-ring"></div> | |
| <div class="loader-ring"></div> | |
| <div class="loader-core"> | |
| <i class="fas fa-brain"></i> | |
| </div> | |
| </div> | |
| <div class="loader-text-area"> | |
| <p id="loadingText">Processing...</p> | |
| <div class="loader-dots"> | |
| <span></span><span></span><span></span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="/static/js/bg-animation.js"></script> | |
| <script> | |
| const uploadBox = document.getElementById('uploadBox'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const documentList = document.getElementById('documentList'); | |
| const statusIndicator = document.getElementById('statusIndicator'); | |
| const statusText = document.getElementById('statusText'); | |
| const docCount = document.getElementById('docCount'); | |
| const clearAllBtn = document.getElementById('clearAllBtn'); | |
| const rebuildBtn = document.getElementById('rebuildBtn'); | |
| const loadingOverlay = document.getElementById('loadingOverlay'); | |
| const loadingText = document.getElementById('loadingText'); | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', loadStatus); | |
| // Upload box events | |
| uploadBox.addEventListener('click', () => fileInput.click()); | |
| fileInput.addEventListener('change', handleFileSelect); | |
| // Drag and drop | |
| uploadBox.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| uploadBox.classList.add('dragover'); | |
| }); | |
| uploadBox.addEventListener('dragleave', () => { | |
| uploadBox.classList.remove('dragover'); | |
| }); | |
| uploadBox.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| uploadBox.classList.remove('dragover'); | |
| if (e.dataTransfer.files.length > 0) { | |
| handleFileUpload(e.dataTransfer.files[0]); | |
| } | |
| }); | |
| // Clear all | |
| clearAllBtn.addEventListener('click', clearAllDocuments); | |
| rebuildBtn.addEventListener('click', rebuildRag); | |
| function handleFileSelect(e) { | |
| if (e.target.files.length > 0) { | |
| handleFileUpload(e.target.files[0]); | |
| } | |
| } | |
| async function handleFileUpload(file) { | |
| if (!file.name.toLowerCase().endsWith('.pdf')) { | |
| showToast('Please upload a PDF file', 'error'); | |
| return; | |
| } | |
| showLoading('Uploading and processing PDF...'); | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| try { | |
| const response = await fetch('/api/upload', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) { | |
| const error = await response.json(); | |
| throw new Error(error.detail || 'Upload failed'); | |
| } | |
| const result = await response.json(); | |
| showToast(result.message, 'success'); | |
| loadStatus(); | |
| } catch (error) { | |
| console.error('Upload error:', error); | |
| showToast(error.message, 'error'); | |
| } finally { | |
| hideLoading(); | |
| fileInput.value = ''; | |
| } | |
| } | |
| async function loadStatus() { | |
| try { | |
| const response = await fetch('/api/status'); | |
| const status = await response.json(); | |
| docCount.textContent = status.documents_count; | |
| if (status.initialized && status.documents_count > 0) { | |
| statusIndicator.className = 'status-indicator ready'; | |
| statusText.textContent = 'Ready'; | |
| clearAllBtn.style.display = 'flex'; | |
| rebuildBtn.style.display = 'flex'; | |
| renderDocuments(status.documents); | |
| } else { | |
| statusIndicator.className = 'status-indicator empty'; | |
| statusText.textContent = 'No documents'; | |
| clearAllBtn.style.display = 'none'; | |
| rebuildBtn.style.display = 'none'; | |
| documentList.innerHTML = ` | |
| <div class="empty-state"> | |
| <i class="fas fa-file-pdf"></i> | |
| <p>No documents uploaded yet</p> | |
| </div> | |
| `; | |
| } | |
| } catch (error) { | |
| console.error('Failed to load status:', error); | |
| } | |
| } | |
| function renderDocuments(documents) { | |
| if (!documents || documents.length === 0) { | |
| documentList.innerHTML = ` | |
| <div class="empty-state"> | |
| <i class="fas fa-file-pdf"></i> | |
| <p>No documents uploaded yet</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| documentList.innerHTML = documents.map(doc => ` | |
| <div class="document-item"> | |
| <div class="document-info"> | |
| <i class="fas fa-file-pdf"></i> | |
| <span class="document-name">${doc}</span> | |
| </div> | |
| <button class="delete-btn" onclick="deleteDocument('${doc}')"> | |
| <i class="fas fa-trash"></i> Delete | |
| </button> | |
| </div> | |
| `).join(''); | |
| } | |
| async function deleteDocument(filename) { | |
| if (!confirm(`Delete "${filename}"?`)) return; | |
| try { | |
| const response = await fetch(`/api/document/${encodeURIComponent(filename)}`, { | |
| method: 'DELETE' | |
| }); | |
| if (response.ok) { | |
| showToast('Document deleted', 'success'); | |
| loadStatus(); | |
| } | |
| } catch (error) { | |
| showToast('Failed to delete', 'error'); | |
| } | |
| } | |
| async function clearAllDocuments() { | |
| if (!confirm('Clear all documents? This cannot be undone.')) return; | |
| showLoading('Clearing all data...'); | |
| try { | |
| const response = await fetch('/api/clear', { method: 'POST' }); | |
| if (response.ok) { | |
| showToast('All documents cleared', 'success'); | |
| loadStatus(); | |
| } | |
| } catch (error) { | |
| showToast('Failed to clear', 'error'); | |
| } finally { | |
| hideLoading(); | |
| } | |
| } | |
| async function rebuildRag() { | |
| showLoading('Rebuilding RAG index from all PDFs...'); | |
| try { | |
| const response = await fetch('/api/rebuild', { method: 'POST' }); | |
| const result = await response.json(); | |
| if (!response.ok || !result.success) { | |
| throw new Error(result.message || 'RAG rebuild failed'); | |
| } | |
| showToast(result.message, 'success'); | |
| loadStatus(); | |
| } catch (error) { | |
| showToast(error.message || 'Failed to rebuild RAG', 'error'); | |
| } finally { | |
| hideLoading(); | |
| } | |
| } | |
| function showLoading(text) { | |
| loadingText.textContent = text; | |
| loadingOverlay.style.display = 'flex'; | |
| } | |
| function hideLoading() { | |
| loadingOverlay.style.display = 'none'; | |
| } | |
| function showToast(message, type) { | |
| const existing = document.querySelector('.toast'); | |
| if (existing) existing.remove(); | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| toast.innerHTML = ` | |
| <i class="fas ${type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle'}"></i> | |
| <span>${message}</span> | |
| `; | |
| document.body.appendChild(toast); | |
| setTimeout(() => { | |
| toast.style.opacity = '0'; | |
| setTimeout(() => toast.remove(), 300); | |
| }, 3000); | |
| } | |
| </script> | |
| </body> | |
| </html> | |