/** * MorphGuard Notification Client * * Provides client-side integration with the server's notification system, * including intelligent notification handling, priority management, * context-aware display, and cognitive load management. */ class NotificationClient { /** * Initialize the notification client * @param {Object} options - Configuration options */ constructor(options = {}) { // Default options this.options = { pollInterval: 10000, // 10 seconds maxNotifications: 5, // Maximum number of notifications to show at once autoHideDelay: 5000, // Auto-hide delay for toast notifications (ms) notificationContainer: '#notification-container', toastContainer: '#toast-container', modalContainer: '#modal-container', enablePolling: true, enableWebSocket: true, debug: false, onNotificationReceived: null, // Callback for new notifications onNotificationRead: null, // Callback for read notifications serverUrl: '/api/notifications', userPreferences: null, // User notification preferences ...options }; // Notification state this.notifications = []; this.unreadCount = 0; this.activeNotifications = new Set(); this.lastPollTime = 0; this.isPolling = false; this.webSocket = null; this.isWebSocketConnected = false; this.interactionHistory = []; this.cognitiveLoadEstimate = 0.0; this.attentionScore = 1.0; this.throttleTimers = {}; // Session context this.sessionContext = { startTime: Date.now(), lastActivity: Date.now(), pageTransitions: 0, formInteractions: 0, scrollEvents: 0, clickEvents: 0, keyboardEvents: 0, errors: 0, activeModals: 0, isActive: true, timeOnPage: 0 }; // Initialize this.init(); } /** * Initialize the notification client */ init() { // Create notification containers if they don't exist this.createContainers(); // Set up event listeners this.setupEventListeners(); // Initialize WebSocket if enabled if (this.options.enableWebSocket) { this.connectWebSocket(); } // Start polling for notifications if (this.options.enablePolling) { this.startPolling(); } // Log initialization this.log('NotificationClient initialized'); } /** * Create notification containers if they don't exist */ createContainers() { const createContainer = (selector, className) => { if (!document.querySelector(selector)) { const container = document.createElement('div'); container.id = selector.substring(1); // Remove the # from the selector container.className = className; document.body.appendChild(container); } }; createContainer(this.options.notificationContainer, 'mg-notification-container'); createContainer(this.options.toastContainer, 'mg-toast-container'); createContainer(this.options.modalContainer, 'mg-modal-container'); } /** * Set up event listeners */ setupEventListeners() { // Track user activity document.addEventListener('click', e => this.trackActivity('click', e)); document.addEventListener('keydown', e => this.trackActivity('keydown', e)); document.addEventListener('scroll', e => this.trackActivity('scroll', e)); document.addEventListener('mousemove', e => this.throttle('mousemove', () => this.trackActivity('mousemove'), 1000)); // Track form interactions document.addEventListener('submit', e => this.trackActivity('form_submit', e)); document.addEventListener('change', e => this.trackActivity('form_change', e)); // Track page visibility document.addEventListener('visibilitychange', () => this.handleVisibilityChange()); // Track page navigation window.addEventListener('popstate', () => this.trackActivity('navigation')); // Track errors window.addEventListener('error', e => this.trackActivity('error', e)); // Set up periodic context updates setInterval(() => this.updateContext(), 5000); } /** * Throttle an event callback * @param {string} key - Throttle key * @param {Function} callback - Function to throttle * @param {number} limit - Throttle time in ms */ throttle(key, callback, limit) { if (!this.throttleTimers[key]) { callback(); this.throttleTimers[key] = setTimeout(() => { delete this.throttleTimers[key]; }, limit); } } /** * Track user activity * @param {string} type - Activity type * @param {Event} event - Browser event */ trackActivity(type, event) { const now = Date.now(); this.sessionContext.lastActivity = now; this.sessionContext.isActive = true; // Update specific activity counters switch (type) { case 'click': this.sessionContext.clickEvents++; break; case 'keydown': this.sessionContext.keyboardEvents++; break; case 'scroll': this.sessionContext.scrollEvents++; break; case 'navigation': this.sessionContext.pageTransitions++; break; case 'form_submit': case 'form_change': this.sessionContext.formInteractions++; break; case 'error': this.sessionContext.errors++; break; } // Record in interaction history (limit to last 50 events) this.interactionHistory.push({ type, timestamp: now, data: event ? this.sanitizeEventData(event) : null }); if (this.interactionHistory.length > 50) { this.interactionHistory.shift(); } // Update cognitive load estimate this.updateCognitiveLoadEstimate(); } /** * Sanitize event data for storage * @param {Event} event - Browser event * @returns {Object} Sanitized event data */ sanitizeEventData(event) { // Extract only needed data to avoid circular references if (!event) return null; const data = {}; if (event.target) { data.target = { tagName: event.target.tagName, id: event.target.id, className: event.target.className, type: event.target.type }; } if (event.key) { data.key = event.key; } return data; } /** * Handle visibility change events */ handleVisibilityChange() { if (document.visibilityState === 'visible') { this.sessionContext.isActive = true; this.sessionContext.lastActivity = Date.now(); // Refresh notifications when tab becomes visible this.fetchNotifications(); } else { this.sessionContext.isActive = false; } } /** * Update the session context */ updateContext() { const now = Date.now(); // Update time on page this.sessionContext.timeOnPage = Math.floor((now - this.sessionContext.startTime) / 1000); // Check if user is still active (inactive after 60 seconds) if (now - this.sessionContext.lastActivity > 60000) { this.sessionContext.isActive = false; } // Calculate attention score based on recent activity this.updateAttentionScore(); } /** * Update cognitive load estimate based on recent activity */ updateCognitiveLoadEstimate() { // Simple cognitive load model based on: // 1. Number of active notifications // 2. Form interaction complexity // 3. Error rate // 4. Page transition frequency // 5. Input frequency const activeNotificationLoad = Math.min(this.activeNotifications.size / 5, 1) * 0.3; // Calculate input frequency (events in the last 30 seconds) const recentEvents = this.interactionHistory.filter(e => Date.now() - e.timestamp < 30000 ).length; const inputLoad = Math.min(recentEvents / 20, 1) * 0.2; // Calculate error impact const errorLoad = Math.min(this.sessionContext.errors / 5, 1) * 0.3; // Calculate form complexity const formLoad = Math.min(this.sessionContext.formInteractions / 10, 1) * 0.1; // Calculate page transition frequency const pageTransitionRate = this.sessionContext.pageTransitions / (Math.max(this.sessionContext.timeOnPage, 1) / 60); // transitions per minute const navigationLoad = Math.min(pageTransitionRate / 3, 1) * 0.1; // Calculate combined load (0-1 scale) this.cognitiveLoadEstimate = Math.min( activeNotificationLoad + inputLoad + errorLoad + formLoad + navigationLoad, 1.0 ); this.log(`Cognitive load estimate updated: ${this.cognitiveLoadEstimate.toFixed(2)}`); } /** * Update attention score based on recent activity */ updateAttentionScore() { // Simple attention model based on: // 1. Time since last activity // 2. Recent input frequency // 3. Page visibility // Calculate time factor (attention decreases with inactivity) const timeSinceActivity = (Date.now() - this.sessionContext.lastActivity) / 1000; // seconds const timeFactor = Math.max(1 - (timeSinceActivity / 60), 0); // 0 after 60 seconds of inactivity // Calculate activity factor (recent events indicate attention) const recentEvents = this.interactionHistory.filter(e => Date.now() - e.timestamp < 10000 // events in last 10 seconds ).length; const activityFactor = Math.min(recentEvents / 5, 1); // Page visibility factor const visibilityFactor = document.visibilityState === 'visible' ? 1 : 0; // Combined attention score (0-1 scale) this.attentionScore = Math.min( (timeFactor * 0.4) + (activityFactor * 0.3) + (visibilityFactor * 0.3), 1.0 ); this.log(`Attention score updated: ${this.attentionScore.toFixed(2)}`); } /** * Connect to WebSocket for real-time notifications */ connectWebSocket() { try { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws/notifications`; this.webSocket = new WebSocket(wsUrl); this.webSocket.onopen = () => { this.isWebSocketConnected = true; this.log('WebSocket connected'); }; this.webSocket.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'notification') { this.handleNewNotification(data.notification); } else if (data.type === 'read') { this.handleNotificationRead(data.notificationId); } } catch (err) { this.log('Error processing WebSocket message:', err); } }; this.webSocket.onclose = () => { this.isWebSocketConnected = false; this.log('WebSocket disconnected, reconnecting in 5 seconds...'); // Reconnect after 5 seconds setTimeout(() => this.connectWebSocket(), 5000); }; this.webSocket.onerror = (error) => { this.log('WebSocket error:', error); this.isWebSocketConnected = false; }; } catch (err) { this.log('Error connecting to WebSocket:', err); } } /** * Start polling for notifications */ startPolling() { if (this.isPolling) return; this.isPolling = true; this.pollForNotifications(); } /** * Poll for notifications */ pollForNotifications() { if (!this.isPolling) return; // Only poll if the page is visible and we're not using WebSockets if (document.visibilityState === 'visible' && !this.isWebSocketConnected) { this.fetchNotifications(); } // Schedule next poll setTimeout(() => this.pollForNotifications(), this.options.pollInterval); } /** * Fetch notifications from the server */ async fetchNotifications() { try { this.lastPollTime = Date.now(); const response = await fetch(`${this.options.serverUrl}?since=${this.lastPollTime - this.options.pollInterval}`); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } const data = await response.json(); // Process new notifications if (data.notifications && Array.isArray(data.notifications)) { data.notifications.forEach(notification => { this.handleNewNotification(notification); }); } // Update unread count if (data.unreadCount !== undefined) { this.unreadCount = data.unreadCount; this.updateUnreadBadge(); } } catch (err) { this.log('Error fetching notifications:', err); } } /** * Handle a new notification * @param {Object} notification - Notification data */ handleNewNotification(notification) { // Add to notifications list this.notifications.unshift(notification); // Limit the number of notifications we keep in memory if (this.notifications.length > 100) { this.notifications = this.notifications.slice(0, 100); } // Update unread count if (notification.state !== 'read') { this.unreadCount++; this.updateUnreadBadge(); } // Determine if we should show the notification based on: // 1. Priority // 2. Current cognitive load // 3. Current attention score // 4. User preferences const shouldShow = this.shouldShowNotification(notification); if (shouldShow) { // Display the notification based on channel this.displayNotification(notification); } else { // Queue for later if not showing now this.queueNotificationForLater(notification); } // Call notification received callback if (this.options.onNotificationReceived) { this.options.onNotificationReceived(notification); } } /** * Determine if a notification should be shown immediately * @param {Object} notification - Notification data * @returns {boolean} Whether to show the notification */ shouldShowNotification(notification) { // Always show critical notifications if (notification.priority === 'CRITICAL') { return true; } // Check user preferences const userPrefs = this.options.userPreferences || {}; const categoryPref = userPrefs[notification.category] || 'normal'; if (categoryPref === 'none') { return false; } // Check cognitive load threshold based on priority const loadThresholds = { 'HIGH': 0.8, // Show high priority unless cognitive load is very high 'MEDIUM': 0.6, // Show medium priority unless cognitive load is high 'LOW': 0.4, // Show low priority only when cognitive load is low 'BACKGROUND': 0.3 // Show background notifications only when cognitive load is very low }; const loadThreshold = loadThresholds[notification.priority] || 0.6; if (this.cognitiveLoadEstimate > loadThreshold) { return false; } // Check attention threshold based on priority const attentionThresholds = { 'HIGH': 0.3, // Show high priority even with low attention 'MEDIUM': 0.5, // Show medium priority with moderate attention 'LOW': 0.7, // Show low priority only with high attention 'BACKGROUND': 0.8 // Show background notifications only with very high attention }; const attentionThreshold = attentionThresholds[notification.priority] || 0.5; if (this.attentionScore < attentionThreshold) { return false; } // Check active notification limit if (this.activeNotifications.size >= this.options.maxNotifications) { // Only show if higher priority than existing notifications const lowestPriorityActive = this.getLowestPriorityActiveNotification(); const priorityValues = { 'CRITICAL': 0, 'HIGH': 1, 'MEDIUM': 2, 'LOW': 3, 'BACKGROUND': 4 }; if (priorityValues[notification.priority] >= priorityValues[lowestPriorityActive]) { return false; } } return true; } /** * Get the lowest priority active notification * @returns {string} Notification priority */ getLowestPriorityActiveNotification() { let lowestPriority = 'CRITICAL'; const priorityValues = { 'CRITICAL': 0, 'HIGH': 1, 'MEDIUM': 2, 'LOW': 3, 'BACKGROUND': 4 }; for (const id of this.activeNotifications) { const notification = this.notifications.find(n => n.id === id); if (notification && priorityValues[notification.priority] > priorityValues[lowestPriority]) { lowestPriority = notification.priority; } } return lowestPriority; } /** * Queue a notification for later display * @param {Object} notification - Notification data */ queueNotificationForLater(notification) { // For now, we just log that we're queueing it this.log(`Queueing notification for later: ${notification.id}`); // In a full implementation, we would store this and periodically check if conditions are right to show it } /** * Display a notification * @param {Object} notification - Notification data */ displayNotification(notification) { // Add to active notifications this.activeNotifications.add(notification.id); // Display based on channel if (notification.channels.includes('MODAL')) { this.showModalNotification(notification); } else if (notification.channels.includes('TOAST')) { this.showToastNotification(notification); } else if (notification.channels.includes('UI')) { this.showUINotification(notification); } // Update cognitive load estimate this.updateCognitiveLoadEstimate(); } /** * Show a modal notification * @param {Object} notification - Notification data */ showModalNotification(notification) { const modalContainer = document.querySelector(this.options.modalContainer); if (!modalContainer) return; // Create modal element const modal = document.createElement('div'); modal.className = `mg-modal mg-priority-${notification.priority.toLowerCase()}`; modal.dataset.notificationId = notification.id; // Add modal content modal.innerHTML = `

${notification.title}

${notification.content}

`; // Add event listeners modal.querySelector('.mg-modal-close').addEventListener('click', () => { this.dismissNotification(notification.id); modal.remove(); }); // Add action event listeners const actionButtons = modal.querySelectorAll('.mg-action-button'); actionButtons.forEach(button => { button.addEventListener('click', (e) => { const actionId = e.target.dataset.actionId; this.handleNotificationAction(notification.id, actionId); // Close modal if the action should dismiss if (e.target.dataset.dismiss === 'true') { this.dismissNotification(notification.id); modal.remove(); } }); }); // Add to DOM modalContainer.appendChild(modal); // Update session context this.sessionContext.activeModals++; } /** * Show a toast notification * @param {Object} notification - Notification data */ showToastNotification(notification) { const toastContainer = document.querySelector(this.options.toastContainer); if (!toastContainer) return; // Create toast element const toast = document.createElement('div'); toast.className = `mg-toast mg-priority-${notification.priority.toLowerCase()}`; toast.dataset.notificationId = notification.id; // Add icon if available const iconHtml = notification.icon ? `
` : ''; // Add toast content toast.innerHTML = ` ${iconHtml}

${notification.title}

${notification.content}

${this.renderNotificationActions(notification)}
`; // Add event listeners toast.querySelector('.mg-toast-close').addEventListener('click', () => { this.dismissNotification(notification.id); this.removeToast(toast); }); // Add action event listeners const actionButtons = toast.querySelectorAll('.mg-action-button'); actionButtons.forEach(button => { button.addEventListener('click', (e) => { const actionId = e.target.dataset.actionId; this.handleNotificationAction(notification.id, actionId); // Close toast if the action should dismiss if (e.target.dataset.dismiss === 'true') { this.dismissNotification(notification.id); this.removeToast(toast); } }); }); // Add to DOM toastContainer.appendChild(toast); // Auto-hide after delay (for non-critical notifications) if (notification.priority !== 'CRITICAL' && !notification.requires_acknowledgment) { setTimeout(() => { this.removeToast(toast); }, this.options.autoHideDelay); } // Animate in setTimeout(() => { toast.classList.add('mg-toast-visible'); }, 10); } /** * Remove a toast notification with animation * @param {HTMLElement} toast - Toast element */ removeToast(toast) { toast.classList.remove('mg-toast-visible'); toast.classList.add('mg-toast-hiding'); // Remove from DOM after animation setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } // Remove from active notifications const notificationId = toast.dataset.notificationId; if (notificationId) { this.activeNotifications.delete(notificationId); } // Update cognitive load estimate this.updateCognitiveLoadEstimate(); }, 300); } /** * Show a UI notification * @param {Object} notification - Notification data */ showUINotification(notification) { const notificationContainer = document.querySelector(this.options.notificationContainer); if (!notificationContainer) return; // Create notification element const notificationEl = document.createElement('div'); notificationEl.className = `mg-notification mg-priority-${notification.priority.toLowerCase()}`; notificationEl.dataset.notificationId = notification.id; // Add icon if available const iconHtml = notification.icon ? `
` : ''; // Add notification content notificationEl.innerHTML = ` ${iconHtml}

${notification.title}

${this.formatTime(notification.created_at)}

${notification.content}

${this.renderNotificationActions(notification)}
`; // Add click event to mark as read notificationEl.addEventListener('click', (e) => { // Don't mark as read if clicking an action button if (e.target.closest('.mg-action-button')) { return; } this.markAsRead(notification.id); }); // Add action event listeners const actionButtons = notificationEl.querySelectorAll('.mg-action-button'); actionButtons.forEach(button => { button.addEventListener('click', (e) => { const actionId = e.target.dataset.actionId; this.handleNotificationAction(notification.id, actionId); }); }); // Add to DOM at the top of the container if (notificationContainer.firstChild) { notificationContainer.insertBefore(notificationEl, notificationContainer.firstChild); } else { notificationContainer.appendChild(notificationEl); } } /** * Render notification action buttons * @param {Object} notification - Notification data * @returns {string} HTML for action buttons */ renderNotificationActions(notification) { if (!notification.actions || !notification.actions.length) { return ''; } return notification.actions.map(action => { const btnClass = action.style || 'primary'; const dismiss = action.dismiss ? 'true' : 'false'; return ``; }).join(''); } /** * Format a timestamp into a human-readable string * @param {string} timestamp - ISO timestamp * @returns {string} Formatted time */ formatTime(timestamp) { const date = new Date(timestamp); const now = new Date(); const diff = now - date; // Show relative time for recent notifications if (diff < 60000) { return 'Just now'; } else if (diff < 3600000) { const minutes = Math.floor(diff / 60000); return `${minutes}m ago`; } else if (diff < 86400000) { const hours = Math.floor(diff / 3600000); return `${hours}h ago`; } else { // For older notifications, show the date return date.toLocaleDateString(); } } /** * Handle a notification action * @param {string} notificationId - Notification ID * @param {string} actionId - Action ID */ async handleNotificationAction(notificationId, actionId) { try { // Find the notification const notification = this.notifications.find(n => n.id === notificationId); if (!notification) return; // Find the action const action = notification.actions.find(a => a.id === actionId); if (!action) return; // Log the action this.log(`Notification action ${actionId} for notification ${notificationId}`); // Send to server await fetch(`${this.options.serverUrl}/${notificationId}/action`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ actionId, timestamp: new Date().toISOString() }) }); // If the action should dismiss the notification if (action.dismiss) { this.dismissNotification(notificationId); } // Execute client-side action handler if available if (action.clientHandler && typeof window[action.clientHandler] === 'function') { window[action.clientHandler](notification, action); } } catch (err) { this.log('Error handling notification action:', err); } } /** * Mark a notification as read * @param {string} notificationId - Notification ID */ async markAsRead(notificationId) { try { // Find the notification const notification = this.notifications.find(n => n.id === notificationId); if (!notification) return; // Skip if already read if (notification.state === 'read') return; // Update state locally notification.state = 'read'; notification.read_at = new Date().toISOString(); // Update unread count this.unreadCount = Math.max(0, this.unreadCount - 1); this.updateUnreadBadge(); // Send to server await fetch(`${this.options.serverUrl}/${notificationId}/read`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ timestamp: notification.read_at }) }); // Update UI const notificationEl = document.querySelector(`[data-notification-id="${notificationId}"]`); if (notificationEl) { notificationEl.classList.add('mg-notification-read'); } // Call notification read callback if (this.options.onNotificationRead) { this.options.onNotificationRead(notification); } } catch (err) { this.log('Error marking notification as read:', err); } } /** * Dismiss a notification * @param {string} notificationId - Notification ID */ dismissNotification(notificationId) { // Remove from active notifications this.activeNotifications.delete(notificationId); // Mark as read this.markAsRead(notificationId); // Update cognitive load estimate this.updateCognitiveLoadEstimate(); } /** * Acknowledge a notification (for notifications that require acknowledgment) * @param {string} notificationId - Notification ID * @param {Object} data - Acknowledgment data */ async acknowledgeNotification(notificationId, data = {}) { try { // Find the notification const notification = this.notifications.find(n => n.id === notificationId); if (!notification) return; // Skip if already acknowledged or doesn't require acknowledgment if (notification.state === 'read' || !notification.requires_acknowledgment) return; // Update state locally notification.state = 'read'; notification.read_at = new Date().toISOString(); notification.acknowledgment_data = { ...data, timestamp: notification.read_at }; // Update unread count this.unreadCount = Math.max(0, this.unreadCount - 1); this.updateUnreadBadge(); // Send to server await fetch(`${this.options.serverUrl}/${notificationId}/acknowledge`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(notification.acknowledgment_data) }); // Update UI const notificationEl = document.querySelector(`[data-notification-id="${notificationId}"]`); if (notificationEl) { notificationEl.classList.add('mg-notification-read'); } // Call notification read callback if (this.options.onNotificationRead) { this.options.onNotificationRead(notification); } } catch (err) { this.log('Error acknowledging notification:', err); } } /** * Handle notification read event * @param {string} notificationId - Notification ID */ handleNotificationRead(notificationId) { // Find the notification const notification = this.notifications.find(n => n.id === notificationId); if (!notification) return; // Skip if already read if (notification.state === 'read') return; // Update state locally notification.state = 'read'; notification.read_at = new Date().toISOString(); // Update unread count this.unreadCount = Math.max(0, this.unreadCount - 1); this.updateUnreadBadge(); // Update UI const notificationEl = document.querySelector(`[data-notification-id="${notificationId}"]`); if (notificationEl) { notificationEl.classList.add('mg-notification-read'); } // Call notification read callback if (this.options.onNotificationRead) { this.options.onNotificationRead(notification); } } /** * Update the unread notification badge */ updateUnreadBadge() { // Find all elements with the notification badge class const badges = document.querySelectorAll('.mg-notification-badge'); badges.forEach(badge => { if (this.unreadCount > 0) { badge.textContent = this.unreadCount > 99 ? '99+' : this.unreadCount; badge.classList.add('mg-notification-badge-visible'); } else { badge.classList.remove('mg-notification-badge-visible'); } }); } /** * Get all notifications * @returns {Array} Notifications */ getNotifications() { return [...this.notifications]; } /** * Get unread notifications * @returns {Array} Unread notifications */ getUnreadNotifications() { return this.notifications.filter(n => n.state !== 'read'); } /** * Get notifications by category * @param {string} category - Notification category * @returns {Array} Notifications in the category */ getNotificationsByCategory(category) { return this.notifications.filter(n => n.category === category); } /** * Clear all notifications */ async clearAllNotifications() { try { // Clear on server await fetch(`${this.options.serverUrl}/clear`, { method: 'POST' }); // Clear locally this.notifications = []; this.unreadCount = 0; this.activeNotifications.clear(); // Update UI this.updateUnreadBadge(); // Clear notification containers document.querySelector(this.options.notificationContainer).innerHTML = ''; document.querySelector(this.options.toastContainer).innerHTML = ''; // Update cognitive load estimate this.updateCognitiveLoadEstimate(); } catch (err) { this.log('Error clearing notifications:', err); } } /** * Get session context for the server * @returns {Object} Session context */ getContextForServer() { return { session_data: { is_active: this.sessionContext.isActive, last_activity: this.sessionContext.lastActivity, time_on_page: this.sessionContext.timeOnPage, page_transitions: this.sessionContext.pageTransitions, form_interactions: this.sessionContext.formInteractions }, cognitive_load: this.cognitiveLoadEstimate, attention_score: this.attentionScore, device_info: { viewport_width: window.innerWidth, viewport_height: window.innerHeight, user_agent: navigator.userAgent, is_mobile: /Mobi|Android/i.test(navigator.userAgent) } }; } /** * Set user preferences for notifications * @param {Object} preferences - User preferences */ setUserPreferences(preferences) { this.options.userPreferences = preferences; } /** * Log debug messages * @param {...any} args - Log arguments */ log(...args) { if (this.options.debug) { console.log('[NotificationClient]', ...args); } } } // CSS Styles for notifications const addNotificationStyles = () => { const style = document.createElement('style'); style.textContent = ` /* Notification containers */ .mg-notification-container { max-height: 600px; overflow-y: auto; width: 320px; } .mg-toast-container { position: fixed; top: 20px; right: 20px; z-index: 9999; max-width: 350px; } .mg-modal-container { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 10000; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; } /* Notification badge */ .mg-notification-badge { display: none; min-width: 20px; height: 20px; border-radius: 10px; background-color: #e74c3c; color: white; font-size: 12px; text-align: center; line-height: 20px; padding: 0 4px; } .mg-notification-badge-visible { display: inline-block; } /* UI Notification */ .mg-notification { display: flex; padding: 12px; margin-bottom: 8px; border-radius: 4px; background-color: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); border-left: 4px solid #3498db; transition: all 0.2s ease; } .mg-notification:hover { box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16); } .mg-notification-read { opacity: 0.7; border-left-color: #bdc3c7; } .mg-notification-content { flex: 1; } .mg-notification-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; } .mg-notification-header h4 { margin: 0; font-size: 14px; font-weight: bold; } .mg-notification-time { font-size: 12px; color: #7f8c8d; } .mg-notification-body p { margin: 0; font-size: 13px; color: #2c3e50; } .mg-notification-actions { margin-top: 8px; display: flex; gap: 8px; } /* Toast notification */ .mg-toast { display: flex; padding: 12px; margin-bottom: 10px; border-radius: 4px; background-color: #fff; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); max-width: 100%; opacity: 0; transform: translateY(-20px); transition: all 0.3s ease; } .mg-toast-visible { opacity: 1; transform: translateY(0); } .mg-toast-hiding { opacity: 0; transform: translateY(-20px); } .mg-toast-icon { margin-right: 12px; font-size: 20px; display: flex; align-items: center; justify-content: center; } .mg-toast-content { flex: 1; } .mg-toast-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; } .mg-toast-header h4 { margin: 0; font-size: 14px; font-weight: bold; } .mg-toast-close { background: none; border: none; font-size: 16px; cursor: pointer; padding: 0; color: #7f8c8d; } .mg-toast-body p { margin: 0; font-size: 13px; color: #2c3e50; } .mg-toast-actions { margin-top: 8px; display: flex; gap: 8px; } /* Modal notification */ .mg-modal { width: 90%; max-width: 500px; background-color: #fff; border-radius: 4px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); overflow: hidden; } .mg-modal-content { padding: 0; } .mg-modal-header { padding: 16px; display: flex; justify-content: space-between; align-items: center; background-color: #f8f9fa; border-bottom: 1px solid #e9ecef; } .mg-modal-header h3 { margin: 0; font-size: 18px; font-weight: 600; } .mg-modal-close { background: none; border: none; font-size: 20px; cursor: pointer; padding: 0; color: #7f8c8d; } .mg-modal-body { padding: 16px; } .mg-modal-body p { margin: 0 0 12px 0; font-size: 14px; color: #2c3e50; line-height: 1.5; } .mg-modal-footer { padding: 12px 16px; border-top: 1px solid #e9ecef; display: flex; justify-content: flex-end; gap: 8px; } /* Priority colors */ .mg-priority-critical { border-left-color: #e74c3c; } .mg-priority-high { border-left-color: #e67e22; } .mg-priority-medium { border-left-color: #3498db; } .mg-priority-low { border-left-color: #2ecc71; } .mg-priority-background { border-left-color: #95a5a6; } /* Action buttons */ .mg-action-button { background: none; border: 1px solid #3498db; border-radius: 4px; padding: 6px 12px; font-size: 12px; cursor: pointer; transition: all 0.2s ease; } .mg-btn-primary { background-color: #3498db; color: white; } .mg-btn-primary:hover { background-color: #2980b9; } .mg-btn-secondary { background-color: transparent; color: #3498db; } .mg-btn-secondary:hover { background-color: #eef8ff; } .mg-btn-danger { background-color: #e74c3c; border-color: #e74c3c; color: white; } .mg-btn-danger:hover { background-color: #c0392b; } .mg-btn-success { background-color: #2ecc71; border-color: #2ecc71; color: white; } .mg-btn-success:hover { background-color: #27ae60; } `; document.head.appendChild(style); }; // Add styles when the DOM is loaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', addNotificationStyles); } else { addNotificationStyles(); } // Create a global reference to the notification client window.notificationClient = null; // Initialize the notification client window.initNotificationClient = (options = {}) => { window.notificationClient = new NotificationClient(options); return window.notificationClient; }; // Export for module usage if (typeof module !== 'undefined' && module.exports) { module.exports = { NotificationClient, initNotificationClient: (options) => { window.notificationClient = new NotificationClient(options); return window.notificationClient; } }; }