/** * Rox AI Admin Panel - Secure Administration Interface * @version 1.0.0 * @description Production-ready admin panel with JWT auth and exact UI matching */ 'use strict'; // ==================== CONSTANTS ==================== const API_BASE = '/admin/api'; const TOKEN_KEY = 'rox_admin_token'; const SESSION_TIMEOUT = 24 * 60 * 60 * 1000; // 24 hours // HTML escape map for XSS prevention const HTML_ESCAPE_MAP = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; // Superscript map for math expressions const SUPERSCRIPTS = { '0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴', '5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹', '+': '⁺', '-': '⁻', '=': '⁼', '(': '⁽', ')': '⁾', 'n': 'ⁿ', 'i': 'ⁱ', 'x': 'ˣ', 'y': 'ʸ', 'a': 'ᵃ', 'b': 'ᵇ', 'c': 'ᶜ', 'd': 'ᵈ', 'e': 'ᵉ', 'f': 'ᶠ', 'g': 'ᵍ', 'h': 'ʰ', 'j': 'ʲ', 'k': 'ᵏ', 'l': 'ˡ', 'm': 'ᵐ', 'o': 'ᵒ', 'p': 'ᵖ', 'r': 'ʳ', 's': 'ˢ', 't': 'ᵗ', 'u': 'ᵘ', 'v': 'ᵛ', 'w': 'ʷ', 'z': 'ᶻ' }; // Subscript map for math expressions const SUBSCRIPTS = { '0': '₀', '1': '₁', '2': '₂', '3': '₃', '4': '₄', '5': '₅', '6': '₆', '7': '₇', '8': '₈', '9': '₉', '+': '₊', '-': '₋', '=': '₌', '(': '₍', ')': '₎', 'a': 'ₐ', 'e': 'ₑ', 'h': 'ₕ', 'i': 'ᵢ', 'j': 'ⱼ', 'k': 'ₖ', 'l': 'ₗ', 'm': 'ₘ', 'n': 'ₙ', 'o': 'ₒ', 'p': 'ₚ', 'r': 'ᵣ', 's': 'ₛ', 't': 'ₜ', 'u': 'ᵤ', 'v': 'ᵥ', 'x': 'ₓ' }; // Language name mappings for code blocks const LANGUAGE_NAMES = { 'js': 'javascript', 'ts': 'typescript', 'py': 'python', 'rb': 'ruby', 'sh': 'bash', 'yml': 'yaml', 'md': 'markdown', 'cs': 'csharp', 'cpp': 'c++' }; // ==================== ADMIN PANEL CLASS ==================== class AdminPanel { constructor() { this.token = null; this.currentUser = null; this.currentChat = null; this.users = []; this.chats = []; this.stats = {}; this.refreshInterval = null; this._init(); } _init() { this._initElements(); this._initEventListeners(); this._initKeyboardShortcuts(); this._checkAuth(); } _initElements() { // Login elements this.loginOverlay = document.getElementById('loginOverlay'); this.loginForm = document.getElementById('loginForm'); this.passwordInput = document.getElementById('passwordInput'); this.loginError = document.getElementById('loginError'); this.loginAttempts = document.getElementById('loginAttempts'); this.loginBtn = document.getElementById('loginBtn'); // App elements this.app = document.getElementById('app'); this.sidebar = document.getElementById('sidebar'); this.sidebarOverlay = document.getElementById('sidebarOverlay'); this.menuBtn = document.getElementById('menuBtn'); this.mobileMenuBtn = document.getElementById('mobileMenuBtn'); this.sidebarCloseBtn = document.getElementById('sidebarCloseBtn'); // Stats elements this.totalUsers = document.getElementById('totalUsers'); this.totalChats = document.getElementById('totalChats'); this.todayQueries = document.getElementById('todayQueries'); this.avgResponseTime = document.getElementById('avgResponseTime'); this.totalMessages = document.getElementById('totalMessages'); this.activeSessions = document.getElementById('activeSessions'); this.modelUsageChart = document.getElementById('modelUsageChart'); this.systemHealthChart = document.getElementById('systemHealthChart'); // User list elements this.userList = document.getElementById('userList'); this.userSearch = document.getElementById('userSearch'); this.selectedUserName = document.getElementById('selectedUserName'); // Chat elements this.chatsPanel = document.getElementById('chatsPanel'); this.chatsPanelCloseBtn = document.getElementById('chatsPanelCloseBtn'); this.chatsPanelOverlay = document.getElementById('chatsPanelOverlay'); this.chatsList = document.getElementById('chatsList'); this.chatView = document.getElementById('chatView'); this.chatHeader = document.getElementById('chatHeader'); this.chatTitle = document.getElementById('chatTitle'); this.chatMeta = document.getElementById('chatMeta'); this.messagesArea = document.getElementById('messagesArea'); // Action buttons this.refreshBtn = document.getElementById('refreshBtn'); this.exportBtn = document.getElementById('exportBtn'); this.exportDropdown = document.getElementById('exportDropdown'); this.exportJsonBtn = document.getElementById('exportJsonBtn'); this.exportPdfBtn = document.getElementById('exportPdfBtn'); this.exportUsersPdfBtn = document.getElementById('exportUsersPdfBtn'); this.exportChatJsonBtn = document.getElementById('exportChatJsonBtn'); this.exportChatPdfBtn = document.getElementById('exportChatPdfBtn'); this.clearBtn = document.getElementById('clearBtn'); this.logoutBtn = document.getElementById('btnLogout'); this.lastUpdated = document.getElementById('lastUpdated'); this.lastUpdateTime = null; // Tabs this.tabBtns = document.querySelectorAll('.tab-btn'); this.usersTab = document.getElementById('usersTab'); this.statsTab = document.getElementById('statsTab'); // Dialog this.dialogOverlay = document.getElementById('dialogOverlay'); this.dialogTitle = document.getElementById('dialogTitle'); this.dialogMessage = document.getElementById('dialogMessage'); this.dialogConfirm = document.getElementById('dialogConfirm'); this.dialogCancel = document.getElementById('dialogCancel'); // Help dialog this.helpOverlay = document.getElementById('helpOverlay'); this.helpBtn = document.getElementById('helpBtn'); this.helpClose = document.getElementById('helpClose'); // Toast container this.toastContainer = document.getElementById('toastContainer'); // Connection status this.connectionStatus = document.getElementById('connectionStatus'); this.connectionDot = this.connectionStatus?.querySelector('.connection-dot'); this.connectionText = this.connectionStatus?.querySelector('span'); // Auto-refresh indicator this.autoRefreshIndicator = document.getElementById('autoRefreshIndicator'); // Search results counter this.searchResultsCount = document.getElementById('searchResultsCount'); // Chat count badge this.chatCountBadge = document.getElementById('chatCountBadge'); this.chatCount = document.getElementById('chatCount'); // Breadcrumb this.breadcrumb = document.getElementById('breadcrumb'); // Performance indicators this.performanceIndicators = document.getElementById('performanceIndicators'); // Trend elements this.usersTrend = document.getElementById('usersTrend'); this.chatsTrend = document.getElementById('chatsTrend'); this.todayTrend = document.getElementById('todayTrend'); // Sparkline elements this.responseTimeSparkline = document.getElementById('responseTimeSparkline'); this.messagesSparkline = document.getElementById('messagesSparkline'); this.activityIndicator = document.getElementById('activityIndicator'); // New enhanced stats elements this.peakHour = document.getElementById('peakHour'); this.peakHourLabel = document.getElementById('peakHourLabel'); this.onlineUsers = document.getElementById('onlineUsers'); this.avgChatsPerUser = document.getElementById('avgChatsPerUser'); this.serverInfoChart = document.getElementById('serverInfoChart'); this.hourlyUsageChart = document.getElementById('hourlyUsageChart'); // Summary elements this.summaryTotalUsers = document.getElementById('summaryTotalUsers'); this.summaryOnline = document.getElementById('summaryOnline'); this.summaryToday = document.getElementById('summaryToday'); // Enhanced analytics elements this.yesterdayQueries = document.getElementById('yesterdayQueries'); this.weekQueries = document.getElementById('weekQueries'); this.monthQueries = document.getElementById('monthQueries'); this.newUsersToday = document.getElementById('newUsersToday'); this.newUsersWeek = document.getElementById('newUsersWeek'); this.avgMessagesPerChat = document.getElementById('avgMessagesPerChat'); this.dailyActivityChart = document.getElementById('dailyActivityChart'); this.browserDistribution = document.getElementById('browserDistribution'); this.osDistribution = document.getElementById('osDistribution'); this.deviceDistribution = document.getElementById('deviceDistribution'); this.languageDistribution = document.getElementById('languageDistribution'); this.refererDistribution = document.getElementById('refererDistribution'); this.countryDistribution = document.getElementById('countryDistribution'); this.screenDistribution = document.getElementById('screenDistribution'); // New enhanced elements this.realtimeOnline = document.getElementById('realtimeOnline'); this.lastHourQueries = document.getElementById('lastHourQueries'); this.returningUsers = document.getElementById('returningUsers'); this.returningRate = document.getElementById('returningRate'); this.totalPageViews = document.getElementById('totalPageViews'); this.avgPageViews = document.getElementById('avgPageViews'); this.avgSessionDurationEl = document.getElementById('avgSessionDuration'); this.totalSessionsEl = document.getElementById('totalSessions'); this.bounceRateEl = document.getElementById('bounceRate'); this.newUsersMonthEl = document.getElementById('newUsersMonth'); this.totalErrorsEl = document.getElementById('totalErrors'); this.errorRateEl = document.getElementById('errorRate'); this.recentErrorsEl = document.getElementById('recentErrors'); this.totalTokensEl = document.getElementById('totalTokens'); this.topUsersChart = document.getElementById('topUsersChart'); // New enhanced elements this.avgEngagement = document.getElementById('avgEngagement'); this.avgTimeOnSite = document.getElementById('avgTimeOnSite'); this.mobilePercent = document.getElementById('mobilePercent'); this.avgSessionsPerUser = document.getElementById('avgSessionsPerUser'); this.weeklyHeatmap = document.getElementById('weeklyHeatmap'); // User details panel this.userDetailsPanel = document.getElementById('userDetailsPanel'); this.userDetailsContent = document.getElementById('userDetailsContent'); this.closeUserDetailsBtn = document.getElementById('closeUserDetails'); } _initEventListeners() { // Login form this.loginForm?.addEventListener('submit', (e) => this._handleLogin(e)); // Logout this.logoutBtn?.addEventListener('click', () => this._logout()); // Mobile menu this.menuBtn?.addEventListener('click', () => this._toggleSidebar()); this.mobileMenuBtn?.addEventListener('click', () => this._toggleSidebar()); this.sidebarCloseBtn?.addEventListener('click', () => this._closeSidebar()); this.sidebarOverlay?.addEventListener('click', () => this._closeSidebar()); // Chats panel mobile this.chatsPanelCloseBtn?.addEventListener('click', () => this._closeChatsPanel()); this.chatsPanelOverlay?.addEventListener('click', () => this._closeChatsPanel()); // Tabs this.tabBtns.forEach(btn => { btn.addEventListener('click', () => this._switchTab(btn.dataset.tab)); }); // User search with debouncing let searchTimeout; this.userSearch?.addEventListener('input', (e) => { clearTimeout(searchTimeout); const query = e.target.value; // Visual feedback for search if (query) { e.target.style.borderColor = 'var(--accent)'; e.target.style.boxShadow = '0 0 0 3px rgba(62, 180, 137, 0.15)'; } else { e.target.style.borderColor = ''; e.target.style.boxShadow = ''; } searchTimeout = setTimeout(() => { this._filterUsers(query); }, 300); }); // Action buttons this.refreshBtn?.addEventListener('click', () => this._refreshData()); this.exportBtn?.addEventListener('click', (e) => this._toggleExportDropdown(e)); this.exportJsonBtn?.addEventListener('click', () => this._exportJson()); this.exportPdfBtn?.addEventListener('click', () => this._exportStatsPdf()); this.exportUsersPdfBtn?.addEventListener('click', () => this._exportUsersPdf()); this.exportChatJsonBtn?.addEventListener('click', () => this._exportChatJson()); this.exportChatPdfBtn?.addEventListener('click', () => this._exportChatPdf()); this.clearBtn?.addEventListener('click', () => this._confirmClearLogs()); // Help button this.helpBtn?.addEventListener('click', () => this._showHelp()); this.helpClose?.addEventListener('click', () => this._hideHelp()); // User details panel close this.closeUserDetailsBtn?.addEventListener('click', () => this._hideUserDetails()); // Close dropdown when clicking outside document.addEventListener('click', (e) => { if (this.exportDropdown && !this.exportDropdown.contains(e.target)) { this.exportDropdown.classList.remove('open'); } }); // Dialog this.dialogCancel?.addEventListener('click', () => this._hideDialog()); this.dialogOverlay?.addEventListener('click', (e) => { if (e.target === this.dialogOverlay) this._hideDialog(); }); this.helpOverlay?.addEventListener('click', (e) => { if (e.target === this.helpOverlay) this._hideHelp(); }); } // ==================== AUTHENTICATION ==================== _checkAuth() { const token = localStorage.getItem(TOKEN_KEY); if (token && this._isTokenValid(token)) { this.token = token; this._showApp(); this._loadData(); } else { localStorage.removeItem(TOKEN_KEY); this._showLogin(); } } _isTokenValid(token) { try { const payload = JSON.parse(atob(token.split('.')[1])); return payload.exp * 1000 > Date.now(); } catch { return false; } } async _handleLogin(e) { e.preventDefault(); const password = this.passwordInput?.value?.trim(); if (!password) { this._showLoginError('Please enter a password'); return; } this._setLoginLoading(true); try { const response = await fetch(`${API_BASE}/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }) }); const data = await response.json(); if (data.success && data.token) { this.token = data.token; localStorage.setItem(TOKEN_KEY, data.token); this._showApp(); this._loadData(); this._showToast('Login successful', 'success'); } else { this._showLoginError(data.error || 'Invalid password'); if (data.attemptsLeft !== undefined) { this.loginAttempts.textContent = `${data.attemptsLeft} attempts remaining`; } if (data.lockoutTime) { this._showLoginError(`Too many attempts. Try again in ${Math.ceil(data.lockoutTime / 60)} minutes`); } } } catch (err) { this._showLoginError('Connection error. Please try again.'); } finally { this._setLoginLoading(false); } } _logout() { localStorage.removeItem(TOKEN_KEY); this.token = null; if (this.refreshInterval) clearInterval(this.refreshInterval); if (this.lastUpdatedInterval) clearInterval(this.lastUpdatedInterval); this._showLogin(); this._showToast('Logged out successfully', 'success'); } _showLogin() { this.loginOverlay.style.display = 'flex'; this.app.style.display = 'none'; this.passwordInput.value = ''; this.loginError.textContent = ''; this.loginAttempts.textContent = ''; } _showApp() { this.loginOverlay.style.display = 'none'; this.app.style.display = 'flex'; // Initialize breadcrumb this._updateBreadcrumb(['Dashboard']); // Auto-refresh every 5 seconds for real-time updates this.refreshInterval = setInterval(() => this._loadData(), 5000); // Update "last updated" display every second this.lastUpdatedInterval = setInterval(() => this._updateLastUpdatedDisplay(), 1000); } _showLoginError(msg) { this.loginError.textContent = msg; } _setLoginLoading(loading) { const btnText = this.loginBtn?.querySelector('.btn-text'); const btnLoading = this.loginBtn?.querySelector('.btn-loading'); if (loading) { btnText.style.display = 'none'; btnLoading.style.display = 'inline-flex'; this.loginBtn.disabled = true; } else { btnText.style.display = 'inline'; btnLoading.style.display = 'none'; this.loginBtn.disabled = false; } } // ==================== ENHANCED DATA LOADING ==================== async _loadData() { try { // Show loading state with context this._setLoadingState(true, 'loading'); const [statsRes, usersRes] = await Promise.all([ this._apiGet('/stats'), this._apiGet('/users') ]); if (statsRes.success) { this.stats = statsRes.data; this._renderStats(); } if (usersRes.success) { // Smart update: only re-render if data actually changed const newUsers = usersRes.data.users || []; if (this._hasUsersChanged(newUsers)) { this.users = newUsers; this._renderUsers(); } else { // Just update timestamps and status this._updateUserTimestamps(newUsers); } } // Update last updated time this.lastUpdateTime = Date.now(); this._updateLastUpdatedDisplay(); // Hide loading state this._setLoadingState(false); } catch (err) { console.error('Failed to load data:', err); this._setLoadingState(false); if (err.message === 'Unauthorized') { this._logout(); } else { this._showToast('Failed to load data', 'error'); } } } _hasUsersChanged(newUsers) { if (!this.users || this.users.length !== newUsers.length) return true; // Quick check for significant changes for (let i = 0; i < newUsers.length; i++) { const oldUser = this.users.find(u => u.ip === newUsers[i].ip); const newUser = newUsers[i]; if (!oldUser || oldUser.chatCount !== newUser.chatCount) { return true; } } return false; } _updateUserTimestamps(newUsers) { // Update existing user cards with new timestamps without full re-render newUsers.forEach(newUser => { const userCard = this.userList?.querySelector(`[data-ip="${this.escapeHtml(newUser.ip)}"]`); if (userCard) { const timeElement = userCard.querySelector('.user-time'); const statusElement = userCard.querySelector('.user-status'); if (timeElement) { timeElement.textContent = this._formatTimeAgo(newUser.lastActivity); } if (statusElement) { statusElement.classList.toggle('online', newUser.isOnline); statusElement.classList.toggle('status-indicator', newUser.isOnline); } } }); // Update the users array this.users = newUsers; } _setLoadingState(loading, context = 'data') { const elements = [this.userList, this.modelUsageChart]; elements.forEach(el => { if (el) { if (loading) { el.classList.add('loading-pulse'); // Add contextual loading message if (context === 'refresh') { el.style.opacity = '0.7'; } } else { el.classList.remove('loading-pulse'); el.style.opacity = '1'; } } }); // Update refresh button state with better feedback if (this.refreshBtn) { if (loading) { this.refreshBtn.disabled = true; this.refreshBtn.classList.add('spinning', 'loading'); this.refreshBtn.querySelector('span').textContent = 'Refreshing...'; } else { this.refreshBtn.disabled = false; this.refreshBtn.classList.remove('spinning', 'loading'); this.refreshBtn.querySelector('span').textContent = 'Refresh'; } } } _updateLastUpdatedDisplay() { if (!this.lastUpdated || !this.lastUpdateTime) return; const seconds = Math.floor((Date.now() - this.lastUpdateTime) / 1000); const indicator = document.getElementById('autoRefreshIndicator'); if (seconds < 5) { this.lastUpdated.textContent = 'Updated just now'; this.lastUpdated.style.color = 'var(--success)'; if (indicator) indicator.style.opacity = '1'; } else if (seconds < 60) { this.lastUpdated.textContent = `Updated ${seconds}s ago`; this.lastUpdated.style.color = 'var(--text-secondary)'; if (indicator) indicator.style.opacity = '0.7'; } else { const mins = Math.floor(seconds / 60); this.lastUpdated.textContent = `Updated ${mins}m ago`; this.lastUpdated.style.color = mins > 5 ? 'var(--warning)' : 'var(--text-tertiary)'; if (indicator) indicator.style.opacity = mins > 5 ? '0.5' : '0.6'; } } async _loadUserChats(userIp) { try { const res = await this._apiGet(`/users/${encodeURIComponent(userIp)}/chats`); if (res.success) { this.chats = res.data.chats || []; this._renderChats(); } } catch (err) { console.error('Failed to load chats:', err); this._showToast('Failed to load chats', 'error'); } } async _loadChatMessages(chatId) { try { const res = await this._apiGet(`/chats/${encodeURIComponent(chatId)}/messages`); if (res.success) { this._currentMessages = res.data.messages || []; this._renderMessages(this._currentMessages, res.data.chat); } } catch (err) { console.error('Failed to load messages:', err); this._showToast('Failed to load messages', 'error'); } } async _apiGet(endpoint) { try { this._updateConnectionStatus('connecting'); const response = await fetch(`${API_BASE}${endpoint}`, { headers: { 'Authorization': `Bearer ${this.token}` } }); if (response.status === 401) { this._updateConnectionStatus('disconnected'); throw new Error('Unauthorized'); } if (!response.ok) { this._updateConnectionStatus('error'); throw new Error(`HTTP ${response.status}`); } this._updateConnectionStatus('connected'); return response.json(); } catch (err) { this._updateConnectionStatus('error'); throw err; } } async _apiPost(endpoint, data) { try { this._updateConnectionStatus('connecting'); const response = await fetch(`${API_BASE}${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.token}` }, body: JSON.stringify(data) }); if (response.status === 401) { this._updateConnectionStatus('disconnected'); throw new Error('Unauthorized'); } if (!response.ok) { this._updateConnectionStatus('error'); throw new Error(`HTTP ${response.status}`); } this._updateConnectionStatus('connected'); return response.json(); } catch (err) { this._updateConnectionStatus('error'); throw err; } } _updateConnectionStatus(status) { if (!this.connectionStatus) return; // Remove all status classes this.connectionStatus.classList.remove('disconnected', 'reconnecting', 'error'); switch (status) { case 'connected': this.connectionText.textContent = 'Connected'; break; case 'connecting': case 'reconnecting': this.connectionStatus.classList.add('reconnecting'); this.connectionText.textContent = 'Connecting...'; break; case 'disconnected': this.connectionStatus.classList.add('disconnected'); this.connectionText.textContent = 'Disconnected'; break; case 'error': this.connectionStatus.classList.add('disconnected'); this.connectionText.textContent = 'Connection Error'; break; } } // ==================== RENDERING ==================== _renderStats() { const s = this.stats; this.totalUsers.textContent = s.totalUsers || 0; this.totalChats.textContent = s.totalChats || 0; this.todayQueries.textContent = s.todayQueries || 0; // Render trend indicators this._renderTrendIndicator(this.usersTrend, s.totalUsers, s.previousUsers); this._renderTrendIndicator(this.chatsTrend, s.totalChats, s.previousChats); this._renderTrendIndicator(this.todayTrend, s.todayQueries, s.previousTodayQueries); if (this.avgResponseTime) { this.avgResponseTime.textContent = `${s.avgResponseTime || 0}ms`; } if (this.totalMessages) { this.totalMessages.textContent = s.totalMessages || 0; } if (this.activeSessions) { this.activeSessions.textContent = s.activeSessions || 0; } // New enhanced stats if (this.peakHour && s.peakHour !== undefined) { const hour = s.peakHour; const hourStr = hour === 0 ? '12 AM' : hour < 12 ? `${hour} AM` : hour === 12 ? '12 PM' : `${hour - 12} PM`; this.peakHour.textContent = hourStr; } if (this.onlineUsers) { this.onlineUsers.textContent = s.activeSessions || 0; } if (this.avgChatsPerUser) { this.avgChatsPerUser.textContent = s.avgChatsPerUser || '0'; } // Enhanced analytics if (this.yesterdayQueries) { this.yesterdayQueries.textContent = s.yesterdayQueries || 0; } if (this.weekQueries) { this.weekQueries.textContent = s.weekQueries || 0; } if (this.monthQueries) { this.monthQueries.textContent = s.monthQueries || 0; } if (this.newUsersToday) { this.newUsersToday.textContent = s.newUsersToday || 0; } if (this.newUsersWeek) { this.newUsersWeek.textContent = s.newUsersWeek || 0; } if (this.avgMessagesPerChat) { this.avgMessagesPerChat.textContent = s.avgMessagesPerChat || '0'; } // Update summary section if (this.summaryTotalUsers) { this.summaryTotalUsers.textContent = s.totalUsers || 0; } if (this.summaryOnline) { this.summaryOnline.textContent = s.activeSessions || 0; } if (this.summaryToday) { this.summaryToday.textContent = s.todayQueries || 0; } // Render sparklines this._renderSparkline(this.responseTimeSparkline, s.responseTimeHistory); this._renderSparkline(this.messagesSparkline, s.messageHistory); this._renderActivityIndicator(this.activityIndicator, s.activeSessions || 0); // Render daily activity chart if (this.dailyActivityChart && s.dailyActivity) { this._renderDailyActivity(s.dailyActivity); } // Render browser and OS distribution if (this.browserDistribution && s.browserStats) { this._renderDistribution(this.browserDistribution, s.browserStats, 'browser'); } if (this.osDistribution && s.osStats) { this._renderDistribution(this.osDistribution, s.osStats, 'os'); } if (this.deviceDistribution && s.deviceStats) { this._renderDistribution(this.deviceDistribution, s.deviceStats, 'device'); } if (this.languageDistribution && s.languageStats) { this._renderDistribution(this.languageDistribution, s.languageStats, 'language'); } if (this.refererDistribution && s.refererStats) { this._renderDistribution(this.refererDistribution, s.refererStats, 'referer'); } if (this.countryDistribution && s.countryStats) { this._renderDistribution(this.countryDistribution, s.countryStats, 'country'); } if (this.screenDistribution && s.screenStats) { this._renderDistribution(this.screenDistribution, s.screenStats, 'screen'); } // Render new real-time metrics if (this.realtimeOnline) { this.realtimeOnline.textContent = s.activeSessions || 0; } if (this.lastHourQueries) { this.lastHourQueries.textContent = s.lastHourQueries || 0; } if (this.returningUsers) { this.returningUsers.textContent = s.returningUsers || 0; } if (this.returningRate) { this.returningRate.textContent = (s.returningRate || 0) + '%'; } // Render engagement metrics if (this.totalPageViews) { this.totalPageViews.textContent = this._formatNumber(s.totalPageViews || 0); } if (this.avgPageViews) { this.avgPageViews.textContent = s.avgPageViewsPerUser || 0; } if (this.avgSessionDurationEl) { this.avgSessionDurationEl.textContent = (s.avgSessionDuration || 0) + 'm'; } if (this.totalSessionsEl) { this.totalSessionsEl.textContent = this._formatNumber(s.totalSessions || 0); } if (this.bounceRateEl) { this.bounceRateEl.textContent = (s.bounceRate || 0) + '%'; } if (this.newUsersMonthEl) { this.newUsersMonthEl.textContent = s.newUsersMonth || 0; } // Render error metrics if (this.totalErrorsEl) { this.totalErrorsEl.textContent = s.totalErrors || 0; } if (this.errorRateEl) { this.errorRateEl.textContent = (s.errorRate || 0) + '%'; } if (this.recentErrorsEl) { this.recentErrorsEl.textContent = s.recentErrors || 0; } if (this.totalTokensEl) { this.totalTokensEl.textContent = this._formatNumber(s.totalTokens || 0); } // Render top users if (this.topUsersChart && s.topUsers) { this._renderTopUsers(s.topUsers); } // Render hourly usage chart if (this.hourlyUsageChart && s.hourlyUsage) { this._renderHourlyUsage(s.hourlyUsage, s.peakHour); } // Render model usage chart if (this.modelUsageChart && s.modelUsage) { this._renderModelUsage(s.modelUsage); } // Render system health if (this.systemHealthChart && s.systemHealth) { this._renderSystemHealth(s.systemHealth); } // Render performance indicators if (this.performanceIndicators && s.systemHealth) { this._renderPerformanceIndicators(s.systemHealth); } // Render server info if (this.serverInfoChart && s.systemHealth) { this._renderServerInfo(s.systemHealth); } // Render user insights this._renderUserInsights(s); // Render weekly heatmap if (this.weeklyHeatmap && s.hourlyUsage) { this._renderWeeklyHeatmap(s.hourlyUsage); } } _renderUserInsights(stats) { // Calculate average engagement from users if (this.avgEngagement && this.users.length > 0) { const totalEngagement = this.users.reduce((sum, u) => sum + (u.engagementScore || 0), 0); const avgEng = Math.round(totalEngagement / this.users.length); this.avgEngagement.textContent = avgEng + '%'; } // Calculate average time on site if (this.avgTimeOnSite) { const avgTime = stats.avgSessionDuration || 0; this.avgTimeOnSite.textContent = avgTime + 'm'; } // Calculate mobile percentage if (this.mobilePercent && stats.deviceStats) { const total = Object.values(stats.deviceStats).reduce((a, b) => a + b, 0) || 1; const mobile = (stats.deviceStats['Mobile'] || 0) + (stats.deviceStats['Tablet'] || 0); const mobilePerc = Math.round((mobile / total) * 100); this.mobilePercent.textContent = mobilePerc + '%'; } // Calculate average sessions per user if (this.avgSessionsPerUser && stats.totalUsers > 0) { const avgSessions = (stats.totalSessions / stats.totalUsers).toFixed(1); this.avgSessionsPerUser.textContent = avgSessions; } } _renderWeeklyHeatmap(hourlyUsage) { if (!this.weeklyHeatmap) return; // Generate mock weekly data based on hourly usage pattern const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; const maxVal = Math.max(...(hourlyUsage || []), 1); let html = '
Users will appear here when they start chatting with Rox AI
No chats found for this user
No messages in this chat
${escapedCode}${this.escapeHtml(code)}`);
return placeholder;
});
// Apply auto-format math expressions AFTER code blocks are protected
formatted = this._autoFormatMathExpressions(formatted);
// Tables
const tablePlaceholders = [];
formatted = this._parseMarkdownTables(formatted, tablePlaceholders);
// Convert escaped HTML entities to markdown
formatted = formatted.replace(/<h([1-6])[^&]*>([\s\S]*?)<\/h\1>/gi, (_, level, text) => `${'#'.repeat(parseInt(level))} ${text.trim()}`);
formatted = formatted.replace(/<strong[^&]*>([\s\S]*?)<\/strong>/gi, '**$1**');
formatted = formatted.replace(/<b[^&]*>([\s\S]*?)<\/b>/gi, '**$1**');
formatted = formatted.replace(/<em[^&]*>([\s\S]*?)<\/em>/gi, '*$1*');
formatted = formatted.replace(/<i[^&]*>([\s\S]*?)<\/i>/gi, '*$1*');
formatted = formatted.replace(/<u[^&]*>([\s\S]*?)<\/u>/gi, '_$1_');
formatted = formatted.replace(/<br[^&]*\/?>/gi, '\n');
formatted = formatted.replace(/<span[^&]*>([\s\S]*?)<\/span>/gi, '$1');
// Handle raw HTML tags
let prev = '';
let maxIter = 10;
while (prev !== formatted && maxIter-- > 0) {
prev = formatted;
formatted = formatted.replace(/${this._formatInlineContent(text)}`); formatted = formatted.replace(/<\/blockquote>\n
/g, '
'); // Horizontal rule formatted = formatted.replace(/^---$/gm, '
'); formatted = formatted.replace(/^\*\*\*$/gm, '
'); formatted = formatted.replace(/^___$/gm, '
'); // Links formatted = formatted.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => { const safeUrl = this._sanitizeUrl(url); if (!safeUrl) return this.escapeHtmlDisplay(text); return `${this.escapeHtmlDisplay(text)}`; }); // Paragraphs const lines = formatted.split('\n'); let result = []; let paragraphContent = []; for (const line of lines) { const trimmed = line.trim(); const isBlockElement = /^<(h[1-6]|ul|ol|li|blockquote|hr|div|pre|__CODE|__TABLE|__MATH)/.test(trimmed) || /<\/(h[1-6]|ul|ol|blockquote|div|pre)>$/.test(trimmed); if (isBlockElement || trimmed === '') { if (paragraphContent.length > 0) { result.push('' + paragraphContent.join('
'); paragraphContent = []; } if (trimmed !== '') result.push(line); } else { paragraphContent.push(trimmed); } } if (paragraphContent.length > 0) { result.push('
') + '' + paragraphContent.join('
'); } formatted = result.join('\n'); // Restore placeholders inlineCodes.forEach((code, i) => { formatted = formatted.replace(new RegExp(`__INLINE_CODE_${i}__`, 'g'), code); }); codeBlocks.forEach((block, i) => { formatted = formatted.replace(new RegExp(`__CODE_BLOCK_${i}__`, 'g'), block); }); tablePlaceholders.forEach((table, i) => { formatted = formatted.replace(new RegExp(`__TABLE_BLOCK_${i}__`, 'g'), table); }); mathBlocks.forEach((item, i) => { const rendered = this._renderMath(item.math, true); formatted = formatted.replace(new RegExp(`__MATH_BLOCK_${i}__`, 'g'), rendered); }); inlineMath.forEach((item, i) => { const rendered = this._renderMath(item.math, false); formatted = formatted.replace(new RegExp(`__INLINE_MATH_${i}__`, 'g'), rendered); }); // Clean up formatted = formatted.replace(/
') + '
<\/p>/g, ''); formatted = formatted.replace(/\s*<\/p>/g, ''); // Final pass for remaining escaped HTML entities formatted = formatted.replace(/<\s*h([1-6])\s*>([\s\S]*?)<\s*\/\s*h\1\s*>/gi, (_, level, text) => `
${text.trim()} `); formatted = formatted.replace(/<\s*strong\s*>([\s\S]*?)<\s*\/\s*strong\s*>/gi, '$1'); formatted = formatted.replace(/<\s*em\s*>([\s\S]*?)<\s*\/\s*em\s*>/gi, '$1'); formatted = formatted.replace(/<\s*br\s*\/?>/gi, '
'); formatted = formatted.replace(/<\s*hr\s*\/?>/gi, '
'); return formatted; } _formatInlineContent(text) { if (!text) return ''; let result = text; // Convert escaped HTML entities to markdown result = result.replace(/<strong[^&]*>([\s\S]*?)<\/strong>/gi, '**$1**'); result = result.replace(/<b[^&]*>([\s\S]*?)<\/b>/gi, '**$1**'); result = result.replace(/<em[^&]*>([\s\S]*?)<\/em>/gi, '*$1*'); result = result.replace(/<i[^&]*>([\s\S]*?)<\/i>/gi, '*$1*'); result = result.replace(/<u[^&]*>([\s\S]*?)<\/u>/gi, '_$1_'); result = result.replace(/<span[^&]*>([\s\S]*?)<\/span>/gi, '$1'); // Handle raw HTML tags let prev = ''; let maxIter = 10; while (prev !== result && maxIter-- > 0) { prev = result; result = result.replace(/]*>([\s\S]*?)<\/strong>/gi, '**$1**'); result = result.replace(/]*>([\s\S]*?)<\/b>/gi, '**$1**'); result = result.replace(/]*>([\s\S]*?)<\/em>/gi, '*$1*'); result = result.replace(/]*>([\s\S]*?)<\/i>/gi, '*$1*'); result = result.replace(/]*>([\s\S]*?)<\/u>/gi, '_$1_'); result = result.replace(/]*>([\s\S]*?)<\/span>/gi, '$1'); } // Store bold/italic with unique markers const bolds = []; const italics = []; const underlines = []; const boldMarker = '\u0000BOLD\u0000'; const italicMarker = '\u0000ITALIC\u0000'; const underlineMarker = '\u0000UNDERLINE\u0000'; result = result.replace(/\*\*([^*]+)\*\*/g, (_, content) => { bolds.push(content); return `${boldMarker}${bolds.length - 1}${boldMarker}`; }); result = result.replace(/(? { italics.push(content); return `${italicMarker}${italics.length - 1}${italicMarker}`; }); result = result.replace(/(? { if (/__/.test(match)) return match; underlines.push(content); return `${underlineMarker}${underlines.length - 1}${underlineMarker}`; }); // Escape for XSS protection result = this.escapeHtmlDisplay(result); // Restore bold with proper HTML bolds.forEach((content, i) => { result = result.replace(new RegExp(`${boldMarker}${i}${boldMarker}`, 'g'), `${this.escapeHtmlDisplay(content)}`); }); // Restore italic with proper HTML italics.forEach((content, i) => { result = result.replace(new RegExp(`${italicMarker}${i}${italicMarker}`, 'g'), `${this.escapeHtmlDisplay(content)}`); }); // Restore underline with proper HTML underlines.forEach((content, i) => { result = result.replace(new RegExp(`${underlineMarker}${i}${underlineMarker}`, 'g'), `${this.escapeHtmlDisplay(content)}`); }); return result; } _renderMath(math, displayMode = false) { if (!math) return ''; try { // Check if KaTeX is available (global scope) const katexLib = typeof katex !== 'undefined' ? katex : (typeof window !== 'undefined' ? window.katex : null); if (katexLib && typeof katexLib.renderToString === 'function') { const html = katexLib.renderToString(math, { displayMode: displayMode, throwOnError: false, errorColor: '#cc0000', strict: false, trust: true, macros: { "\\R": "\\mathbb{R}", "\\N": "\\mathbb{N}", "\\Z": "\\mathbb{Z}", "\\Q": "\\mathbb{Q}", "\\C": "\\mathbb{C}" } }); if (displayMode) { return `${html}`; } return `${html}`; } } catch (e) { console.warn('KaTeX rendering failed:', e); } // Fallback: convert common LaTeX to Unicode let fallback = math // Handle \text{} - extract text content .replace(/\\text\{([^}]+)\}/g, (_, text) => text.replace(/_/g, ' ')) .replace(/\\textit\{([^}]+)\}/g, '$1') .replace(/\\textbf\{([^}]+)\}/g, '$1') .replace(/\\mathrm\{([^}]+)\}/g, '$1') .replace(/\\mathit\{([^}]+)\}/g, '$1') .replace(/\\mathbf\{([^}]+)\}/g, '$1') // Remove \left and \right (sizing hints) .replace(/\\left\s*/g, '') .replace(/\\right\s*/g, '') .replace(/\\big\s*/g, '') .replace(/\\Big\s*/g, '') .replace(/\\bigg\s*/g, '') .replace(/\\Bigg\s*/g, '') // Fractions .replace(/\\frac\{([^}]+)\}\{([^}]+)\}/g, '($1/$2)') .replace(/\\dfrac\{([^}]+)\}\{([^}]+)\}/g, '($1/$2)') .replace(/\\sqrt\{([^}]+)\}/g, '√($1)') .replace(/\\sqrt(\d+)/g, '√$1') .replace(/\\int_\{?([^}\s]+)\}?\^\{?([^}\s]+)\}?/g, '∫[$1→$2]') .replace(/\\int/g, '∫') .replace(/\\sum_\{?([^}\s]+)\}?\^\{?([^}\s]+)\}?/g, 'Σ[$1→$2]') .replace(/\\sum/g, 'Σ') .replace(/\\prod/g, '∏') .replace(/\\partial/g, '∂') .replace(/\\nabla/g, '∇') .replace(/\\infty/g, '∞') .replace(/\\alpha/g, 'α').replace(/\\beta/g, 'β').replace(/\\gamma/g, 'γ') .replace(/\\delta/g, 'δ').replace(/\\Delta/g, 'Δ') .replace(/\\epsilon/g, 'ε').replace(/\\varepsilon/g, 'ε') .replace(/\\theta/g, 'θ').replace(/\\Theta/g, 'Θ') .replace(/\\lambda/g, 'λ').replace(/\\Lambda/g, 'Λ') .replace(/\\mu/g, 'μ').replace(/\\nu/g, 'ν') .replace(/\\pi/g, 'π').replace(/\\Pi/g, 'Π') .replace(/\\sigma/g, 'σ').replace(/\\Sigma/g, 'Σ') .replace(/\\omega/g, 'ω').replace(/\\Omega/g, 'Ω') .replace(/\\phi/g, 'φ').replace(/\\Phi/g, 'Φ') .replace(/\\psi/g, 'ψ').replace(/\\Psi/g, 'Ψ') .replace(/\\rho/g, 'ρ').replace(/\\tau/g, 'τ') .replace(/\\eta/g, 'η').replace(/\\zeta/g, 'ζ') .replace(/\\xi/g, 'ξ').replace(/\\Xi/g, 'Ξ') .replace(/\\kappa/g, 'κ').replace(/\\iota/g, 'ι') .replace(/\\chi/g, 'χ').replace(/\\upsilon/g, 'υ') .replace(/\\times/g, '×').replace(/\\div/g, '÷').replace(/\\pm/g, '±') .replace(/\\mp/g, '∓').replace(/\\cdot/g, '·') .replace(/\\leq/g, '≤').replace(/\\geq/g, '≥').replace(/\\neq/g, '≠') .replace(/\\le/g, '≤').replace(/\\ge/g, '≥').replace(/\\ne/g, '≠') .replace(/\\approx/g, '≈').replace(/\\equiv/g, '≡') .replace(/\\propto/g, '∝').replace(/\\sim/g, '∼') .replace(/\\rightarrow/g, '→').replace(/\\leftarrow/g, '←') .replace(/\\Rightarrow/g, '⇒').replace(/\\Leftarrow/g, '⇐') .replace(/\\leftrightarrow/g, '↔').replace(/\\Leftrightarrow/g, '⇔') .replace(/\\to/g, '→').replace(/\\gets/g, '←') .replace(/\\forall/g, '∀').replace(/\\exists/g, '∃') .replace(/\\in/g, '∈').replace(/\\notin/g, '∉') .replace(/\\subset/g, '⊂').replace(/\\supset/g, '⊃') .replace(/\\subseteq/g, '⊆').replace(/\\supseteq/g, '⊇') .replace(/\\cup/g, '∪').replace(/\\cap/g, '∩') .replace(/\\emptyset/g, '∅').replace(/\\varnothing/g, '∅') .replace(/\\land/g, '∧').replace(/\\lor/g, '∨').replace(/\\neg/g, '¬') .replace(/\\angle/g, '∠').replace(/\\perp/g, '⊥').replace(/\\parallel/g, '∥') .replace(/\\ldots/g, '…').replace(/\\cdots/g, '⋯') // Spacing .replace(/\\,/g, ' ').replace(/\\;/g, ' ').replace(/\\:/g, ' ') .replace(/\\!/g, '').replace(/\\quad/g, ' ').replace(/\\qquad/g, ' ') .replace(/\\ /g, ' ') // Remove remaining LaTeX commands but keep content in braces .replace(/\\[a-zA-Z]+\{([^}]*)\}/g, '$1') .replace(/\\[a-zA-Z]+/g, '') .replace(/\{([^{}]*)\}/g, '$1'); // Handle superscripts and subscripts with iterative processing for (let i = 0; i < 5; i++) { fallback = fallback.replace(/\^(\{[^{}]+\})/g, (_, exp) => { const content = exp.slice(1, -1); return content.split('').map(c => SUPERSCRIPTS[c] || SUPERSCRIPTS[c.toLowerCase()] || c).join(''); }); fallback = fallback.replace(/\^([0-9a-zA-Z+\-])/g, (_, c) => { return SUPERSCRIPTS[c] || SUPERSCRIPTS[c.toLowerCase()] || `^${c}`; }); fallback = fallback.replace(/_(\{[^{}]+\})/g, (_, exp) => { const content = exp.slice(1, -1); return content.split('').map(c => SUBSCRIPTS[c] || SUBSCRIPTS[c.toLowerCase()] || c).join(''); }); fallback = fallback.replace(/_([0-9a-zA-Z])/g, (_, c) => { return SUBSCRIPTS[c] || SUBSCRIPTS[c.toLowerCase()] || `_${c}`; }); } // Final cleanup fallback = fallback.replace(/[{}]/g, ''); if (displayMode) { return `${this.escapeHtmlDisplay(fallback)}`; } return `${this.escapeHtmlDisplay(fallback)}`; } _parseMarkdownTables(content, placeholders) { if (!content) return content; const lines = content.split('\n'); const result = []; let i = 0; while (i < lines.length) { const line = lines[i]; if (line.trim().startsWith('|') && line.trim().endsWith('|')) { const nextLine = lines[i + 1]; if (nextLine && /^\|[\s:|-]+\|$/.test(nextLine.trim())) { const tableLines = [line]; let j = i + 1; while (j < lines.length && lines[j].trim().startsWith('|')) { tableLines.push(lines[j]); j++; } const tableHtml = this._convertTableToHtml(tableLines); if (tableHtml) { const placeholder = `__TABLE_BLOCK_${placeholders.length}__`; placeholders.push(tableHtml); result.push(placeholder); i = j; continue; } } } result.push(line); i++; } return result.join('\n'); } _convertTableToHtml(lines) { if (lines.length < 2) return null; const headerLine = lines[0].trim(); const headerCells = headerLine.split('|').filter(c => c.trim()).map(c => c.trim()); const separatorLine = lines[1].trim(); const aligns = separatorLine.split('|').filter(c => c.trim()).map(c => { const cell = c.trim(); if (cell.startsWith(':') && cell.endsWith(':')) return 'center'; if (cell.endsWith(':')) return 'right'; return 'left'; }); let html = ''; return html; } _initCodeCopyButtons() { this.messagesArea?.querySelectorAll('.code-copy-btn').forEach(btn => { btn.addEventListener('click', async () => { const codeBlock = btn.closest('.code-block'); const code = codeBlock?.querySelector('code')?.textContent; if (code) { try { await navigator.clipboard.writeText(code); btn.classList.add('copied'); btn.querySelector('span').textContent = 'Copied!'; setTimeout(() => { btn.classList.remove('copied'); btn.querySelector('span').textContent = 'Copy'; }, 2000); } catch (err) { console.error('Failed to copy:', err); } } }); }); } // ==================== USER INTERACTIONS ==================== _selectUser(ip) { this.currentUser = ip; this.currentChat = null; this.selectedUserName.textContent = this._maskIp(ip); // Update breadcrumb this._updateBreadcrumb(['Dashboard', `User: ${this._maskIp(ip)}`]); // Update chat count badge const user = this.users.find(u => u.ip === ip); if (this.chatCountBadge && this.chatCount && user) { this.chatCount.textContent = user.chatCount || 0; this.chatCountBadge.style.display = 'flex'; } // Render user details panel if (user) { this._renderUserDetailsPanel(user); } // Update active state this.userList.querySelectorAll('.user-card').forEach(card => { card.classList.toggle('active', card.dataset.ip === ip); }); // Load chats this._loadUserChats(ip); // Clear messages this.chatHeader.style.display = 'none'; this.messagesArea.innerHTML = `
'; headerCells.forEach((h, i) => { html += ` '; for (let i = 2; i < lines.length; i++) { const cells = lines[i].split('|').filter(c => c !== '').map(c => c.trim()); html += '${this._formatInlineContent(h)} `; }); html += ''; cells.forEach((c, j) => { if (c !== undefined) { html += ` '; } html += '${this._formatInlineContent(c)} `; } }); html += '`; // On mobile, close sidebar and show chats panel if (window.innerWidth <= 768) { this._closeSidebar(); this._openChatsPanel(); } } _selectChat(chatId) { this.currentChat = chatId; // Update breadcrumb const chat = this.chats.find(c => c.id === chatId); const chatTitle = chat?.title || 'Chat'; this._updateBreadcrumb(['Dashboard', `User: ${this._maskIp(this.currentUser)}`, chatTitle]); // Update active state this.chatsList.querySelectorAll('.chat-card').forEach(card => { card.classList.toggle('active', card.dataset.id === chatId); }); // Load messages this._loadChatMessages(chatId); // On mobile, hide chats panel if (window.innerWidth <= 768) { this._closeChatsPanel(); } } _updateBreadcrumb(items) { if (!this.breadcrumb) return; const html = items.map((item, index) => { const isLast = index === items.length - 1; const isClickable = index < items.length - 1; let breadcrumbHtml = ``; if (!isLast) { breadcrumbHtml += ''; } return breadcrumbHtml; }).join(''); this.breadcrumb.innerHTML = html; // Add click handlers for navigation this.breadcrumb.querySelectorAll('.breadcrumb-item[data-level]').forEach(item => { item.addEventListener('click', () => { const level = parseInt(item.dataset.level); this._navigateToBreadcrumbLevel(level); }); }); } _navigateToBreadcrumbLevel(level) { if (level === 0) { // Navigate to dashboard this.currentUser = null; this.currentChat = null; this.selectedUserName.textContent = '-'; this._updateBreadcrumb(['Dashboard']); if (this.chatCountBadge) { this.chatCountBadge.style.display = 'none'; } // Clear active states this.userList.querySelectorAll('.user-card').forEach(card => { card.classList.remove('active'); }); // Clear chats and messages this.chatsList.innerHTML = 'Select a chat to view messages
'; this.messagesArea.innerHTML = 'Select a user to view chats
'; this.chatHeader.style.display = 'none'; } else if (level === 1 && this.currentUser) { // Navigate back to user level this.currentChat = null; this._updateBreadcrumb(['Dashboard', `User: ${this._maskIp(this.currentUser)}`]); // Clear chat selection this.chatsList.querySelectorAll('.chat-card').forEach(card => { card.classList.remove('active'); }); // Clear messages this.messagesArea.innerHTML = 'Select a chat to view messages
'; this.chatHeader.style.display = 'none'; } } _filterUsers(query) { const q = query.toLowerCase().trim(); let visibleCount = 0; this.userList.querySelectorAll('.user-card').forEach(card => { const ip = card.dataset.ip.toLowerCase(); const deviceText = Array.from(card.querySelectorAll('.user-badge')) .map(badge => badge.textContent.toLowerCase()).join(' '); const isVisible = ip.includes(q) || deviceText.includes(q); card.style.display = isVisible ? '' : 'none'; if (isVisible) { visibleCount++; // Highlight matching text this._highlightSearchText(card, q); } }); // Update search results counter if (this.searchResultsCount) { if (q && visibleCount > 0) { this.searchResultsCount.textContent = visibleCount; this.searchResultsCount.style.display = 'block'; } else { this.searchResultsCount.style.display = 'none'; } } // Show search results count if (q && visibleCount === 0) { this.userList.innerHTML += 'Select a chat to view messages
No users match your search'; } } _highlightSearchText(element, query) { if (!query) return; const textNodes = this._getTextNodes(element); textNodes.forEach(node => { const text = node.textContent; const regex = new RegExp(`(${this.escapeRegex(query)})`, 'gi'); if (regex.test(text)) { const highlightedText = text.replace(regex, '$1'); const wrapper = document.createElement('span'); wrapper.innerHTML = highlightedText; node.parentNode.replaceChild(wrapper, node); } }); } _getTextNodes(element) { const textNodes = []; const walker = document.createTreeWalker( element, NodeFilter.SHOW_TEXT, null, false ); let node; while (node = walker.nextNode()) { if (node.textContent.trim()) { textNodes.push(node); } } return textNodes; } escapeRegex(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } _switchTab(tab) { // Add smooth transition feedback const currentTab = document.querySelector('.tab-btn.active'); if (currentTab) { currentTab.style.transform = 'scale(0.95)'; setTimeout(() => { currentTab.style.transform = ''; }, 150); } this.tabBtns.forEach(btn => { const isActive = btn.dataset.tab === tab; btn.classList.toggle('active', isActive); if (isActive) { btn.style.transform = 'scale(1.05)'; setTimeout(() => { btn.style.transform = ''; }, 150); } }); // Smooth content transition const fadeOut = (element) => { if (element) { element.style.opacity = '0'; element.style.transform = 'translateY(10px)'; } }; const fadeIn = (element) => { if (element) { setTimeout(() => { element.style.display = ''; element.style.opacity = '1'; element.style.transform = 'translateY(0)'; }, 150); } }; if (tab === 'users') { fadeOut(this.statsTab); setTimeout(() => { this.statsTab.style.display = 'none'; fadeIn(this.usersTab); }, 150); } else { fadeOut(this.usersTab); setTimeout(() => { this.usersTab.style.display = 'none'; fadeIn(this.statsTab); }, 150); } } _toggleSidebar() { this.sidebar.classList.toggle('open'); this.sidebarOverlay.classList.toggle('active'); // Prevent body scroll when sidebar is open on mobile document.body.style.overflow = this.sidebar.classList.contains('open') ? 'hidden' : ''; } _closeSidebar() { this.sidebar.classList.remove('open'); this.sidebarOverlay.classList.remove('active'); document.body.style.overflow = ''; } _openChatsPanel() { this.chatsPanel?.classList.add('open'); this.chatsPanelOverlay?.classList.add('active'); document.body.style.overflow = 'hidden'; } _closeChatsPanel() { this.chatsPanel?.classList.remove('open'); this.chatsPanelOverlay?.classList.remove('active'); document.body.style.overflow = ''; } // ==================== ENHANCED ACTIONS ==================== async _refreshData() { if (this.refreshBtn) { this.refreshBtn.disabled = true; this.refreshBtn.classList.add('spinning'); } try { // Show context-aware loading this._setLoadingState(true, 'refresh'); await this._loadData(); // Refresh current user's chats if selected if (this.currentUser) { await this._loadUserChats(this.currentUser); } // Refresh current chat messages if selected if (this.currentChat) { await this._loadChatMessages(this.currentChat); } this._showToast('Data refreshed successfully', 'success', 3000); } catch (err) { console.error('Refresh failed:', err); this._showToast('Failed to refresh data', 'error'); } finally { this._setLoadingState(false); if (this.refreshBtn) { this.refreshBtn.disabled = false; this.refreshBtn.classList.remove('spinning'); this.refreshBtn.querySelector('span').textContent = 'Refresh'; } } } // Enhanced keyboard shortcuts _initKeyboardShortcuts() { document.addEventListener('keydown', (e) => { // Only handle shortcuts when not typing in inputs if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; switch (e.key) { case 'Escape': this._hideDialog(); this._hideHelp(); this._closeSidebar(); this._closeChatsPanel(); break; case 'r': case 'R': if (e.ctrlKey || e.metaKey) { e.preventDefault(); this._refreshData(); } break; case 'f': case 'F': if (e.ctrlKey || e.metaKey) { e.preventDefault(); this.userSearch?.focus(); } break; case '1': if (e.ctrlKey || e.metaKey) { e.preventDefault(); this._switchTab('users'); } break; case '2': if (e.ctrlKey || e.metaKey) { e.preventDefault(); this._switchTab('stats'); } break; } }); } // ==================== EXPORT FUNCTIONS ==================== _toggleExportDropdown(e) { e.stopPropagation(); this.exportDropdown?.classList.toggle('open'); } async _exportJson() { this.exportDropdown?.classList.remove('open'); try { const response = await fetch(`${API_BASE}/export?format=json`, { headers: { 'Authorization': `Bearer ${this.token}` } }); if (!response.ok) throw new Error('Export failed'); const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `rox-admin-export-${new Date().toISOString().slice(0, 10)}.json`; a.click(); URL.revokeObjectURL(url); this._showToast('JSON export downloaded', 'success'); } catch (err) { this._showToast('Export failed', 'error'); } } _exportStatsPdf() { this.exportDropdown?.classList.remove('open'); const s = this.stats; const now = new Date(); const dateStr = now.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); // Build model usage HTML let modelUsageHtml = ''; if (s.modelUsage && Object.keys(s.modelUsage).length > 0) { const total = Object.values(s.modelUsage).reduce((a, b) => a + b, 0) || 1; modelUsageHtml = Object.entries(s.modelUsage).map(([model, count]) => { const percent = Math.round((count / total) * 100); return `${this.escapeHtml(model)}${count} (${percent}%)`; }).join(''); } const pdfHtml = this._getPdfTemplate('Rox AI Admin Report', dateStr, timeStr, `${modelUsageHtml ? `${s.totalUsers || 0}Total Users${s.totalChats || 0}Total Chats${s.todayQueries || 0}Today's Queries${s.totalMessages || 0}Total Messages${s.activeSessions || 0}Active Sessions${s.avgResponseTime || 0}msAvg Response TimeModel Usage
${modelUsageHtml}` : ''} `); this._printPdf(pdfHtml); this._showToast('Stats PDF ready - uncheck "Headers and footers" in print options', 'success'); } _exportUsersPdf() { this.exportDropdown?.classList.remove('open'); const now = new Date(); const dateStr = now.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); const usersHtml = this.users.map(user => ``).join(''); const pdfHtml = this._getPdfTemplate('Rox AI Users Report', dateStr, timeStr, ` ${this.escapeHtml(this._maskIp(user.ip))} ${this.escapeHtml(user.device?.browser || '-')} ${this.escapeHtml(user.device?.os || '-')} ${user.chatCount || 0} ${user.isOnline ? 'Online' : 'Offline'} ${this.escapeHtml(this._formatTimeAgo(user.lastActivity))} Total Users: ${this.users.length}
`); this._printPdf(pdfHtml); this._showToast('Users PDF ready - uncheck "Headers and footers" in print options', 'success'); } _exportChatJson() { if (!this.currentChat) { this._showToast('No chat selected', 'error'); return; } const chat = this.chats.find(c => c.id === this.currentChat); if (!chat) { this._showToast('Chat not found', 'error'); return; } const data = { chatId: this.currentChat, title: chat.title, exportedAt: new Date().toISOString(), messageCount: chat.messageCount, messages: this._currentMessages || [] }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `chat-${this.currentChat.slice(0, 8)}-${new Date().toISOString().slice(0, 10)}.json`; a.click(); URL.revokeObjectURL(url); this._showToast('Chat JSON downloaded', 'success'); } _exportChatPdf() { if (!this.currentChat || !this._currentMessages?.length) { this._showToast('No chat selected or no messages', 'error'); return; } const chat = this.chats.find(c => c.id === this.currentChat); const now = new Date(); const dateStr = now.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); // Format messages with proper markdown rendering const messagesHtml = this._currentMessages.map(msg => { const isUser = msg.role === 'user'; const time = msg.timestamp ? new Date(msg.timestamp).toLocaleString() : ''; const model = msg.model || ''; const formattedContent = this._formatContentForPdf(msg.content || ''); return ` `; }).join(''); const pdfHtml = this._getPdfTemplate( chat?.title || 'Chat Export', dateStr, timeStr, `
${usersHtml} IP Address Browser OS Chats Status Last Active ${this._currentMessages.length} messages
`, true // isChat flag for chat-specific styles ); this._printPdf(pdfHtml); this._showToast('Chat PDF ready - uncheck "Headers and footers" in print options', 'success'); } // Format content for PDF with proper markdown rendering (same approach as main app.js) _formatContentForPdf(content) { if (!content || typeof content !== 'string') return ''; let formatted = content; // Remove internet search indicator lines formatted = formatted.replace(/^🌐\s*Searching for.*?\.\.\.?\s*$/gm, ''); formatted = formatted.replace(/^🌐\s*LIVE INTERNET SEARCH RESULTS:?\s*$/gm, ''); formatted = formatted.replace(/^\s*\n+/g, '').replace(/\n{3,}/g, '\n\n'); // ==================== COMPREHENSIVE NUMBER-TEXT SPACING FIX ==================== // This fixes ALL cases where numbers are attached to text without proper spacing // STEP 1: Fix numbered list items at start of lines // Handle "1AI" -> "1. AI" (number + uppercase letter) formatted = formatted.replace(/^(\d+)([A-Z][a-zA-Z])/gm, '$1. $2'); // Handle "1**text**" -> "1. **text**" (number + bold markdown) formatted = formatted.replace(/^(\d+)(\*\*[^*]+\*\*)/gm, '$1. $2'); // Handle "1text" -> "1. text" (number + any letter at line start) formatted = formatted.replace(/^(\d+)([a-zA-Z])/gm, '$1. $2'); // Handle "1.text" -> "1. text" (dot but no space) formatted = formatted.replace(/^(\d+)\.([^\s\d])/gm, '$1. $2'); // Handle "1)" -> "1. " (parenthesis style to dot style) formatted = formatted.replace(/^(\d+)\)\s*/gm, '$1. '); // Handle "1-" -> "1. " (dash style to dot style) formatted = formatted.replace(/^(\d+)-\s*/gm, '$1. '); // STEP 2: Fix numbers attached to words ANYWHERE in text (not just line start) // This catches cases like "Cost1Use" -> "Cost 1. Use" or inline "see step1for" -> "see step 1 for" // Pattern: word boundary + number + letter (not at line start) formatted = formatted.replace(/(\s)(\d+)([A-Z][a-zA-Z])/g, '$1$2. $3'); formatted = formatted.replace(/(\s)(\d+)([a-z])/g, '$1$2. $3'); // STEP 3: Fix specific patterns seen in the screenshot // "Word1Word" -> "Word 1. Word" (word + number + word) formatted = formatted.replace(/([a-zA-Z])(\d+)([A-Z][a-zA-Z])/g, '$1\n$2. $3'); // "word1word" -> "word 1. word" (lowercase word + number + lowercase word) formatted = formatted.replace(/([a-z])(\d+)([a-z])/g, '$1 $2. $3'); // STEP 4: Fix bullet points without space formatted = formatted.replace(/^-([A-Za-z])/gm, '- $1'); formatted = formatted.replace(/^\*([A-Za-z])/gm, '* $1'); formatted = formatted.replace(/^•([A-Za-z])/gm, '• $1'); // STEP 5: Ensure proper spacing after list markers // "1.Word" -> "1. Word" formatted = formatted.replace(/^(\d+)\.([A-Za-z])/gm, '$1. $2'); // "-Word" -> "- Word" formatted = formatted.replace(/^-([A-Za-z])/gm, '- $1'); // STEP 6: Fix "For Word" type headers followed by numbered lists // Pattern: "For Something\n1Text" -> "For Something\n1. Text" formatted = formatted.replace(/\n(\d+)([A-Z])/g, '\n$1. $2'); formatted = formatted.replace(/\n(\d+)([a-z])/g, '\n$1. $2'); // Strip ALL HTML tags (with ANY attributes including style) to plain text/markdown let prevStrip = ''; let maxIterations = 15; while (prevStrip !== formatted && maxIterations-- > 0) { prevStrip = formatted; formatted = formatted.replace(/]*>([\s\S]*?)<\/strong>/gi, '**$1**'); formatted = formatted.replace(/]*>([\s\S]*?)<\/b>/gi, '**$1**'); formatted = formatted.replace(/]*>([\s\S]*?)<\/em>/gi, '*$1*'); formatted = formatted.replace(/]*>([\s\S]*?)<\/i>/gi, '*$1*'); formatted = formatted.replace(/]*>([\s\S]*?)<\/u>/gi, '_$1_'); formatted = formatted.replace(/]*>([\s\S]*?)<\/span>/gi, '$1'); formatted = formatted.replace(/]*>([\s\S]*?)<\/div>/gi, '$1\n'); formatted = formatted.replace(/]*>([\s\S]*?)<\/p>/gi, '$1\n\n'); formatted = formatted.replace(/
]*\/?>/gi, '\n'); formatted = formatted.replace(/]*>([\s\S]*?)<\/h1>/gi, '# $1\n'); formatted = formatted.replace(/
]*>([\s\S]*?)<\/h2>/gi, '## $1\n'); formatted = formatted.replace(/
]*>([\s\S]*?)<\/h3>/gi, '### $1\n'); formatted = formatted.replace(/
]*>([\s\S]*?)<\/h4>/gi, '#### $1\n'); formatted = formatted.replace(/
]*>([\s\S]*?)<\/h5>/gi, '##### $1\n'); formatted = formatted.replace(/
]*>([\s\S]*?)<\/h6>/gi, '###### $1\n'); formatted = formatted.replace(/]*>([\s\S]*?)<\/a>/gi, '$1'); formatted = formatted.replace(/
- ]*>([\s\S]*?)<\/li>/gi, '- $1\n'); formatted = formatted.replace(/<\/?(?:ul|ol)\b[^>]*>/gi, ''); formatted = formatted.replace(/
]*>([\s\S]*?)<\/blockquote>/gi, '> $1\n'); formatted = formatted.replace(/]*>([\s\S]*?)<\/code>/gi, '`$1`'); formatted = formatted.replace(/]*>([\s\S]*?)<\/pre>/gi, '```\n$1\n```'); formatted = formatted.replace(/
]*\/?>/gi, '\n---\n'); } // Decode common HTML entities formatted = formatted.replace(/&/g, '&'); formatted = formatted.replace(/</g, '<'); formatted = formatted.replace(/>/g, '>'); formatted = formatted.replace(/"/g, '"'); formatted = formatted.replace(/'/g, "'"); formatted = formatted.replace(/'/g, "'"); formatted = formatted.replace(/'/g, "'"); formatted = formatted.replace(/ /g, ' '); // After decoding entities, run HTML stripping again prevStrip = ''; maxIterations = 5; while (prevStrip !== formatted && maxIterations-- > 0) { prevStrip = formatted; formatted = formatted.replace(/]*>([\s\S]*?)<\/strong>/gi, '**$1**'); formatted = formatted.replace(/]*>([\s\S]*?)<\/b>/gi, '**$1**'); formatted = formatted.replace(/]*>([\s\S]*?)<\/em>/gi, '*$1*'); formatted = formatted.replace(/]*>([\s\S]*?)<\/i>/gi, '*$1*'); formatted = formatted.replace(/]*>([\s\S]*?)<\/span>/gi, '$1'); } // Clean up any remaining self-closing tags (but not our placeholders) formatted = formatted.replace(/<[a-z][^>]*\/>/gi, ''); // Remove any remaining opening/closing tags that weren't caught (but preserve content) formatted = formatted.replace(/<\/?(?:font|center|marquee|blink|nobr|wbr|s|strike|del|ins|mark|small|big|sub|sup|abbr|acronym|cite|dfn|kbd|samp|var|tt)\b[^>]*>/gi, ''); // Additional pass: Fix any remaining number-text spacing issues after HTML stripping // These patterns may have been hidden inside HTML tags - COMPREHENSIVE FIX // STEP 1: Fix numbered list items at start of lines formatted = formatted.replace(/^(\d+)([A-Z][a-zA-Z])/gm, '$1. $2'); formatted = formatted.replace(/^(\d+)(\*\*[^*]+\*\*)/gm, '$1. $2'); formatted = formatted.replace(/^(\d+)([a-zA-Z])/gm, '$1. $2'); formatted = formatted.replace(/^(\d+)\.([^\s\d])/gm, '$1. $2'); formatted = formatted.replace(/^(\d+)\)\s*/gm, '$1. '); formatted = formatted.replace(/^(\d+)-\s*/gm, '$1. '); // STEP 2: Fix numbers attached to words ANYWHERE in text formatted = formatted.replace(/(\s)(\d+)([A-Z][a-zA-Z])/g, '$1$2. $3'); formatted = formatted.replace(/(\s)(\d+)([a-z])/g, '$1$2. $3'); // STEP 3: Fix specific patterns like "Word1Word" formatted = formatted.replace(/([a-zA-Z])(\d+)([A-Z][a-zA-Z])/g, '$1\n$2. $3'); formatted = formatted.replace(/([a-z])(\d+)([a-z])/g, '$1 $2. $3'); // STEP 4: Fix bullet points without space formatted = formatted.replace(/^-([A-Za-z])/gm, '- $1'); formatted = formatted.replace(/^\*([A-Za-z])/gm, '* $1'); formatted = formatted.replace(/^•([A-Za-z])/gm, '• $1'); // STEP 5: Fix after newlines formatted = formatted.replace(/\n(\d+)([A-Z])/g, '\n$1. $2'); formatted = formatted.replace(/\n(\d+)([a-z])/g, '\n$1. $2'); // STEP 6: Fix numbers inside bold markdown (e.g., **1Star** -> **1. Star**) formatted = formatted.replace(/\*\*(\d+)([A-Z][a-zA-Z])/g, '**$1. $2'); formatted = formatted.replace(/\*\*(\d+)([a-z])/g, '**$1. $2'); // Clean up multiple newlines and spaces formatted = formatted.replace(/\n{3,}/g, '\n\n'); formatted = formatted.replace(/ +/g, ' '); // Store math blocks temporarily for PDF handling const mathBlocks = []; const inlineMathPdf = []; // Protect display math blocks: $$ ... $$ or \[ ... \] formatted = formatted.replace(/\$\$([\s\S]*?)\$\$/g, (_, math) => { const placeholder = `__PDF_MATH_BLOCK_${mathBlocks.length}__`; mathBlocks.push(math.trim()); return placeholder; }); formatted = formatted.replace(/\\\[([\s\S]*?)\\\]/g, (_, math) => { const placeholder = `__PDF_MATH_BLOCK_${mathBlocks.length}__`; mathBlocks.push(math.trim()); return placeholder; }); // Protect inline math: $ ... $ or \( ... \) formatted = formatted.replace(/(? { const placeholder = `__PDF_INLINE_MATH_${inlineMathPdf.length}__`; inlineMathPdf.push(math.trim()); return placeholder; }); formatted = formatted.replace(/\\\((.+?)\\\)/gs, (_, math) => { const placeholder = `__PDF_INLINE_MATH_${inlineMathPdf.length}__`; inlineMathPdf.push(math.trim()); return placeholder; }); // Store code blocks temporarily const codeBlocks = []; formatted = formatted.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => { const placeholder = `__PDF_CODE_BLOCK_${codeBlocks.length}__`; const language = lang ? lang.toUpperCase() : 'CODE'; codeBlocks.push(``); return placeholder; }); // Store inline codes const inlineCodes = []; formatted = formatted.replace(/`([^`]+)`/g, (_, code) => { const placeholder = `__PDF_INLINE_CODE_${inlineCodes.length}__`; inlineCodes.push(`${this.escapeHtml(language)}${this.escapeHtml(code.trim())}${this.escapeHtml(code)}`); return placeholder; }); // Headings with PDF-optimized styling (use _formatInlineContentForPdf for inline formatting) formatted = formatted.replace(/^###### (.+)$/gm, (_, text) => `${this._formatInlineContentForPdf(text)}
`); formatted = formatted.replace(/^##### (.+)$/gm, (_, text) => `${this._formatInlineContentForPdf(text)}
`); formatted = formatted.replace(/^#### (.+)$/gm, (_, text) => `${this._formatInlineContentForPdf(text)}
`); formatted = formatted.replace(/^### (.+)$/gm, (_, text) => `${this._formatInlineContentForPdf(text)}
`); formatted = formatted.replace(/^## (.+)$/gm, (_, text) => `${this._formatInlineContentForPdf(text)}
`); formatted = formatted.replace(/^# (.+)$/gm, (_, text) => `${this._formatInlineContentForPdf(text)}
`); // Ordered lists with styled numbers (use _formatInlineContentForPdf for inline formatting) formatted = formatted.replace(/^(\d+)\.\s*(.+)$/gm, (_, num, text) => `${num}${this._formatInlineContentForPdf(text.trim())}`); // Unordered lists with bullet points (use _formatInlineContentForPdf for inline formatting) formatted = formatted.replace(/^[-*•]\s*(.+)$/gm, (_, text) => `•${this._formatInlineContentForPdf(text.trim())}`); // Blockquotes (use _formatInlineContentForPdf for inline formatting) formatted = formatted.replace(/^> (.+)$/gm, (_, text) => `${this._formatInlineContentForPdf(text)}`); // Tables for PDF formatted = this._parseMarkdownTablesForPdf(formatted); // Horizontal rules formatted = formatted.replace(/^---$/gm, '
'); // Links (format link text for inline markdown) formatted = formatted.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => { const safeUrl = this._sanitizeUrl(url); if (!safeUrl) return this._formatInlineContentForPdf(text); return `${this._formatInlineContentForPdf(text)}`; }); // Paragraphs - format inline content for any remaining markdown const lines = formatted.split('\n'); const result = []; let paragraphContent = []; for (const line of lines) { const trimmed = line.trim(); const isBlockElement = /^<(h[1-6]|div|blockquote|hr|pre|table|__PDF)/.test(trimmed) || /<\/(h[1-6]|div|blockquote|pre|table)>$/.test(trimmed); if (isBlockElement || trimmed === '') { if (paragraphContent.length > 0) { const formattedParagraph = paragraphContent.map(p => this._formatInlineContentForPdf(p)).join('
'); result.push('' + formattedParagraph + '
'); paragraphContent = []; } if (trimmed !== '') result.push(line); } else { paragraphContent.push(trimmed); } } if (paragraphContent.length > 0) { const formattedParagraph = paragraphContent.map(p => this._formatInlineContentForPdf(p)).join('
'); result.push('' + formattedParagraph + '
'); } formatted = result.join('\n'); // Restore inline codes inlineCodes.forEach((code, i) => { formatted = formatted.replace(new RegExp(`__PDF_INLINE_CODE_${i}__`, 'g'), code); }); // Restore code blocks codeBlocks.forEach((block, i) => { formatted = formatted.replace(new RegExp(`__PDF_CODE_BLOCK_${i}__`, 'g'), block); }); // Restore math blocks with PDF-friendly rendering mathBlocks.forEach((math, i) => { const rendered = this._renderMathForPdf(math, true); formatted = formatted.replace(new RegExp(`__PDF_MATH_BLOCK_${i}__`, 'g'), rendered); }); // Restore inline math inlineMathPdf.forEach((math, i) => { const rendered = this._renderMathForPdf(math, false); formatted = formatted.replace(new RegExp(`__PDF_INLINE_MATH_${i}__`, 'g'), rendered); }); // Clean up formatted = formatted.replace(/]*>
<\/p>/g, ''); formatted = formatted.replace(/]*>\s*<\/p>/g, ''); return formatted; } // Render math for PDF with Unicode fallback (matches app.js _renderMathForPDF) _renderMathForPdf(math, displayMode = false) { if (!math) return ''; // Convert LaTeX to Unicode for PDF let rendered = math // Handle \text{} - extract text content .replace(/\\text\{([^}]+)\}/g, (_, text) => text.replace(/_/g, ' ')) .replace(/\\textit\{([^}]+)\}/g, '$1') .replace(/\\textbf\{([^}]+)\}/g, '$1') .replace(/\\mathrm\{([^}]+)\}/g, '$1') .replace(/\\mathit\{([^}]+)\}/g, '$1') .replace(/\\mathbf\{([^}]+)\}/g, '$1') // Remove sizing hints .replace(/\\left\s*/g, '').replace(/\\right\s*/g, '') .replace(/\\big\s*/g, '').replace(/\\Big\s*/g, '') .replace(/\\bigg\s*/g, '').replace(/\\Bigg\s*/g, '') // Fractions - handle nested ones .replace(/\\frac\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/g, '($1)/($2)') .replace(/\\frac\{([^}]+)\}\{([^}]+)\}/g, '($1/$2)') .replace(/\\dfrac\{([^}]+)\}\{([^}]+)\}/g, '($1/$2)') .replace(/\\tfrac\{([^}]+)\}\{([^}]+)\}/g, '($1/$2)') // Square roots .replace(/\\sqrt\[([^\]]+)\]\{([^}]+)\}/g, (_, n, x) => n === '3' ? `∛(${x})` : n === '4' ? `∜(${x})` : `${n}√(${x})`) .replace(/\\sqrt\{([^}]+)\}/g, '√($1)') .replace(/\\sqrt(\d+)/g, '√$1') // Integrals and sums .replace(/\\int_\{([^}]+)\}\^\{([^}]+)\}/g, '∫[$1→$2]') .replace(/\\int/g, '∫').replace(/\\oint/g, '∮') .replace(/\\sum_\{([^}]+)\}\^\{([^}]+)\}/g, 'Σ[$1→$2]') .replace(/\\sum/g, 'Σ').replace(/\\prod/g, '∏') .replace(/\\lim_\{([^}]+)\}/g, 'lim[$1]').replace(/\\lim/g, 'lim') // Calculus .replace(/\\partial/g, '∂').replace(/\\nabla/g, '∇').replace(/\\infty/g, '∞') // Greek letters (lowercase) .replace(/\\alpha/g, 'α').replace(/\\beta/g, 'β').replace(/\\gamma/g, 'γ') .replace(/\\delta/g, 'δ').replace(/\\epsilon/g, 'ε').replace(/\\varepsilon/g, 'ε') .replace(/\\zeta/g, 'ζ').replace(/\\eta/g, 'η').replace(/\\theta/g, 'θ') .replace(/\\iota/g, 'ι').replace(/\\kappa/g, 'κ').replace(/\\lambda/g, 'λ') .replace(/\\mu/g, 'μ').replace(/\\nu/g, 'ν').replace(/\\xi/g, 'ξ') .replace(/\\pi/g, 'π').replace(/\\rho/g, 'ρ').replace(/\\sigma/g, 'σ') .replace(/\\tau/g, 'τ').replace(/\\upsilon/g, 'υ').replace(/\\phi/g, 'φ') .replace(/\\chi/g, 'χ').replace(/\\psi/g, 'ψ').replace(/\\omega/g, 'ω') // Greek letters (uppercase) .replace(/\\Gamma/g, 'Γ').replace(/\\Delta/g, 'Δ').replace(/\\Theta/g, 'Θ') .replace(/\\Lambda/g, 'Λ').replace(/\\Xi/g, 'Ξ').replace(/\\Pi/g, 'Π') .replace(/\\Sigma/g, 'Σ').replace(/\\Phi/g, 'Φ').replace(/\\Psi/g, 'Ψ').replace(/\\Omega/g, 'Ω') // Math operators .replace(/\\times/g, '×').replace(/\\div/g, '÷').replace(/\\pm/g, '±') .replace(/\\mp/g, '∓').replace(/\\cdot/g, '·').replace(/\\ast/g, '∗') .replace(/\\star/g, '⋆').replace(/\\circ/g, '∘') // Comparisons .replace(/\\leq/g, '≤').replace(/\\geq/g, '≥').replace(/\\neq/g, '≠') .replace(/\\le/g, '≤').replace(/\\ge/g, '≥').replace(/\\ne/g, '≠') .replace(/\\approx/g, '≈').replace(/\\equiv/g, '≡').replace(/\\sim/g, '∼') .replace(/\\propto/g, '∝').replace(/\\ll/g, '≪').replace(/\\gg/g, '≫') // Arrows .replace(/\\rightarrow/g, '→').replace(/\\leftarrow/g, '←') .replace(/\\Rightarrow/g, '⇒').replace(/\\Leftarrow/g, '⇐') .replace(/\\leftrightarrow/g, '↔').replace(/\\Leftrightarrow/g, '⇔') .replace(/\\to/g, '→').replace(/\\gets/g, '←').replace(/\\mapsto/g, '↦') // Set theory .replace(/\\forall/g, '∀').replace(/\\exists/g, '∃') .replace(/\\in/g, '∈').replace(/\\notin/g, '∉') .replace(/\\subset/g, '⊂').replace(/\\supset/g, '⊃') .replace(/\\subseteq/g, '⊆').replace(/\\supseteq/g, '⊇') .replace(/\\cup/g, '∪').replace(/\\cap/g, '∩') .replace(/\\emptyset/g, '∅').replace(/\\varnothing/g, '∅') // Logic .replace(/\\land/g, '∧').replace(/\\lor/g, '∨').replace(/\\lnot/g, '¬').replace(/\\neg/g, '¬') // Misc .replace(/\\angle/g, '∠').replace(/\\triangle/g, '△') .replace(/\\perp/g, '⊥').replace(/\\parallel/g, '∥') .replace(/\\therefore/g, '∴').replace(/\\because/g, '∵') .replace(/\\ldots/g, '…').replace(/\\cdots/g, '⋯') .replace(/\\prime/g, '′').replace(/\\degree/g, '°') // Spacing .replace(/\\,/g, ' ').replace(/\\;/g, ' ').replace(/\\:/g, ' ') .replace(/\\!/g, '').replace(/\\quad/g, ' ').replace(/\\qquad/g, ' ') .replace(/\\ /g, ' ') // Remove remaining LaTeX commands but keep content in braces .replace(/\\[a-zA-Z]+\{([^}]*)\}/g, '$1') .replace(/\\[a-zA-Z]+/g, '') .replace(/\{([^{}]*)\}/g, '$1'); // Handle superscripts and subscripts for (let i = 0; i < 5; i++) { rendered = rendered.replace(/\^(\{[^{}]+\})/g, (_, exp) => { const content = exp.slice(1, -1); return content.split('').map(c => SUPERSCRIPTS[c] || SUPERSCRIPTS[c.toLowerCase()] || c).join(''); }); rendered = rendered.replace(/\^([0-9a-zA-Z+\-])/g, (_, c) => { return SUPERSCRIPTS[c] || SUPERSCRIPTS[c.toLowerCase()] || `^${c}`; }); rendered = rendered.replace(/_(\{[^{}]+\})/g, (_, exp) => { const content = exp.slice(1, -1); return content.split('').map(c => SUBSCRIPTS[c] || SUBSCRIPTS[c.toLowerCase()] || c).join(''); }); rendered = rendered.replace(/_([0-9a-zA-Z])/g, (_, c) => { return SUBSCRIPTS[c] || SUBSCRIPTS[c.toLowerCase()] || `_${c}`; }); } // Final cleanup rendered = rendered.replace(/[{}]/g, ''); if (displayMode) { return `
${this.escapeHtml(rendered)}`; } return `${this.escapeHtml(rendered)}`; } _parseMarkdownTablesForPdf(content) { if (!content) return content; const lines = content.split('\n'); const result = []; let i = 0; while (i < lines.length) { const line = lines[i]; if (line.trim().startsWith('|') && line.trim().endsWith('|')) { const nextLine = lines[i + 1]; if (nextLine && /^\|[\s:|-]+\|$/.test(nextLine.trim())) { const tableLines = [line]; let j = i + 1; while (j < lines.length && lines[j].trim().startsWith('|')) { tableLines.push(lines[j]); j++; } result.push(this._convertTableToHtmlForPdf(tableLines)); i = j; continue; } } result.push(line); i++; } return result.join('\n'); } _convertTableToHtmlForPdf(lines) { if (lines.length < 2) return lines.join('\n'); const sep = lines[1]; const aligns = sep.split('|').filter(c => c.trim()).map(c => { const t = c.trim(); if (t.startsWith(':') && t.endsWith(':')) return 'center'; if (t.endsWith(':')) return 'right'; return 'left'; }); const headerCells = lines[0].split('|').filter(c => c.trim()).map(c => c.trim()); let html = ''; return html; } // Format inline content for PDF (bold, italic, code, underline) _formatInlineContentForPdf(text) { if (!text) return ''; let result = text; // Convert any escaped HTML tags back to markdown first result = result.replace(/<strong[^&]*>([\s\S]*?)<\/strong>/gi, '**$1**'); result = result.replace(/<b[^&]*>([\s\S]*?)<\/b>/gi, '**$1**'); result = result.replace(/<em[^&]*>([\s\S]*?)<\/em>/gi, '*$1*'); result = result.replace(/<i[^&]*>([\s\S]*?)<\/i>/gi, '*$1*'); result = result.replace(/<u[^&]*>([\s\S]*?)<\/u>/gi, '_$1_'); result = result.replace(/<code[^&]*>([\s\S]*?)<\/code>/gi, '`$1`'); // Convert raw HTML tags to markdown result = result.replace(/]*>([\s\S]*?)<\/strong>/gi, '**$1**'); result = result.replace(/]*>([\s\S]*?)<\/b>/gi, '**$1**'); result = result.replace(/]*>([\s\S]*?)<\/em>/gi, '*$1*'); result = result.replace(/]*>([\s\S]*?)<\/i>/gi, '*$1*'); result = result.replace(/]*>([\s\S]*?)<\/u>/gi, '_$1_'); result = result.replace(/]*>([\s\S]*?)<\/code>/gi, '`$1`'); result = result.replace(/]*>([\s\S]*?)<\/span>/gi, '$1'); // Store formatted content with markers const bolds = []; const italics = []; const underlines = []; const codes = []; const boldMarker = '\u0000BOLD\u0000'; const italicMarker = '\u0000ITALIC\u0000'; const underlineMarker = '\u0000UNDERLINE\u0000'; const codeMarker = '\u0000CODE\u0000'; // Extract bold **text** result = result.replace(/\*\*([^*]+)\*\*/g, (_, content) => { bolds.push(content); return `${boldMarker}${bolds.length - 1}${boldMarker}`; }); // Extract italic *text* result = result.replace(/(? { italics.push(content); return `${italicMarker}${italics.length - 1}${italicMarker}`; }); // Extract underline _text_ result = result.replace(/(? { if (/__/.test(match)) return match; underlines.push(content); return `${underlineMarker}${underlines.length - 1}${underlineMarker}`; }); // Extract inline code `code` result = result.replace(/`([^`]+)`/g, (_, content) => { codes.push(content); return `${codeMarker}${codes.length - 1}${codeMarker}`; }); // Escape HTML for XSS protection result = this.escapeHtmlDisplay(result); // Restore bold with styled HTML for PDF bolds.forEach((content, i) => { result = result.replace(new RegExp(`${boldMarker}${i}${boldMarker}`, 'g'), `${this.escapeHtmlDisplay(content)}`); }); // Restore italic with styled HTML for PDF italics.forEach((content, i) => { result = result.replace(new RegExp(`${italicMarker}${i}${italicMarker}`, 'g'), `${this.escapeHtmlDisplay(content)}`); }); // Restore underline with styled HTML for PDF underlines.forEach((content, i) => { result = result.replace(new RegExp(`${underlineMarker}${i}${underlineMarker}`, 'g'), `${this.escapeHtmlDisplay(content)}`); }); // Restore inline code with styled HTML for PDF codes.forEach((content, i) => { result = result.replace(new RegExp(`${codeMarker}${i}${codeMarker}`, 'g'), `${this.escapeHtmlDisplay(content)}`); }); return result; } // Generate PDF HTML template (same professional styling as main app.js) _getPdfTemplate(title, dateStr, timeStr, content, isChat = false) { return `${this.escapeHtml(title)} ${this.escapeHtml(title)}${content} `; } // Print PDF using browser print dialog (same approach as main app.js) _printPdf(pdfHtml) { let existingFrame = document.getElementById('adminPdfFrame'); if (existingFrame) existingFrame.remove(); const printFrame = document.createElement('iframe'); printFrame.id = 'adminPdfFrame'; printFrame.style.cssText = 'position:fixed;right:0;bottom:0;width:0;height:0;border:0;'; document.body.appendChild(printFrame); const frameWindow = printFrame.contentWindow; if (!frameWindow) { this._showToast('Failed to create print frame', 'error'); return; } const doc = frameWindow.document; doc.open(); doc.write(pdfHtml); doc.close(); setTimeout(() => { try { frameWindow.focus(); frameWindow.print(); } catch (e) { const printWindow = window.open('', '_blank', 'width=800,height=600'); if (printWindow) { printWindow.document.write(pdfHtml); printWindow.document.close(); printWindow.focus(); setTimeout(() => printWindow.print(), 300); } } }, 500); } // Store current messages for export _currentMessages = []; // ==================== USER DETAILS PANEL ==================== _renderUserDetailsPanel(user) { if (!this.userDetailsContent || !user) return; const isOnline = user.isOnline; const firstSeenDate = user.firstSeen ? new Date(user.firstSeen).toLocaleDateString() : 'Unknown'; const lastActiveDate = user.lastActivity ? new Date(user.lastActivity).toLocaleString() : 'Unknown'; const engagementScore = user.engagementScore || 0; const userType = user.userType || 'New'; // Generate avatar initials from IP const ipParts = user.ip.split('.'); const avatarText = ipParts.length >= 2 ? ipParts[0].slice(-1) + ipParts[1].slice(-1) : 'U'; // Build activity chart let activityChartHtml = ''; if (user.activityByHour && user.activityByHour.some(v => v > 0)) { const maxActivity = Math.max(...user.activityByHour, 1); activityChartHtml = ``; } // Build user tags const tags = []; if (user.sessionCount <= 1) tags.push({ label: 'New User', class: 'new' }); else if (user.sessionCount > 5) tags.push({ label: 'Power User', class: 'power' }); else tags.push({ label: 'Returning', class: 'returning' }); if (user.bounceStatus === 'Bounced') tags.push({ label: 'Bounced', class: 'dormant' }); if (user.totalTokensUsed > 10000) tags.push({ label: 'Heavy Usage', class: 'power' }); if (user.errorCount > 0) tags.push({ label: `${user.errorCount} Errors`, class: 'dormant' }); const tagsHtml = tags.map(t => `${t.label}`).join(''); const html = ` ${user.favoriteModel || user.totalTokensUsed ? ` ` : ''} ${user.doNotTrack ? ` ` : ''} `; this.userDetailsContent.innerHTML = html; // Show the panel if (this.userDetailsPanel) { this.userDetailsPanel.classList.add('active'); } } _hideUserDetails() { if (this.userDetailsPanel) { this.userDetailsPanel.classList.remove('active'); } if (this.userDetailsContent) { this.userDetailsContent.innerHTML = `12AM 6AM 12PM 6PM 11PM`; } } _confirmClearLogs() { this._showDialog({ title: 'Clear All Logs', message: 'This will permanently delete all chat logs and user data. This action cannot be undone.', type: 'danger', onConfirm: () => this._clearLogs() }); } async _clearLogs() { try { const res = await this._apiPost('/clear-logs', { confirm: true }); if (res.success) { this._showToast(`Cleared ${res.cleared} logs`, 'success'); this._loadData(); this.currentUser = null; this.currentChat = null; this.selectedUserName.textContent = '-'; this.chatsList.innerHTML = 'Select a user to view details
'; this.messagesArea.innerHTML = 'Select a user to view chats
'; this.chatHeader.style.display = 'none'; } else { this._showToast(res.error || 'Clear failed', 'error'); } } catch (err) { this._showToast('Clear failed', 'error'); } } // ==================== UI HELPERS ==================== _showDialog({ title, message, type = 'warning', onConfirm }) { this.dialogTitle.textContent = title; this.dialogMessage.textContent = message; const icon = document.getElementById('dialogIcon'); icon.className = `dialog-icon ${type}`; icon.innerHTML = type === 'danger' ? '' : ''; this.dialogConfirm.onclick = () => { this._hideDialog(); if (onConfirm) onConfirm(); }; this.dialogOverlay.style.display = 'flex'; } _hideDialog() { this.dialogOverlay.style.display = 'none'; } _showHelp() { this.helpOverlay.style.display = 'flex'; } _hideHelp() { this.helpOverlay.style.display = 'none'; } _showToast(message, type = 'info', duration = 5000) { const toast = document.createElement('div'); toast.className = `toast ${type}`; const icons = { success: '', error: '', warning: '', info: '' }; toast.innerHTML = ` `; const closeBtn = toast.querySelector('.toast-close'); closeBtn.onclick = () => this._removeToast(toast); this.toastContainer.appendChild(toast); // Auto-remove with enhanced animation const timeoutId = setTimeout(() => this._removeToast(toast), duration); // Pause auto-remove on hover toast.addEventListener('mouseenter', () => clearTimeout(timeoutId)); toast.addEventListener('mouseleave', () => { setTimeout(() => this._removeToast(toast), 1000); }); // Add progress bar for visual feedback if (duration > 2000) { const progressBar = document.createElement('div'); progressBar.className = 'toast-progress'; progressBar.style.cssText = ` position: absolute; bottom: 0; left: 0; height: 2px; background: currentColor; opacity: 0.3; animation: toast-progress ${duration}ms linear; `; toast.appendChild(progressBar); } } _removeToast(toast) { if (toast && toast.parentNode) { toast.classList.add('removing'); setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, 300); } } // ==================== UTILITY FUNCTIONS ==================== escapeHtml(str) { if (!str || typeof str !== 'string') return ''; return str.replace(/[&<>"']/g, c => HTML_ESCAPE_MAP[c] || c); } // Display-safe escape - only escapes < and > for XSS, preserves & and quotes for readability escapeHtmlDisplay(text) { if (typeof text !== 'string') return ''; return text.replace(//g, '>'); } // Auto-format math expressions (superscripts, subscripts, symbols) _autoFormatMathExpressions(content) { if (!content || typeof content !== 'string') return content; let formatted = content; // Convert superscript patterns: x^2, r^6, n^{10}, etc. formatted = formatted.replace(/\b([a-zA-Z])\^(\{([^}]+)\}|([0-9]+))\b/g, (match, base, _, bracedExp, plainExp) => { const exp = bracedExp || plainExp; const converted = exp.split('').map(c => SUPERSCRIPTS[c] || SUPERSCRIPTS[c.toLowerCase()] || c).join(''); const allConverted = exp.split('').every(c => SUPERSCRIPTS[c] || SUPERSCRIPTS[c.toLowerCase()]); if (allConverted) return base + converted; return match; }); // Convert subscript patterns: x_1, a_n, x_{10}, etc. formatted = formatted.replace(/\b([a-zA-Z])_(\{([^}]+)\}|([0-9]))\b/g, (match, base, _, bracedSub, plainSub) => { if (match.includes('__')) return match; const sub = bracedSub || plainSub; const converted = sub.split('').map(c => SUBSCRIPTS[c] || SUBSCRIPTS[c.toLowerCase()] || c).join(''); const allConverted = sub.split('').every(c => SUBSCRIPTS[c] || SUBSCRIPTS[c.toLowerCase()]); if (allConverted) return base + converted; return match; }); // Convert scientific notation: 1e6 → 1×10⁶ formatted = formatted.replace(/\b(\d+\.?\d*)[eE]([+-]?\d+)\b/g, (_, num, exp) => { const expConverted = exp.split('').map(c => SUPERSCRIPTS[c] || c).join(''); return `${num}×10${expConverted}`; }); // Style common math symbols const mathSymbols = [ '≈', '≠', '≤', '≥', '±', '∓', '×', '÷', '·', '→', '←', '↔', '⇒', '⇐', '⇔', '↑', '↓', '∞', '∝', '∂', '∇', '∫', '∑', '∏', '∈', '∉', '⊂', '⊃', '⊆', '⊇', '∪', '∩', '∅', '∀', '∃', '∧', '∨', '¬', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'π', 'ρ', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω', 'Γ', 'Δ', 'Θ', 'Λ', 'Ξ', 'Π', 'Σ', 'Φ', 'Ψ', 'Ω', '√', '∛', '∜' ]; const symbolPattern = new RegExp(`([${mathSymbols.join('')}])`, 'g'); formatted = formatted.replace(symbolPattern, '$1'); formatted = formatted.replace(/([^<]+)<\/span><\/span>/g, '$1'); return formatted; } _sanitizeUrl(url) { if (!url || typeof url !== 'string') return null; const trimmed = url.trim(); if (trimmed.startsWith('javascript:') || trimmed.startsWith('data:')) return null; if (trimmed.startsWith('http://') || trimmed.startsWith('https://') || trimmed.startsWith('/')) { return trimmed; } return null; } _maskIp(ip) { // Show full IP address for admin panel if (!ip) return 'Unknown'; return ip; } _formatTimeAgo(timestamp) { if (!timestamp) return 'Unknown'; const date = new Date(timestamp); const now = new Date(); const diff = now - date; const minutes = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); if (minutes < 1) return 'Just now'; if (minutes < 60) return `${minutes}m ago`; if (hours < 24) return `${hours}h ago`; if (days < 7) return `${days}d ago`; return date.toLocaleDateString(); } _formatDate(timestamp) { if (!timestamp) return 'Unknown'; return new Date(timestamp).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); } _formatTime(timestamp) { if (!timestamp) return ''; return new Date(timestamp).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }); } } // ==================== INITIALIZE ==================== document.addEventListener('DOMContentLoaded', () => { window.adminPanel = new AdminPanel(); });Select a chat to view messages