Spaces:
Running
Running
| // Global state | |
| let conversationHistory = JSON.parse(localStorage.getItem('claudeChat_history')) || []; | |
| let selectedModel = localStorage.getItem('claudeChat_selectedModel') || 'claude-sonnet-4-5'; | |
| let isProcessing = false; | |
| let isStreaming = false; | |
| // DOM Elements | |
| const modelSelector = document.getElementById('model-selector'); | |
| const messageInput = document.getElementById('message-input'); | |
| const sendBtn = document.getElementById('send-btn'); | |
| const streamBtn = document.getElementById('stream-btn'); | |
| const chatContainer = document.getElementById('chat-container'); | |
| const messagesContainer = document.getElementById('messages'); | |
| const examplePrompts = document.getElementById('example-prompts'); | |
| const exampleButtons = document.querySelectorAll('.example-btn'); | |
| const statusBar = document.getElementById('status-bar'); | |
| const statusMessage = document.getElementById('status-message'); | |
| const retryBtn = document.getElementById('retry-btn'); | |
| const typingIndicator = document.getElementById('typing-indicator'); | |
| const charCount = document.getElementById('char-count'); | |
| const newChatBtn = document.getElementById('new-chat-btn'); | |
| const appTitle = document.getElementById('app-title'); | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Set initial model | |
| modelSelector.value = selectedModel; | |
| // Load conversation history | |
| renderConversationHistory(); | |
| // Set up event listeners | |
| setupEventListeners(); | |
| // Update character count | |
| updateCharCount(); | |
| }); | |
| // Set up event listeners | |
| function setupEventListeners() { | |
| // Model selection | |
| modelSelector.addEventListener('change', handleModelChange); | |
| // Message input | |
| messageInput.addEventListener('input', updateCharCount); | |
| messageInput.addEventListener('keydown', handleKeyDown); | |
| // Send buttons | |
| sendBtn.addEventListener('click', () => sendMessage(false)); | |
| streamBtn.addEventListener('click', () => sendMessage(true)); | |
| // Example prompts | |
| exampleButtons.forEach(button => { | |
| button.addEventListener('click', () => { | |
| messageInput.value = button.textContent; | |
| messageInput.focus(); | |
| updateCharCount(); | |
| }); | |
| }); | |
| // Status bar | |
| retryBtn.addEventListener('click', retryLastMessage); | |
| // New chat | |
| newChatBtn.addEventListener('click', resetConversation); | |
| appTitle.addEventListener('click', resetConversation); | |
| // Auto-scroll | |
| chatContainer.addEventListener('scroll', handleScroll); | |
| } | |
| // Handle model change | |
| function handleModelChange() { | |
| const newModel = modelSelector.value; | |
| const previousModel = selectedModel; | |
| selectedModel = newModel; | |
| localStorage.setItem('claudeChat_selectedModel', selectedModel); | |
| // If conversation exists, add system message | |
| if (conversationHistory.length > 0) { | |
| const modelName = modelSelector.options[modelSelector.selectedIndex].text; | |
| addSystemMessage(`Model changed to ${modelName} — previous context retained`); | |
| } | |
| } | |
| // Handle keyboard shortcuts | |
| function handleKeyDown(e) { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| if (!isProcessing && messageInput.value.trim()) { | |
| sendMessage(false); | |
| } | |
| } | |
| // Clear input shortcut | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'k') { | |
| e.preventDefault(); | |
| messageInput.value = ''; | |
| updateCharCount(); | |
| } | |
| // New chat shortcut | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'n') { | |
| e.preventDefault(); | |
| resetConversation(); | |
| } | |
| } | |
| // Update character count | |
| function updateCharCount() { | |
| const count = messageInput.value.length; | |
| charCount.textContent = `${count}/10000`; | |
| if (count > 8000) { | |
| charCount.classList.add('text-red-500'); | |
| } else { | |
| charCount.classList.remove('text-red-500'); | |
| } | |
| // Enable/disable buttons | |
| const hasText = count > 0; | |
| sendBtn.disabled = !hasText || isProcessing; | |
| streamBtn.disabled = !hasText || isProcessing; | |
| } | |
| // Send message | |
| async function sendMessage(stream = false) { | |
| const message = messageInput.value.trim(); | |
| if (!message || isProcessing) return; | |
| isProcessing = true; | |
| isStreaming = stream; | |
| updateUIState(); | |
| // Add user message to UI | |
| addUserMessage(message); | |
| // Clear input | |
| messageInput.value = ''; | |
| updateCharCount(); | |
| // Show typing indicator for non-streaming | |
| if (!stream) { | |
| typingIndicator.classList.remove('hidden'); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| try { | |
| // Add to history | |
| conversationHistory.push({ role: 'user', content: message }); | |
| saveConversation(); | |
| // Call API | |
| if (stream) { | |
| await streamResponse(message); | |
| } else { | |
| await getResponse(message); | |
| } | |
| } catch (error) { | |
| handleError(error); | |
| } finally { | |
| isProcessing = false; | |
| isStreaming = false; | |
| updateUIState(); | |
| typingIndicator.classList.add('hidden'); | |
| } | |
| } | |
| // Get response from API (non-streaming) | |
| async function getResponse(userMessage) { | |
| try { | |
| const response = await puter.ai.chat(userMessage, { | |
| model: selectedModel, | |
| conversationHistory: conversationHistory | |
| }); | |
| const assistantMessage = response.message?.content?.[0]?.text || "Sorry, I couldn't process that."; | |
| // Add to history | |
| conversationHistory.push({ role: 'assistant', content: assistantMessage }); | |
| saveConversation(); | |
| // Add to UI | |
| addAssistantMessage(assistantMessage); | |
| } catch (error) { | |
| throw error; | |
| } | |
| } | |
| // Stream response from API | |
| async function streamResponse(userMessage) { | |
| try { | |
| const response = await puter.ai.chat(userMessage, { | |
| model: selectedModel, | |
| conversationHistory: conversationHistory, | |
| stream: true | |
| }); | |
| // Create assistant message element | |
| const messageElement = document.createElement('div'); | |
| messageElement.className = 'assistant-message bg-white p-4 rounded-lg shadow-sm'; | |
| messageElement.innerHTML = ` | |
| <div class="flex items-center mb-2"> | |
| <div class="w-6 h-6 rounded-full bg-purple-500 flex items-center justify-center mr-2"> | |
| <span class="text-white text-xs">A</span> | |
| </div> | |
| <span class="font-semibold text-gray-700">Assistant</span> | |
| </div> | |
| <div class="text-gray-700"></div> | |
| `; | |
| const contentElement = messageElement.querySelector('.text-gray-700'); | |
| messagesContainer.appendChild(messageElement); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| let fullResponse = ''; | |
| // Process stream | |
| for await (const chunk of response) { | |
| const text = chunk?.message?.content?.[0]?.text || ''; | |
| fullResponse += text; | |
| contentElement.textContent = fullResponse; | |
| // Scroll to bottom | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| // Add to history | |
| conversationHistory.push({ role: 'assistant', content: fullResponse }); | |
| saveConversation(); | |
| } catch (error) { | |
| throw error; | |
| } | |
| } | |
| // Add user message to UI | |
| function addUserMessage(content) { | |
| const messageElement = document.createElement('div'); | |
| messageElement.className = 'user-message bg-blue-500 text-white p-4 rounded-lg shadow-sm ml-auto'; | |
| messageElement.innerHTML = ` | |
| <div class="flex items-center mb-2"> | |
| <div class="w-6 h-6 rounded-full bg-blue-300 flex items-center justify-center mr-2"> | |
| <span class="text-blue-800 text-xs">Y</span> | |
| </div> | |
| <span class="font-semibold">You</span> | |
| </div> | |
| <div class="text-white">${content}</div> | |
| `; | |
| messagesContainer.appendChild(messageElement); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| // Hide example prompts after first message | |
| if (conversationHistory.length === 0) { | |
| examplePrompts.classList.add('hidden'); | |
| } | |
| } | |
| // Add assistant message to UI | |
| function addAssistantMessage(content) { | |
| const messageElement = document.createElement('div'); | |
| messageElement.className = 'assistant-message bg-white p-4 rounded-lg shadow-sm'; | |
| messageElement.innerHTML = ` | |
| <div class="flex items-center mb-2"> | |
| <div class="w-6 h-6 rounded-full bg-purple-500 flex items-center justify-center mr-2"> | |
| <span class="text-white text-xs">A</span> | |
| </div> | |
| <span class="font-semibold text-gray-700">Assistant</span> | |
| </div> | |
| <div class="text-gray-700">${content}</div> | |
| `; | |
| messagesContainer.appendChild(messageElement); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| // Add system message to UI | |
| function addSystemMessage(content) { | |
| const messageElement = document.createElement('div'); | |
| messageElement.className = 'system-message bg-gray-100 text-gray-600 p-3 rounded-lg text-center text-sm my-2'; | |
| messageElement.textContent = content; | |
| messagesContainer.appendChild(messageElement); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| // Handle errors | |
| function handleError(error) { | |
| console.error('Error:', error); | |
| showStatus(`Request failed: ${error.message}`, 'error'); | |
| retryBtn.classList.remove('hidden'); | |
| } | |
| // Retry last message | |
| function retryLastMessage() { | |
| if (conversationHistory.length >= 2) { | |
| const lastUserMessage = conversationHistory[conversationHistory.length - 2].content; | |
| messageInput.value = lastUserMessage; | |
| updateCharCount(); | |
| retryBtn.classList.add('hidden'); | |
| sendMessage(isStreaming); | |
| } | |
| } | |
| // Reset conversation | |
| function resetConversation() { | |
| conversationHistory = []; | |
| localStorage.removeItem('claudeChat_history'); | |
| messagesContainer.innerHTML = ` | |
| <div class="assistant-message bg-white p-4 rounded-lg shadow-sm"> | |
| <div class="flex items-center mb-2"> | |
| <div class="w-6 h-6 rounded-full bg-purple-500 flex items-center justify-center mr-2"> | |
| <span class="text-white text-xs">A</span> | |
| </div> | |
| <span class="font-semibold text-gray-700">Assistant</span> | |
| </div> | |
| <div class="text-gray-700"> | |
| <p>Hello! I'm Claude, an AI assistant. How can I help you today?</p> | |
| </div> | |
| </div> | |
| `; | |
| examplePrompts.classList.remove('hidden'); | |
| hideStatus(); | |
| } | |
| // Save conversation to localStorage | |
| function saveConversation() { | |
| localStorage.setItem('claudeChat_history', JSON.stringify(conversationHistory)); | |
| localStorage.setItem('claudeChat_timestamp', Date.now()); | |
| } | |
| // Render conversation history | |
| function renderConversationHistory() { | |
| if (conversationHistory.length === 0) return; | |
| messagesContainer.innerHTML = ''; | |
| examplePrompts.classList.add('hidden'); | |
| conversationHistory.forEach(msg => { | |
| if (msg.role === 'user') { | |
| addUserMessage(msg.content); | |
| } else if (msg.role === 'assistant') { | |
| addAssistantMessage(msg.content); | |
| } | |
| }); | |
| } | |
| // Update UI state based on processing status | |
| function updateUIState() { | |
| if (isProcessing) { | |
| messageInput.disabled = true; | |
| sendBtn.disabled = true; | |
| streamBtn.disabled = true; | |
| modelSelector.disabled = true; | |
| messageInput.placeholder = "Waiting for response..."; | |
| } else { | |
| messageInput.disabled = false; | |
| updateCharCount(); | |
| modelSelector.disabled = false; | |
| messageInput.placeholder = "Type your message here... (Shift+Enter for new line)"; | |
| } | |
| } | |
| // Show status message | |
| function showStatus(message, type) { | |
| statusMessage.textContent = message; | |
| statusBar.className = `px-6 py-3 text-sm text-white ${type}`; | |
| statusBar.classList.remove('hidden'); | |
| // Auto-hide success messages | |
| if (type === 'success') { | |
| setTimeout(hideStatus, 3000); | |
| } | |
| // Auto-hide error messages (but keep retry button) | |
| if (type === 'error') { | |
| setTimeout(() => { | |
| if (!retryBtn.classList.contains('hidden')) return; | |
| hideStatus(); | |
| }, 5000); | |
| } | |
| } | |
| // Hide status message | |
| function hideStatus() { | |
| statusBar.classList.add('hidden'); | |
| retryBtn.classList.add('hidden'); | |
| } | |
| // Handle scroll for auto-scroll detection | |
| function handleScroll() { | |
| const threshold = 50; | |
| const atBottom = chatContainer.scrollTop + chatContainer.clientHeight >= chatContainer.scrollHeight - threshold; | |
| // In a real implementation, you might add a "scroll to bottom" button here | |
| // when the user scrolls up and new messages arrive | |
| } |