UNPKG

besper-frontend-site-dev-main

Version:

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

676 lines (584 loc) 16.3 kB
/** * Demo Widget - Modular Version * Main demo widget component that orchestrates all demo sub-components */ import { getBotOperationsEndpoint } from '../../services/centralizedApi.js'; import { getBrowserLanguage } from '../../utils/i18n.js'; import { DemoHeader } from './DemoHeader.js'; import { KnowledgePanel } from './KnowledgePanel.js'; /** * B-esper Demo Widget - Modular Implementation * Provides demo interface showing chat capabilities */ export class BesperDemoWidget { constructor(botId, options = {}) { this.botId = botId; this.options = { environment: 'prod', ...options, }; this.state = { messages: [], isTyping: false, sessionData: null, threadId: null, }; this.widget = null; this.apiEndpoint = getBotOperationsEndpoint(); // Initialize sub-components this.demoHeader = new DemoHeader(botId, options); this.knowledgePanel = new KnowledgePanel(this.apiEndpoint, botId); } async init() { try { console.log('[LOADING] Initializing B-esper demo widget (modular)...'); // Check if mobile device and exit if so if (this.isMobileDevice()) { console.log('📱 Mobile device detected, hiding demo'); return null; } // Create demo UI this.createWidget(); // Initialize sub-components await this.initializeComponents(); // Create session await this.createSession(); // Add welcome message this.addWelcomeMessage(); console.log( '[SUCCESS] B-esper demo widget (modular) initialized successfully' ); return this; } catch (error) { console.error('[ERROR] Failed to initialize B-esper demo widget:', error); return null; } } /** * Creates the main widget structure */ createWidget() { // Create main container this.widget = document.createElement('div'); this.widget.className = 'besper-demo-widget'; this.widget.innerHTML = this.renderWidget(); // Inject styles this.injectStyles(); // Append to body or specified container const container = this.options.container || document.body; container.appendChild(this.widget); } /** * Renders the complete widget HTML structure * @returns {string} Widget HTML */ renderWidget() { return ` ${this.demoHeader.render()} <div class="demo-content"> <div class="demo-sidebar"> ${this.knowledgePanel.render()} </div> <div class="demo-main"> <div class="demo-chat-container"> <div class="demo-chat-header"> <div class="besper-h3"> <i class="chat-icon">💬</i> Try the Chat </div> <div class="demo-chat-status"> <span class="status-dot"></span> <span>Ready to chat</span> </div> </div> <div class="demo-messages" id="demo-messages"> <!-- Messages will be added here --> </div> <div class="demo-input-container"> <div class="demo-input-wrapper"> <input type="text" id="demo-input" placeholder="Type your message here..." class="demo-input" /> <button id="demo-send-btn" class="demo-send-btn" disabled> <i class="send-icon">→</i> </button> </div> <div class="demo-typing-indicator" id="demo-typing" style="display: none;"> <span class="typing-dot"></span> <span class="typing-dot"></span> <span class="typing-dot"></span> <span class="typing-text">AI is thinking...</span> </div> </div> </div> </div> </div> `; } /** * Initializes all sub-components */ async initializeComponents() { // Initialize knowledge panel with question click handler await this.knowledgePanel.init(question => { this.sendMessage(question); }); // Set up event listeners for demo-specific elements this.setupEventListeners(); } /** * Sets up event listeners for the demo widget */ setupEventListeners() { const input = this.widget.querySelector('#demo-input'); const sendBtn = this.widget.querySelector('#demo-send-btn'); // Input validation and send button state input?.addEventListener('input', e => { const hasText = e.target.value.trim().length > 0; sendBtn.disabled = !hasText; sendBtn.classList.toggle('active', hasText); }); // Send message on button click sendBtn?.addEventListener('click', () => { if (input?.value.trim()) { this.sendMessage(input.value.trim()); input.value = ''; sendBtn.disabled = true; sendBtn.classList.remove('active'); } }); // Send message on Enter key input?.addEventListener('keypress', e => { if (e.key === 'Enter' && input.value.trim()) { this.sendMessage(input.value.trim()); input.value = ''; sendBtn.disabled = true; sendBtn.classList.remove('active'); } }); } /** * Sends a message and handles the response * @param {string} message - Message to send */ async sendMessage(message) { // Add user message to chat this.addMessage(message, 'user'); // Show typing indicator this.showTyping(); try { const response = await this.callChatAPI(message); // Hide typing indicator this.hideTyping(); if (response && response.message) { this.addMessage(response.message, 'assistant'); } else { this.addMessage( 'Sorry, I encountered an error. Please try again.', 'assistant' ); } } catch (error) { console.error('Error sending message:', error); this.hideTyping(); this.addMessage( 'Sorry, I encountered an error. Please try again.', 'assistant' ); } } /** * Adds a message to the chat display * @param {string} content - Message content * @param {string} role - Message role ('user' or 'assistant') */ addMessage(content, role) { const messagesContainer = this.widget.querySelector('#demo-messages'); if (!messagesContainer) return; const messageEl = document.createElement('div'); messageEl.className = `demo-message demo-message-${role}`; const timestamp = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', }); messageEl.innerHTML = ` <div class="message-content"> <div class="message-text">${this.escapeHtml(content)}</div> <div class="message-time">${timestamp}</div> </div> <div class="message-avatar"> <div class="bsp-icon-circle-sm">${role === 'user' ? 'U' : 'B'}</div> </div> `; messagesContainer.appendChild(messageEl); messagesContainer.scrollTop = messagesContainer.scrollHeight; // Add entrance animation setTimeout(() => { messageEl.classList.add('message-visible'); }, 100); } /** * Shows typing indicator */ showTyping() { const typing = this.widget.querySelector('#demo-typing'); if (typing) { typing.style.display = 'flex'; } } /** * Hides typing indicator */ hideTyping() { const typing = this.widget.querySelector('#demo-typing'); if (typing) { typing.style.display = 'none'; } } /** * Adds the initial welcome message */ addWelcomeMessage() { setTimeout(() => { this.addMessage( "Hello! I'm your AI assistant. I'm here to help answer your questions and provide information. Feel free to ask me anything!", 'assistant' ); }, 500); } /** * Creates a new chat session */ async createSession() { try { const response = await fetch( `${this.apiEndpoint}/bot/${this.botId}/session`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, } ); if (response.ok) { const data = await response.json(); this.state.sessionData = data; this.state.threadId = data.thread_id; console.log('[SUCCESS] Demo session created:', data.thread_id); } else { console.error('[ERROR] Failed to create demo session'); } } catch (error) { console.error('[ERROR] Error creating demo session:', error); } } /** * Calls the chat API with a message * @param {string} message - Message to send * @returns {Promise<Object>} API response */ async callChatAPI(message) { if (!this.state.threadId) { throw new Error('No active session'); } const response = await fetch(`${this.apiEndpoint}/bot/${this.botId}/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ message, thread_id: this.state.threadId, language: getBrowserLanguage(), }), }); if (!response.ok) { throw new Error(`API error: ${response.status}`); } return await response.json(); } /** * Checks if device is mobile * @returns {boolean} True if mobile device */ isMobileDevice() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( navigator.userAgent ); } /** * Escapes HTML in text content * @param {string} text - Text to escape * @returns {string} Escaped text */ escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Injects CSS styles for the demo widget */ injectStyles() { if (document.getElementById('besper-demo-styles')) return; const style = document.createElement('style'); style.id = 'besper-demo-styles'; style.textContent = ` ${this.demoHeader.getStyles()} ${this.knowledgePanel.getStyles()} ${this.getDemoStyles()} `; document.head.appendChild(style); } /** * Gets the demo-specific CSS styles * @returns {string} CSS styles */ getDemoStyles() { return ` .besper-demo-widget { max-width: 1400px; margin: 2rem auto; padding: 0 1rem; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .demo-content { display: grid; grid-template-columns: 350px 1fr; gap: 2rem; align-items: start; } .demo-sidebar { position: sticky; top: 2rem; } .demo-main { min-height: 600px; } .demo-chat-container { background: white; border: 1px solid #e9ecef; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); display: flex; flex-direction: column; height: 600px; } .demo-chat-header { background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); color: white; padding: 1rem 1.5rem; display: flex; justify-content: space-between; align-items: center; } .demo-chat-header h3 { margin: 0; font-size: 1.2rem; display: flex; align-items: center; gap: 0.5rem; } .chat-icon { font-size: 1.3rem; } .demo-chat-status { display: flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; } .status-dot { width: 8px; height: 8px; background: #28a745; border-radius: 50%; animation: pulse 2s infinite; } .demo-messages { flex: 1; padding: 1rem; overflow-y: auto; display: flex; flex-direction: column; gap: 1rem; } .demo-message { display: flex; gap: 0.75rem; opacity: 0; transform: translateY(20px); transition: all 0.3s ease; } .demo-message.message-visible { opacity: 1; transform: translateY(0); } .demo-message-user { flex-direction: row-reverse; } .demo-message-user .message-content { background: #007bff; color: white; border-radius: 18px 18px 5px 18px; } .demo-message-assistant .message-content { background: #f8f9fa; color: #333; border: 1px solid #e9ecef; border-radius: 18px 18px 18px 5px; } .message-content { max-width: 70%; padding: 0.75rem 1rem; position: relative; } .message-text { margin: 0; line-height: 1.4; } .message-time { font-size: 0.75rem; opacity: 0.7; margin-top: 0.25rem; } .message-avatar { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 1.2rem; background: #f8f9fa; border: 1px solid #e9ecef; flex-shrink: 0; } .demo-input-container { padding: 1rem; border-top: 1px solid #e9ecef; background: #f8f9fa; } .demo-input-wrapper { display: flex; gap: 0.5rem; align-items: center; } .demo-input { flex: 1; padding: 0.75rem 1rem; border: 1px solid #ced4da; border-radius: 25px; font-size: 1rem; outline: none; transition: border-color 0.3s ease; } .demo-input:focus { border-color: #007bff; box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .demo-send-btn { width: 40px; height: 40px; border: none; border-radius: 50%; background: #6c757d; color: white; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.3s ease; font-size: 1rem; } .demo-send-btn:disabled { opacity: 0.5; cursor: not-allowed; } .demo-send-btn.active, .demo-send-btn:not(:disabled):hover { background: #007bff; transform: scale(1.05); } .demo-typing-indicator { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.5rem; color: #6c757d; font-size: 0.9rem; } .typing-dot { width: 6px; height: 6px; background: #007bff; border-radius: 50%; animation: typing 1.4s infinite ease-in-out; } .typing-dot:nth-child(2) { animation-delay: 0.2s; } .typing-dot:nth-child(3) { animation-delay: 0.4s; } @keyframes typing { 0%, 80%, 100% { transform: scale(0.8); opacity: 0.5; } 40% { transform: scale(1); opacity: 1; } } @media (max-width: 1024px) { .demo-content { grid-template-columns: 1fr; gap: 1.5rem; } .demo-sidebar { order: 2; position: static; } .demo-main { order: 1; } } @media (max-width: 768px) { .besper-demo-widget { margin: 1rem auto; padding: 0 0.5rem; } .demo-chat-container { height: 500px; } .demo-chat-header { padding: 0.75rem 1rem; } .demo-messages { padding: 0.75rem; } .message-content { max-width: 85%; padding: 0.5rem 0.75rem; } } `; } /** * Destroys the demo widget and cleans up */ destroy() { if (this.widget) { this.widget.remove(); } const styles = document.getElementById('besper-demo-styles'); if (styles) { styles.remove(); } } }