UNPKG

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
/** * 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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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