// Claude AI Chat Application State Management class ClaudeChatApp { constructor() { this.conversationHistory = []; this.selectedModel = 'claude-sonnet-4-5'; this.isProcessing = false; this.isStreaming = false; this.messageQueue = []; this.userScrolled = false; this.initializeApp(); } initializeApp() { this.loadFromStorage(); this.setupEventListeners(); this.renderMessages(); this.updateUIState(); // Check if Puter.js loaded if (typeof puter === 'undefined') { this.showStatus('error', 'Failed to connect to AI service. Refresh page.', 0); } } loadFromStorage() { try { const history = localStorage.getItem('claudeChat_history'); const model = localStorage.getItem('claudeChat_selectedModel'); if (history) { this.conversationHistory = JSON.parse(history); } if (model) { this.selectedModel = model; } } catch (error) { console.error('Error loading from storage:', error); } } saveToStorage() { try { localStorage.setItem('claudeChat_history', JSON.stringify(this.conversationHistory.slice(-50))); localStorage.setItem('claudeChat_selectedModel', this.selectedModel); } catch (error) { console.error('Error saving to storage:', error); } } setupEventListeners() { // Message input events const messageInput = document.getElementById('messageInput'); messageInput.addEventListener('input', () => this.handleInputChange()); messageInput.addEventListener('keydown', (e) => this.handleKeyPress(e)); // Button events document.getElementById('sendBtn').addEventListener('click', () => this.sendMessage(false)); document.getElementById('streamBtn').addEventListener('click', () => this.sendMessage(true)); // Example prompt buttons document.querySelectorAll('.example-btn').forEach(btn => { btn.addEventListener('click', () => { const prompt = btn.dataset.prompt; messageInput.value = prompt; messageInput.focus(); this.handleInputChange(); // Auto-hide examples after first click if (this.conversationHistory.length === 0) { setTimeout(() => this.hideExamples(), 500); } }); }); // Scroll to bottom button const scrollBtn = document.getElementById('scrollToBottom'); scrollBtn.addEventListener('click', () => { this.scrollToBottom(); scrollBtn.classList.add('hidden'); }); // Chat container scroll detection const chatContainer = document.getElementById('chatContainer'); chatContainer.addEventListener('scroll', () => { const isAtBottom = chatContainer.scrollTop + chatContainer.clientHeight >= chatContainer.scrollHeight - 50; this.userScrolled = !isAtBottom; if (!isAtBottom && this.conversationHistory.length > 2) { scrollBtn.classList.remove('hidden'); } else { scrollBtn.classList.add('hidden'); } }); // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.metaKey || e.ctrlKey) { switch (e.key.toLowerCase()) { case 'k': e.preventDefault(); messageInput.value = ''; this.handleInputChange(); break; case 'n': e.preventDefault(); this.newChat(); break; } } }); } handleInputChange() { const messageInput = document.getElementById('messageInput'); const charCount = messageInput.value.length; const charCounter = document.getElementById('charCounter'); const charCountDisplay = document.getElementById('charCount'); // Update character counter if (charCount > 0) { charCounter.classList.remove('hidden'); charCountDisplay.textContent = charCount; if (charCount > 8000) { charCounter.classList.add('text-red-500'); } else { charCounter.classList.remove('text-red-500'); } } else { charCounter.classList.add('hidden'); } // Auto-resize textarea messageInput.style.height = 'auto'; messageInput.style.height = Math.min(messageInput.scrollHeight, 150) + 'px'; this.updateUIState(); } handleKeyPress(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (!this.isProcessing) { this.sendMessage(false); } } } updateUIState() { const messageInput = document.getElementById('messageInput'); const sendBtn = document.getElementById('sendBtn'); const streamBtn = document.getElementById('streamBtn'); const hasText = messageInput.value.trim().length > 0; sendBtn.disabled = !hasText || this.isProcessing; streamBtn.disabled = !hasText || this.isProcessing; // Update button labels when processing if (this.isProcessing) { sendBtn.innerHTML = ''; streamBtn.innerHTML = ''; } else { sendBtn.innerHTML = ''; streamBtn.innerHTML = ''; } feather.replace(); } async sendMessage(stream = false) { const messageInput = document.getElementById('messageInput'); const message = messageInput.value.trim(); if (!message || this.isProcessing) return; this.isProcessing = true; this.isStreaming = stream; this.updateUIState(); this.hideExamples(); // Add user message this.addMessage('user', message); // Clear input messageInput.value = ''; this.handleInputChange(); // Show typing indicator or empty message if (stream) { this.addMessage('assistant', '', true); } else { document.getElementById('typingIndicator').classList.remove('hidden'); } try { if (typeof puter === 'undefined') { throw new Error('AI service not available'); } if (stream) { await this.handleStreamResponse(message); } else { await this.handleStandardResponse(message); } this.showStatus('success', 'Response received', 3000); } catch (error) { console.error('Error sending message:', error); this.showStatus('error', `Request failed: ${error.message}`, 5000); if (stream) { // Update streaming message with error const lastMessage = this.conversationHistory[this.conversationHistory.length - 1]; lastMessage.content += '\n\n*Streaming interrupted due to an error*'; this.renderMessages(); } else { document.getElementById('typingIndicator').classList.add('hidden'); } } finally { this.isProcessing = false; this.isStreaming = false; this.updateUIState(); this.saveToStorage(); if (!stream) { document.getElementById('typingIndicator').classList.add('hidden'); } } } async handleStandardResponse(message) { const response = await puter.ai.chat(message, { model: this.selectedModel }); if (response && response.message && response.message.content && response.message.content[0]) { const content = response.message.content[0].text; this.addMessage('assistant', content); } else { throw new Error('Invalid response format'); } } async handleStreamResponse(message) { const response = await puter.ai.chat(message, { model: this.selectedModel, stream: true }); let fullContent = ''; const messageIndex = this.conversationHistory.length - 1; try { for await (const part of response) { if (part && part.choices && part.choices[0] && part.choices[0].delta) { const chunk = part.choices[0].delta.content || ''; fullContent += chunk; this.conversationHistory[messageIndex].content = fullContent; this.updateStreamingMessage(messageIndex, fullContent); } } } catch (error) { console.error('Stream error:', error); throw error; } } updateStreamingMessage(index, content) { const messagesContainer = document.getElementById('messageThread'); const messageElement = messagesContainer.children[index]; const contentElement = messageElement.querySelector('.message-content'); if (contentElement) { contentElement.innerHTML = this.parseMarkdown(content); } if (!this.userScrolled) { this.scrollToBottom(); } } addMessage(role, content, isStreaming = false) { const message = { role, content, timestamp: new Date().toISOString(), id: Date.now() }; this.conversationHistory.push(message); if (!isStreaming) { this.renderMessages(); if (!this.userScrolled) { this.scrollToBottom(); } } else { // Create empty message element for streaming this.renderSingleMessage(this.conversationHistory.length - 1); } } renderMessages() { const messagesContainer = document.getElementById('messageThread'); messagesContainer.innerHTML = ''; // Show empty state if no messages if (this.conversationHistory.length === 0) { this.showEmptyState(); return; } // Render all messages this.conversationHistory.forEach((message, index) => { this.renderSingleMessage(index); }); } renderSingleMessage(index) { const messagesContainer = document.getElementById('messageThread'); const message = this.conversationHistory[index]; const messageDiv = document.createElement('div'); messageDiv.className = `message ${message.role === 'system' ? 'message-system' : message.role === 'user' ? 'message-user' : 'message-assistant'} p-4`; messageDiv.setAttribute('role', 'article'); messageDiv.setAttribute('aria-label', `${message.role === 'user' ? 'You' : 'Claude'} message`); const headerDiv = document.createElement('div'); headerDiv.className = 'message-header mb-2'; headerDiv.textContent = message.role === 'user' ? 'You' : message.role === 'system' ? 'System' : 'Claude'; const contentDiv = document.createElement('div'); contentDiv.className = 'message-content'; if (message.content) { contentDiv.innerHTML = this.parseMarkdown(message.content); } else { contentDiv.innerHTML = 'Thinking...'; } messageDiv.appendChild(headerDiv); messageDiv.appendChild(contentDiv); messagesContainer.appendChild(messageDiv); // Add animation requestAnimationFrame(() => { messageDiv.style.opacity = '1'; }); } parseMarkdown(text) { // Simple markdown parser return text // Headers .replace(/^### (.*$)/gim, '

$1

') .replace(/^## (.*$)/gim, '

$1

') .replace(/^# (.*$)/gim, '

$1

') // Bold .replace(/\*\*(.+?)\*\*/g, '$1') // Italic .replace(/\*(.+?)\*/g, '$1') // Code blocks .replace(/