Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>PolicyWise - Document Management</title> | |
| <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| /* Management Dashboard Styles */ | |
| .management-container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 2rem; | |
| } | |
| .dashboard-header { | |
| text-align: center; | |
| margin-bottom: 3rem; | |
| } | |
| .dashboard-header h1 { | |
| color: var(--primary-color, #2563eb); | |
| margin-bottom: 0.5rem; | |
| } | |
| .dashboard-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 2rem; | |
| margin-bottom: 3rem; | |
| } | |
| .card { | |
| background: white; | |
| border-radius: 12px; | |
| padding: 2rem; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
| border: 1px solid #e5e7eb; | |
| } | |
| .card h2 { | |
| margin-top: 0; | |
| color: #374151; | |
| font-size: 1.5rem; | |
| margin-bottom: 1.5rem; | |
| } | |
| /* Upload Section */ | |
| .upload-area { | |
| border: 2px dashed #d1d5db; | |
| border-radius: 8px; | |
| padding: 3rem; | |
| text-align: center; | |
| background: #f9fafb; | |
| transition: all 0.3s ease; | |
| cursor: pointer; | |
| } | |
| .upload-area:hover { | |
| border-color: #6366f1; | |
| background: #f0f9ff; | |
| } | |
| .upload-area.dragover { | |
| border-color: #6366f1; | |
| background: #eff6ff; | |
| } | |
| .upload-icon { | |
| font-size: 3rem; | |
| margin-bottom: 1rem; | |
| color: #6b7280; | |
| } | |
| .upload-area h3 { | |
| margin: 0 0 0.5rem 0; | |
| color: #374151; | |
| } | |
| .upload-area p { | |
| margin: 0; | |
| color: #6b7280; | |
| } | |
| .file-input { | |
| display: none; | |
| } | |
| .upload-btn { | |
| background: #6366f1; | |
| color: white; | |
| border: none; | |
| padding: 0.75rem 1.5rem; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-weight: 500; | |
| margin-top: 1rem; | |
| transition: background 0.2s; | |
| } | |
| .upload-btn:hover { | |
| background: #5856eb; | |
| } | |
| .upload-btn:disabled { | |
| background: #9ca3af; | |
| cursor: not-allowed; | |
| } | |
| /* Progress Section */ | |
| .progress-section { | |
| margin-top: 2rem; | |
| display: none; | |
| } | |
| .progress-item { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 0.75rem; | |
| background: #f3f4f6; | |
| border-radius: 6px; | |
| margin-bottom: 0.5rem; | |
| } | |
| .progress-bar { | |
| width: 100px; | |
| height: 8px; | |
| background: #e5e7eb; | |
| border-radius: 4px; | |
| overflow: hidden; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: #10b981; | |
| border-radius: 4px; | |
| transition: width 0.3s ease; | |
| } | |
| /* Status Section */ | |
| .status-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 1rem; | |
| } | |
| .status-card { | |
| background: #f8fafc; | |
| border-radius: 8px; | |
| padding: 1.5rem; | |
| text-align: center; | |
| } | |
| .status-value { | |
| display: block; | |
| font-size: 2rem; | |
| font-weight: 700; | |
| color: #1f2937; | |
| margin-bottom: 0.5rem; | |
| } | |
| .status-label { | |
| color: #6b7280; | |
| font-size: 0.875rem; | |
| } | |
| /* Jobs List */ | |
| .jobs-list { | |
| max-height: 400px; | |
| overflow-y: auto; | |
| } | |
| .job-item { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 1rem; | |
| border-bottom: 1px solid #e5e7eb; | |
| } | |
| .job-item:last-child { | |
| border-bottom: none; | |
| } | |
| .job-info { | |
| flex: 1; | |
| } | |
| .job-name { | |
| font-weight: 500; | |
| color: #374151; | |
| } | |
| .job-status { | |
| font-size: 0.875rem; | |
| color: #6b7280; | |
| margin-top: 0.25rem; | |
| } | |
| .status-badge { | |
| padding: 0.25rem 0.75rem; | |
| border-radius: 9999px; | |
| font-size: 0.75rem; | |
| font-weight: 500; | |
| text-transform: uppercase; | |
| } | |
| .status-completed { | |
| background: #d1fae5; | |
| color: #065f46; | |
| } | |
| .status-processing { | |
| background: #dbeafe; | |
| color: #1e40af; | |
| } | |
| .status-failed { | |
| background: #fee2e2; | |
| color: #991b1b; | |
| } | |
| .status-pending { | |
| background: #fef3c7; | |
| color: #92400e; | |
| } | |
| /* Navigation */ | |
| .nav-link { | |
| display: inline-block; | |
| margin-bottom: 2rem; | |
| color: #6366f1; | |
| text-decoration: none; | |
| font-weight: 500; | |
| } | |
| .nav-link:hover { | |
| text-decoration: underline; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .dashboard-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .management-container { | |
| padding: 1rem; | |
| } | |
| .upload-area { | |
| padding: 2rem; | |
| } | |
| } | |
| /* Notification */ | |
| .notification { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| padding: 1rem 1.5rem; | |
| border-radius: 8px; | |
| color: white; | |
| font-weight: 500; | |
| z-index: 1000; | |
| transform: translateX(100%); | |
| transition: transform 0.3s ease; | |
| } | |
| .notification.show { | |
| transform: translateX(0); | |
| } | |
| .notification.success { | |
| background: #10b981; | |
| } | |
| .notification.error { | |
| background: #ef4444; | |
| } | |
| .notification.info { | |
| background: #6366f1; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="management-container"> | |
| <a href="/" class="nav-link">← Back to Chat</a> | |
| <header class="dashboard-header"> | |
| <h1>Document Management</h1> | |
| <p>Upload and manage documents for the PolicyWise knowledge base</p> | |
| </header> | |
| <div class="dashboard-grid"> | |
| <!-- Upload Section --> | |
| <div class="card"> | |
| <h2>Upload Documents</h2> | |
| <div class="upload-area" id="uploadArea"> | |
| <div class="upload-icon">📄</div> | |
| <h3>Drag and drop files here</h3> | |
| <p>or click to select files</p> | |
| <p style="font-size: 0.75rem; margin-top: 1rem; color: #9ca3af;"> | |
| Supported: PDF, Word, Markdown, Text files (max 50MB each) | |
| </p> | |
| </div> | |
| <input type="file" id="fileInput" class="file-input" multiple accept=".pdf,.doc,.docx,.txt,.md"> | |
| <button id="uploadBtn" class="upload-btn" disabled>Select Files to Upload</button> | |
| <div class="progress-section" id="progressSection"> | |
| <h3>Upload Progress</h3> | |
| <div id="progressList"></div> | |
| </div> | |
| </div> | |
| <!-- System Status --> | |
| <div class="card"> | |
| <h2>System Status</h2> | |
| <div class="status-grid" id="statusGrid"> | |
| <div class="status-card"> | |
| <span class="status-value" id="totalFiles">-</span> | |
| <span class="status-label">Total Files</span> | |
| </div> | |
| <div class="status-card"> | |
| <span class="status-value" id="queueSize">-</span> | |
| <span class="status-label">Queue Size</span> | |
| </div> | |
| <div class="status-card"> | |
| <span class="status-value" id="activeJobs">-</span> | |
| <span class="status-label">Processing</span> | |
| </div> | |
| <div class="status-card"> | |
| <span class="status-value" id="completedJobs">-</span> | |
| <span class="status-label">Completed</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Processing Jobs --> | |
| <div class="card"> | |
| <h2>Recent Processing Jobs</h2> | |
| <div class="jobs-list" id="jobsList"> | |
| <div style="text-align: center; color: #6b7280; padding: 2rem;"> | |
| Loading jobs... | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| class DocumentManager { | |
| constructor() { | |
| this.apiBase = '/api/documents'; | |
| this.uploadQueue = []; | |
| this.init(); | |
| } | |
| init() { | |
| this.setupUploadHandlers(); | |
| this.loadStatus(); | |
| this.loadJobs(); | |
| // Refresh data every 5 seconds | |
| setInterval(() => { | |
| this.loadStatus(); | |
| this.loadJobs(); | |
| }, 5000); | |
| } | |
| setupUploadHandlers() { | |
| const uploadArea = document.getElementById('uploadArea'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const uploadBtn = document.getElementById('uploadBtn'); | |
| // Drag and drop | |
| uploadArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.add('dragover'); | |
| }); | |
| uploadArea.addEventListener('dragleave', () => { | |
| uploadArea.classList.remove('dragover'); | |
| }); | |
| uploadArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.remove('dragover'); | |
| this.handleFiles(e.dataTransfer.files); | |
| }); | |
| uploadArea.addEventListener('click', () => { | |
| fileInput.click(); | |
| }); | |
| fileInput.addEventListener('change', (e) => { | |
| this.handleFiles(e.target.files); | |
| }); | |
| uploadBtn.addEventListener('click', () => { | |
| this.uploadFiles(); | |
| }); | |
| } | |
| handleFiles(files) { | |
| this.uploadQueue = Array.from(files); | |
| const uploadBtn = document.getElementById('uploadBtn'); | |
| if (this.uploadQueue.length > 0) { | |
| uploadBtn.disabled = false; | |
| uploadBtn.textContent = `Upload ${this.uploadQueue.length} files`; | |
| } else { | |
| uploadBtn.disabled = true; | |
| uploadBtn.textContent = 'Select Files to Upload'; | |
| } | |
| } | |
| async uploadFiles() { | |
| if (this.uploadQueue.length === 0) return; | |
| const progressSection = document.getElementById('progressSection'); | |
| const progressList = document.getElementById('progressList'); | |
| const uploadBtn = document.getElementById('uploadBtn'); | |
| progressSection.style.display = 'block'; | |
| progressList.innerHTML = ''; | |
| uploadBtn.disabled = true; | |
| uploadBtn.textContent = 'Uploading...'; | |
| for (let i = 0; i < this.uploadQueue.length; i++) { | |
| const file = this.uploadQueue[i]; | |
| const progressItem = this.createProgressItem(file, i); | |
| progressList.appendChild(progressItem); | |
| try { | |
| await this.uploadSingleFile(file, i); | |
| } catch (error) { | |
| console.error('Upload failed:', error); | |
| this.updateProgress(i, 'failed', error.message); | |
| } | |
| } | |
| this.showNotification('Upload completed', 'success'); | |
| uploadBtn.disabled = false; | |
| uploadBtn.textContent = 'Select Files to Upload'; | |
| this.uploadQueue = []; | |
| // Refresh status after upload | |
| setTimeout(() => { | |
| this.loadStatus(); | |
| this.loadJobs(); | |
| }, 1000); | |
| } | |
| createProgressItem(file, index) { | |
| const item = document.createElement('div'); | |
| item.className = 'progress-item'; | |
| item.innerHTML = ` | |
| <div class="job-info"> | |
| <div class="job-name">${file.name}</div> | |
| <div class="job-status" id="status-${index}">Preparing...</div> | |
| </div> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" id="progress-${index}" style="width: 0%"></div> | |
| </div> | |
| `; | |
| return item; | |
| } | |
| async uploadSingleFile(file, index) { | |
| const formData = new FormData(); | |
| formData.append('files', file); | |
| formData.append('auto_process', 'true'); | |
| this.updateProgress(index, 'uploading', 'Uploading...'); | |
| const response = await fetch(`${this.apiBase}/upload`, { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`Upload failed: ${response.statusText}`); | |
| } | |
| const result = await response.json(); | |
| if (result.status === 'success') { | |
| this.updateProgress(index, 'completed', 'Upload completed'); | |
| } else { | |
| throw new Error(result.message || 'Upload failed'); | |
| } | |
| } | |
| updateProgress(index, status, message) { | |
| const statusEl = document.getElementById(`status-${index}`); | |
| const progressEl = document.getElementById(`progress-${index}`); | |
| if (statusEl) statusEl.textContent = message; | |
| if (progressEl) { | |
| switch (status) { | |
| case 'uploading': | |
| progressEl.style.width = '50%'; | |
| break; | |
| case 'completed': | |
| progressEl.style.width = '100%'; | |
| progressEl.style.background = '#10b981'; | |
| break; | |
| case 'failed': | |
| progressEl.style.width = '100%'; | |
| progressEl.style.background = '#ef4444'; | |
| break; | |
| } | |
| } | |
| } | |
| async loadStatus() { | |
| try { | |
| const response = await fetch(`${this.apiBase}/stats`); | |
| const data = await response.json(); | |
| if (data.status === 'success') { | |
| this.updateStatusDisplay(data.stats); | |
| } | |
| } catch (error) { | |
| console.error('Failed to load status:', error); | |
| } | |
| } | |
| updateStatusDisplay(stats) { | |
| const elements = { | |
| totalFiles: document.getElementById('totalFiles'), | |
| queueSize: document.getElementById('queueSize'), | |
| activeJobs: document.getElementById('activeJobs'), | |
| completedJobs: document.getElementById('completedJobs') | |
| }; | |
| if (stats.upload_stats) { | |
| elements.totalFiles.textContent = stats.upload_stats.total_files || 0; | |
| } | |
| if (stats.processing_queue) { | |
| elements.queueSize.textContent = stats.processing_queue.queue_size || 0; | |
| elements.activeJobs.textContent = stats.processing_queue.active_jobs || 0; | |
| elements.completedJobs.textContent = stats.processing_queue.completed_jobs || 0; | |
| } | |
| } | |
| async loadJobs() { | |
| try { | |
| const response = await fetch(`${this.apiBase}/jobs`); | |
| const data = await response.json(); | |
| if (data.status === 'success') { | |
| this.updateJobsDisplay(data.jobs); | |
| } | |
| } catch (error) { | |
| console.error('Failed to load jobs:', error); | |
| } | |
| } | |
| updateJobsDisplay(jobs) { | |
| const jobsList = document.getElementById('jobsList'); | |
| if (jobs.length === 0) { | |
| jobsList.innerHTML = '<div style="text-align: center; color: #6b7280; padding: 2rem;">No processing jobs found</div>'; | |
| return; | |
| } | |
| jobsList.innerHTML = jobs.slice(0, 10).map(job => ` | |
| <div class="job-item"> | |
| <div class="job-info"> | |
| <div class="job-name">${job.file_info?.original_name || 'Unknown'}</div> | |
| <div class="job-status"> | |
| Started: ${job.started_at ? new Date(job.started_at).toLocaleString() : 'Not started'} | |
| ${job.error_message ? `• Error: ${job.error_message}` : ''} | |
| </div> | |
| </div> | |
| <span class="status-badge status-${job.status}"> | |
| ${job.status} | |
| </span> | |
| </div> | |
| `).join(''); | |
| } | |
| showNotification(message, type = 'info') { | |
| const notification = document.createElement('div'); | |
| notification.className = `notification ${type}`; | |
| notification.textContent = message; | |
| document.body.appendChild(notification); | |
| setTimeout(() => notification.classList.add('show'), 100); | |
| setTimeout(() => { | |
| notification.classList.remove('show'); | |
| setTimeout(() => document.body.removeChild(notification), 300); | |
| }, 3000); | |
| } | |
| } | |
| // Initialize when page loads | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new DocumentManager(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |