Spaces:
Paused
Paused
| /** | |
| * Notification System for Legal Dashboard | |
| * ====================================== | |
| * | |
| * Handles real-time notifications, WebSocket connections, and notification management. | |
| */ | |
| class NotificationManager { | |
| constructor() { | |
| this.websocket = null; | |
| this.reconnectAttempts = 0; | |
| this.maxReconnectAttempts = 5; | |
| this.reconnectDelay = 1000; | |
| this.notifications = []; | |
| this.unreadCount = 0; | |
| this.isConnected = false; | |
| this.userId = null; | |
| // Initialize notification elements | |
| this.initElements(); | |
| // Start WebSocket connection | |
| this.connectWebSocket(); | |
| // Set up periodic cleanup | |
| setInterval(() => this.cleanupExpiredNotifications(), 60000); // Every minute | |
| } | |
| initElements() { | |
| // Create notification container if it doesn't exist | |
| if (!document.getElementById('notification-container')) { | |
| const container = document.createElement('div'); | |
| container.id = 'notification-container'; | |
| container.className = 'notification-container'; | |
| document.body.appendChild(container); | |
| } | |
| // Create notification bell if it doesn't exist | |
| if (!document.getElementById('notification-bell')) { | |
| const bell = document.createElement('div'); | |
| bell.id = 'notification-bell'; | |
| bell.className = 'notification-bell'; | |
| bell.innerHTML = ` | |
| <i class="fas fa-bell"></i> | |
| <span class="notification-badge" id="notification-badge">0</span> | |
| `; | |
| bell.addEventListener('click', () => this.toggleNotificationPanel()); | |
| document.body.appendChild(bell); | |
| } | |
| // Create notification panel if it doesn't exist | |
| if (!document.getElementById('notification-panel')) { | |
| const panel = document.createElement('div'); | |
| panel.id = 'notification-panel'; | |
| panel.className = 'notification-panel hidden'; | |
| panel.innerHTML = ` | |
| <div class="notification-header"> | |
| <h3>Notifications</h3> | |
| <button class="close-btn" onclick="notificationManager.closeNotificationPanel()">×</button> | |
| </div> | |
| <div class="notification-list" id="notification-list"></div> | |
| <div class="notification-footer"> | |
| <button onclick="notificationManager.markAllAsRead()">Mark All as Read</button> | |
| <button onclick="notificationManager.clearAll()">Clear All</button> | |
| </div> | |
| `; | |
| document.body.appendChild(panel); | |
| } | |
| // Add CSS styles | |
| this.addStyles(); | |
| } | |
| addStyles() { | |
| if (!document.getElementById('notification-styles')) { | |
| const styles = document.createElement('style'); | |
| styles.id = 'notification-styles'; | |
| styles.textContent = ` | |
| .notification-container { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| z-index: 10000; | |
| max-width: 400px; | |
| } | |
| .notification-bell { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| background: #007bff; | |
| color: white; | |
| padding: 10px; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| z-index: 10001; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.2); | |
| transition: all 0.3s ease; | |
| } | |
| .notification-bell:hover { | |
| transform: scale(1.1); | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.3); | |
| } | |
| .notification-badge { | |
| position: absolute; | |
| top: -5px; | |
| right: -5px; | |
| background: #dc3545; | |
| color: white; | |
| border-radius: 50%; | |
| padding: 2px 6px; | |
| font-size: 12px; | |
| min-width: 18px; | |
| text-align: center; | |
| } | |
| .notification-panel { | |
| position: fixed; | |
| top: 70px; | |
| right: 20px; | |
| width: 350px; | |
| max-height: 500px; | |
| background: white; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.15); | |
| z-index: 10002; | |
| overflow: hidden; | |
| transition: all 0.3s ease; | |
| } | |
| .notification-panel.hidden { | |
| transform: translateX(100%); | |
| opacity: 0; | |
| } | |
| .notification-header { | |
| padding: 15px; | |
| border-bottom: 1px solid #eee; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .notification-header h3 { | |
| margin: 0; | |
| color: #333; | |
| } | |
| .close-btn { | |
| background: none; | |
| border: none; | |
| font-size: 20px; | |
| cursor: pointer; | |
| color: #666; | |
| } | |
| .notification-list { | |
| max-height: 350px; | |
| overflow-y: auto; | |
| } | |
| .notification-item { | |
| padding: 15px; | |
| border-bottom: 1px solid #f0f0f0; | |
| cursor: pointer; | |
| transition: background-color 0.2s ease; | |
| } | |
| .notification-item:hover { | |
| background-color: #f8f9fa; | |
| } | |
| .notification-item.unread { | |
| background-color: #e3f2fd; | |
| border-left: 4px solid #2196f3; | |
| } | |
| .notification-title { | |
| font-weight: bold; | |
| margin-bottom: 5px; | |
| color: #333; | |
| } | |
| .notification-message { | |
| color: #666; | |
| font-size: 14px; | |
| margin-bottom: 5px; | |
| } | |
| .notification-meta { | |
| font-size: 12px; | |
| color: #999; | |
| display: flex; | |
| justify-content: space-between; | |
| } | |
| .notification-footer { | |
| padding: 15px; | |
| border-top: 1px solid #eee; | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .notification-footer button { | |
| flex: 1; | |
| padding: 8px; | |
| border: 1px solid #ddd; | |
| background: white; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 12px; | |
| } | |
| .notification-footer button:hover { | |
| background: #f8f9fa; | |
| } | |
| .notification-toast { | |
| background: white; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.15); | |
| margin-bottom: 10px; | |
| padding: 15px; | |
| border-left: 4px solid #007bff; | |
| animation: slideIn 0.3s ease; | |
| } | |
| .notification-toast.success { | |
| border-left-color: #28a745; | |
| } | |
| .notification-toast.warning { | |
| border-left-color: #ffc107; | |
| } | |
| .notification-toast.error { | |
| border-left-color: #dc3545; | |
| } | |
| @keyframes slideIn { | |
| from { | |
| transform: translateX(100%); | |
| opacity: 0; | |
| } | |
| to { | |
| transform: translateX(0); | |
| opacity: 1; | |
| } | |
| } | |
| .notification-type-icon { | |
| margin-right: 8px; | |
| } | |
| .notification-type-icon.info { color: #007bff; } | |
| .notification-type-icon.success { color: #28a745; } | |
| .notification-type-icon.warning { color: #ffc107; } | |
| .notification-type-icon.error { color: #dc3545; } | |
| `; | |
| document.head.appendChild(styles); | |
| } | |
| } | |
| connectWebSocket() { | |
| try { | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| const wsUrl = `${protocol}//${window.location.host}/api/notifications/ws`; | |
| this.websocket = new WebSocket(wsUrl); | |
| this.websocket.onopen = () => { | |
| console.log('WebSocket connected'); | |
| this.isConnected = true; | |
| this.reconnectAttempts = 0; | |
| // Send user authentication if available | |
| const token = localStorage.getItem('auth_token'); | |
| if (token) { | |
| this.websocket.send(JSON.stringify({ | |
| type: 'auth', | |
| token: token | |
| })); | |
| } | |
| }; | |
| this.websocket.onmessage = (event) => { | |
| try { | |
| const data = JSON.parse(event.data); | |
| this.handleNotification(data); | |
| } catch (error) { | |
| console.error('Error parsing WebSocket message:', error); | |
| } | |
| }; | |
| this.websocket.onclose = () => { | |
| console.log('WebSocket disconnected'); | |
| this.isConnected = false; | |
| this.handleReconnect(); | |
| }; | |
| this.websocket.onerror = (error) => { | |
| console.error('WebSocket error:', error); | |
| this.isConnected = false; | |
| }; | |
| } catch (error) { | |
| console.error('Error connecting to WebSocket:', error); | |
| this.handleReconnect(); | |
| } | |
| } | |
| handleReconnect() { | |
| if (this.reconnectAttempts < this.maxReconnectAttempts) { | |
| this.reconnectAttempts++; | |
| console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`); | |
| setTimeout(() => { | |
| this.connectWebSocket(); | |
| }, this.reconnectDelay * this.reconnectAttempts); | |
| } else { | |
| console.error('Max reconnection attempts reached'); | |
| this.showToast('Connection lost. Please refresh the page.', 'error'); | |
| } | |
| } | |
| handleNotification(data) { | |
| if (data.type === 'connection_established') { | |
| console.log('Notification service connected'); | |
| this.userId = data.user_id; | |
| return; | |
| } | |
| // Add notification to list | |
| const notification = { | |
| id: data.id || Date.now(), | |
| type: data.type || 'info', | |
| title: data.title || 'Notification', | |
| message: data.message || '', | |
| priority: data.priority || 'medium', | |
| created_at: data.created_at || new Date().toISOString(), | |
| metadata: data.metadata || {}, | |
| read: false | |
| }; | |
| this.notifications.unshift(notification); | |
| this.unreadCount++; | |
| this.updateBadge(); | |
| this.showToast(notification); | |
| this.updateNotificationPanel(); | |
| // Play notification sound if enabled | |
| this.playNotificationSound(); | |
| } | |
| showToast(notification) { | |
| const container = document.getElementById('notification-container'); | |
| const toast = document.createElement('div'); | |
| toast.className = `notification-toast ${notification.type}`; | |
| const icon = this.getNotificationIcon(notification.type); | |
| toast.innerHTML = ` | |
| <div class="notification-title"> | |
| <i class="fas ${icon} notification-type-icon ${notification.type}"></i> | |
| ${notification.title} | |
| </div> | |
| <div class="notification-message">${notification.message}</div> | |
| <div class="notification-meta"> | |
| <span>${this.formatTime(notification.created_at)}</span> | |
| <span>${notification.priority}</span> | |
| </div> | |
| `; | |
| container.appendChild(toast); | |
| // Auto-remove after 5 seconds | |
| setTimeout(() => { | |
| if (toast.parentNode) { | |
| toast.parentNode.removeChild(toast); | |
| } | |
| }, 5000); | |
| } | |
| getNotificationIcon(type) { | |
| const icons = { | |
| 'info': 'fa-info-circle', | |
| 'success': 'fa-check-circle', | |
| 'warning': 'fa-exclamation-triangle', | |
| 'error': 'fa-times-circle', | |
| 'upload_complete': 'fa-upload', | |
| 'ocr_complete': 'fa-file-text', | |
| 'scraping_complete': 'fa-spider', | |
| 'system_error': 'fa-exclamation-circle', | |
| 'user_activity': 'fa-user' | |
| }; | |
| return icons[type] || 'fa-bell'; | |
| } | |
| formatTime(timestamp) { | |
| const date = new Date(timestamp); | |
| const now = new Date(); | |
| const diff = now - date; | |
| if (diff < 60000) { // Less than 1 minute | |
| return 'Just now'; | |
| } else if (diff < 3600000) { // Less than 1 hour | |
| const minutes = Math.floor(diff / 60000); | |
| return `${minutes} minute${minutes > 1 ? 's' : ''} ago`; | |
| } else if (diff < 86400000) { // Less than 1 day | |
| const hours = Math.floor(diff / 3600000); | |
| return `${hours} hour${hours > 1 ? 's' : ''} ago`; | |
| } else { | |
| return date.toLocaleDateString(); | |
| } | |
| } | |
| updateBadge() { | |
| const badge = document.getElementById('notification-badge'); | |
| if (badge) { | |
| badge.textContent = this.unreadCount; | |
| badge.style.display = this.unreadCount > 0 ? 'block' : 'none'; | |
| } | |
| } | |
| toggleNotificationPanel() { | |
| const panel = document.getElementById('notification-panel'); | |
| if (panel) { | |
| panel.classList.toggle('hidden'); | |
| if (!panel.classList.contains('hidden')) { | |
| this.updateNotificationPanel(); | |
| } | |
| } | |
| } | |
| closeNotificationPanel() { | |
| const panel = document.getElementById('notification-panel'); | |
| if (panel) { | |
| panel.classList.add('hidden'); | |
| } | |
| } | |
| updateNotificationPanel() { | |
| const list = document.getElementById('notification-list'); | |
| if (!list) return; | |
| list.innerHTML = ''; | |
| this.notifications.slice(0, 20).forEach(notification => { | |
| const item = document.createElement('div'); | |
| item.className = `notification-item ${notification.read ? '' : 'unread'}`; | |
| item.onclick = () => this.markAsRead(notification.id); | |
| const icon = this.getNotificationIcon(notification.type); | |
| item.innerHTML = ` | |
| <div class="notification-title"> | |
| <i class="fas ${icon} notification-type-icon ${notification.type}"></i> | |
| ${notification.title} | |
| </div> | |
| <div class="notification-message">${notification.message}</div> | |
| <div class="notification-meta"> | |
| <span>${this.formatTime(notification.created_at)}</span> | |
| <span>${notification.priority}</span> | |
| </div> | |
| `; | |
| list.appendChild(item); | |
| }); | |
| } | |
| markAsRead(notificationId) { | |
| const notification = this.notifications.find(n => n.id === notificationId); | |
| if (notification && !notification.read) { | |
| notification.read = true; | |
| this.unreadCount = Math.max(0, this.unreadCount - 1); | |
| this.updateBadge(); | |
| this.updateNotificationPanel(); | |
| // Send to server | |
| this.sendToServer('mark_read', { notification_id: notificationId }); | |
| } | |
| } | |
| markAllAsRead() { | |
| this.notifications.forEach(notification => { | |
| notification.read = true; | |
| }); | |
| this.unreadCount = 0; | |
| this.updateBadge(); | |
| this.updateNotificationPanel(); | |
| // Send to server | |
| this.sendToServer('mark_all_read', {}); | |
| } | |
| clearAll() { | |
| this.notifications = []; | |
| this.unreadCount = 0; | |
| this.updateBadge(); | |
| this.updateNotificationPanel(); | |
| // Send to server | |
| this.sendToServer('clear_all', {}); | |
| } | |
| sendToServer(action, data) { | |
| if (this.websocket && this.isConnected) { | |
| this.websocket.send(JSON.stringify({ | |
| action: action, | |
| ...data | |
| })); | |
| } | |
| } | |
| playNotificationSound() { | |
| // Create audio context for notification sound | |
| try { | |
| const audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| const oscillator = audioContext.createOscillator(); | |
| const gainNode = audioContext.createGain(); | |
| oscillator.connect(gainNode); | |
| gainNode.connect(audioContext.destination); | |
| oscillator.frequency.setValueAtTime(800, audioContext.currentTime); | |
| oscillator.frequency.setValueAtTime(600, audioContext.currentTime + 0.1); | |
| gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); | |
| gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2); | |
| oscillator.start(audioContext.currentTime); | |
| oscillator.stop(audioContext.currentTime + 0.2); | |
| } catch (error) { | |
| console.log('Could not play notification sound:', error); | |
| } | |
| } | |
| cleanupExpiredNotifications() { | |
| const now = new Date(); | |
| const expired = this.notifications.filter(notification => { | |
| const created = new Date(notification.created_at); | |
| const diff = now - created; | |
| return diff > 24 * 60 * 60 * 1000; // 24 hours | |
| }); | |
| expired.forEach(notification => { | |
| const index = this.notifications.indexOf(notification); | |
| if (index > -1) { | |
| this.notifications.splice(index, 1); | |
| } | |
| }); | |
| this.updateNotificationPanel(); | |
| } | |
| // Public methods for external use | |
| showInfo(title, message, duration = 5000) { | |
| this.showCustomNotification('info', title, message, duration); | |
| } | |
| showSuccess(title, message, duration = 5000) { | |
| this.showCustomNotification('success', title, message, duration); | |
| } | |
| showWarning(title, message, duration = 5000) { | |
| this.showCustomNotification('warning', title, message, duration); | |
| } | |
| showError(title, message, duration = 5000) { | |
| this.showCustomNotification('error', title, message, duration); | |
| } | |
| showCustomNotification(type, title, message, duration) { | |
| const notification = { | |
| id: Date.now(), | |
| type: type, | |
| title: title, | |
| message: message, | |
| priority: 'medium', | |
| created_at: new Date().toISOString(), | |
| metadata: {}, | |
| read: false | |
| }; | |
| this.notifications.unshift(notification); | |
| this.unreadCount++; | |
| this.updateBadge(); | |
| this.showToast(notification); | |
| this.updateNotificationPanel(); | |
| if (duration > 0) { | |
| setTimeout(() => { | |
| const index = this.notifications.indexOf(notification); | |
| if (index > -1) { | |
| this.notifications.splice(index, 1); | |
| this.updateNotificationPanel(); | |
| } | |
| }, duration); | |
| } | |
| } | |
| } | |
| // Initialize notification manager when DOM is loaded | |
| let notificationManager; | |
| document.addEventListener('DOMContentLoaded', () => { | |
| notificationManager = new NotificationManager(); | |
| }); | |
| // Global function for external use | |
| window.showNotification = (type, title, message) => { | |
| if (notificationManager) { | |
| notificationManager.showCustomNotification(type, title, message); | |
| } | |
| }; |