Spaces:
Runtime error
Runtime error
| /** | |
| * 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 = `<p>${content}</p>`; | |
| 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 `<pre><code class="language-${detectedLang}">${this.escapeHtml(code.trim())}</code></pre>`; | |
| }); | |
| // Convert inline code | |
| content = content.replace(/`([^`]+)`/g, '<code>$1</code>'); | |
| // Convert line breaks to paragraphs | |
| const paragraphs = content.split('\n\n').filter(p => p.trim()); | |
| if (paragraphs.length > 1) { | |
| content = paragraphs.map(p => `<p>${p.replace(/\n/g, '<br>')}</p>`).join(''); | |
| } else { | |
| content = `<p>${content.replace(/\n/g, '<br>')}</p>`; | |
| } | |
| 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 = '<p></p>'; | |
| 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; |