UNPKG

besper-frontend-site-dev-main

Version:

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

1,036 lines (970 loc) 108 kB
/** * Knowledge Tab Component * Handles knowledge base management functionality */ export class KnowledgeTab { constructor( widget, managementApiCall, credentials, managementEndpoint, environment ) { this.widget = widget; this.managementApiCall = managementApiCall; this.credentials = credentials; this.managementEndpoint = managementEndpoint; this.environment = environment; 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')); } /** * 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 toggle state from localStorage const savedToggleState = localStorage.getItem('bm-knowledge-toggle-state') || 'direct-knowledge'; const isDirectKnowledgeActive = savedToggleState === 'direct-knowledge'; const isWebsitesActive = savedToggleState === 'websites'; return ` <div class="bm-tab-content" id="bm-tab-knowledge"> <!-- Knowledge Statistics --> <div class="bm-section"> <h2 class="bm-section-title" style="display: flex; align-items: center; gap: 8px;"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M9 19c-5 0-9-2.5-9-5s4-5 9-5 9 2.5 9 5-4 5-9 5"></path> <path d="m19 9 2 2-2 2"></path> <path d="M20 6 18 4"></path> <path d="M12 6V4"></path> <path d="M10 4 8 6"></path> <path d="M2 9 4 7"></path> </svg> Knowledge Base Statistics </h2> <div class="bm-stats-grid"> <div class="bm-stat-card"> <div class="bm-stat-number">${this.getDirectKnowledgeCount()}</div> <div class="bm-stat-label">Direct Knowledge</div> </div> <div class="bm-stat-card"> <div class="bm-stat-number">${this.websites.length}</div> <div class="bm-stat-label">Web Pages</div> </div> <div class="bm-stat-card"> <div class="bm-stat-number">${this.calculateTotalWords()}</div> <div class="bm-stat-label">Total Words</div> </div> </div> </div> <!-- Add New Knowledge --> <div class="bm-section"> <h2 class="bm-section-title" style="display: flex; align-items: center; gap: 8px;"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="10"></circle> <line x1="12" y1="8" x2="12" y2="16"></line> <line x1="8" y1="12" x2="16" y2="12"></line> </svg> Add Knowledge to Your Bot </h2> <div class="bm-action-buttons"> <button class="bm-btn bm-btn-primary" id="bm-add-text-knowledge"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Add Text Knowledge </button> <button class="bm-btn bm-btn-primary" id="bm-add-website"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="10"></circle> <path d="M2 12h20"></path> <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path> </svg> Connect Website </button> <button class="bm-btn bm-btn-primary" id="bm-upload-document"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Upload Document </button> </div> </div> <!-- Knowledge View Toggle --> <div class="bm-section"> <h2 class="bm-section-title" style="display: flex; align-items: center; gap: 8px; justify-content: space-between;"> <div style="display: flex; align-items: center; gap: 8px;"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path> <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path> </svg> Browse Your Knowledge Base </div> <div class="bm-section-actions" style="display: flex; align-items: center; gap: 12px;"> <div class="bm-toggle-group"> <button class="bm-btn ${isDirectKnowledgeActive ? 'bm-btn-primary' : 'bm-btn-secondary'}" id="bm-view-direct-knowledge"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> </svg> Documents & Text </button> <button class="bm-btn ${isWebsitesActive ? 'bm-btn-primary' : 'bm-btn-secondary'}" id="bm-view-websites"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="10"></circle> <path d="M2 12h20"></path> <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path> </svg> Website Sources </button> </div> <button class="bm-btn bm-btn-secondary" id="bm-refresh-knowledge"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <polyline points="23 4 23 10 17 10"></polyline> <polyline points="1 20 1 14 7 14"></polyline> <path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4-4.64 4.36A9 9 0 0 1 3.51 15"></path> </svg> Refresh </button> </div> </h2> <!-- Direct Knowledge View --> <div class="bm-knowledge-view" id="bm-direct-knowledge-view" style="display: ${isDirectKnowledgeActive ? 'block' : 'none'};"> <!-- Search and Filter Controls --> <div class="bm-search-controls" style="margin-bottom: 16px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap;"> <div style="display: flex; align-items: center; gap: 8px; flex: 1; min-width: 200px;"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color: #6c757d;"> <circle cx="11" cy="11" r="8"></circle> <path d="m21 21-4.35-4.35"></path> </svg> <input type="text" id="bm-knowledge-search" placeholder="Search knowledge items..." style=" flex: 1; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 6px; font-size: 14px; outline: none; " /> </div> <div style="font-size: 14px; color: #6c757d; white-space: nowrap;"> <span id="bm-knowledge-count-display">${this.knowledgeItems.length} items</span> </div> </div> <div class="bm-knowledge-list" id="bm-knowledge-list"> ${this.renderKnowledgeList()} </div> <!-- Pagination Controls --> <div class="bm-pagination" id="bm-knowledge-pagination" style="margin-top: 16px; display: flex; justify-content: center; align-items: center; gap: 8px;"> <!-- Pagination will be rendered dynamically --> </div> </div> <!-- Website Sources View --> <div class="bm-website-view" id="bm-website-sources-view" style="display: ${isWebsitesActive ? 'block' : 'none'};"> <!-- Search and Filter Controls --> <div class="bm-search-controls" style="margin-bottom: 16px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap;"> <div style="display: flex; align-items: center; gap: 8px; flex: 1; min-width: 200px;"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color: #6c757d;"> <circle cx="11" cy="11" r="8"></circle> <path d="m21 21-4.35-4.35"></path> </svg> <input type="text" id="bm-website-search" placeholder="Search websites..." style=" flex: 1; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 6px; font-size: 14px; outline: none; " /> </div> <div style="font-size: 14px; color: #6c757d; white-space: nowrap;"> <span id="bm-website-count-display">${this.configuredWebsites.length} sources</span> </div> </div> <div class="bm-website-list" id="bm-website-list"> ${this.renderWebsiteList()} </div> <!-- Pagination Controls --> <div class="bm-pagination" id="bm-website-pagination" style="margin-top: 16px; display: flex; justify-content: center; align-items: center; gap: 8px;"> <!-- Pagination will be rendered dynamically --> </div> </div> </div> <!-- Knowledge Management Actions --> <div class="bm-section"> <h2 class="bm-section-title" style="display: flex; align-items: center; gap: 8px;"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon> </svg> Bulk Knowledge Management </h2> <div class="bm-action-buttons" style="display: flex; gap: 12px; flex-wrap: wrap;"> <button class="bm-btn bm-btn-secondary" id="bm-clear-web-knowledge"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="10"></circle> <path d="M2 12h20"></path> <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path> </svg> Clear Web Knowledge </button> <button class="bm-btn bm-btn-secondary" id="bm-clear-uploaded-files"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="12" y1="11" x2="12" y2="17"></line> <polyline points="9 14 12 17 15 14"></polyline> </svg> Clear Direct Knowledge </button> </div> <div class="bm-help-text" style="margin-top: 16px; padding: 16px; background: #f8f9fa; border-radius: 8px; border-left: 4px solid #007bff;"> <div style="font-size: 14px; color: #495057; font-weight: 600; margin-bottom: 8px;"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: text-bottom; margin-right: 6px;"> <circle cx="12" cy="12" r="10"></circle> <path d="M9,9 Q9,6 12,6 Q15,6 15,9 Q15,10 12,11"></path> <line x1="12" y1="17" x2="12" y2="17.01"></line> </svg> Knowledge Management Tips </div> <div style="font-size: 13px; color: #6c757d; line-height: 1.6;"> <div style="margin-bottom: 6px;"><strong>Web Knowledge:</strong> Pages automatically scraped from connected websites</div> <div style="margin-bottom: 6px;"><strong>Direct Knowledge:</strong> Manually added text content and uploaded documents</div> <div><strong>Clear actions:</strong> Permanently delete knowledge by category - use with caution</div> </div> </div> </div> <!-- Add 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">Question/Topic</label> <input type="text" id="bm-knowledge-question" class="bm-form-input" placeholder="What question does this answer?" /> </div> <div class="bm-form-group"> <label class="bm-form-label">Answer/Content</label> <textarea id="bm-knowledge-content" class="bm-form-input" rows="6" placeholder="Enter the answer or knowledge content..."></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="Custom title for this document" /> </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> `; } /** * 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) { // COMPREHENSIVE identification of web knowledge (must be true for ANY of these) return !!( // PRIMARY SOURCE MARKERS: Check top-level source field ( item.source === 'Website' || item.source === 'enhanced_web_scraping' || item.source === 'web_scraping' || item.source === 'site_scraping' || // METADATA SOURCE MARKERS: Check metadata source field item.metadata?.source === 'enhanced_web_scraping' || item.metadata?.source === 'web_scraping' || item.metadata?.source === 'site_scraping' || // METADATA CLASSIFICATION: Tags and types item.metadata?.tags?.includes('web_knowledge') || item.metadata?.classification === 'website_page' || item.metadata?.type === 'connected_website' || item.metadata?.type === 'scraping_summary' || // CONTENT CLASSIFICATION: Check if content indicates connected website item.content === 'connected_website' || // SCRAPING SUMMARIES: Check filename patterns item.file_name?.startsWith('scraping_summary_') || // URL-BASED DETECTION: Check if filename or title is a URL item.file_name?.startsWith('http://') || item.file_name?.startsWith('https://') || item.title?.startsWith('http://') || item.title?.startsWith('https://') || // CONFIGURED WEBSITE DETECTION: Match against connected websites (this.configuredWebsites && this.configuredWebsites.length > 0 && (this.configuredWebsites.includes(item.file_name) || this.configuredWebsites.includes(item.title) || this.configuredWebsites.includes(item.content) || // Check if any configured website is contained in the item fields this.configuredWebsites.some( website => (item.file_name && item.file_name.includes(website)) || (item.title && item.title.includes(website)) || (item.content && item.content.includes(website)) ))) || // WEB CONTENT FILES: Files with web-related metadata (item.file_name?.includes('.txt') && item.metadata?.url && (item.metadata?.source?.includes('scraping') || item.metadata?.tags?.includes('web_knowledge'))) || // METADATA URL: Any item with URL metadata (unless explicitly marked as direct) (item.metadata?.url && !item.metadata?.tags?.includes('direct_knowledge')) ) ); } /** * Renders the knowledge items list with improved organization * @returns {string} Knowledge items HTML */ renderKnowledgeList() { // Filter knowledge items by source type - COMPREHENSIVE filtering for all web content // Direct knowledge = manually uploaded files or text entries, NOT web-scraped content // Debug logging for classification issues if (this.isDebugMode) { console.log('🔍 Knowledge Classification Debug:', { totalItems: this.knowledgeItems.length, configuredWebsites: this.configuredWebsites, itemSamples: this.knowledgeItems.slice(0, 3).map(item => ({ file_name: item.file_name, title: item.title, source: item.source, content: item.content?.substring(0, 50), metadata: item.metadata, isWebKnowledge: this.isWebKnowledge(item), })), }); } // knowledgeItems is already filtered to contain only direct knowledge in BotManagement.js let directUploads = this.knowledgeItems; // Apply search filter if (this.knowledgeSearchTerm) { directUploads = directUploads.filter(item => { const searchTerm = this.knowledgeSearchTerm.toLowerCase(); return ( (item.title || '').toLowerCase().includes(searchTerm) || (item.filename || '').toLowerCase().includes(searchTerm) || (item.content || '').toLowerCase().includes(searchTerm) || (item.source || '').toLowerCase().includes(searchTerm) ); }); } if (directUploads.length === 0) { return ` <div class="bm-empty-state"> <div class="besper-h3">${this.knowledgeSearchTerm ? 'No Matching Results' : 'No Direct Knowledge Items'}</div> <div class="besper-p">${this.knowledgeSearchTerm ? 'Try adjusting your search terms.' : 'Add text knowledge or upload documents to see them here.'}</div> </div> `; } // Calculate pagination const totalItems = directUploads.length; const totalPages = Math.ceil(totalItems / this.knowledgePageSize); const startIndex = (this.knowledgeCurrentPage - 1) * this.knowledgePageSize; const endIndex = Math.min(startIndex + this.knowledgePageSize, totalItems); const paginatedItems = directUploads.slice(startIndex, endIndex); return ( ` <div class="bm-knowledge-filter-info" style="margin-bottom: 16px; padding: 12px; background-color: #f8f9fa; border-radius: 6px; border-left: 4px solid #6c757d;"> <div style="font-size: 14px; color: #0056b3; font-weight: 500;"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: text-bottom; margin-right: 6px;"> <circle cx="12" cy="12" r="10"></circle> <path d="M9,9 Q9,6 12,6 Q15,6 15,9 Q15,10 12,11"></path> <line x1="12" y1="17" x2="12" y2="17.01"></line> </svg> Direct Knowledge Items (${totalItems}) - Page ${this.knowledgeCurrentPage} of ${totalPages} </div> <div style="font-size: 13px; color: #0056b3; margin-top: 4px;"> ${ this.knowledgeSearchTerm ? `Showing ${paginatedItems.length} of ${totalItems} results for "${this.knowledgeSearchTerm}"` : 'This section shows manually added content and uploaded documents. Web scraped content is managed in the "Website Sources" section below.' } </div> <div style="font-size: 12px; color: #0056b3; margin-top: 6px; font-weight: 500;"> 📊 Organization: Individual documents and text knowledge | [API] Website pages are grouped by domain below </div> </div> ` + paginatedItems .map( item => ` <div class="bm-knowledge-item" data-id="${item.id || ''}"> <div class="bm-knowledge-content"> <div class="bm-knowledge-header"> <div class="bm-knowledge-question"> ${this.escapeHtml(item.title || item.filename || 'Untitled')} </div> <div class="bm-knowledge-source-badge bm-source-${(item.source || 'unknown').toLowerCase().replace(' ', '-')}"> ${this.escapeHtml(item.source || 'Unknown')} </div> </div> <div class="bm-knowledge-preview"> ${this.escapeHtml(this.truncateText(item.content || '', 150))} </div> <div class="bm-knowledge-meta"> <span class="bm-meta-item"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect> <line x1="16" y1="2" x2="16" y2="6"></line> <line x1="8" y1="2" x2="8" y2="6"></line> <line x1="3" y1="10" x2="21" y2="10"></line> </svg> ${this.formatDate(item.created_at || new Date())} </span> <span class="bm-meta-item"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path> <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path> </svg> ${item.word_count || 0} words </span> <span class="bm-meta-item"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M9 19c-5 0-9-2.5-9-5s4-5 9-5 9 2.5 9 5-4 5-9 5"></path> <path d="m19 9 2 2-2 2"></path> <path d="M20 6 18 4"></path> <path d="M12 6V4"></path> <path d="M10 4 8 6"></path> <path d="M2 9 4 7"></path> </svg> ${item.usage_count || 0} uses </span> ${ item.last_used_at ? ` <span class="bm-meta-item"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="10"></circle> <polyline points="12,6 12,12 16,14"></polyline> </svg> Last used: ${this.formatDate(item.last_used_at)} </span> ` : '' } </div> ${this.renderUsageTimeline(item)} </div> <div class="bm-knowledge-actions"> <button class="bm-btn bm-btn-sm bm-btn-secondary" data-action="preview" data-id="${item.id}"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path> <circle cx="12" cy="12" r="3"></circle> </svg> Preview </button> <button class="bm-btn bm-btn-sm bm-btn-secondary" data-action="edit" data-id="${item.id}"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M12 20h9"></path> <path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"></path> </svg> Edit </button> <button class="bm-btn bm-btn-sm bm-btn-danger" data-action="delete" data-id="${item.id}"> <svg width="16" height="16" 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> Delete </button> </div> </div> ` ) .join('') ); } /** * Renders the website sources list showing configured websites from bot config * @returns {string} Website sources HTML */ renderWebsiteList() { // If showing pages for a specific website, render that view instead if (this.selectedWebsite) { return this.renderWebsitePages(this.selectedWebsite); } // Show configured websites from bot configuration if (!this.configuredWebsites || this.configuredWebsites.length === 0) { return ` <div class="bm-empty-state"> <div class="besper-h3">No Connected Websites</div> <div class="besper-p">Add websites to automatically extract knowledge content from their pages.</div> </div> `; } return ` <div class="bm-website-filter-info" style="margin-bottom: 16px; padding: 12px; background-color: #f8f9fa; border-radius: 8px; border-left: 4px solid #007bff; box-shadow: 0 2px 4px rgba(0,0,0,0.05);"> <div style="font-size: 14px; color: #155724; font-weight: 600;"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: text-bottom; margin-right: 6px;"> <circle cx="12" cy="12" r="10"></circle> <path d="M2 12h20"></path> <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path> </svg> Configured Websites (${this.configuredWebsites.length}) </div> <div style="font-size: 13px; color: #155724; margin-top: 4px;"> These are the websites configured in your bot settings. Click on a website to view scraped pages from that domain. </div> </div> ${this.configuredWebsites .map(websiteUrl => { const pagesFromThisWebsite = this.getScrapedPagesForWebsite(websiteUrl); const domain = this.getDomainFromUrl(websiteUrl); return ` <div class="bm-website-item" data-url="${websiteUrl}" style="background: white; border: 1px solid #e5e7eb; border-radius: 12px; margin-bottom: 16px; transition: all 0.2s ease; box-shadow: 0 2px 8px rgba(0,0,0,0.04); cursor: pointer;" onclick="window.knowledgeTab?.showWebsitePages('${this.escapeHtml(websiteUrl)}')"> <div class="bm-website-content" style="padding: 20px;"> <div class="bm-website-header" style="margin-bottom: 16px;"> <div class="bm-website-url" style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color: #007bff;"> <circle cx="12" cy="12" r="10"></circle> <path d="M2 12h20"></path> <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path> </svg> <a href="${websiteUrl}" target="_blank" rel="noopener" style="font-weight: 600; font-size: 16px; color: #007bff; text-decoration: none;" onclick="event.stopPropagation();"> ${this.escapeHtml(domain)} </a> </div> <div class="bm-website-status" style="background: #28a745; color: white; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 500; display: inline-block;"> Configured </div> </div> <div class="bm-website-meta" style="display: flex; flex-wrap: wrap; gap: 16px; margin-bottom: 12px;"> <span class="bm-meta-item" style="display: flex; align-items: center; gap: 6px; color: #6c757d; font-size: 14px;"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M9 19c-5 0-9-2.5-9-5s4-5 9-5 9 2.5 9 5-4 5-9 5"></path> <path d="m19 9 2 2-2 2"></path> <path d="M20 6 18 4"></path> <path d="M12 6V4"></path> <path d="M10 4 8 6"></path> <path d="M2 9 4 7"></path> </svg> ${pagesFromThisWebsite.length} pages scraped </span> <span class="bm-meta-item" style="display: flex; align-items: center; gap: 6px; color: #6c757d; font-size: 14px;"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path> <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path> </svg> ${pagesFromThisWebsite.reduce((total, page) => total + (page.word_count || 0), 0)} words </span> <span class="bm-meta-item" style="display: flex; align-items: center; gap: 6px; color: #6c757d; font-size: 14px;"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="3"></circle> <polyline points="12,6 12,12 16,14"></polyline> </svg> Click to view pages </span> </div> </div> </div> `; }) .join('')} `; } /** * Renders pages for a specific website */ renderWebsitePages(websiteUrl) { const pages = this.getScrapedPagesForWebsite(websiteUrl); const domain = this.getDomainFromUrl(websiteUrl); return ` <div class="bm-website-pages-header" style="margin-bottom: 16px; padding: 12px; background-color: #f8f9fa; border-radius: 8px; border-left: 4px solid #007bff;"> <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;"> <div style="display: flex; align-items: center; gap: 8px;"> <button onclick="window.knowledgeTab?.backToWebsites()" style="background: none; border: none; color: #007bff; cursor: pointer; display: flex; align-items: center; gap: 4px; font-size: 14px;"> <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> </div> <div style="font-size: 16px; font-weight: 600; color: #333;"> Pages from ${this.escapeHtml(domain)} </div> <div style="font-size: 14px; color: #6c757d; margin-top: 4px;"> ${pages.length} pages scraped from this website </div> </div> ${ pages.length === 0 ? ` <div class="bm-empty-state"> <div class="besper-h3">No Pages Found</div> <div class="besper-p">No pages have been scraped from this website yet.</div> </div> ` : pages .map( page => ` <div class="bm-website-item" data-id="${page.id}" style="background: white; border: 1px solid #e5e7eb; border-radius: 12px; margin-bottom: 12px; transition: all 0.2s ease; box-shadow: 0 2px 6px rgba(0,0,0,0.03);"> <div class="bm-website-content" style="padding: 16px;"> <div class="bm-website-header" style="margin-bottom: 12px;"> <div class="bm-website-url" style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color: #007bff;"> <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path> <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path> </svg> <span style="font-weight: 600; font-size: 16px; color: #333;"> ${this.escapeHtml(page.title || page.page_name || page.filename || 'Untitled Page')} </span> </div> ${ page.url ? ` <div style="font-size: 13px; color: #6c757d; margin-bottom: 6px;"> <a href="${page.url}" target="_blank" rel="noopener" style="color: #007bff; text-decoration: none;">${this.escapeHtml(page.url)}</a> </div> ` : '' } <div class="bm-website-status" style="background: #007bff; color: white; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 500; display: inline-block;"> Scraped Page </div> </div> <div class="bm-website-meta" style="display: flex; flex-wrap: wrap; gap: 16px; margin-bottom: 8px;"> <span class="bm-meta-item" style="display: flex; align-items: center; gap: 6px; color: #6c757d; font-size: 14px;"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path> <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path> </svg> ${page.word_count || 0} words </span> <span class="bm-meta-item" style="display: flex; align-items: center; gap: 6px; color: #6c757d; font-size: 14px;"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect> <line x1="16" y1="2" x2="16" y2="6"></line> <line x1="8" y1="2" x2="8" y2="6"></line> <line x1="3" y1="10" x2="21" y2="10"></line> </svg> ${this.formatDate(page.scraped_at || page.created_at || new Date())} </span> </div> ${page.content ? `<div style="font-size: 13px; color: #6c757d; padding: 8px; background: #f8f9fa; border-radius: 6px; margin-top: 8px;">${this.escapeHtml(this.truncateText(page.content, 150))}</div>` : ''} </div> <div class="bm-website-actions" style="padding: 12px 16px; border-top: 1px solid #f8f9fa; display: flex; gap: 8px; flex-wrap: wrap;"> <button class="bm-btn bm-btn-sm bm-btn-secondary" data-action="preview" data-id="${page.id}" style="display: flex; align-items: center; gap: 6px;"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path> <circle cx="12" cy="12" r="3"></circle> </svg> Preview </button> <button class="bm-btn bm-btn-sm bm-btn-danger" data-action="delete" data-id="${page.id}" style="display: flex; align-items: center; gap: 6px;"> <svg width="16" height="16" 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> Delete </button> </div> </div> ` ) .join('') } `; } /** * Get scraped pages for a specific website */ getScrapedPagesForWebsite(websiteUrl) { const domain = this.getDomainFromUrl(websiteUrl); return this.websites.filter(page => { // Check if page URL matches the website domain if (page.url) { const pageDomain = this.getDomainFromUrl(page.url); return pageDomain === domain; } // Also check metadata for domain matching if (page.metadata?.domain) { return page.metadata.domain === domain; } return false; }); } /** * Show pages for a specific website */ showWebsitePages(websiteUrl) { this.selectedWebsite = websiteUrl; const websiteList = this.widget.querySelector('#bm-website-list'); if (websiteList) { websiteList.innerHTML = this.renderWebsiteList(); } } /** * Go back to the main websites view */ backToWebsites() { this.selectedWebsite = null; const websiteList = this.widget.querySelector('#bm-website-list'); if (websiteList) { websiteList.innerHTML = this.renderWebsiteList(); } } /** * Sets up event listeners for the knowledge tab */ setupEventListeners() { // Use a small timeout to ensure DOM is fully rendered setTimeout(() => { this._setupEventListenersInternal(); }, 100); } /** * Internal method to set up event listeners after DOM is ready */ _setupEventListenersInternal() { // Add text knowledge const addTextBtn = this.widget.querySelector('#bm-add-text-knowledge'); addTextBtn?.addEventListener('click', () => this.showTextKnowledgeModal()); // Add website (delegated to parent component) const addWebsiteBtn = this.widget.querySelector('#bm-add-website'); addWebsiteBtn?.addEventListener('click', () => { if (this.onAddWebsite) { this.onAddWebsite(); } }); // Upload document const uploadBtn = this.widget.querySelector('#bm-upload-document'); uploadBtn?.addEventListener('click', () => this.showFileUploadModal()); // Modal handling for file upload const fileModal = this.widget.querySelector('#bm-file-upload-modal'); const fileCloseBtn = fileModal?.querySelector('.bm-modal-close'); const fileCancelBtn = fileModal?.querySelector('#bm-cancel-file-upload'); const fileSaveBtn = fileModal?.querySelector('#bm-save-file-upload'); const fileInput = fileModal?.querySelector('#bm-file-input'); fileCloseBtn?.addEventListener('click', () => this.hideFileUploadModal()); fileCancelBtn?.addEventListener('click', () => this.hideFileUploadModal()); fileSaveBtn?.addEventListener('click', () => this.saveFileUpload()); fileInput?.addEventListener('change', e => this.handleFileSelection(e)); // Modal handling for text knowledge const modal = this.widget.querySelector('#bm-text-knowledge-modal'); const closeBtn = modal?.querySelector('.bm-modal-close'); const cancelBtn = modal?.querySelector('#bm-cancel-text-knowledge'); const saveBtn = modal?.querySelector('#bm-save-text-knowledge'); closeBtn?.addEventListener('click', () => this.hideTextKnowledgeModal()); cancelBtn?.addEventListener('click', () => this.hideTextKnowledgeModal()); saveBtn?.addEventListener('click', () => this.saveTextKnowledge()); // Modal handling for preview const previewModal = this.widget.querySelector( '#bm-knowledge-preview-modal' ); const previewCloseBtn = previewModal?.querySelector('.bm-modal-close'); const previewCloseFooterBtn = previewModal?.querySelector('#bm-close-preview'); const editFromPreviewBtn = previewModal?.querySelector( '#bm-edit-from-preview' ); previewCloseBtn?.addEventListener('click', () => this.hidePreviewModal()); previewCloseFooterBtn?.addEventListener('click', () => this.hidePreviewModal() ); editFromPreviewBtn?.addEventListener('click', () => this.editFromPreview()); // Modal handling for edit const editModal = this.widget.querySelector('#bm-knowledge-edit-modal'); const editCloseBtn = editModal?.querySelector('.bm-modal-close'); const editCancelBtn = editModal?.querySelector('#bm-cancel-edit'); const editSaveBtn = editModal?.querySelector('#bm-save-edit'); editCloseBtn?.addEventListener('click', () => this.hideEditModal()); editCancelBtn?.addEventListener('click', () => this.hideEditModal()); editSaveBtn?.addEventListener('click', () => this.saveEdit()); // Modal handling for website pages const pagesModal = this.widget.querySelector('#bm-website-pages-modal'); const pagesCloseBtn = pagesModal?.querySelector('.bm-modal-close'); const pagesCloseFooterBtn = pagesModal?.querySelector( '#bm-close-website-pages' ); const pagesRefreshBtn = pagesModal?.querySelector( '#bm-refresh-website-pages' ); pagesCloseBtn?.addEventListener('click', () => this.hideWebsitePagesModal() ); pagesCloseFooterBtn?.addEventListener('cli