besper-frontend-site-dev-main
Version:
Professional B-esper Frontend Site - Site-wide integration toolkit for full website bot deployment
970 lines (887 loc) • 180 kB
JavaScript
/**
* Knowledge Tab Component
* Handles knowledge base management functionality
*/
export class KnowledgeTab {
constructor(
widget,
managementApiCall,
credentials,
managementEndpoint,
environment,
i18n = null
) {
this.widget = widget;
this.managementApiCall = managementApiCall;
this.credentials = credentials;
this.managementEndpoint = managementEndpoint;
this.environment = environment;
this.i18n = i18n;
this.knowledgeItems = [];
this.websites = [];
this.configuredWebsites = [];
this.selectedWebsite = null; // For showing pages of a specific website
// Pagination and search state
this.knowledgePageSize = 10;
this.websitePageSize = 10;
this.knowledgeCurrentPage = 1;
this.websiteCurrentPage = 1;
this.knowledgeSearchTerm = '';
this.websiteSearchTerm = '';
// Debug mode detection for console logging
this.isDebugMode =
typeof window !== 'undefined' &&
(window.location?.hostname?.includes('localhost') ||
window.location?.search?.includes('debug=true'));
}
/**
* Update language
* @param {string} language - New language code
*/
updateLanguage(language) {
// Re-render knowledge tab content if needed
// This would be called from the main BotManagement when language changes
if (this.i18n) {
console.log(`[KnowledgeTab] Language updated to: ${language}`);
// For now, just log - full re-rendering would require more complex state management
}
}
/**
* 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(`knowledge.${key}`, variables) : key;
}
/**
* Safe execution wrapper to handle errors gracefully
* @param {Function} fn - Function to execute
* @param {string} operation - Operation name for error reporting
* @returns {Promise} Result or null if error
*/
async safeExecute(fn, operation = 'operation') {
try {
return await fn();
} catch (error) {
if (this.isDebugMode) {
console.error(`Error in ${operation}:`, error);
} else {
console.error(`Error in ${operation}: ${error.message}`);
}
return null;
}
}
/**
* Renders the knowledge tab content
* @param {Object} state - Current management state
* @returns {string} Knowledge tab HTML
*/
render(state = {}) {
this.knowledgeItems = state.knowledgeItems || [];
this.websites = state.websites || [];
this.configuredWebsites = state.configuredWebsites || [];
// Get saved tab state from localStorage
const savedTabState =
localStorage.getItem('bm-knowledge-tab-state') || 'direct';
// Navigation state
this.currentView = 'list'; // 'list' or 'analytics'
this.selectedWebsite = null;
// Calculate storage usage (convert to MB) from actual knowledge data
const totalStorageMB = this.calculateTotalStorageMB();
const maxStorageGB = 1; // 1GB limit
const storagePercentage = Math.min(
(totalStorageMB / (maxStorageGB * 1024)) * 100,
100
);
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 with Storage Indicator -->
<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: 24px; font-weight: 500; color: #022d54; letter-spacing: -0.02em; margin: 0;">Knowledge Base</h1>
<div class="kb-storage-indicator" style="display: flex; align-items: center; gap: 12px; font-size: 13px; color: #6b7684;">
<span class="kb-storage-text" style="font-variant-numeric: tabular-nums;">${totalStorageMB.toFixed(1)} MB / ${maxStorageGB} GB</span>
<div class="kb-storage-bar" style="width: 120px; height: 4px; background: #e1e7ef; border-radius: 2px; overflow: hidden; position: relative;">
<div class="kb-storage-fill" style="height: 100%; background: #022d54; width: ${storagePercentage}%; transition: width 0.6s ease;"></div>
</div>
</div>
</div>
<!-- Knowledge Type Tabs -->
<div class="kb-tabs" style="display: flex; gap: 0; border-bottom: 1px solid #e1e7ef; margin-bottom: 24px;">
<button class="kb-tab ${savedTabState === 'web' ? 'active' : ''}" id="kb-tab-web" style="padding: 12px 24px; font-size: 14px; font-weight: 500; color: ${savedTabState === 'web' ? '#022d54' : '#6b7684'}; background: none; border: none; border-bottom: 2px solid ${savedTabState === 'web' ? '#022d54' : 'transparent'}; cursor: pointer; transition: all 0.3s ease; position: relative;">Web Knowledge</button>
<button class="kb-tab ${savedTabState === 'direct' ? 'active' : ''}" id="kb-tab-direct" style="padding: 12px 24px; font-size: 14px; font-weight: 500; color: ${savedTabState === 'direct' ? '#022d54' : '#6b7684'}; background: none; border: none; border-bottom: 2px solid ${savedTabState === 'direct' ? '#022d54' : 'transparent'}; cursor: pointer; transition: all 0.3s ease; position: relative;">Direct Knowledge</button>
</div>
<!-- Breadcrumb -->
<div class="kb-breadcrumb" id="kb-breadcrumb" style="font-size: 14px; color: #6b7684; display: flex; align-items: center; gap: 8px;">
${this.renderBreadcrumb()}
</div>
</div>
<!-- Content Area -->
<div id="kb-content">
${this.renderContent()}
</div>
</div>
</div>
<!-- Text Knowledge Modal -->
<div class="bm-modal" id="bm-text-knowledge-modal" style="display: none;">
<div class="bm-modal-content">
<div class="bm-modal-header">
<div class="besper-h3">Add Text Knowledge</div>
<button class="bm-modal-close">×</button>
</div>
<div class="bm-modal-body">
<div class="bm-form-group">
<label class="bm-form-label">Title</label>
<input type="text" id="bm-knowledge-question" class="bm-form-input"
placeholder="${this.t('placeholders.knowledgeTitle')}" />
</div>
<div class="bm-form-group">
<label class="bm-form-label">Content</label>
<textarea id="bm-knowledge-content" class="bm-form-input" rows="6"
placeholder="${this.t('placeholders.knowledgeContent')}"></textarea>
</div>
<div class="bm-form-group">
<label class="bm-form-label">Category (Optional)</label>
<input type="text" id="bm-knowledge-category" class="bm-form-input"
placeholder="e.g., Product Info, FAQ, Support" />
</div>
</div>
<div class="bm-modal-footer">
<button class="bm-btn bm-btn-secondary" id="bm-cancel-text-knowledge">Cancel</button>
<button class="bm-btn bm-btn-primary" id="bm-save-text-knowledge">Save Knowledge</button>
</div>
</div>
</div>
<!-- File Upload Modal -->
<div class="bm-modal" id="bm-file-upload-modal" style="display: none;">
<div class="bm-modal-content">
<div class="bm-modal-header">
<div class="besper-h3">Upload Document</div>
<button class="bm-modal-close">×</button>
</div>
<div class="bm-modal-body">
<p class="bm-modal-description">
Upload documents to your knowledge base. Supported formats: PDF, TXT, DOC, DOCX (Max size: 10MB)
</p>
<div class="bm-form-group">
<label class="bm-form-label">Select File</label>
<input type="file" id="bm-file-input" class="bm-file-input"
accept=".pdf,.txt,.doc,.docx" />
<div class="bm-file-info" id="bm-file-info" style="display: none;">
<span class="bm-file-name"></span>
<span class="bm-file-size"></span>
</div>
</div>
<div class="bm-form-group">
<label class="bm-form-label">Title (Optional)</label>
<input type="text" id="bm-file-title" class="bm-form-input"
placeholder="${this.t('placeholders.documentTitle')}" />
</div>
<div class="bm-form-group">
<label class="bm-form-label">Category (Optional)</label>
<input type="text" id="bm-file-category" class="bm-form-input"
placeholder="e.g., Documentation, FAQ, Policies" />
</div>
</div>
<div class="bm-modal-footer">
<button class="bm-btn bm-btn-secondary" id="bm-cancel-file-upload">Cancel</button>
<button class="bm-btn bm-btn-primary" id="bm-save-file-upload">Upload Document</button>
</div>
</div>
</div>
<!-- Knowledge Preview Modal -->
<div class="bm-modal" id="bm-knowledge-preview-modal" style="display: none;">
<div class="bm-modal-content bm-modal-large">
<div class="bm-modal-header">
<div class="besper-h3" id="bm-preview-title">Knowledge Preview</div>
<button class="bm-modal-close">×</button>
</div>
<div class="bm-modal-body">
<div class="bm-preview-content" id="bm-preview-content">
<!-- Content will be loaded here -->
</div>
</div>
<div class="bm-modal-footer">
<button class="bm-btn bm-btn-secondary" id="bm-close-preview">Close</button>
<button class="bm-btn bm-btn-primary" id="bm-edit-from-preview">Edit</button>
</div>
</div>
</div>
<!-- Knowledge Edit Modal -->
<div class="bm-modal" id="bm-knowledge-edit-modal" style="display: none;">
<div class="bm-modal-content bm-modal-large">
<div class="bm-modal-header">
<div class="besper-h3">Edit Knowledge Item</div>
<button class="bm-modal-close">×</button>
</div>
<div class="bm-modal-body">
<div class="bm-form-group">
<label class="bm-form-label">Title</label>
<input type="text" id="bm-edit-title" class="bm-form-input" />
</div>
<div class="bm-form-group">
<label class="bm-form-label">Content</label>
<textarea id="bm-edit-content" class="bm-form-textarea" rows="10"></textarea>
<div class="bm-char-counter">
<span id="bm-edit-char-count">0</span> characters
</div>
</div>
<div class="bm-form-group">
<label class="bm-form-label">Category</label>
<input type="text" id="bm-edit-category" class="bm-form-input" />
</div>
</div>
<div class="bm-modal-footer">
<button class="bm-btn bm-btn-secondary" id="bm-cancel-edit">Cancel</button>
<button class="bm-btn bm-btn-primary" id="bm-save-edit">Save Changes</button>
</div>
</div>
</div>
<!-- Website Pages Modal -->
<div class="bm-modal" id="bm-website-pages-modal" style="display: none;">
<div class="bm-modal-content bm-modal-large">
<div class="bm-modal-header">
<div class="besper-h3" id="bm-website-pages-title">Website Pages</div>
<button class="bm-modal-close">×</button>
</div>
<div class="bm-modal-body">
<div class="bm-website-pages-list" id="bm-website-pages-list">
<!-- Pages will be loaded here -->
</div>
</div>
<div class="bm-modal-footer">
<button class="bm-btn bm-btn-secondary" id="bm-close-website-pages">Close</button>
<button class="bm-btn bm-btn-primary" id="bm-refresh-website-pages">Refresh Pages</button>
</div>
</div>
</div>
<!-- Confirmation Modal -->
<div class="bm-modal" id="bm-confirmation-modal" style="display: none;">
<div class="bm-modal-content" style="max-width: 500px;">
<div class="bm-modal-header">
<div class="besper-h3" id="bm-confirmation-title">Confirm Action</div>
<button class="bm-modal-close">×</button>
</div>
<div class="bm-modal-body">
<div class="besper-p" id="bm-confirmation-message">Are you sure you want to proceed?</div>
</div>
<div class="bm-modal-footer" style="display: flex; gap: 12px; justify-content: flex-end; padding: 16px; border-top: 1px solid #e5e7eb;">
<button class="bm-btn bm-btn-secondary" id="bm-confirmation-cancel">Cancel</button>
<button class="bm-btn bm-btn-danger" id="bm-confirmation-confirm">Confirm</button>
</div>
</div>
</div>
</div>
`;
}
/**
* Renders the breadcrumb navigation
* @returns {string} Breadcrumb HTML
*/
renderBreadcrumb() {
if (this.currentView === 'analytics' && this.selectedWebsite) {
const domain = this.getDomainFromUrl(
this.selectedWebsite.url || this.selectedWebsite
);
return `
<a href="#" onclick="window.knowledgeTab?.showWebsites(); return false;" style="color: #022d54; text-decoration: none; transition: color 0.3s ease;">
Websites
</a>
<span style="color: #c5cdd8;">/</span>
<span style="color: #6b7684;">${this.escapeHtml(domain)}</span>
`;
}
const savedTabState =
localStorage.getItem('bm-knowledge-tab-state') || 'direct';
if (savedTabState === 'web') {
return `<span style="color: #6b7684;">Websites</span>`;
} else {
return `<span style="color: #6b7684;">Files & Documents</span>`;
}
}
/**
* Renders the main content area based on current view and tab
* @returns {string} Content HTML
*/
renderContent() {
const savedTabState =
localStorage.getItem('bm-knowledge-tab-state') || 'direct';
if (this.currentView === 'analytics') {
return this.renderWebsiteAnalytics();
}
if (savedTabState === 'web') {
return this.renderWebsitesView();
} else {
return this.renderFilesView();
}
}
/**
* Renders website analytics view with comprehensive metrics and charts
* @returns {string} Website analytics HTML
*/
renderWebsiteAnalytics() {
if (!this.selectedWebsite) {
return '<div class="kb-empty-state" style="background: white; border: 1px solid #e1e7ef; border-radius: 4px; padding: 60px 40px; text-align: center; color: #6b7684;"><div style="font-size: 18px; font-weight: 500; color: #1a1f2c; margin-bottom: 8px;">No Website Selected</div></div>';
}
const domain = this.getDomainFromUrl(this.selectedWebsite.url);
const pagesFromWebsite = this.getScrapedPagesForWebsite(
this.selectedWebsite.url
);
// Calculate metrics
const totalUsage = this.calculateUsageFromEvents(
this.selectedWebsite.usage_events || []
);
const totalStorageMB = (
this.selectedWebsite.word_count * 0.005 || 0
).toFixed(1);
const avgStoragePerPage =
pagesFromWebsite.length > 0
? (totalStorageMB / pagesFromWebsite.length).toFixed(1)
: '0';
const indexedPages = pagesFromWebsite.length;
// Generate chart data (30 days)
const chartData = this.generateUsageChartData(
this.selectedWebsite.usage_events || []
);
const maxValue = Math.max(...chartData, 1);
const points = chartData
.map((value, index) => {
const x = (index / (chartData.length - 1)) * 100;
const y = 100 - (value / maxValue) * 100;
return `${x},${y}`;
})
.join(' ');
return `
<button class="kb-back-button" onclick="window.knowledgeTab?.showWebsites()" style="display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; margin-bottom: 24px; background: white; border: 1px solid #e1e7ef; border-radius: 4px; color: #1a1f2c; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s ease;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
Back to Websites
</button>
<div class="kb-analytics-header" style="background: white; border: 1px solid #e1e7ef; border-radius: 4px; padding: 32px; margin-bottom: 24px;">
<div class="kb-analytics-title" style="display: flex; align-items: baseline; gap: 16px; margin-bottom: 32px;">
<h2 style="font-size: 20px; font-weight: 500; color: #1a1f2c; margin: 0;">${this.escapeHtml(domain)}</h2>
<span style="font-size: 14px; color: #6b7684;">Last 30 Days Analytics</span>
</div>
<div class="kb-metrics-grid" style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 32px;">
<div class="kb-metric" style="border-left: 1px solid #e1e7ef; padding-left: 24px;">
<div style="font-size: 32px; font-weight: 300; color: #022d54; margin-bottom: 4px; font-variant-numeric: tabular-nums;">${totalUsage}</div>
<div style="font-size: 13px; color: #6b7684; margin-bottom: 8px;">Total Usage in Conversations</div>
<div style="font-size: 13px; color: #6b7684; font-variant-numeric: tabular-nums;">${this.t('storage.conversationsBudget', { percentage: ((totalUsage / 20000) * 100).toFixed(1) })} <span title="${this.t('tooltips.conversationsHelp')}" style="cursor: help;">ⓘ</span></div>
</div>
<div class="kb-metric" style="border-left: 1px solid #e1e7ef; padding-left: 24px;">
<div style="font-size: 32px; font-weight: 300; color: #022d54; margin-bottom: 4px; font-variant-numeric: tabular-nums;">${totalStorageMB} MB</div>
<div style="font-size: 13px; color: #6b7684; margin-bottom: 8px;">Storage Used</div>
<div style="font-size: 13px; color: #6b7684; font-variant-numeric: tabular-nums;">${this.t('storage.storageLimit', { percentage: ((totalStorageMB / 1000) * 100).toFixed(1) })} <span title="${this.t('tooltips.storageHelp')}" style="cursor: help;">ⓘ</span></div>
</div>
<div class="kb-metric" style="border-left: 1px solid #e1e7ef; padding-left: 24px;">
<div style="font-size: 32px; font-weight: 300; color: #022d54; margin-bottom: 4px; font-variant-numeric: tabular-nums;">${avgStoragePerPage} MB</div>
<div style="font-size: 13px; color: #6b7684; margin-bottom: 8px;">Average Storage per Page</div>
<div style="font-size: 13px; color: #6b7684; font-variant-numeric: tabular-nums;">Average page size</div>
</div>
<div class="kb-metric" style="padding-left: 24px;">
<div style="font-size: 32px; font-weight: 300; color: #022d54; margin-bottom: 4px; font-variant-numeric: tabular-nums;">${indexedPages}</div>
<div style="font-size: 13px; color: #6b7684; margin-bottom: 8px;">Indexed Pages</div>
<div style="font-size: 13px; color: #6b7684; font-variant-numeric: tabular-nums;">Total pages indexed</div>
</div>
</div>
</div>
<div class="kb-charts-container" style="display: grid; grid-template-columns: 2fr 1fr; gap: 24px; margin-bottom: 24px;">
<div class="kb-chart-card" style="background: white; border: 1px solid #e1e7ef; border-radius: 4px; padding: 24px;">
<div class="kb-chart-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
<h3 style="font-size: 16px; font-weight: 500; color: #1a1f2c; margin: 0;">Daily Usage Trend (Last 30 Days)</h3>
<div style="display: flex; gap: 0; border: 1px solid #e1e7ef; border-radius: 4px; overflow: hidden;">
<button style="padding: 6px 12px; font-size: 12px; background: #022d54; border: none; color: white; cursor: pointer;">30D</button>
</div>
</div>
<div class="kb-line-chart" style="height: 200px; position: relative; padding: 0 0 20px 40px; overflow: hidden;">
<div class="kb-y-axis" style="position: absolute; left: 0; top: 0; bottom: 20px; width: 40px; display: flex; flex-direction: column; justify-content: space-between; font-size: 11px; color: #6b7684; text-align: right; padding-right: 8px;">
<span>${(maxValue / 1000).toFixed(1)}k</span>
<span>${((maxValue * 0.75) / 1000).toFixed(1)}k</span>
<span>${((maxValue * 0.5) / 1000).toFixed(1)}k</span>
<span>${((maxValue * 0.25) / 1000).toFixed(1)}k</span>
<span>0</span>
</div>
<div class="kb-chart-grid" style="position: absolute; top: 0; left: 40px; right: 0; bottom: 20px; display: flex; flex-direction: column;">
<div style="flex: 1; border-top: 1px solid #f0f4f8;"></div>
<div style="flex: 1; border-top: 1px solid #f0f4f8;"></div>
<div style="flex: 1; border-top: 1px solid #f0f4f8;"></div>
<div style="flex: 1; border-top: 1px solid #f0f4f8;"></div>
<div style="flex: 1; border-top: 1px solid #f0f4f8;"></div>
</div>
<svg class="kb-chart-svg" viewBox="0 0 100 100" preserveAspectRatio="none" style="position: absolute; top: 0; left: 40px; right: 0; bottom: 20px; width: calc(100% - 40px); height: calc(100% - 20px);">
<polyline points="${points}" fill="none" stroke="#022d54" stroke-width="2" vector-effect="non-scaling-stroke"/>
<polyline points="0,100 ${points} 100,100" fill="url(#gradient)" opacity="0.1"/>
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#022d54;stop-opacity:1" />
<stop offset="100%" style="stop-color:#022d54;stop-opacity:0" />
</linearGradient>
</defs>
</svg>
</div>
</div>
<div class="kb-chart-card" style="background: white; border: 1px solid #e1e7ef; border-radius: 4px; padding: 24px;">
<div class="kb-chart-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
<h3 style="font-size: 16px; font-weight: 500; color: #1a1f2c; margin: 0;">Top Pages by Usage</h3>
</div>
<div class="kb-distribution-chart" style="display: flex; flex-direction: column; gap: 16px;">
${this.renderTopPagesDistribution(pagesFromWebsite)}
</div>
</div>
</div>
${this.renderPagePerformanceTable(pagesFromWebsite, this.selectedWebsite.url)}
`;
}
/**
* Generates usage chart data for the last 30 days
* @param {Array} events - Usage events
* @returns {Array} Array of usage counts per day
*/
generateUsageChartData(events) {
const data = [];
const now = new Date();
for (let i = 29; i >= 0; i--) {
const date = new Date(now.getTime() - i * 24 * 60 * 60 * 1000);
const dateKey = date.toISOString().split('T')[0];
const dayUsage = events.filter(event => {
const eventDate = new Date(event.timestamp).toISOString().split('T')[0];
return eventDate === dateKey;
}).length;
data.push(dayUsage);
}
return data;
}
/**
* Generates sparkline data for page usage trends
* @param {Array} events - Usage events
* @returns {string} SVG points string
*/
generateSparklineData(events) {
const chartData = this.generateUsageChartData(events);
const maxValue = Math.max(...chartData, 1);
return chartData
.map((value, index) => {
const x = (index / (chartData.length - 1)) * 100;
const y = 20 - (value / maxValue) * 20;
return `${x},${y}`;
})
.join(' ');
}
/**
* Renders top pages distribution bars
* @param {Array} pages - Array of pages
* @returns {string} Distribution HTML
*/
renderTopPagesDistribution(pages) {
if (pages.length === 0) {
return '<div style="text-align: center; color: #6b7684; font-style: italic;">No pages data available</div>';
}
// Sort pages by usage and take top 4
const sortedPages = pages
.map(page => ({
title: page.title || page.filename || 'Untitled',
usage: this.calculateUsageFromEvents(page.usage_events || []),
storage: ((page.word_count || 0) * 0.005).toFixed(1),
}))
.sort((a, b) => b.usage - a.usage)
.slice(0, 4);
const maxUsage = Math.max(...sortedPages.map(p => p.usage), 1);
return sortedPages
.map(
page => `
<div style="display: flex; align-items: center; gap: 16px;">
<span style="font-size: 13px; color: #6b7684; width: 120px;">${this.escapeHtml(this.truncateText(page.title, 15))}</span>
<div style="flex: 1; height: 24px; background: #f0f4f8; border-radius: 4px; overflow: hidden; position: relative;">
<div style="height: 100%; background: #022d54; width: ${(page.usage / maxUsage) * 100}%; transition: width 0.6s ease;"></div>
</div>
<span style="font-size: 13px; color: #1a1f2c; font-weight: 500; width: 60px; text-align: right; font-variant-numeric: tabular-nums;">${page.storage} MB</span>
</div>
`
)
.join('');
}
/**
* Renders the websites list view
* @returns {string} Websites view HTML
*/
renderWebsitesView() {
// Group websites by domain from scraped content
const groupedWebsites = this.groupPagesByDomain(this.websites);
if (groupedWebsites.length === 0) {
return `
<div class="kb-websites-container" style="background: white; border: 1px solid #e1e7ef; border-radius: 4px; overflow: hidden;">
<div class="kb-table-header" style="padding: 20px 24px; border-bottom: 1px solid #e1e7ef; display: flex; justify-content: space-between; align-items: center;">
<h3 style="font-size: 16px; font-weight: 500; color: #1a1f2c; margin: 0;">Websites</h3>
<div style="display: flex; gap: 12px;">
<button class="kb-btn" id="kb-refresh-knowledge" title="${this.t('tooltips.refreshKnowledge')}" style="padding: 8px 16px; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; background: white; border: 1px solid #e1e7ef; color: #1a1f2c; transition: all 0.2s ease;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 8px;">
<polyline points="23 4 23 10 17 10"></polyline>
<polyline points="1 20 1 14 7 14"></polyline>
<path d="m3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
</svg>
Refresh
</button>
<button class="kb-btn-primary" id="kb-add-website-from-table" style="padding: 8px 16px; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; background: #022d54; border: 1px solid #022d54; color: white; transition: all 0.2s ease;">Add Website</button>
</div>
</div>
<div class="kb-empty-state" style="padding: 60px 40px; text-align: center; color: #6b7684;">
<div style="font-size: 18px; font-weight: 500; color: #1a1f2c; margin-bottom: 8px;">No Connected Websites</div>
<div style="font-size: 14px;">Add websites to automatically extract knowledge content from their pages.</div>
</div>
</div>
`;
}
return `
<div class="kb-websites-container" style="background: white; border: 1px solid #e1e7ef; border-radius: 4px; overflow: hidden;">
<div class="kb-table-header" style="padding: 20px 24px; border-bottom: 1px solid #e1e7ef; display: flex; justify-content: space-between; align-items: center;">
<h3 style="font-size: 16px; font-weight: 500; color: #1a1f2c; margin: 0;">Websites</h3>
<div style="display: flex; gap: 12px;">
<button class="kb-btn" id="kb-refresh-knowledge" title="${this.t('tooltips.refreshKnowledge')}" style="padding: 8px 16px; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; background: white; border: 1px solid #e1e7ef; color: #1a1f2c; transition: all 0.2s ease;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 8px;">
<polyline points="23 4 23 10 17 10"></polyline>
<polyline points="1 20 1 14 7 14"></polyline>
<path d="m3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
</svg>
Refresh
</button>
<button class="kb-btn-primary" id="kb-add-website-from-table" style="padding: 8px 16px; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; background: #022d54; border: 1px solid #022d54; color: white; transition: all 0.2s ease;">Add Website</button>
</div>
</div>
<table class="kb-websites-table" style="width: 100%; border-collapse: collapse;">
<thead>
<tr>
<th style="text-align: left; padding: 16px 24px; font-size: 12px; font-weight: 600; color: #6b7684; background: #fafbfc; border-bottom: 1px solid #e1e7ef; text-transform: uppercase; letter-spacing: 0.05em;">WEBSITE</th>
<th style="text-align: left; padding: 16px 24px; font-size: 12px; font-weight: 600; color: #6b7684; background: #fafbfc; border-bottom: 1px solid #e1e7ef; text-transform: uppercase; letter-spacing: 0.05em;">USAGE (30D)</th>
<th style="text-align: left; padding: 16px 24px; font-size: 12px; font-weight: 600; color: #6b7684; background: #fafbfc; border-bottom: 1px solid #e1e7ef; text-transform: uppercase; letter-spacing: 0.05em;">STORAGE</th>
<th style="text-align: left; padding: 16px 24px; font-size: 12px; font-weight: 600; color: #6b7684; background: #fafbfc; border-bottom: 1px solid #e1e7ef; text-transform: uppercase; letter-spacing: 0.05em;">AVG/PAGE</th>
<th style="text-align: left; padding: 16px 24px; font-size: 12px; font-weight: 600; color: #6b7684; background: #fafbfc; border-bottom: 1px solid #e1e7ef; text-transform: uppercase; letter-spacing: 0.05em;">LAST USED</th>
<th style="text-align: left; padding: 16px 24px; font-size: 12px; font-weight: 600; color: #6b7684; background: #fafbfc; border-bottom: 1px solid #e1e7ef; text-transform: uppercase; letter-spacing: 0.05em;">STATUS</th>
<th style="text-align: center; padding: 16px 24px; font-size: 12px; font-weight: 600; color: #6b7684; background: #fafbfc; border-bottom: 1px solid #e1e7ef; text-transform: uppercase; letter-spacing: 0.05em; width: 120px;">ACTIONS</th>
</tr>
</thead>
<tbody>
${groupedWebsites
.map(website => {
const domain = this.getDomainFromUrl(website.url);
const totalUsage = this.calculateUsageFromEvents(
website.usage_events || []
);
const avgStoragePerPage =
website.page_count > 0
? (
(website.word_count * 0.005) /
website.page_count
).toFixed(1)
: '0';
const totalStorageMB = (website.word_count * 0.005).toFixed(1);
const lastUsed = website.last_used_at
? this.formatRelativeTime(website.last_used_at)
: 'Never';
return `
<tr style="transition: background-color 0.2s ease;">
<td onclick="window.knowledgeTab?.showWebsiteAnalytics('${this.escapeHtml(website.url)}')" style="padding: 16px 24px; border-bottom: 1px solid #f0f4f8; font-size: 14px; color: #1a1f2c; cursor: pointer;">
<div style="font-weight: 500; margin-bottom: 4px;">${this.escapeHtml(domain)}</div>
<div style="color: #6b7684; font-size: 13px;">${this.escapeHtml(website.url)}</div>
</td>
<td onclick="window.knowledgeTab?.showWebsiteAnalytics('${this.escapeHtml(website.url)}')" style="padding: 16px 24px; border-bottom: 1px solid #f0f4f8; font-size: 14px; color: #1a1f2c; cursor: pointer;">${totalUsage}</td>
<td onclick="window.knowledgeTab?.showWebsiteAnalytics('${this.escapeHtml(website.url)}')" style="padding: 16px 24px; border-bottom: 1px solid #f0f4f8; font-size: 14px; color: #1a1f2c; cursor: pointer;">${totalStorageMB} MB</td>
<td onclick="window.knowledgeTab?.showWebsiteAnalytics('${this.escapeHtml(website.url)}')" style="padding: 16px 24px; border-bottom: 1px solid #f0f4f8; font-size: 14px; color: #1a1f2c; cursor: pointer;">${avgStoragePerPage} MB</td>
<td onclick="window.knowledgeTab?.showWebsiteAnalytics('${this.escapeHtml(website.url)}')" style="padding: 16px 24px; border-bottom: 1px solid #f0f4f8; font-size: 14px; color: #6b7684; cursor: pointer;">${lastUsed}</td>
<td onclick="window.knowledgeTab?.showWebsiteAnalytics('${this.escapeHtml(website.url)}')" style="padding: 16px 24px; border-bottom: 1px solid #f0f4f8; font-size: 14px; color: #1a1f2c; cursor: pointer;">
<div style="display: flex; align-items: center; gap: 8px;">
<div style="width: 8px; height: 8px; border-radius: 50%; background: #10b981;"></div>
<span style="font-size: 12px; color: #6b7684;">Active</span>
</div>
</td>
<td style="padding: 16px 24px; border-bottom: 1px solid #f0f4f8; text-align: center;">
<div style="display: flex; gap: 8px; justify-content: center;">
<button
class="kb-website-action-btn"
data-action="remove"
data-url="${this.escapeHtml(website.url)}"
style="padding: 6px 12px; border: 1px solid #dc3545; background: #fff; color: #dc3545; border-radius: 4px; font-size: 12px; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; gap: 4px;"
onmouseover="this.style.background='#dc3545'; this.style.color='#fff';"
onmouseout="this.style.background='#fff'; this.style.color='#dc3545';"
title="${this.t('tooltips.removeWebsite')}"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"></path>
</svg>
Remove
</button>
</div>
</td>
</tr>
`;
})
.join('')}
</tbody>
</table>
</div>
`;
}
/**
* Renders the files/documents view for Direct Knowledge
* @returns {string} Files view HTML
*/
renderFilesView() {
// Filter to get only direct knowledge (non-web scraped content)
const directKnowledge = this.knowledgeItems.filter(
item => !this.isWebKnowledge(item)
);
if (directKnowledge.length === 0) {
return `
<div class="kb-files-container" style="background: white; border: 1px solid #e1e7ef; border-radius: 4px; overflow: hidden;">
<div class="kb-table-header" style="padding: 20px 24px; border-bottom: 1px solid #e1e7ef; display: flex; justify-content: space-between; align-items: center;">
<h3 style="font-size: 16px; font-weight: 500; color: #1a1f2c; margin: 0;">Files & Documents</h3>
<div style="display: flex; gap: 12px;">
<button class="kb-btn" id="kb-refresh-knowledge-files" title="${this.t('tooltips.refreshKnowledge')}" style="padding: 8px 16px; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; background: white; border: 1px solid #e1e7ef; color: #1a1f2c; transition: all 0.2s ease;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 8px;">
<polyline points="23 4 23 10 17 10"></polyline>
<polyline points="1 20 1 14 7 14"></polyline>
<path d="m3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
</svg>
Refresh
</button>
<button class="kb-btn-secondary" id="kb-create-text-from-table" style="padding: 8px 16px; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; background: white; border: 1px solid #e1e7ef; color: #1a1f2c; transition: all 0.2s ease;">Create Text File</button>
<button class="kb-btn-primary" id="kb-upload-file-from-table" style="padding: 8px 16px; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; background: #022d54; border: 1px solid #022d54; color: white; transition: all 0.2s ease;">Upload File</button>
</div>
</div>
<div class="kb-empty-state" style="padding: 60px 40px; text-align: center; color: #6b7684;">
<div style="font-size: 18px; font-weight: 500; color: #1a1f2c; margin-bottom: 8px;">No Direct Knowledge</div>
<div style="font-size: 14px;">Upload documents or add text knowledge to see them here.</div>
</div>
</div>
`;
}
return `
<div class="kb-files-container" style="background: white; border: 1px solid #e1e7ef; border-radius: 4px; overflow: hidden;">
<div class="kb-table-header" style="padding: 20px 24px; border-bottom: 1px solid #e1e7ef; display: flex; justify-content: space-between; align-items: center;">
<h3 style="font-size: 16px; font-weight: 500; color: #1a1f2c; margin: 0;">Files & Documents</h3>
<div style="display: flex; gap: 12px;">
<button class="kb-btn" id="kb-refresh-knowledge-files" title="${this.t('tooltips.refreshKnowledge')}" style="padding: 8px 16px; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; background: white; border: 1px solid #e1e7ef; color: #1a1f2c; transition: all 0.2s ease;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 8px;">
<polyline points="23 4 23 10 17 10"></polyline>
<polyline points="1 20 1 14 7 14"></polyline>
<path d="m3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
</svg>
Refresh
</button>
<button class="kb-btn-secondary" id="kb-create-text-from-table" style="padding: 8px 16px; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; background: white; border: 1px solid #e1e7ef; color: #1a1f2c; transition: all 0.2s ease;">Create Text File</button>
<button class="kb-btn-primary" id="kb-upload-file-from-table" style="padding: 8px 16px; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; background: #022d54; border: 1px solid #022d54; color: white; transition: all 0.2s ease;">Upload File</button>
</div>
</div>
<table class="kb-files-table" style="width: 100%; border-collapse: collapse;">
<thead>
<tr>
<th style="text-align: left; padding: 16px 24px; font-size: 12px; font-weight: 600; color: #6b7684; background: #fafbfc; border-bottom: 1px solid #e1e7ef; text-transform: uppercase; letter-spacing: 0.05em;">NAME</th>
<th style="text-align: left; padding: 16px 24px; font-size: 12px; font-weight: 600; color: #6b7684; background: #fafbfc; border-bottom: 1px solid #e1e7ef; text-transform: uppercase; letter-spacing: 0.05em;">TYPE</th>
<th style="text-align: left; padding: 16px 24px; font-size: 12px; font-weight: 600; color: #6b7684; background: #fafbfc; border-bottom: 1px solid #e1e7ef; text-transform: uppercase; letter-spacing: 0.05em;">SIZE</th>
<th style="text-align: left; padding: 16px 24px; font-size: 12px; font-weight: 600; color: #6b7684; background: #fafbfc; border-bottom: 1px solid #e1e7ef; text-transform: uppercase; letter-spacing: 0.05em;">USAGE (30D)</th>
<th style="text-align: left; padding: 16px 24px; font-size: 12px; font-weight: 600; color: #6b7684; background: #fafbfc; border-bottom: 1px solid #e1e7ef; text-transform: uppercase; letter-spacing: 0.05em;">CREATED</th>
<th style="text-align: left; padding: 16px 24px; font-size: 12px; font-weight: 600; color: #6b7684; background: #fafbfc; border-bottom: 1px solid #e1e7ef; text-transform: uppercase; letter-spacing: 0.05em;">MODIFIED</th>
<th style="text-align: left; padding: 16px 24px; font-size: 12px; font-weight: 600; color: #6b7684; background: #fafbfc; border-bottom: 1px solid #e1e7ef; text-transform: uppercase; letter-spacing: 0.05em;">ACTIONS</th>
</tr>
</thead>
<tbody>
${directKnowledge
.map(file => {
const usageCount = this.calculateUsageFromEvents(
file.usage_events || []
);
const fileType = this.getFileType(file);
const fileSize = this.formatFileSize(
file.file_size || file.word_count * 5 || 0
);
return `
<tr style="transition: background-color 0.2s ease;">
<td style="padding: 16px 24px; border-bottom: 1px solid #f0f4f8; font-size: 14px; color: #1a1f2c;">${this.escapeHtml(file.title || file.filename || 'Untitled')}</td>
<td style="padding: 16px 24px; border-bottom: 1px solid #f0f4f8; font-size: 14px; color: #1a1f2c;">${fileType}</td>
<td style="padding: 16px 24px; border-bottom: 1px solid #f0f4f8; font-size: 14px; color: #6b7684;">${fileSize}</td>
<td style="padding: 16px 24px; border-bottom: 1px solid #f0f4f8; font-size: 14px; color: #1a1f2c;">${usageCount}</td>
<td style="padding: 16px 24px; border-bottom: 1px solid #f0f4f8; font-size: 14px; color: #6b7684;">${this.formatDate(file.created_at)}</td>
<td style="padding: 16px 24px; border-bottom: 1px solid #f0f4f8; font-size: 14px; color: #6b7684;">${this.formatDate(file.updated_at || file.created_at)}</td>
<td style="padding: 16px 24px; border-bottom: 1px solid #f0f4f8; font-size: 14px; color: #1a1f2c;">
<button class="kb-btn-action" onclick="window.knowledgeTab?.previewKnowledge('${file.id}')" style="padding: 4px 8px; border-radius: 4px; font-size: 13px; background: white; border: 1px solid #e1e7ef; color: #1a1f2c; cursor: pointer; margin-right: 8px;">View</button>
</td>
</tr>
`;
})
.join('')}
</tbody>
</table>
</div>
`;
}
/**
* Calculates total storage in MB
* @returns {number} Total storage in MB
*/
calculateTotalStorageMB() {
// Debug logging for storage calculation
if (this.isDebugMode) {
console.log('Storage calculation debug:', {
knowledgeItemsCount: this.knowledgeItems?.length || 0,
websitesCount: this.websites?.length || 0,
sampleKnowledgeItem: this.knowledgeItems?.[0] || null,
sampleWebsiteItem: this.websites?.[0] || null,
});
}
const knowledgeStorage = this.knowledgeItems.reduce((total, item) => {
const itemSize = item.file_size || item.word_count * 5 || 0;
if (this.isDebugMode && itemSize > 0) {
console.log('Knowledge item storage:', {
title: item.title || item.filename,
file_size: item.file_size,
word_count: item.word_count,
calculated: itemSize,
});
}
return total + itemSize;
}, 0);
const websiteStorage = this.websites.reduce((total, item) => {
const itemSize = item.file_size || item.word_count * 5 || 0;
if (this.isDebugMode && itemSize > 0) {
console.log('Website item storage:', {
title: item.title || item.filename,
file_size: item.file_size,
word_count: item.word_count,
calculated: itemSize,
});
}
return total + itemSize;
}, 0);
const totalBytes = knowledgeStorage + websiteStorage;
const totalMB = totalBytes / (1024 * 1024);
if (this.isDebugMode) {
console.log('Storage calculation result:', {
knowledgeStorageBytes: knowledgeStorage,
websiteStorageBytes: websiteStorage,
totalBytes,
totalMB,
});
}
return totalMB; // Convert to MB
}
/**
* Updates the storage display with current data
*/
updateStorageDisplay() {
const storageTextElement = this.widget.querySelector('.kb-storage-text');
const storageFillElement = this.widget.querySelector('.kb-storage-fill');
if (storageTextElement && storageFillElement) {
const totalStorageMB = this.calculateTotalStorageMB();
const maxStorageGB = 1; // 1GB limit
const storagePercentage = Math.min(
(totalStorageMB / (maxStorageGB * 1024)) * 100,
100
);
// Update text display
storageTextElement.textContent = `${totalStorageMB.toFixed(1)} MB / ${maxStorageGB} GB`;
// Update progress bar
storageFillElement.style.width = `${storagePercentage}%`;
}
}
/**
* Calculates usage from events array
* @param {Array} events - Usage events
* @returns {number} Usage count
*/
calculateUsageFromEvents(events) {
if (!events || !Array.isArray(events)) return 0;
// Filter to last 30 days
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
return events.filter(event => new Date(event.timestamp) > thirtyDaysAgo)
.length;
}
/**
* Gets file type from knowledge item
* @param {Object} item - Knowledge item
* @returns {string} File type
*/
getFileType(item) {
if (item.source === 'text_editor') return 'Text';
if (item.filename) {
const ext = item.filename.split('.').pop().toLowerCase();
switch (ext) {
case 'pdf':
return 'PDF';
case 'doc':
case 'docx':
return 'Word';
case 'txt':
return 'Text';
default:
return 'Document';
}
}
return 'Text';
}
/**
* Shows website analytics
* @param {string} websiteUrl - Website URL
*/
showWebsiteAnalytics(websiteUrl) {
this.currentView = 'analytics';
const website = this.groupPagesByDomain(this.websites).find(
w => w.url === websiteUrl
);
this.selectedWebsite = website || { url: websiteUrl };
this.updateDisplay();
}
/**
* Shows websites list
*/
showWebsites() {
this.currentView = 'list';
this.selectedWebsite = null;
this.updateDisplay();
}
/**
* Updates the display
*/
updateDisplay() {
const content = this.widget.querySelector('#kb-content');
const breadcrumb = this.widget.querySelector('#kb-breadcrumb');
if (content) {
content.innerHTML = this.renderContent();
}
if (breadcrumb) {
breadcrumb.innerHTML = this.renderBreadcrumb();
}
// Add event listeners for new functionality
this.attachEventListeners();
// Ensure tab switching event listeners are attached
this.attachTabEventListeners();
}
/**
* Formats relative time
* @param {string} timestamp - ISO timestamp
* @returns {string} Relative time
*/
formatRelativeTime(timestamp) {
if (!timestamp) return 'Never';
const now = new Date();
const date = new Date(timestamp);
const diffInMinutes = Math.floor((now - date) / (1000 * 60));
if (diffInMinutes < 60) {
return `${diffInMinutes} min ago`;
} else if (diffInMinutes < 1440) {
return `${Math.floor(diffInMinutes / 60)} hours ago`;
} else {
return `${Math.floor(diffInMinutes / 1440)} days ago`;
}
}
/**
* Determines if a knowledge item is web knowledge (scraped content, connected websites, etc.)
* @param {Object} item - Knowledge item to check
* @returns {boolean} True if item is web knowledge, false if direct knowledge
*/
isWebKnowledge(item) {
// FIXED: Enhanced identification specifically targeting the user's data
// Based on the API response, items with source: "enhanced_web_scraping" should be