UNPKG

besper-frontend-site-dev-main

Version:

Professional B-esper Frontend Site - Site-wide integration toolkit for full website bot deployment

1,130 lines (1,007 loc) 49.7 kB
/** * Conversations Tab - View and analyze bot conversations * Provides timeline visualization, table view, and detailed conversation insights */ import { managementApiCall } from '../../../services/centralizedApi.js'; export class ConversationsTab { constructor( credentials, managementEndpoint, callbacks = {}, environment = 'prod', i18n = null ) { this.credentials = credentials; this.managementEndpoint = managementEndpoint; this.callbacks = callbacks; this.environment = environment; this.i18n = i18n; this.currentConversations = []; this.currentFilter = { days: 30, start_date: null, end_date: null, }; this.searchQuery = ''; this.analyticsFilters = {}; this.selectedConversation = null; } /** * Update language * @param {string} language - New language code */ updateLanguage(language) { if (this.i18n) { console.log(`[ConversationsTab] Language updated to: ${language}`); // Re-render conversations content if needed } } /** * Get translated text * @param {string} key - Translation key * @param {Object} variables - Variables for substitution * @returns {string} Translated text */ t(key, variables = {}) { return this.i18n ? this.i18n.t(`conversations.${key}`, variables) : key; } /** * Generate the conversations tab HTML */ generateHTML() { return ` <div class="kb-container" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #fafbfc; padding: 40px 20px; color: #1a1f2c; line-height: 1.5; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;"> <div style="max-width: 1400px; margin: 0 auto;"> <!-- Header --> <div class="kb-header" style="margin-bottom: 40px;"> <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px;"> <h1 style="font-size: 28px; font-weight: 600; color: #022d54; letter-spacing: -0.02em; margin: 0;">Conversation Analytics</h1> <div class="filter-controls" style="display: flex; gap: 16px; align-items: center;"> <div class="date-filter" style="display: flex; align-items: center; gap: 8px;"> <label style="font-size: 14px; color: #6b7684; font-weight: 500;">Timespan:</label> <select id="conversations-days-filter" style="padding: 10px 14px; border: 1px solid #d0d7de; border-radius: 8px; background: white; font-size: 14px; color: #1a1f2c; box-shadow: 0 1px 3px rgba(22, 27, 36, 0.1);"> <option value="1">Last 24 hours</option> <option value="7">Last 7 days</option> <option value="30" selected>Last 30 days</option> <option value="90">Last 90 days</option> <option value="custom">Custom range</option> </select> </div> <div class="custom-date-range" id="custom-date-range" style="display: none; gap: 8px; align-items: center;"> <input type="date" id="start-date" style="padding: 10px 14px; border: 1px solid #d0d7de; border-radius: 8px; font-size: 14px; box-shadow: 0 1px 3px rgba(22, 27, 36, 0.1);"> <span style="color: #6b7684; font-size: 14px;">to</span> <input type="date" id="end-date" style="padding: 10px 14px; border: 1px solid #d0d7de; border-radius: 8px; font-size: 14px; box-shadow: 0 1px 3px rgba(22, 27, 36, 0.1);"> </div> <button id="refresh-conversations" style="padding: 10px 20px; background: linear-gradient(135deg, #022d54 0%, #034a75 100%); color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s ease; box-shadow: 0 2px 6px rgba(2, 45, 84, 0.2);" onmouseover="this.style.transform='translateY(-1px)'; this.style.boxShadow='0 4px 12px rgba(2, 45, 84, 0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 6px rgba(2, 45, 84, 0.2)'"> [LOADING] Refresh </button> </div> </div> <!-- Search and Filter Controls --> <div class="search-filter-section" style="background: white; padding: 24px; border-radius: 12px; border: 1px solid #d0d7de; box-shadow: 0 2px 8px rgba(22, 27, 36, 0.08); margin-bottom: 24px;"> <div style="display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap;"> <!-- Search Input --> <div style="flex: 1; min-width: 300px;"> <label style="display: block; font-size: 14px; color: #1a1f2c; font-weight: 500; margin-bottom: 8px;">🔍 Search Conversations</label> <div style="position: relative;"> <input type="text" id="search-query" placeholder="${this.t('search.placeholder')}" style="width: 100%; padding: 12px 16px 12px 44px; border: 1px solid #d0d7de; border-radius: 8px; font-size: 14px; background: #fafbfc; transition: all 0.2s ease;" onfocus="this.style.borderColor='#022d54'; this.style.background='white';" onblur="this.style.borderColor='#d0d7de'; this.style.background='#fafbfc';"> <div style="position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: #6b7684; font-size: 16px;">🔍</div> </div> <div style="display: flex; gap: 12px; margin-top: 8px;"> <label style="display: flex; align-items: center; gap: 6px; font-size: 14px; color: #6b7684; cursor: pointer;"> <input type="checkbox" id="vector-search" checked style="width: 16px; height: 16px; accent-color: #022d54;"> <span>🧠 AI Search</span> </label> <button id="search-btn" style="padding: 8px 16px; background: #28a745; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; transition: all 0.2s ease;" onmouseover="this.style.background='#218838'" onmouseout="this.style.background='#28a745'">Search</button> <button id="clear-search" style="padding: 8px 16px; background: #6c757d; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; transition: all 0.2s ease;" onmouseover="this.style.background='#5a6268'" onmouseout="this.style.background='#6c757d'">Clear</button> </div> </div> <!-- Quick Filters --> <div style="display: flex; flex-direction: column; gap: 12px; min-width: 280px;"> <label style="font-size: 14px; color: #1a1f2c; font-weight: 500;">Quick Filters</label> <div style="display: flex; gap: 8px; flex-wrap: wrap;"> <select id="sentiment-filter" style="padding: 8px 12px; border: 1px solid #d0d7de; border-radius: 6px; font-size: 13px; background: white; min-width: 120px;"> <option value="">All Sentiment</option> <option value="positive">Positive</option> <option value="neutral">Neutral</option> <option value="negative">Negative</option> </select> <select id="language-filter" style="padding: 8px 12px; border: 1px solid #d0d7de; border-radius: 6px; font-size: 13px; background: white; min-width: 100px;"> <option value="">All Languages</option> <option value="en">🇺🇸 English</option> <option value="es">🇪🇸 Spanish</option> <option value="fr">🇫🇷 French</option> <option value="de">🇩🇪 German</option> <option value="it">🇮🇹 Italian</option> </select> </div> <div style="display: flex; gap: 8px; flex-wrap: wrap;"> <select id="resolution-filter" style="padding: 8px 12px; border: 1px solid #d0d7de; border-radius: 6px; font-size: 13px; background: white; min-width: 130px;"> <option value="">All Status</option> <option value="resolved">Resolved</option> <option value="partial">Partial</option> <option value="unresolved">Unresolved</option> </select> <label style="display: flex; align-items: center; gap: 6px; font-size: 13px; color: #6b7684; cursor: pointer; padding: 6px 12px; border: 1px solid #d0d7de; border-radius: 6px; background: white; transition: all 0.2s ease;" onmouseover="this.style.borderColor='#022d54'" onmouseout="this.style.borderColor='#d0d7de'"> <input type="checkbox" id="attention-filter" style="width: 14px; height: 14px; accent-color: #dc3545;"> <span>Needs Attention</span> </label> </div> </div> </div> </div> </div> <div class="conversations-content"> <!-- Statistics Overview --> <div class="stats-overview" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; margin-bottom: 32px;"> <div class="stat-card" style="background: white; padding: 20px; border-radius: 12px; border: 1px solid #d0d7de; box-shadow: 0 1px 3px rgba(22, 27, 36, 0.04);"> <div class="stat-value" id="total-conversations" style="font-size: 28px; font-weight: 600; color: #022d54; margin-bottom: 4px;">0</div> <div class="stat-label" style="color: #6b7684; font-size: 13px; font-weight: 500;">Total Conversations</div> </div> <div class="stat-card" style="background: white; padding: 20px; border-radius: 12px; border: 1px solid #d0d7de; box-shadow: 0 1px 3px rgba(22, 27, 36, 0.04);"> <div class="stat-value" id="resolved-conversations" style="font-size: 28px; font-weight: 600; color: #28a745; margin-bottom: 4px;">0</div> <div class="stat-label" style="color: #6b7684; font-size: 13px; font-weight: 500;">Resolved Issues</div> </div> <div class="stat-card" style="background: white; padding: 20px; border-radius: 12px; border: 1px solid #d0d7de; box-shadow: 0 1px 3px rgba(22, 27, 36, 0.04);"> <div class="stat-value" id="avg-messages" style="font-size: 28px; font-weight: 600; color: #fd7e14; margin-bottom: 4px;">0</div> <div class="stat-label" style="color: #6b7684; font-size: 13px; font-weight: 500;">Avg Quality Rating</div> </div> <div class="stat-card" style="background: white; padding: 20px; border-radius: 12px; border: 1px solid #d0d7de; box-shadow: 0 1px 3px rgba(22, 27, 36, 0.04);"> <div class="stat-value" id="attention-needed" style="font-size: 28px; font-weight: 600; color: #dc3545; margin-bottom: 4px;">0</div> <div class="stat-label" style="color: #6b7684; font-size: 13px; font-weight: 500;">Need Attention</div> </div> </div> <!-- Timeline Visualization --> <div class="timeline-section" style="margin-bottom: 32px;"> <h2 style="font-size: 18px; font-weight: 500; color: #022d54; margin-bottom: 16px; letter-spacing: -0.01em;">Conversation Timeline</h2> <div class="timeline-container" id="conversation-timeline" style="height: 200px; background: white; border-radius: 12px; padding: 24px; position: relative; border: 1px solid #d0d7de; box-shadow: 0 1px 3px rgba(22, 27, 36, 0.04);"> <div class="timeline-loading" style="display: flex; align-items: center; justify-content: center; height: 100%; color: #6b7684; font-size: 14px;"> Load conversations to view timeline </div> </div> </div> <!-- Conversations Table --> <div class="table-section"> <h2 style="font-size: 18px; font-weight: 500; color: #022d54; margin-bottom: 16px; letter-spacing: -0.01em;">Conversation History</h2> <div class="table-container" style="background: white; border-radius: 12px; border: 1px solid #d0d7de; box-shadow: 0 1px 3px rgba(22, 27, 36, 0.04); overflow: hidden;"> <table id="conversations-table" style="width: 100%; border-collapse: collapse;"> <thead style="background: linear-gradient(135deg, #f6f8fa 0%, #eef2f5 100%); border-bottom: 2px solid #d0d7de;"> <tr> <th style="padding: 18px 20px; text-align: left; font-size: 13px; font-weight: 600; color: #1a1f2c; text-transform: uppercase; letter-spacing: 0.05em; border-right: 1px solid #e9ecef;">Date</th> <th style="padding: 18px 20px; text-align: left; font-size: 13px; font-weight: 600; color: #1a1f2c; text-transform: uppercase; letter-spacing: 0.05em; border-right: 1px solid #e9ecef;">Thread ID</th> <th style="padding: 18px 20px; text-align: left; font-size: 13px; font-weight: 600; color: #1a1f2c; text-transform: uppercase; letter-spacing: 0.05em; border-right: 1px solid #e9ecef;">Language</th> <th style="padding: 18px 20px; text-align: left; font-size: 13px; font-weight: 600; color: #1a1f2c; text-transform: uppercase; letter-spacing: 0.05em; border-right: 1px solid #e9ecef;">Sentiment</th> <th style="padding: 18px 20px; text-align: left; font-size: 13px; font-weight: 600; color: #1a1f2c; text-transform: uppercase; letter-spacing: 0.05em; border-right: 1px solid #e9ecef;">Status</th> <th style="padding: 18px 20px; text-align: left; font-size: 13px; font-weight: 600; color: #1a1f2c; text-transform: uppercase; letter-spacing: 0.05em; border-right: 1px solid #e9ecef;">Quality</th> <th style="padding: 18px 20px; text-align: left; font-size: 13px; font-weight: 600; color: #1a1f2c; text-transform: uppercase; letter-spacing: 0.05em; border-right: 1px solid #e9ecef;">Attention</th> <th style="padding: 18px 20px; text-align: left; font-size: 13px; font-weight: 600; color: #1a1f2c; text-transform: uppercase; letter-spacing: 0.05em;">Actions</th> </tr> </thead> <tbody id="conversations-table-body"> <tr> <td colspan="8" style="padding: 60px 20px; text-align: center; color: #6b7684; font-size: 14px;"> <div style="display: flex; flex-direction: column; align-items: center; gap: 12px;"> <div style="font-size: 48px;"><div class="bsp-icon-circle">C</div></div> <div style="font-weight: 500;">No conversation analytics found</div> <div style="font-size: 13px; opacity: 0.8;">Click "Refresh" to load analyzed conversations or try adjusting your search filters</div> </div> </td> </tr> </tbody> </table> </div> </div> </div> <!-- Loading Overlay --> <div id="conversations-loading" style="display: none; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255,255,255,0.95); z-index: 1000; align-items: center; justify-content: center; border-radius: 12px;"> <div style="text-align: center;"> <div style="width: 40px; height: 40px; border: 3px solid #f3f3f3; border-top: 3px solid #022d54; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 16px;"></div> <div style="color: #6b7684; font-size: 14px; font-weight: 500;">Loading conversations...</div> </div> </div> <!-- Conversation Detail Modal --> <div id="conversation-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10000; align-items: center; justify-content: center;"> <div class="modal-content" style="background: white; width: 90%; max-width: 800px; height: 80%; border-radius: 12px; display: flex; flex-direction: column; overflow: hidden;"> <div class="modal-header" style="padding: 20px; border-bottom: 1px solid #dee2e6; display: flex; justify-content: space-between; align-items: center;"> <h4 style="margin: 0;">Conversation Details</h4> <button id="close-conversation-modal" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #666;">&times;</button> </div> <div class="modal-body" style="flex: 1; display: flex; overflow: hidden;"> <!-- Chat Messages --> <div class="chat-section" style="flex: 2; padding: 20px; overflow-y: auto; border-right: 1px solid #dee2e6;"> <div id="conversation-messages"></div> </div> <!-- Analytics Panel --> <div class="analytics-section" style="flex: 1; padding: 20px; background: #f8f9fa; overflow-y: auto;"> <h5 style="margin-top: 0;">Analytics</h5> <div id="conversation-analytics"></div> </div> </div> </div> </div> </div> <style> @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .conversation-message { margin-bottom: 15px; padding: 10px; border-radius: 8px; } .message-user { background: #e3f2fd; margin-left: 20px; } .message-assistant { background: #f5f5f5; margin-right: 20px; } .message-timestamp { font-size: 12px; color: #666; margin-bottom: 5px; } .timeline-bar { position: absolute; bottom: 20px; height: 30px; background: #007cba; border-radius: 3px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: white; font-size: 12px; } .sentiment-positive { color: #28a745; font-weight: 600; } .sentiment-neutral { color: #6c757d; font-weight: 600; } .sentiment-negative { color: #dc3545; font-weight: 600; } .status-resolved { color: #28a745; font-weight: 600; } .status-partial { color: #ffc107; font-weight: 600; } .status-unresolved { color: #dc3545; font-weight: 600; } .attention-needed { color: #dc3545; font-weight: 600; } .attention-ok { color: #28a745; font-weight: 600; } </style> `; } /** * Initialize the conversations tab */ async init(container) { container.innerHTML = this.generateHTML(); window.conversationsTab = this; // Set global reference for button clicks this.attachEventListeners(); this.loadConversations(); } /** * Attach event listeners */ attachEventListeners() { // Filter controls const daysFilter = document.getElementById('conversations-days-filter'); const customDateRange = document.getElementById('custom-date-range'); const refreshBtn = document.getElementById('refresh-conversations'); const closeModal = document.getElementById('close-conversation-modal'); // Search controls const searchQuery = document.getElementById('search-query'); const searchBtn = document.getElementById('search-btn'); const clearSearchBtn = document.getElementById('clear-search'); // Filter controls const sentimentFilter = document.getElementById('sentiment-filter'); const languageFilter = document.getElementById('language-filter'); const resolutionFilter = document.getElementById('resolution-filter'); const attentionFilter = document.getElementById('attention-filter'); daysFilter?.addEventListener('change', e => { if (e.target.value === 'custom') { customDateRange.style.display = 'flex'; } else { customDateRange.style.display = 'none'; this.currentFilter.days = parseInt(e.target.value); this.currentFilter.start_date = null; this.currentFilter.end_date = null; } }); refreshBtn?.addEventListener('click', () => { this.loadConversations(); }); // Search functionality searchBtn?.addEventListener('click', () => { this.performSearch(); }); clearSearchBtn?.addEventListener('click', () => { this.clearSearch(); }); searchQuery?.addEventListener('keypress', e => { if (e.key === 'Enter') { this.performSearch(); } }); // Filter change listeners [ sentimentFilter, languageFilter, resolutionFilter, attentionFilter, ].forEach(filter => { filter?.addEventListener('change', () => { this.updateFiltersAndSearch(); }); }); closeModal?.addEventListener('click', () => { this.closeConversationModal(); }); // Close modal on background click document .getElementById('conversation-modal') ?.addEventListener('click', e => { if (e.target.id === 'conversation-modal') { this.closeConversationModal(); } }); } /** * Load conversations from the API using the correct list_conversations endpoint */ async loadConversations() { this.showLoading(true); try { // Build payload according to APIM swagger specification const payload = { bot_id: this.credentials.botId, management_id: this.credentials.managementId, management_secret: this.credentials.managementSecret, action: 'list', }; // Add date range according to APIM spec const daysFilter = document.getElementById('conversations-days-filter'); if (daysFilter?.value === 'custom') { const startDate = document.getElementById('start-date')?.value; const endDate = document.getElementById('end-date')?.value; if (startDate && endDate) { payload.start_date = new Date(startDate).toISOString(); payload.end_date = new Date(endDate + 'T23:59:59').toISOString(); } } else { // Use days filter as per APIM spec payload.days = parseInt(daysFilter?.value || '30'); } // Add timezone offset if available const timezoneOffset = new Date().getTimezoneOffset(); payload.timezone_offset = timezoneOffset; console.log('🔍 Loading conversations with payload:', payload); const response = await managementApiCall( 'list_conversations', 'POST', payload ); console.log('📊 Conversations API response:', response); if (response.success) { this.currentConversations = response.conversations || []; this.searchMetadata = response.date_range || {}; console.log( '📊 Loaded conversations:', this.currentConversations.length ); this.updateStatistics(); this.updateTimeline(); this.updateTable(); } else { console.error('Failed to load conversations:', response.error); this.showError( 'Failed to load conversations: ' + (response.error || 'Unknown error') ); } } catch (error) { console.error('Error loading conversations:', error); this.showError('Error loading conversations: ' + error.message); } finally { this.showLoading(false); } } /** * Collect current filter values from UI */ collectCurrentFilters() { const filters = {}; const sentimentFilter = document.getElementById('sentiment-filter')?.value; if (sentimentFilter) { filters.sentiment = sentimentFilter; } const languageFilter = document.getElementById('language-filter')?.value; if (languageFilter) { filters.detected_language = languageFilter; } const resolutionFilter = document.getElementById('resolution-filter')?.value; if (resolutionFilter) { filters.resolution_status = resolutionFilter; } const attentionFilter = document.getElementById('attention-filter')?.checked; if (attentionFilter) { filters.requires_attention = true; } return filters; } /** * Perform search with current query and filters */ async performSearch() { const searchInput = document.getElementById('search-query'); this.searchQuery = searchInput?.value.trim() || ''; this.loadConversations(); } /** * Clear search and reset filters */ clearSearch() { const searchInput = document.getElementById('search-query'); if (searchInput) { searchInput.value = ''; } this.searchQuery = ''; // Reset filters const sentimentFilter = document.getElementById('sentiment-filter'); const languageFilter = document.getElementById('language-filter'); const resolutionFilter = document.getElementById('resolution-filter'); const attentionFilter = document.getElementById('attention-filter'); if (sentimentFilter) sentimentFilter.value = ''; if (languageFilter) languageFilter.value = ''; if (resolutionFilter) resolutionFilter.value = ''; if (attentionFilter) attentionFilter.checked = false; this.loadConversations(); } /** * Update filters and perform search */ updateFiltersAndSearch() { this.loadConversations(); } updateStatistics() { const conversations = this.currentConversations; // Calculate statistics from conversation analytics const totalConversations = conversations.length; let resolvedCount = 0; let attentionNeededCount = 0; let qualitySum = 0; let qualityCount = 0; conversations.forEach(conv => { const analytics = conv.analytics || {}; if (analytics.resolution_status === 'resolved') { resolvedCount++; } if ( analytics.requires_attention === true || analytics.sentiment === 'negative' ) { attentionNeededCount++; } if ( analytics.conversation_quality && typeof analytics.conversation_quality === 'number' ) { qualitySum += analytics.conversation_quality; qualityCount++; } }); const avgQuality = qualityCount > 0 ? Math.round((qualitySum / qualityCount) * 10) / 10 : 0; // Update display elements - only if they exist in the DOM const totalElement = document.getElementById('total-conversations'); if (totalElement) { totalElement.textContent = totalConversations; } const resolvedElement = document.getElementById('resolved-conversations'); if (resolvedElement) { resolvedElement.textContent = resolvedCount; } const avgElement = document.getElementById('avg-messages'); if (avgElement) { avgElement.textContent = avgQuality || 'N/A'; } const attentionElement = document.getElementById('attention-needed'); if (attentionElement) { attentionElement.textContent = attentionNeededCount; } // Update labels to reflect analytics data const avgLabel = document.querySelector('#avg-messages'); if ( avgLabel && avgLabel.nextElementSibling && avgLabel.nextElementSibling.classList.contains('stat-label') ) { avgLabel.nextElementSibling.textContent = 'Avg Quality Rating'; } } /** * Update timeline visualization with enhanced analytics */ updateTimeline() { const timelineContainer = document.getElementById('conversation-timeline'); if (!timelineContainer) return; if (this.currentConversations.length === 0) { timelineContainer.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #666; flex-direction: column; gap: 12px;"><div style="font-size: 36px;"><div class="bsp-icon-circle">C</div></div><div>No analytics in selected timeframe</div></div>'; return; } // Generate activity data from conversation analytics const activityData = {}; // Group conversations by date this.currentConversations.forEach(conv => { const date = new Date(conv.created_at).toDateString(); if (!activityData[date]) { activityData[date] = 0; } activityData[date]++; }); const timeKeys = Object.keys(activityData).sort(); const maxActivity = Math.max(...Object.values(activityData)); // Create enhanced timeline with analytics timelineContainer.innerHTML = this.generateInteractiveTimeline( activityData, timeKeys, maxActivity ); // Add analytics summary if we have data if (this.currentConversations.length > 0) { this.addAnalyticsSummary(timelineContainer); } } /** * Update conversations table */ updateTable() { const tableBody = document.getElementById('conversations-table-body'); if (!tableBody) return; if (this.currentConversations.length === 0) { tableBody.innerHTML = ` <tr> <td colspan="8" style="padding: 50px 20px; text-align: center; color: #6b7684;"> <div style="display: flex; flex-direction: column; align-items: center; gap: 16px;"> <div style="font-size: 48px;">🔍</div> <div style="font-weight: 500; font-size: 16px;">No analytics found</div> <div style="font-size: 14px; opacity: 0.8;">Try adjusting your search query or filters</div> </div> </td> </tr> `; return; } const rows = this.currentConversations .map(conv => { const date = new Date(conv.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }); const threadId = conv.thread_id || 'N/A'; const shortThreadId = threadId.length > 12 ? `${threadId.substring(0, 12)}...` : threadId; const language = conv.analytics?.detected_language || 'Unknown'; const sentiment = conv.analytics?.sentiment || 'neutral'; const status = conv.analytics?.resolution_status || 'unknown'; const quality = conv.analytics?.conversation_quality || 'N/A'; const needsAttention = conv.analytics?.requires_attention || false; const similarityScore = conv.similarity_score || 0; // Enhanced styling for better data presentation const sentimentIcon = sentiment === 'positive' ? '😊' : sentiment === 'negative' ? '😞' : '😐'; const statusIcon = status === 'resolved' ? '[SUCCESS]' : status === 'partial' ? '⏳' : '[ERROR]'; const attentionIcon = needsAttention ? '🚨' : '[SUCCESS]'; const attentionText = needsAttention ? 'Yes' : 'No'; const attentionClass = needsAttention ? 'attention-needed' : 'attention-ok'; return ` <tr style="border-bottom: 1px solid #e9ecef; transition: background-color 0.2s ease;" onmouseover="this.style.backgroundColor='#f8f9fa'" onmouseout="this.style.backgroundColor='white'"> <td style="padding: 16px 20px; font-size: 14px; color: #495057; border-right: 1px solid #f1f3f4;">${date}</td> <td style="padding: 16px 20px; font-size: 13px; color: #495057; font-family: monospace; border-right: 1px solid #f1f3f4;" title="${threadId}"> <div style="display: flex; align-items: center; gap: 6px;"> <span style="background: #e9ecef; padding: 2px 6px; border-radius: 4px; font-size: 11px;">${shortThreadId}</span> ${similarityScore > 0 && similarityScore < 1 ? `<span style="background: #d1ecf1; color: #0c5460; padding: 2px 6px; border-radius: 4px; font-size: 10px;">${Math.round(similarityScore * 100)}% match</span>` : ''} </div> </td> <td style="padding: 16px 20px; border-right: 1px solid #f1f3f4;"> <span style="background: #f8f9fa; padding: 4px 8px; border-radius: 6px; font-size: 12px; font-weight: 500; color: #495057;">${language.toUpperCase()}</span> </td> <td style="padding: 16px 20px; border-right: 1px solid #f1f3f4;"> <span class="sentiment-${sentiment}" style="display: flex; align-items: center; gap: 6px; font-weight: 500;"> ${sentimentIcon} ${sentiment.charAt(0).toUpperCase() + sentiment.slice(1)} </span> </td> <td style="padding: 16px 20px; border-right: 1px solid #f1f3f4;"> <span class="status-${status}" style="display: flex; align-items: center; gap: 6px; font-weight: 500;"> ${statusIcon} ${status.charAt(0).toUpperCase() + status.slice(1)} </span> </td> <td style="padding: 16px 20px; border-right: 1px solid #f1f3f4;"> <div style="display: flex; align-items: center; gap: 6px;"> <span style="font-weight: 600; color: ${quality >= 4 ? '#28a745' : quality >= 3 ? '#ffc107' : '#dc3545'};">${quality !== 'N/A' ? quality + '/5' : 'N/A'}</span> ${quality !== 'N/A' && quality >= 4 ? '⭐' : ''} </div> </td> <td style="padding: 16px 20px; border-right: 1px solid #f1f3f4;"> <span class="${attentionClass}" style="display: flex; align-items: center; gap: 6px; font-weight: 500; color: ${needsAttention ? '#dc3545' : '#28a745'};"> ${attentionIcon} ${attentionText} </span> </td> <td style="padding: 16px 20px;"> <button onclick="window.conversationsTab?.viewConversation('${conv.thread_id}')" style="padding: 8px 16px; background: linear-gradient(135deg, #007cba 0%, #005a8a 100%); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 500; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(0, 124, 186, 0.2);" onmouseover="this.style.transform='translateY(-1px)'; this.style.boxShadow='0 4px 8px rgba(0, 124, 186, 0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 4px rgba(0, 124, 186, 0.2)'"> 👁️ View Details </button> </td> </tr> `; }) .join(''); tableBody.innerHTML = rows; } /** * View detailed conversation */ async viewConversation(threadId) { try { this.showLoading(true); const response = await managementApiCall( 'list_conversations', // Use dedicated conversations endpoint 'POST', { action: 'conversation_details', // Specify conversation details action thread_id: threadId, bot_id: this.credentials.botId, management_id: this.credentials.managementId, management_secret: this.credentials.managementSecret, } ); if (response.success && response.conversations?.length > 0) { this.selectedConversation = response.conversations[0]; this.showConversationModal(); } else { this.showError('Failed to load conversation details'); } } catch (error) { console.error('Error loading conversation details:', error); this.showError('Error loading conversation details: ' + error.message); } finally { this.showLoading(false); } } /** * Show conversation modal */ showConversationModal() { const modal = document.getElementById('conversation-modal'); const messagesContainer = document.getElementById('conversation-messages'); const analyticsContainer = document.getElementById( 'conversation-analytics' ); if (!modal || !this.selectedConversation) return; // Render messages const messages = this.selectedConversation.messages || []; const messagesHTML = messages .map( msg => ` <div class="conversation-message message-${msg.role}"> <div class="message-timestamp">${new Date(msg.timestamp).toLocaleString()}</div> <div class="message-content">${this.formatMessageContent(msg.content)}</div> </div> ` ) .join(''); messagesContainer.innerHTML = messagesHTML; // Render analytics const analytics = this.selectedConversation.analytics || {}; const analyticsHTML = ` <div style="margin-bottom: 15px;"> <strong>Language:</strong> ${analytics.detected_language || 'Unknown'} </div> <div style="margin-bottom: 15px;"> <strong>Sentiment:</strong> <span class="sentiment-${analytics.sentiment || 'neutral'}">${analytics.sentiment || 'neutral'}</span> </div> <div style="margin-bottom: 15px;"> <strong>Resolution:</strong> <span class="status-${analytics.resolution_status || 'unknown'}">${analytics.resolution_status || 'unknown'}</span> </div> <div style="margin-bottom: 15px;"> <strong>Quality:</strong> ${analytics.conversation_quality || 'N/A'}/5 </div> <div style="margin-bottom: 15px;"> <strong>Needs Attention:</strong> ${analytics.requires_attention ? 'Yes' : 'No'} </div> <div style="margin-bottom: 15px;"> <strong>Main Topics:</strong> <ul style="margin: 5px 0; padding-left: 20px;"> ${(analytics.main_subjects || []).map(topic => `<li>${topic}</li>`).join('')} </ul> </div> <div style="margin-bottom: 15px;"> <strong>Outcome:</strong> ${analytics.conversation_outcome || 'No outcome recorded'} </div> `; analyticsContainer.innerHTML = analyticsHTML; modal.style.display = 'flex'; } /** * Close conversation modal */ closeConversationModal() { const modal = document.getElementById('conversation-modal'); if (modal) { modal.style.display = 'none'; } this.selectedConversation = null; } /** * Format message content (basic markdown support) */ formatMessageContent(content) { if (!content) return ''; // Basic markdown formatting return content .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') .replace(/\*(.*?)\*/g, '<em>$1</em>') .replace( /`(.*?)`/g, '<code style="background: #f1f1f1; padding: 2px 4px; border-radius: 3px;">$1</code>' ) .replace(/\n/g, '<br>'); } /** * Show/hide loading overlay */ showLoading(show) { const loadingOverlay = document.getElementById('conversations-loading'); if (loadingOverlay) { loadingOverlay.style.display = show ? 'flex' : 'none'; } } /** * Show error message */ showError(message) { // You can customize this to match your error handling pattern alert('Error: ' + message); } /** * Generate interactive timeline with multiple metrics */ generateInteractiveTimeline(activityData, timeKeys, maxActivity) { const timelineWidth = 800; // Fixed width for consistent visualization const barWidth = Math.max(15, timelineWidth / timeKeys.length - 2); let timelineHTML = ` <div style="position: relative; height: 180px; padding: 20px; overflow-x: auto;"> <div style="font-size: 12px; color: #666; margin-bottom: 10px;">Conversation Activity Over Time</div> <div style="position: relative; height: 140px; width: ${timelineWidth}px;"> `; timeKeys.forEach((timeKey, index) => { const count = activityData[timeKey]; const height = Math.max(8, (count / maxActivity) * 120); const left = index * (barWidth + 2); // Color code based on activity level let barColor = '#007cba'; if (count > maxActivity * 0.7) barColor = '#28a745'; // High activity - green else if (count < maxActivity * 0.3) barColor = '#ffc107'; // Low activity - yellow const displayTime = timeKey.length > 10 ? timeKey.substring(5, 10) : timeKey.substring(0, 6); timelineHTML += ` <div class="timeline-bar" style="position: absolute; left: ${left}px; width: ${barWidth}px; height: ${height}px; bottom: 20px; background: ${barColor}; border-radius: 2px; cursor: pointer; display: flex; align-items: end; justify-content: center; color: white; font-size: 10px; font-weight: bold; transition: all 0.2s;" title="${timeKey}: ${count} conversations" onmouseover="this.style.transform='scale(1.1)'; this.style.zIndex='10';" onmouseout="this.style.transform='scale(1)'; this.style.zIndex='1';"> ${count} </div> <div style="position: absolute; left: ${left}px; width: ${barWidth}px; bottom: 0; font-size: 8px; text-align: center; color: #666; transform: rotate(-45deg); transform-origin: center; white-space: nowrap;"> ${displayTime} </div> `; }); timelineHTML += ` </div> </div> `; return timelineHTML; } /** * Add sentiment overlay to timeline */ addSentimentOverlay(container, sentimentDistribution) { const total = sentimentDistribution.positive + sentimentDistribution.neutral + sentimentDistribution.negative; if (total === 0) return; const overlayHTML = ` <div style="position: absolute; top: 5px; right: 5px; background: rgba(255,255,255,0.9); padding: 8px; border-radius: 4px; font-size: 11px; border: 1px solid #ddd;"> <div style="font-weight: bold; margin-bottom: 4px;">Sentiment</div> <div style="color: #28a745;">😊 ${sentimentDistribution.positive} (${Math.round((sentimentDistribution.positive / total) * 100)}%)</div> <div style="color: #6c757d;">😐 ${sentimentDistribution.neutral} (${Math.round((sentimentDistribution.neutral / total) * 100)}%)</div> <div style="color: #dc3545;">😞 ${sentimentDistribution.negative} (${Math.round((sentimentDistribution.negative / total) * 100)}%)</div> </div> `; container.insertAdjacentHTML('beforeend', overlayHTML); } /** * Update response time metric display */ updateResponseTimeMetric(avgResponseTime) { const responseTimeText = avgResponseTime < 1000 ? `${Math.round(avgResponseTime)}ms` : `${(avgResponseTime / 1000).toFixed(1)}s`; // Add response time card if not exists let responseTimeCard = document.getElementById('response-time-card'); if (!responseTimeCard) { const statsOverview = document.querySelector('.stats-overview'); if (statsOverview) { responseTimeCard = document.createElement('div'); responseTimeCard.id = 'response-time-card'; responseTimeCard.className = 'stat-card'; responseTimeCard.style.cssText = 'background: #f8f9fa; padding: 15px; border-radius: 8px; border-left: 4px solid #17a2b8;'; responseTimeCard.innerHTML = ` <div class="stat-value" id="avg-response-time" style="font-size: 24px; font-weight: bold; color: #333;">0ms</div> <div class="stat-label" style="color: #666;">Avg Response Time</div> `; statsOverview.appendChild(responseTimeCard); } } const responseTimeElement = document.getElementById('avg-response-time'); if (responseTimeElement) { responseTimeElement.textContent = responseTimeText; } } /** * Update language distribution display */ updateLanguageDistribution(languages) { // Add language distribution widget let languageWidget = document.getElementById('language-widget'); if (!languageWidget) { const timelineSection = document.querySelector('.timeline-section'); if (timelineSection) { languageWidget = document.createElement('div'); languageWidget.id = 'language-widget'; languageWidget.style.cssText = 'position: absolute; top: 40px; left: 20px; background: rgba(255,255,255,0.95); padding: 10px; border-radius: 6px; border: 1px solid #ddd; font-size: 11px;'; timelineSection.style.position = 'relative'; timelineSection.appendChild(languageWidget); } } if (languageWidget) { const total = Object.values(languages).reduce( (sum, count) => sum + count, 0 ); const languageList = Object.entries(languages) .sort(([, a], [, b]) => b - a) .slice(0, 5) // Top 5 languages .map( ([lang, count]) => `<div>${lang.toUpperCase()}: ${count} (${Math.round((count / total) * 100)}%)</div>` ) .join(''); languageWidget.innerHTML = ` <div style="font-weight: bold; margin-bottom: 4px;">Languages</div> ${languageList} `; } } /** * Update quality metrics display */ updateQualityMetrics(qualityMetrics) { // Add quality metrics to the existing attention needed card const attentionCard = document .querySelector('#attention-needed') .closest('.stat-card'); if (attentionCard && qualityMetrics.avg_coherence) { const coherencePercent = Math.round(qualityMetrics.avg_coherence * 100); const engagementPercent = Math.round(qualityMetrics.avg_engagement * 100); const contextPercent = Math.round( qualityMetrics.context_usage_rate * 100 ); // Add quality indicator let qualityIndicator = attentionCard.querySelector('.quality-indicator'); if (!qualityIndicator) { qualityIndicator = document.createElement('div'); qualityIndicator.className = 'quality-indicator'; qualityIndicator.style.cssText = 'font-size: 10px; color: #666; margin-top: 5px;'; attentionCard.appendChild(qualityIndicator); } qualityIndicator.innerHTML = ` Quality: ${coherencePercent}% | Engagement: ${engagementPercent}% | Context: ${contextPercent}% `; } } /** * Add analytics summary overlay to timeline */ addAnalyticsSummary(container) { const conversations = this.currentConversations; const sentimentCounts = { positive: 0, neutral: 0, negative: 0 }; const languageCounts = {}; const statusCounts = { resolved: 0, partial: 0, unresolved: 0 }; conversations.forEach(conv => { const analytics = conv.analytics || {}; // Count sentiments const sentiment = analytics.sentiment || 'neutral'; if (sentimentCounts[sentiment] !== undefined) { sentimentCounts[sentiment]++; } // Count languages const language = analytics.detected_language || 'unknown'; languageCounts[language] = (languageCounts[language] || 0) + 1; // Count statuses const status = analytics.resolution_status || 'unresolved'; if (statusCounts[status] !== undefined) { statusCounts[status]++; } }); const total = conversations.length; const topLanguages = Object.entries(languageCounts) .sort(([, a], [, b]) => b - a) .slice(0, 3) .map(([lang, count]) => `${lang.toUpperCase()}: ${count}`) .join(', '); const overlayHTML = ` <div style="position: absolute; top: 10px; right: 10px; background: rgba(255,255,255,0.95); padding: 12px; border-radius: 8px; font-size: 11px; border: 1px solid #ddd; box-shadow: 0 2px 8px rgba(0,0,0,0.1); min-width: 200px;"> <div style="font-weight: bold; margin-bottom: 8px; color: #022d54;">📊 Analytics Summary</div> <div style="margin-bottom: 6px;"><strong>Sentiment:</strong></div> <div style="display: flex; gap: 8px; margin-bottom: 8px; font-size: 10px;"> <span style="color: #28a745;">😊 ${sentimentCounts.positive} (${Math.round((sentimentCounts.positive / total) * 100)}%)</span> <span style="color: #6c757d;">😐 ${sentimentCounts.neutral} (${Math.round((sentimentCounts.neutral / total) * 100)}%)</span> <span style="color: #dc3545;">😞 ${sentimentCounts.negative} (${Math.round((sentimentCounts.negative / total) * 100)}%)</span> </div> <div style="margin-bottom: 6px;"><strong>Resolution:</strong></div> <div style="display: flex; gap: 8px; margin-bottom: 8px; font-size: 10px;"> <span style="color: #28a745;">[SUCCESS] ${statusCounts.resolved}</span> <span style="color: #ffc107;">⏳ ${statusCounts.partial}</span> <span style="color: #dc3545;">[ERROR] ${statusCounts.unresolved}</span> </div> <div style="margin-bottom: 4px;"><strong>Languages:</strong></div> <div style="font-size: 10px; color: #666;">${topLanguages || 'No data'}</div> </div> `; container.insertAdjacentHTML('beforeend', overlayHTML); } /** * Cleanup when tab is destroyed */ destroy() { // Remove any global references if (window.conversationsTab === this) { delete window.conversationsTab; } } } // Make it globally accessible for button clicks window.conversationsTab = null;