class ChatbotUI { constructor() { // Auto-detect API base URL for different deployment environments if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { this.apiBase = 'http://localhost:7860/api'; } else { // For Hugging Face Spaces or other deployments, use relative path this.apiBase = '/api'; } this.isTyping = false; this.hasDocuments = false; this.conversations = []; this.currentConversationId = null; this.isStreaming = false; // Speech recognition properties this.recognition = null; this.isRecording = false; this.isListening = false; this.initializeElements(); this.attachEventListeners(); this.applyInitialTheme(); this.loadConversations(); this.initializeSpeechRecognition(); } initializeElements() { // Main elements this.messagesContainer = document.getElementById('messagesContainer'); this.messageInput = document.getElementById('messageInput'); this.sendBtn = document.getElementById('sendBtn'); this.fileInput = document.getElementById('fileInput'); this.documentStatus = document.getElementById('documentStatus'); this.welcomeMessage = document.getElementById('welcomeMessage'); // Buttons this.attachBtn = document.getElementById('attachBtn'); this.memoryBtn = document.getElementById('memoryBtn'); this.clearBtn = document.getElementById('clearBtn'); this.themeToggleBtn = document.getElementById('themeToggleBtn'); this.voiceBtn = document.getElementById('voiceBtn'); this.stopVoiceBtn = document.getElementById('stopVoiceBtn'); this.conversationsBtn = document.getElementById('conversationsBtn'); // Modal and sidebar this.memoryModal = document.getElementById('memoryModal'); this.closeModalBtn = document.getElementById('closeModalBtn'); this.memoryContent = document.getElementById('memoryContent'); this.sidebar = document.getElementById('sidebar'); this.closeSidebarBtn = document.getElementById('closeSidebarBtn'); // Voice modal and loading this.voiceModal = document.getElementById('voiceModal'); this.loadingOverlay = document.getElementById('loadingOverlay'); } initializeSpeechRecognition() { // Check if speech recognition is supported if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; this.recognition = new SpeechRecognition(); // Configure recognition settings this.recognition.continuous = true; this.recognition.interimResults = true; this.recognition.lang = 'en-US'; // Event handlers this.recognition.onstart = () => { console.log('🎤 Speech recognition started'); this.isListening = true; this.voiceBtn.classList.add('voice-recording'); this.showVoiceModal(); }; this.recognition.onresult = (event) => { let finalTranscript = ''; let interimTranscript = ''; for (let i = event.resultIndex; i < event.results.length; i++) { const transcript = event.results[i][0].transcript; if (event.results[i].isFinal) { finalTranscript += transcript; } else { interimTranscript += transcript; } } // Update input field with transcription this.messageInput.value = finalTranscript + interimTranscript; this.autoResizeInput(); this.updateSendButton(); }; this.recognition.onerror = (event) => { console.error('🎤 Speech recognition error:', event.error); this.showNotification(`Voice recognition error: ${event.error}`, 'error'); this.stopVoiceRecording(); }; this.recognition.onend = () => { console.log('🎤 Speech recognition ended'); this.isListening = false; this.stopVoiceRecording(); }; } else { console.warn('🎤 Speech recognition not supported in this browser'); } } attachEventListeners() { // Send message events this.sendBtn.addEventListener('click', () => this.sendMessage()); // Shift+Enter for new line this.messageInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { if (e.shiftKey) { return; } else { e.preventDefault(); this.sendMessage(); } } }); // File upload events this.attachBtn.addEventListener('click', () => this.fileInput.click()); this.fileInput.addEventListener('change', (e) => this.handleFileUpload(e)); // Voice events this.voiceBtn.addEventListener('click', () => this.toggleVoiceRecording()); this.stopVoiceBtn.addEventListener('click', () => this.stopVoiceRecording()); // Close voice modal on click outside this.voiceModal.addEventListener('click', (e) => { if (e.target === this.voiceModal) { this.stopVoiceRecording(); } }); // Conversations button (Menu button) this.conversationsBtn.addEventListener('click', () => this.toggleSidebar()); // Modal events this.memoryBtn.addEventListener('click', () => this.showMemory()); this.closeModalBtn.addEventListener('click', () => this.hideMemory()); this.memoryModal.addEventListener('click', (e) => { if (e.target === this.memoryModal) this.hideMemory(); }); // Clear session this.clearBtn.addEventListener('click', () => this.clearSession()); // Sidebar events this.closeSidebarBtn.addEventListener('click', () => this.closeSidebar()); // Auto-resize textarea this.messageInput.addEventListener('input', () => { this.autoResizeInput(); this.updateSendButton(); }); // Theme toggle this.themeToggleBtn.addEventListener('click', () => this.toggleTheme()); // Web scraping button const scrapeBtn = document.getElementById('scrapeBtn'); if (scrapeBtn) { scrapeBtn.addEventListener('click', () => this.scrapeWebsite()); } // Save conversation button const saveConvBtn = document.getElementById('saveConvBtn'); if (saveConvBtn) { saveConvBtn.addEventListener('click', () => this.saveConversation()); } // New conversation button const newConvBtn = document.getElementById('newConvBtn'); if (newConvBtn) { newConvBtn.addEventListener('click', () => this.newConversation()); } } // Quick Action Button Helper fillInput(text) { this.messageInput.value = text; this.updateSendButton(); this.autoResizeInput(); this.messageInput.focus(); } // Toggle sidebar method toggleSidebar() { this.sidebar.classList.toggle('active'); if (this.sidebar.classList.contains('active')) { this.loadConversations(); // Refresh conversations when opening } } // Loading overlay methods showLoading() { if (this.loadingOverlay) { this.loadingOverlay.classList.add('active'); } } hideLoading() { if (this.loadingOverlay) { this.loadingOverlay.classList.remove('active'); } } // Voice Recording Methods toggleVoiceRecording() { if (!this.recognition) { this.showNotification('Speech recognition not supported in this browser', 'error'); return; } if (this.isRecording) { this.stopVoiceRecording(); } else { this.startVoiceRecording(); } } startVoiceRecording() { if (this.isRecording || !this.recognition) return; try { this.isRecording = true; this.recognition.start(); console.log('🎤 Starting voice recording...'); } catch (error) { console.error('🎤 Error starting voice recording:', error); this.showNotification('Failed to start voice recording', 'error'); this.isRecording = false; } } stopVoiceRecording() { if (!this.isRecording && !this.isListening) return; try { if (this.recognition) { this.recognition.stop(); } this.isRecording = false; this.isListening = false; this.voiceBtn.classList.remove('voice-recording'); this.hideVoiceModal(); console.log('🎤 Voice recording stopped'); // Auto-send if there's content and user preference if (this.messageInput.value.trim()) { this.updateSendButton(); this.messageInput.focus(); } } catch (error) { console.error('🎤 Error stopping voice recording:', error); } } showVoiceModal() { this.voiceModal.classList.add('active'); document.body.style.overflow = 'hidden'; } hideVoiceModal() { this.voiceModal.classList.remove('active'); document.body.style.overflow = 'auto'; } async sendMessage() { const message = this.messageInput.value.trim(); if (!message || this.isTyping) return; // Use streaming for better UX await this.sendMessageWithStreaming(message); } // Conversation Management async loadConversations() { try { const response = await fetch(`${this.apiBase}/conversations`); const data = await response.json(); if (response.ok) { this.conversations = data.conversations; this.updateConversationsList(); } } catch (error) { console.error('Error loading conversations:', error); } } updateConversationsList() { const conversationsList = document.getElementById('conversationsList'); if (!conversationsList) return; conversationsList.innerHTML = ''; this.conversations.forEach(conversation => { const conversationItem = document.createElement('div'); conversationItem.className = 'conversation-item'; if (conversation.id === this.currentConversationId) { conversationItem.classList.add('active'); } conversationItem.innerHTML = `
$1');
}
showTypingIndicator() {
this.isTyping = true;
this.sendBtn.disabled = true;
const typingDiv = document.createElement('div');
typingDiv.className = 'message bot typing fade-in';
typingDiv.innerHTML = `
`;
this.messagesContainer.appendChild(typingDiv);
this.scrollToBottom();
}
hideTypingIndicator() {
this.isTyping = false;
this.sendBtn.disabled = false;
const typingIndicator = this.messagesContainer.querySelector('.typing');
if (typingIndicator) {
typingIndicator.remove();
}
}
handleFileUpload(event) {
const file = event.target.files[0];
if (file) {
this.processFile(file);
}
}
async processFile(file) {
if (file.type !== 'application/pdf') {
this.showNotification('Please select a PDF file', 'error');
return;
}
const formData = new FormData();
formData.append('file', file);
this.showLoading();
this.showNotification(`Uploading ${file.name}...`, 'info');
try {
const response = await fetch(`${this.apiBase}/upload`, {
method: 'POST',
body: formData
});
const data = await response.json();
this.hideLoading();
if (response.ok) {
this.hasDocuments = true;
this.updateDocumentStatus(`Processed: ${data.filename}`, true);
// Hide welcome message when document is uploaded
if (this.welcomeMessage) {
this.welcomeMessage.style.display = 'none';
}
this.addMessage(
`📄 **Document Processed Successfully**\n\n**File:** ${data.filename}\n**Length:** ${data.text_length.toLocaleString()} characters\n\n**Summary:**\n${data.summary}`,
'bot'
);
this.showNotification('Document processed successfully!', 'success');
} else {
throw new Error(data.error || 'Failed to upload file');
}
} catch (error) {
this.hideLoading();
this.showNotification(`Upload failed: ${error.message}`, 'error');
}
this.fileInput.value = '';
}
updateDocumentStatus(message, hasDocuments) {
const iconClass = hasDocuments ? 'fas fa-file-pdf' : 'fas fa-file-pdf';
const color = hasDocuments ? 'var(--success-color)' : 'var(--text-muted)';
this.documentStatus.innerHTML = `${message}`;
}
async showMemory() {
try {
const response = await fetch(`${this.apiBase}/memory`);
const data = await response.json();
let memoryHtml = `**Session ID:** ${data.session_id}
**Chat History:** ${data.chat_history_length} messages
**Documents Loaded:** ${data.has_vectorstore ? 'Yes' : 'No'}
**Memory Data:**
${JSON.stringify(data.memory, null, 2)}`;
this.memoryContent.innerHTML = `${memoryHtml}`;
this.memoryModal.classList.add('active');
} catch (error) {
this.showNotification(`Failed to load memory: ${error.message}`, 'error');
}
}
hideMemory() {
this.memoryModal.classList.remove('active');
}
async clearSession() {
if (!confirm('Are you sure you want to clear the current session? This will remove all chat history and uploaded documents.')) {
return;
}
try {
const response = await fetch(`${this.apiBase}/clear`, {
method: 'POST'
});
const data = await response.json();
if (response.ok) {
this.messagesContainer.innerHTML = `
`;
// Re-initialize welcome message reference
this.welcomeMessage = document.getElementById('welcomeMessage');
this.hasDocuments = false;
this.updateDocumentStatus('No documents loaded', false);
this.showNotification('Session cleared successfully!', 'success');
this.currentConversationId = null;
this.loadConversations();
} else {
throw new Error(data.error || 'Failed to clear session');
}
} catch (error) {
this.showNotification(`Clear failed: ${error.message}`, 'error');
}
}
async scrapeWebsite() {
const url = prompt('Enter the website URL to scrape:');
if (!url) return;
this.showLoading();
this.showNotification('Scraping website...', 'info');
try {
const response = await fetch(`${this.apiBase}/scrape`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ url: url })
});
const data = await response.json();
this.hideLoading();
if (response.ok) {
this.hasDocuments = true;
this.updateDocumentStatus(`Scraped: ${data.url}`, true);
// Hide welcome message when content is scraped
if (this.welcomeMessage) {
this.welcomeMessage.style.display = 'none';
}
this.addMessage(
`🌐 **Website Scraped Successfully**\n\n**URL:** ${data.url}\n**Content Length:** ${data.content_length.toLocaleString()} characters\n\n**Summary:**\n${data.summary}`,
'bot'
);
this.showNotification('Website scraped successfully!', 'success');
} else {
throw new Error(data.error || 'Failed to scrape website');
}
} catch (error) {
this.hideLoading();
this.showNotification(`Scraping failed: ${error.message}`, 'error');
}
}
closeSidebar() {
this.sidebar.classList.remove('active');
}
autoResizeInput() {
this.messageInput.style.height = 'auto';
this.messageInput.style.height = Math.min(this.messageInput.scrollHeight, 120) + 'px';
}
updateSendButton() {
const hasText = this.messageInput.value.trim().length > 0;
this.sendBtn.disabled = !hasText || this.isTyping;
}
scrollToBottom() {
this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
}
showNotification(message, type = 'info', duration = 4000) {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
document.body.appendChild(notification);
// Auto-hide notifications except errors
if (type !== 'error') {
setTimeout(() => {
if (document.body.contains(notification)) {
notification.remove();
}
}, duration);
} else {
setTimeout(() => {
if (document.body.contains(notification)) {
notification.remove();
}
}, 7000); // Errors stay longer
}
}
applyInitialTheme() {
const savedTheme = localStorage.getItem('theme') || 'light';
document.body.setAttribute('data-theme', savedTheme);
this.updateThemeIcon(savedTheme);
}
toggleTheme() {
const currentTheme = document.body.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.body.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
this.updateThemeIcon(newTheme);
}
updateThemeIcon(theme) {
const icon = this.themeToggleBtn.querySelector('i');
icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
}
// Conversation management methods
async saveConversation() {
const title = prompt('Enter conversation title:') || `Chat ${new Date().toLocaleString()}`;
try {
const response = await fetch(`${this.apiBase}/conversations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ title: title })
});
const data = await response.json();
if (response.ok) {
this.showNotification('Conversation saved successfully!', 'success');
this.loadConversations();
} else {
throw new Error(data.error || 'Failed to save conversation');
}
} catch (error) {
this.showNotification(`Save failed: ${error.message}`, 'error');
}
}
async loadConversation(conversationId) {
try {
const response = await fetch(`${this.apiBase}/conversations/${conversationId}`);
const data = await response.json();
if (response.ok) {
this.currentConversationId = conversationId;
this.messagesContainer.innerHTML = '';
// Hide welcome message when loading conversation
if (this.welcomeMessage) {
this.welcomeMessage.style.display = 'none';
}
// Load messages
data.conversation.messages.forEach(msg => {
this.addMessage(msg.user, 'user');
this.addMessage(msg.bot, 'bot');
});
this.showNotification('Conversation loaded successfully!', 'success');
this.updateConversationsList();
this.closeSidebar();
} else {
throw new Error(data.error || 'Failed to load conversation');
}
} catch (error) {
this.showNotification(`Load failed: ${error.message}`, 'error');
}
}
async deleteConversation(conversationId) {
if (!confirm('Are you sure you want to delete this conversation?')) {
return;
}
try {
const response = await fetch(`${this.apiBase}/conversations/${conversationId}`, {
method: 'DELETE'
});
const data = await response.json();
if (response.ok) {
this.showNotification('Conversation deleted successfully!', 'success');
this.loadConversations();
if (this.currentConversationId === conversationId) {
this.currentConversationId = null;
}
} else {
throw new Error(data.error || 'Failed to delete conversation');
}
} catch (error) {
this.showNotification(`Delete failed: ${error.message}`, 'error');
}
}
newConversation() {
this.currentConversationId = null;
this.messagesContainer.innerHTML = `
`;
// Re-initialize welcome message reference
this.welcomeMessage = document.getElementById('welcomeMessage');
this.updateConversationsList();
this.closeSidebar();
this.showNotification('New conversation started!', 'success');
}
}
// Initialize the chatbot when the page loads
let chatbot;
document.addEventListener('DOMContentLoaded', () => {
chatbot = new ChatbotUI();
});