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