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
JavaScript
/**
* 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;">×</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;