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
JavaScript
/**
* 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;
}
typing {
0%, 80%, 100% {
transform: scale(0.8);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
(max-width: 1024px) {
.demo-content {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.demo-sidebar {
order: 2;
position: static;
}
.demo-main {
order: 1;
}
}
(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();
}
}
}