/** * Chat Interface JavaScript * Handles WebSocket communication, UI interactions, and real-time chat functionality */ class ChatClient { constructor() { this.socket = null; this.sessionId = null; this.currentLanguage = 'python'; this.isConnected = false; this.isTyping = false; this.messageQueue = []; // DOM elements this.elements = { chatMessages: document.getElementById('chatMessages'), messageInput: document.getElementById('messageInput'), sendButton: document.getElementById('sendButton'), languageSelect: document.getElementById('languageSelect'), connectionStatus: document.getElementById('connectionStatus'), statusIndicator: document.getElementById('statusIndicator'), statusText: document.getElementById('statusText'), typingIndicator: document.getElementById('typingIndicator'), errorMessage: document.getElementById('errorMessage'), errorText: document.getElementById('errorText'), errorClose: document.getElementById('errorClose'), characterCount: document.getElementById('characterCount'), notificationToast: document.getElementById('notificationToast'), notificationText: document.getElementById('notificationText') }; this.init(); } init() { this.setupEventListeners(); this.connectWebSocket(); this.updateCharacterCount(); } setupEventListeners() { // Send button click this.elements.sendButton.addEventListener('click', () => this.sendMessage()); // Message input events this.elements.messageInput.addEventListener('input', () => { this.updateCharacterCount(); this.updateSendButton(); this.autoResize(); }); this.elements.messageInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.sendMessage(); } }); // Language selection this.elements.languageSelect.addEventListener('change', (e) => { this.switchLanguage(e.target.value); }); // Error message close this.elements.errorClose.addEventListener('click', () => { this.hideError(); }); // Auto-hide error after 10 seconds let errorTimeout; const showError = this.showError.bind(this); this.showError = (message) => { showError(message); clearTimeout(errorTimeout); errorTimeout = setTimeout(() => this.hideError(), 10000); }; } connectWebSocket() { try { this.updateConnectionStatus('connecting', 'Connecting...'); // For demo purposes, create a temporary session ID and user ID // In a real app, these would come from authentication const tempSessionId = this.generateSessionId(); const tempUserId = this.generateUserId(); // Store for later use this.tempSessionId = tempSessionId; this.tempUserId = tempUserId; // Initialize Socket.IO connection with auth this.socket = io({ transports: ['websocket', 'polling'], timeout: 5000, reconnection: true, reconnectionAttempts: 5, reconnectionDelay: 1000, auth: { session_id: tempSessionId, user_id: tempUserId } }); // Connection events this.socket.on('connect', () => { console.log('WebSocket connected'); this.isConnected = true; this.updateConnectionStatus('connected', 'Connected'); // Session will be created automatically by the server this.processMessageQueue(); }); this.socket.on('disconnect', (reason) => { console.log('WebSocket disconnected:', reason); this.isConnected = false; this.updateConnectionStatus('disconnected', 'Disconnected'); if (reason === 'io server disconnect') { // Server initiated disconnect, try to reconnect this.socket.connect(); } }); this.socket.on('connect_error', (error) => { console.error('WebSocket connection error:', error); this.updateConnectionStatus('disconnected', 'Connection failed'); this.showError('Failed to connect to chat server. Please refresh the page.'); }); this.socket.on('reconnect', (attemptNumber) => { console.log('WebSocket reconnected after', attemptNumber, 'attempts'); this.updateConnectionStatus('connected', 'Reconnected'); this.hideError(); }); this.socket.on('reconnect_error', (error) => { console.error('WebSocket reconnection error:', error); this.showError('Reconnection failed. Please refresh the page.'); }); // Chat events this.socket.on('connection_status', (data) => { console.log('Connection status:', data); if (data.status === 'connected') { this.sessionId = data.session_id; this.currentLanguage = data.language; this.elements.languageSelect.value = data.language; } }); this.socket.on('response_start', (data) => { console.log('Response start:', data); this.hideTypingIndicator(); this.startStreamingResponse(data.session_id); }); this.socket.on('response_chunk', (data) => { console.log('Response chunk:', data); this.appendToStreamingResponse(data.content); }); this.socket.on('response_complete', (data) => { console.log('Response complete:', data); this.endStreamingResponse(); }); this.socket.on('language_switched', (data) => { console.log('Language switched:', data); this.currentLanguage = data.new_language; this.elements.languageSelect.value = data.new_language; this.addSystemMessage(data.message); }); this.socket.on('user_typing', (data) => { // For future multi-user support console.log('User typing:', data); }); this.socket.on('user_typing_stop', (data) => { // For future multi-user support console.log('User typing stop:', data); }); this.socket.on('error', (data) => { console.error('WebSocket error:', data); this.hideTypingIndicator(); this.showError(data.message || 'An error occurred while processing your message.'); }); } catch (error) { console.error('Failed to initialize WebSocket:', error); this.updateConnectionStatus('disconnected', 'Connection failed'); this.showError('Failed to initialize chat connection.'); } } createSession() { if (!this.isConnected) return; this.socket.emit('create_session', { language: this.currentLanguage, metadata: { user_agent: navigator.userAgent, timestamp: new Date().toISOString() } }); } sendMessage() { const message = this.elements.messageInput.value.trim(); if (!message || !this.isConnected) return; if (message.length > 2000) { this.showError('Message is too long. Please keep it under 2000 characters.'); return; } // Add user message to UI this.addMessage('user', message, this.currentLanguage); // Clear input this.elements.messageInput.value = ''; this.updateCharacterCount(); this.updateSendButton(); this.autoResize(); // Show typing indicator this.showTypingIndicator(); // Send message via WebSocket if (this.sessionId) { this.socket.emit('message', { content: message, language: this.currentLanguage, timestamp: new Date().toISOString() }); } else { // Queue message if session not ready this.messageQueue.push({ content: message, language: this.currentLanguage, timestamp: new Date().toISOString() }); } } switchLanguage(language) { if (language === this.currentLanguage) return; if (this.isConnected && this.sessionId) { this.socket.emit('language_switch', { language: language }); } else { // Update locally if not connected this.currentLanguage = language; this.addSystemMessage(`Language set to ${this.getLanguageDisplayName(language)}.`); } } addMessage(role, content, language, timestamp = null) { const messageDiv = document.createElement('div'); messageDiv.className = `message ${role}-message`; const contentDiv = document.createElement('div'); contentDiv.className = 'message-content'; // Process content for code highlighting const processedContent = this.processMessageContent(content, language); contentDiv.innerHTML = processedContent; messageDiv.appendChild(contentDiv); // Add timestamp if (timestamp || role === 'user') { const timestampDiv = document.createElement('div'); timestampDiv.className = 'message-timestamp'; timestampDiv.textContent = timestamp ? new Date(timestamp).toLocaleTimeString() : new Date().toLocaleTimeString(); messageDiv.appendChild(timestampDiv); } this.elements.chatMessages.appendChild(messageDiv); this.scrollToBottom(); // Apply syntax highlighting this.applySyntaxHighlighting(contentDiv); } addSystemMessage(content) { const messageDiv = document.createElement('div'); messageDiv.className = 'message assistant-message'; const contentDiv = document.createElement('div'); contentDiv.className = 'message-content'; contentDiv.style.background = 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)'; contentDiv.style.color = 'white'; contentDiv.style.border = 'none'; contentDiv.innerHTML = `

${content}

`; messageDiv.appendChild(contentDiv); this.elements.chatMessages.appendChild(messageDiv); this.scrollToBottom(); } processMessageContent(content, language) { // Convert markdown-style code blocks to HTML content = content.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { const detectedLang = lang || language || 'text'; return `
${this.escapeHtml(code.trim())}
`; }); // Convert inline code content = content.replace(/`([^`]+)`/g, '$1'); // Convert line breaks to paragraphs const paragraphs = content.split('\n\n').filter(p => p.trim()); if (paragraphs.length > 1) { content = paragraphs.map(p => `

${p.replace(/\n/g, '
')}

`).join(''); } else { content = `

${content.replace(/\n/g, '
')}

`; } return content; } applySyntaxHighlighting(element) { // Apply Prism.js syntax highlighting if (window.Prism) { Prism.highlightAllUnder(element); } } startStreamingResponse(messageId) { const messageDiv = document.createElement('div'); messageDiv.className = 'message assistant-message'; messageDiv.id = `streaming-${messageId}`; const contentDiv = document.createElement('div'); contentDiv.className = 'message-content streaming-response'; contentDiv.innerHTML = '

'; messageDiv.appendChild(contentDiv); this.elements.chatMessages.appendChild(messageDiv); this.scrollToBottom(); this.streamingElement = contentDiv.querySelector('p'); } appendToStreamingResponse(chunk) { if (this.streamingElement) { this.streamingElement.textContent += chunk; this.scrollToBottom(); } } endStreamingResponse() { if (this.streamingElement) { const content = this.streamingElement.textContent; const processedContent = this.processMessageContent(content, this.currentLanguage); this.streamingElement.parentElement.innerHTML = processedContent; this.applySyntaxHighlighting(this.streamingElement.parentElement); this.streamingElement.parentElement.classList.remove('streaming-response'); this.streamingElement = null; } } showTypingIndicator() { if (!this.isTyping) { this.isTyping = true; this.elements.typingIndicator.style.display = 'block'; this.scrollToBottom(); } } hideTypingIndicator() { if (this.isTyping) { this.isTyping = false; this.elements.typingIndicator.style.display = 'none'; } } updateConnectionStatus(status, text) { this.elements.statusIndicator.className = `status-indicator ${status}`; this.elements.statusText.textContent = text; } showError(message) { this.elements.errorText.textContent = message; this.elements.errorMessage.style.display = 'flex'; } hideError() { this.elements.errorMessage.style.display = 'none'; } updateCharacterCount() { const length = this.elements.messageInput.value.length; const maxLength = 2000; this.elements.characterCount.textContent = `${length}/${maxLength}`; if (length > maxLength * 0.9) { this.elements.characterCount.className = 'character-count error'; } else if (length > maxLength * 0.8) { this.elements.characterCount.className = 'character-count warning'; } else { this.elements.characterCount.className = 'character-count'; } } updateSendButton() { const hasText = this.elements.messageInput.value.trim().length > 0; this.elements.sendButton.disabled = !hasText || !this.isConnected; } autoResize() { const textarea = this.elements.messageInput; textarea.style.height = 'auto'; textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px'; } scrollToBottom() { requestAnimationFrame(() => { this.elements.chatMessages.scrollTop = this.elements.chatMessages.scrollHeight; }); } processMessageQueue() { if (this.messageQueue.length > 0 && this.sessionId) { this.messageQueue.forEach(message => { this.socket.emit('message', message); }); this.messageQueue = []; } } generateSessionId() { // Generate a temporary session ID for demo purposes // In production, this would be handled by proper authentication return 'session_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now(); } generateUserId() { // Generate a temporary user ID for demo purposes // In production, this would come from authentication let userId = localStorage.getItem('temp_user_id'); if (!userId) { userId = 'user_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now(); localStorage.setItem('temp_user_id', userId); } return userId; } getLanguageDisplayName(language) { const languageNames = { python: 'Python', javascript: 'JavaScript', java: 'Java', cpp: 'C++', csharp: 'C#', go: 'Go', rust: 'Rust', typescript: 'TypeScript' }; return languageNames[language] || language; } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } showNotification(message, type = 'success') { this.elements.notificationText.textContent = message; this.elements.notificationToast.className = `notification-toast ${type}`; this.elements.notificationToast.style.display = 'block'; // Auto-hide after 3 seconds setTimeout(() => { this.elements.notificationToast.style.display = 'none'; }, 3000); } } // Utility functions for enhanced UX class ChatUtils { static formatTimestamp(timestamp) { const date = new Date(timestamp); const now = new Date(); const diffMs = now - date; const diffMins = Math.floor(diffMs / 60000); if (diffMins < 1) return 'Just now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffMins < 1440) return `${Math.floor(diffMins / 60)}h ago`; return date.toLocaleDateString(); } static detectCodeLanguage(code) { // Simple language detection based on common patterns if (code.includes('def ') || code.includes('import ') || code.includes('print(')) return 'python'; if (code.includes('function ') || code.includes('const ') || code.includes('console.log')) return 'javascript'; if (code.includes('public class ') || code.includes('System.out.println')) return 'java'; if (code.includes('#include') || code.includes('std::')) return 'cpp'; if (code.includes('using System') || code.includes('Console.WriteLine')) return 'csharp'; if (code.includes('func ') || code.includes('package main')) return 'go'; if (code.includes('fn ') || code.includes('println!')) return 'rust'; if (code.includes('interface ') || code.includes(': string')) return 'typescript'; return 'text'; } static copyToClipboard(text) { if (navigator.clipboard) { navigator.clipboard.writeText(text).then(() => { console.log('Text copied to clipboard'); if (window.chatClient) { window.chatClient.showNotification('Code copied to clipboard!'); } }).catch(err => { console.error('Failed to copy text: ', err); if (window.chatClient) { window.chatClient.showNotification('Failed to copy code', 'error'); } }); } else { // Fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = text; document.body.appendChild(textArea); textArea.select(); const success = document.execCommand('copy'); document.body.removeChild(textArea); if (window.chatClient) { if (success) { window.chatClient.showNotification('Code copied to clipboard!'); } else { window.chatClient.showNotification('Failed to copy code', 'error'); } } } } } // Initialize chat client when DOM is loaded document.addEventListener('DOMContentLoaded', () => { window.chatClient = new ChatClient(); // Add copy functionality to code blocks document.addEventListener('click', (e) => { if (e.target.tagName === 'CODE' && e.target.parentElement.tagName === 'PRE') { ChatUtils.copyToClipboard(e.target.textContent); // Show temporary feedback const originalText = e.target.textContent; e.target.textContent = 'Copied!'; setTimeout(() => { e.target.textContent = originalText; }, 1000); } }); // Handle page visibility changes document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible' && window.chatClient && !window.chatClient.isConnected) { // Try to reconnect when page becomes visible setTimeout(() => { if (!window.chatClient.isConnected) { window.chatClient.connectWebSocket(); } }, 1000); } }); }); // Export for potential external use window.ChatClient = ChatClient; window.ChatUtils = ChatUtils;