JUNO_AI / script.js
KURUPRASATH-J's picture
Update script.js
28d460b verified
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 = `
<div class="conversation-info">
<div class="conversation-title">${conversation.title}</div>
<div class="conversation-meta">${conversation.message_count} messages • ${new Date(conversation.last_updated).toLocaleDateString()}</div>
</div>
<div class="conversation-actions">
<button class="btn-icon" onclick="chatbot.loadConversation('${conversation.id}')" title="Load">
<i class="fas fa-folder-open"></i>
</button>
<button class="btn-icon" onclick="chatbot.deleteConversation('${conversation.id}')" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
`;
conversationsList.appendChild(conversationItem);
});
}
async sendMessageWithStreaming(message) {
this.addMessage(message, 'user');
this.messageInput.value = '';
this.autoResizeInput();
this.updateSendButton();
this.showTypingIndicator();
try {
const response = await fetch(`${this.apiBase}/chat/stream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: message })
});
const data = await response.json();
this.hideTypingIndicator();
if (response.ok) {
const botMessage = data.response;
const hasContext = data.has_context;
// Add bot message without any prefix
this.addMessage(botMessage, 'bot');
// Only show context notification when document context is actually used
if (hasContext && botMessage.includes('document')) {
this.showNotification('Response based on uploaded documents', 'info');
}
} else {
throw new Error(data.error || 'Failed to get response');
}
} catch (error) {
this.hideTypingIndicator();
this.addMessage(`❌ Error: ${error.message}`, 'bot');
this.showNotification(`Error: ${error.message}`, 'error');
}
}
addMessage(content, sender) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${sender} fade-in`;
const timestamp = new Date().toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
// Create avatar HTML for both user and bot
let avatarHtml = '';
if (sender === 'user') {
// User avatar with 'U'
avatarHtml = `
<div class="message-avatar user-avatar">
<span class="avatar-text">U</span>
</div>
`;
} else {
// Bot avatar with image
avatarHtml = `
<div class="message-avatar bot-avatar">
<img src="juno avatar.jpg" alt="Bot Avatar">
</div>
`;
}
messageDiv.innerHTML = `
${avatarHtml}
<div class="message-content">
<div class="message-bubble">
<div class="message-text">${this.formatMessage(content)}</div>
</div>
<div class="message-time">${timestamp}</div>
</div>
`;
this.messagesContainer.appendChild(messageDiv);
this.scrollToBottom();
// Hide welcome message after first user message
if (sender === 'user' && this.welcomeMessage) {
this.welcomeMessage.style.display = 'none';
}
}
formatMessage(text) {
return text
.replace(/\n/g, '<br>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/`(.*?)`/g, '<code>$1</code>');
}
showTypingIndicator() {
this.isTyping = true;
this.sendBtn.disabled = true;
const typingDiv = document.createElement('div');
typingDiv.className = 'message bot typing fade-in';
typingDiv.innerHTML = `
<div class="message-avatar bot-avatar">
<img src="juno avatar.jpg" alt="Bot Avatar">
</div>
<div class="message-content">
<div class="typing-indicator">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
</div>
`;
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 = `<i class="${iconClass}" style="color: ${color};"></i><span>${message}</span>`;
}
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 = `<pre>${memoryHtml}</pre>`;
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 = `
<div class="welcome-message" id="welcomeMessage">
<div class="welcome-icon">
<img src="juno logo.jpg" alt="Juno Logo" class="welcome-logo">
</div>
<h2>Welcome to AI Assistant</h2>
<p>Upload documents, ask questions, or start a conversation!</p>
<div class="feature-grid">
<div class="feature-item">
<i class="fas fa-file-upload"></i>
<span>Upload PDFs</span>
</div>
<div class="feature-item">
<i class="fas fa-brain"></i>
<span>Smart Memory</span>
</div>
<div class="feature-item">
<i class="fas fa-globe"></i>
<span>Web Scraping</span>
</div>
<div class="feature-item">
<i class="fas fa-microphone"></i>
<span>Voice Input</span>
</div>
</div>
<div class="quick-actions">
<button class="quick-btn" onclick="chatbot.fillInput('Summarize this document')">
<i class="fas fa-file-text"></i> Summarize Doc
</button>
<button class="quick-btn" onclick="chatbot.fillInput('Extract key insights from this content')">
<i class="fas fa-lightbulb"></i> Key Insights
</button>
<button class="quick-btn" onclick="chatbot.fillInput('What is the main topic of this document?')">
<i class="fas fa-bullseye"></i> Main Topic
</button>
<button class="quick-btn" onclick="chatbot.fillInput('List the important points mentioned')">
<i class="fas fa-list"></i> Key Points
</button>
</div>
</div>
`;
// 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 = `
<div class="welcome-message" id="welcomeMessage">
<div class="welcome-icon">
<img src="juno logo.jpg" alt="Juno Logo" class="welcome-logo">
</div>
<h2>Welcome to AI Assistant</h2>
<p>Upload documents, ask questions, or start a conversation!</p>
<div class="feature-grid">
<div class="feature-item">
<i class="fas fa-file-upload"></i>
<span>Upload PDFs</span>
</div>
<div class="feature-item">
<i class="fas fa-brain"></i>
<span>Smart Memory</span>
</div>
<div class="feature-item">
<i class="fas fa-globe"></i>
<span>Web Scraping</span>
</div>
<div class="feature-item">
<i class="fas fa-microphone"></i>
<span>Voice Input</span>
</div>
</div>
<div class="quick-actions">
<button class="quick-btn" onclick="chatbot.fillInput('Summarize this document')">
<i class="fas fa-file-text"></i> Summarize Doc
</button>
<button class="quick-btn" onclick="chatbot.fillInput('Extract key insights from this content')">
<i class="fas fa-lightbulb"></i> Key Insights
</button>
<button class="quick-btn" onclick="chatbot.fillInput('What is the main topic of this document?')">
<i class="fas fa-bullseye"></i> Main Topic
</button>
<button class="quick-btn" onclick="chatbot.fillInput('List the important points mentioned')">
<i class="fas fa-list"></i> Key Points
</button>
</div>
</div>
`;
// 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();
});