Spaces:
Sleeping
Sleeping
| // Global variables | |
| let categories = []; | |
| let documents = []; | |
| let authToken = null; | |
| let currentUser = null; | |
| let currentView = 'list'; | |
| // Initialize app | |
| document.addEventListener('DOMContentLoaded', function() { | |
| checkAuth(); | |
| setupSidebar(); | |
| }); | |
| // Sidebar functionality | |
| function setupSidebar() { | |
| const sidebarToggle = document.getElementById('sidebarToggle'); | |
| const sidebar = document.getElementById('sidebar'); | |
| const mainContent = document.querySelector('.main-content'); | |
| if (sidebarToggle) { | |
| sidebarToggle.addEventListener('click', function() { | |
| sidebar.classList.toggle('collapsed'); | |
| mainContent.classList.toggle('sidebar-collapsed'); | |
| }); | |
| } | |
| // Auto-collapse on mobile | |
| if (window.innerWidth <= 768) { | |
| sidebar.classList.add('collapsed'); | |
| mainContent.classList.add('sidebar-collapsed'); | |
| } | |
| } | |
| // Authentication functions | |
| function checkAuth() { | |
| authToken = localStorage.getItem('authToken'); | |
| currentUser = localStorage.getItem('currentUser'); | |
| if (authToken && currentUser) { | |
| showMainApp(); | |
| document.getElementById('welcomeUser').textContent = `Welcome, ${currentUser}`; | |
| loadStats(); | |
| loadCategories(); | |
| setupFileUploads(); | |
| } else { | |
| showLoginModal(); | |
| } | |
| } | |
| function showLoginModal() { | |
| document.getElementById('loginModal').style.display = 'flex'; | |
| document.getElementById('mainApp').style.display = 'none'; | |
| } | |
| function showMainApp() { | |
| document.getElementById('loginModal').style.display = 'none'; | |
| document.getElementById('mainApp').style.display = 'block'; | |
| } | |
| function logout() { | |
| localStorage.removeItem('authToken'); | |
| localStorage.removeItem('currentUser'); | |
| authToken = null; | |
| currentUser = null; | |
| showLoginModal(); | |
| } | |
| // Login form handler | |
| document.getElementById('loginForm').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const username = document.getElementById('username').value; | |
| const password = document.getElementById('password').value; | |
| const resultDiv = document.getElementById('loginResult'); | |
| const formData = new FormData(); | |
| formData.append('username', username); | |
| formData.append('password', password); | |
| try { | |
| const response = await fetch('/api/login', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const result = await response.json(); | |
| if (response.ok) { | |
| authToken = result.access_token; | |
| currentUser = result.username; | |
| localStorage.setItem('authToken', authToken); | |
| localStorage.setItem('currentUser', currentUser); | |
| showMainApp(); | |
| document.getElementById('welcomeUser').textContent = `Welcome, ${currentUser}`; | |
| loadStats(); | |
| loadCategories(); | |
| setupFileUploads(); | |
| } else { | |
| showResult(resultDiv, result.detail, 'error'); | |
| } | |
| } catch (error) { | |
| showResult(resultDiv, 'Login failed: ' + error.message, 'error'); | |
| } | |
| }); | |
| // API request with authentication | |
| async function authenticatedFetch(url, options = {}) { | |
| if (!authToken) { | |
| throw new Error('No authentication token'); | |
| } | |
| const defaultOptions = { | |
| headers: { | |
| 'Authorization': `Bearer ${authToken}`, | |
| ...options.headers | |
| } | |
| }; | |
| const response = await fetch(url, { ...options, ...defaultOptions }); | |
| if (response.status === 401) { | |
| logout(); | |
| throw new Error('Authentication failed'); | |
| } | |
| return response; | |
| } | |
| // Enhanced tab management with sidebar highlighting | |
| function showTab(tabName) { | |
| // Hide all tabs | |
| document.querySelectorAll('.tab-content').forEach(tab => { | |
| tab.classList.remove('active'); | |
| }); | |
| // Remove active class from all sidebar links | |
| document.querySelectorAll('.sidebar-nav .nav-link').forEach(link => { | |
| link.classList.remove('active'); | |
| }); | |
| // Show selected tab | |
| document.getElementById(tabName).classList.add('active'); | |
| // Highlight active sidebar link | |
| const activeLink = document.querySelector(`[onclick="showTab('${tabName}')"]`); | |
| if (activeLink) { | |
| activeLink.classList.add('active'); | |
| } | |
| // Load data for specific tabs | |
| if (tabName === 'browse') { | |
| loadCategories(); | |
| loadAllDocuments(); | |
| } else if (tabName === 'dashboard') { | |
| loadStats(); | |
| loadRecentActivity(); | |
| createCategoryChart(); | |
| } | |
| } | |
| // Setup file upload drag & drop | |
| function setupFileUploads() { | |
| const uploads = [ | |
| { div: 'categoryUpload', input: 'categoryFile' }, | |
| { div: 'classifyUpload', input: 'classifyFile' }, | |
| { div: 'ocrUpload', input: 'ocrFile' } | |
| ]; | |
| uploads.forEach(upload => { | |
| const uploadDiv = document.getElementById(upload.div); | |
| const fileInput = document.getElementById(upload.input); | |
| uploadDiv.addEventListener('click', () => fileInput.click()); | |
| uploadDiv.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| uploadDiv.classList.add('dragover'); | |
| }); | |
| uploadDiv.addEventListener('dragleave', () => { | |
| uploadDiv.classList.remove('dragover'); | |
| }); | |
| uploadDiv.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| uploadDiv.classList.remove('dragover'); | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) { | |
| fileInput.files = files; | |
| uploadDiv.querySelector('p').textContent = files[0].name; | |
| } | |
| }); | |
| fileInput.addEventListener('change', () => { | |
| if (fileInput.files.length > 0) { | |
| uploadDiv.querySelector('p').textContent = fileInput.files[0].name; | |
| } | |
| }); | |
| }); | |
| } | |
| // Enhanced stats loading with sidebar display | |
| async function loadStats() { | |
| try { | |
| const response = await authenticatedFetch('/api/stats'); | |
| const stats = await response.json(); | |
| // Dashboard stats | |
| const dashboardStatsHtml = ` | |
| <div class="col-md-4"> | |
| <div class="card stat-card"> | |
| <div class="card-body"> | |
| <h3>${stats.total_categories}</h3> | |
| <p><i class="fas fa-tags me-2"></i>Total Categories</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-md-4"> | |
| <div class="card stat-card"> | |
| <div class="card-body"> | |
| <h3>${stats.total_documents}</h3> | |
| <p><i class="fas fa-file me-2"></i>Documents Archived</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-md-4"> | |
| <div class="card stat-card"> | |
| <div class="card-body"> | |
| <h3>35%</h3> | |
| <p><i class="fas fa-percentage me-2"></i>Min Confidence</p> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| // Sidebar stats | |
| const sidebarStatsHtml = ` | |
| <div class="sidebar-stat"> | |
| <h6>${stats.total_categories}</h6> | |
| <small>Categories</small> | |
| </div> | |
| <div class="sidebar-stat"> | |
| <h6>${stats.total_documents}</h6> | |
| <small>Documents</small> | |
| </div> | |
| `; | |
| document.getElementById('dashboardStats').innerHTML = dashboardStatsHtml; | |
| document.getElementById('sidebarStats').innerHTML = sidebarStatsHtml; | |
| } catch (error) { | |
| console.error('Error loading stats:', error); | |
| } | |
| } | |
| // Enhanced categories loading with sidebar display | |
| async function loadCategories() { | |
| try { | |
| const response = await authenticatedFetch('/api/categories'); | |
| const data = await response.json(); | |
| categories = data.categories; | |
| // Main category buttons | |
| const buttonsHtml = ` | |
| <button class="category-btn active" onclick="filterDocuments('all')"> | |
| <i class="fas fa-th-large me-2"></i>All Documents | |
| </button> | |
| ${categories.map(cat => ` | |
| <button class="category-btn" onclick="filterDocuments('${cat}')"> | |
| <i class="fas fa-folder me-2"></i>${cat} (${data.counts[cat] || 0}) | |
| </button> | |
| `).join('')} | |
| `; | |
| // Sidebar categories | |
| const sidebarCategoriesHtml = categories.map(cat => ` | |
| <div class="sidebar-category" onclick="filterDocuments('${cat}')"> | |
| <div class="sidebar-category-icon"></div> | |
| <span>${cat} (${data.counts[cat] || 0})</span> | |
| </div> | |
| `).join(''); | |
| document.getElementById('categoryButtons').innerHTML = buttonsHtml; | |
| document.getElementById('sidebarCategories').innerHTML = sidebarCategoriesHtml; | |
| } catch (error) { | |
| console.error('Error loading categories:', error); | |
| } | |
| } | |
| // Load all documents | |
| async function loadAllDocuments() { | |
| try { | |
| const response = await authenticatedFetch('/api/documents'); | |
| const data = await response.json(); | |
| documents = data.documents; | |
| displayDocuments(documents); | |
| } catch (error) { | |
| console.error('Error loading documents:', error); | |
| } | |
| } | |
| // Filter documents by category | |
| async function filterDocuments(category) { | |
| // Update active button | |
| document.querySelectorAll('.category-btn').forEach(btn => { | |
| btn.classList.remove('active'); | |
| }); | |
| event.target.classList.add('active'); | |
| try { | |
| let filteredDocs; | |
| if (category === 'all') { | |
| const response = await authenticatedFetch('/api/documents'); | |
| const data = await response.json(); | |
| filteredDocs = data.documents; | |
| } else { | |
| const response = await authenticatedFetch(`/api/documents/${category}`); | |
| const data = await response.json(); | |
| filteredDocs = data.documents; | |
| } | |
| displayDocuments(filteredDocs); | |
| } catch (error) { | |
| console.error('Error filtering documents:', error); | |
| } | |
| } | |
| // Delete document | |
| async function deleteDocument(documentId, filename) { | |
| if (!confirm(`Are you sure you want to delete "${filename}"? This action cannot be undone.`)) { | |
| return; | |
| } | |
| try { | |
| const response = await authenticatedFetch(`/api/documents/${documentId}`, { | |
| method: 'DELETE' | |
| }); | |
| const result = await response.json(); | |
| if (response.ok) { | |
| // Refresh the current view | |
| loadAllDocuments(); | |
| loadStats(); | |
| loadCategories(); | |
| alert('Document deleted successfully'); | |
| } else { | |
| alert('Failed to delete document: ' + result.detail); | |
| } | |
| } catch (error) { | |
| alert('Error deleting document: ' + error.message); | |
| } | |
| } | |
| // Enhanced document display with image preview | |
| function displayDocuments(docs) { | |
| const container = document.getElementById('documentsContainer'); | |
| if (docs.length === 0) { | |
| container.innerHTML = ` | |
| <div class="text-center py-5"> | |
| <i class="fas fa-folder-open fa-4x text-muted mb-3"></i> | |
| <h4 class="text-muted">No documents found</h4> | |
| <p class="text-muted">Upload some documents to get started</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| const docsHtml = docs.map(doc => { | |
| const similarityClass = doc.similarity >= 0.7 ? 'similarity-high' : | |
| doc.similarity >= 0.5 ? 'similarity-medium' : 'similarity-low'; | |
| const confidenceText = doc.similarity >= 0.7 ? 'High' : | |
| doc.similarity >= 0.5 ? 'Medium' : 'Low'; | |
| return ` | |
| <div class="document-card"> | |
| <img src="/api/document-preview/${doc.id}" | |
| class="document-preview" | |
| alt="${doc.original_filename}" | |
| onclick="showDocumentPreview('${doc.id}', '${doc.original_filename}')" | |
| onerror="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iI2Y4ZjlmYSIvPgo8dGV4dCB4PSI1MCUiIHk9IjUwJSIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBmaWxsPSIjNmM3NTdkIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSI+RG9jdW1lbnQ8L3RleHQ+Cjwvc3ZnPg=='"> | |
| <div class="document-title">${doc.original_filename}</div> | |
| <div class="document-meta"> | |
| <i class="fas fa-tag me-1"></i> | |
| <strong>${doc.category}</strong> | |
| </div> | |
| <div class="document-meta"> | |
| <i class="fas fa-chart-line me-1"></i> | |
| Confidence: <span class="similarity-badge ${similarityClass}"> | |
| ${(doc.similarity * 100).toFixed(1)}% ${confidenceText} | |
| </span> | |
| </div> | |
| <div class="document-meta"> | |
| <i class="fas fa-calendar me-1"></i> | |
| ${new Date(doc.upload_date).toLocaleDateString()} | |
| </div> | |
| <div class="mt-3"> | |
| <button class="btn btn-primary btn-sm me-2" onclick="showDocumentPreview('${doc.id}', '${doc.original_filename}')"> | |
| <i class="fas fa-eye me-1"></i>Preview | |
| </button> | |
| <button class="btn btn-danger btn-sm" onclick="deleteDocument('${doc.id}', '${doc.original_filename}')"> | |
| <i class="fas fa-trash me-1"></i>Delete | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| container.innerHTML = `<div class="document-grid">${docsHtml}</div>`; | |
| } | |
| // Document preview functionality | |
| function showDocumentPreview(documentId, filename) { | |
| const modal = new bootstrap.Modal(document.getElementById('documentModal')); | |
| const preview = document.getElementById('documentPreview'); | |
| const details = document.getElementById('documentDetails'); | |
| preview.src = `/api/document-preview/${documentId}`; | |
| details.innerHTML = `<h6>${filename}</h6>`; | |
| modal.show(); | |
| } | |
| // View toggle functionality | |
| function toggleView(viewType) { | |
| currentView = viewType; | |
| // Update button states | |
| document.querySelectorAll('.btn-group .btn').forEach(btn => { | |
| btn.classList.remove('active'); | |
| }); | |
| event.target.classList.add('active'); | |
| // Reload documents with new view | |
| loadAllDocuments(); | |
| } | |
| // Create category distribution chart | |
| function createCategoryChart() { | |
| const ctx = document.getElementById('categoryChart'); | |
| if (!ctx) return; | |
| // Sample data - you can modify this to use real data | |
| const data = { | |
| labels: categories.slice(0, 5), // Show top 5 categories | |
| datasets: [{ | |
| label: 'Documents', | |
| data: categories.slice(0, 5).map(() => Math.floor(Math.random() * 20) + 1), | |
| backgroundColor: [ | |
| 'rgba(13, 110, 253, 0.8)', | |
| 'rgba(25, 135, 84, 0.8)', | |
| 'rgba(255, 193, 7, 0.8)', | |
| 'rgba(220, 53, 69, 0.8)', | |
| 'rgba(13, 202, 240, 0.8)' | |
| ], | |
| borderColor: [ | |
| 'rgb(13, 110, 253)', | |
| 'rgb(25, 135, 84)', | |
| 'rgb(255, 193, 7)', | |
| 'rgb(220, 53, 69)', | |
| 'rgb(13, 202, 240)' | |
| ], | |
| borderWidth: 2 | |
| }] | |
| }; | |
| new Chart(ctx, { | |
| type: 'doughnut', | |
| data: data, | |
| options: { | |
| responsive: true, | |
| plugins: { | |
| legend: { | |
| position: 'bottom', | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Load recent activity | |
| function loadRecentActivity() { | |
| const recentActivity = document.getElementById('recentActivity'); | |
| // Sample recent activity - you can modify this to use real data | |
| const activities = [ | |
| { action: 'Document classified', item: 'passport.jpg', time: '2 min ago' }, | |
| { action: 'Category added', item: 'driver_license', time: '1 hour ago' }, | |
| { action: 'Document uploaded', item: 'certificate.pdf', time: '3 hours ago' } | |
| ]; | |
| const activityHtml = activities.map(activity => ` | |
| <div class="d-flex justify-content-between align-items-center mb-2 p-2 bg-light rounded"> | |
| <div> | |
| <small class="fw-bold">${activity.action}</small><br> | |
| <small class="text-muted">${activity.item}</small> | |
| </div> | |
| <small class="text-muted">${activity.time}</small> | |
| </div> | |
| `).join(''); | |
| recentActivity.innerHTML = activityHtml; | |
| } | |
| // Form submissions | |
| document.getElementById('uploadForm').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const fileInput = document.getElementById('categoryFile'); | |
| const labelInput = document.getElementById('categoryLabel'); | |
| const resultDiv = document.getElementById('uploadResult'); | |
| if (!fileInput.files[0] || !labelInput.value.trim()) { | |
| showResult(resultDiv, 'Please select a file and enter a label.', 'error'); | |
| return; | |
| } | |
| const formData = new FormData(); | |
| formData.append('file', fileInput.files[0]); | |
| formData.append('label', labelInput.value.trim()); | |
| showResult(resultDiv, '<div class="loading"></div> Uploading...', 'info'); | |
| try { | |
| const response = await authenticatedFetch('/api/upload-category', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const result = await response.json(); | |
| if (response.ok) { | |
| showResult(resultDiv, result.message, 'success'); | |
| labelInput.value = ''; | |
| fileInput.value = ''; | |
| document.querySelector('#categoryUpload p').textContent = 'Click to select or drag & drop files here'; | |
| loadStats(); | |
| loadCategories(); | |
| } else { | |
| showResult(resultDiv, result.detail, 'error'); | |
| } | |
| } catch (error) { | |
| showResult(resultDiv, 'Upload failed: ' + error.message, 'error'); | |
| } | |
| }); | |
| document.getElementById('classifyForm').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const fileInput = document.getElementById('classifyFile'); | |
| const resultDiv = document.getElementById('classifyResult'); | |
| if (!fileInput.files[0]) { | |
| showResult(resultDiv, 'Please select a file to classify.', 'error'); | |
| return; | |
| } | |
| const formData = new FormData(); | |
| formData.append('file', fileInput.files[0]); | |
| showResult(resultDiv, '<div class="loading"></div> Classifying...', 'info'); | |
| try { | |
| const response = await authenticatedFetch('/api/classify-document', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const result = await response.json(); | |
| if (response.ok) { | |
| const confidenceText = result.confidence === 'high' ? 'β High Confidence' : 'β οΈ Low Confidence'; | |
| const savedText = result.document_saved ? '\nπ Document saved to archive' : ''; | |
| let matchesText = '\n\nTop matches:\n'; | |
| result.matches.forEach(match => { | |
| matchesText += `β’ ${match.category}: ${(match.similarity * 100).toFixed(1)}%\n`; | |
| }); | |
| showResult(resultDiv, | |
| `π― Classification: ${result.category}\n` + | |
| `${confidenceText} (${(result.similarity * 100).toFixed(1)}%)${savedText}${matchesText}`, | |
| result.confidence === 'high' ? 'success' : 'warning' | |
| ); | |
| fileInput.value = ''; | |
| document.querySelector('#classifyUpload p').textContent = 'Click to select or drag & drop files here'; | |
| loadStats(); | |
| loadCategories(); | |
| } else { | |
| // Handle different error types | |
| let errorMessage = result.error || result.detail || 'Classification failed'; | |
| if (errorMessage.includes('No categories')) { | |
| errorMessage = 'β οΈ Please add some document categories first before classifying documents.'; | |
| } else if (errorMessage.includes('Failed to process')) { | |
| errorMessage = 'β Could not process the uploaded file. Please ensure it\'s a valid image or PDF.'; | |
| } else if (errorMessage.includes('JSON serializable')) { | |
| errorMessage = 'π§ Processing error occurred. Please try again.'; | |
| } | |
| showResult(resultDiv, errorMessage, 'error'); | |
| } | |
| } catch (error) { | |
| console.error('Classification error:', error); | |
| showResult(resultDiv, 'β Network error: Please check your connection and try again.', 'error'); | |
| } | |
| }); | |
| document.getElementById('ocrForm').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const fileInput = document.getElementById('ocrFile'); | |
| const resultDiv = document.getElementById('ocrResult'); | |
| if (!fileInput.files[0]) { | |
| showResult(resultDiv, 'Please select a file for OCR.', 'error'); | |
| return; | |
| } | |
| const formData = new FormData(); | |
| formData.append('file', fileInput.files[0]); | |
| showResult(resultDiv, '<div class="loading"></div> Extracting text with advanced OCR...', 'info'); | |
| try { | |
| const response = await authenticatedFetch('/api/ocr', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const result = await response.json(); | |
| if (response.ok) { | |
| const ocrInfo = result.enhanced_features ? | |
| `π€ Processed with ${result.ocr_method} (Enhanced Features: Tables, LaTeX, Watermarks)\n\n` : | |
| `π Processed with ${result.ocr_method}\n\n`; | |
| showResult(resultDiv, ocrInfo + result.text, 'success'); | |
| } else { | |
| showResult(resultDiv, result.error || result.detail, 'error'); | |
| } | |
| } catch (error) { | |
| showResult(resultDiv, 'OCR failed: ' + error.message, 'error'); | |
| } | |
| }); | |
| // Utility function to show results | |
| function showResult(element, message, type) { | |
| const className = type === 'success' ? 'result-success' : | |
| type === 'error' ? 'result-error' : | |
| type === 'warning' ? 'result-warning' : ''; | |
| element.innerHTML = `<div class="result-box ${className}">${message}</div>`; | |
| } | |