// Chat UI components for contextual hints class ChatUI { constructor(gameLogic) { this.game = gameLogic; this.activeChatBlank = null; this.chatModal = null; this.isOpen = false; this.messageHistory = new Map(); // blankId -> array of messages for persistent history this.setupChatModal(); } // Create and setup chat modal setupChatModal() { // Create modal HTML const modalHTML = ` `; // Insert modal into page document.body.insertAdjacentHTML('beforeend', modalHTML); this.chatModal = document.getElementById('chat-modal'); this.setupEventListeners(); } // Setup event listeners for chat modal setupEventListeners() { const closeBtn = document.getElementById('chat-close'); // Close modal closeBtn.addEventListener('click', () => this.closeChat()); this.chatModal.addEventListener('click', (e) => { if (e.target === this.chatModal) this.closeChat(); }); // ESC key to close document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.isOpen) this.closeChat(); }); } // Open chat for specific blank async openChat(blankIndex) { this.activeChatBlank = blankIndex; this.isOpen = true; // Update title const title = document.getElementById('chat-title'); title.textContent = `Help with Word #${blankIndex + 1}`; // Restore previous messages or show intro this.restoreMessages(blankIndex); // Load question buttons this.loadQuestionButtons(); // Show modal this.chatModal.classList.remove('hidden'); } // Close chat modal closeChat() { this.isOpen = false; this.chatModal.classList.add('hidden'); this.activeChatBlank = null; } // Clear messages and show intro clearMessages() { const messagesContainer = document.getElementById('chat-messages'); messagesContainer.innerHTML = `
Choose a question below to get help with this word.
`; } // Restore messages for a specific blank or show intro restoreMessages(blankIndex) { const messagesContainer = document.getElementById('chat-messages'); const blankId = `blank_${blankIndex}`; const history = this.messageHistory.get(blankId); if (history && history.length > 0) { // Restore previous messages messagesContainer.innerHTML = ''; history.forEach(msg => { this.displayMessage(msg.sender, msg.content, msg.isUser); }); } else { // Show intro for new conversation this.clearMessages(); } } // Display a message without storing it (used for restoration) displayMessage(sender, content, isUser) { const messagesContainer = document.getElementById('chat-messages'); const alignment = isUser ? 'flex justify-end' : 'flex justify-start'; const messageClass = isUser ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-900'; const displaySender = isUser ? 'You' : sender; const messageHTML = `
${displaySender}
${this.escapeHtml(content)}
`; messagesContainer.insertAdjacentHTML('beforeend', messageHTML); messagesContainer.scrollTop = messagesContainer.scrollHeight; } // Clear all chat history (called when round ends) clearChatHistory() { this.messageHistory.clear(); } // Load question dropdown with disabled state for used questions loadQuestionButtons() { const dropdown = document.getElementById('question-dropdown'); const questions = this.game.getSuggestedQuestionsForBlank(this.activeChatBlank); // Clear existing content dropdown.innerHTML = ''; // Build dropdown options questions.forEach(question => { const isDisabled = question.used; const optionText = isDisabled ? `${question.text} ✓` : question.text; // Add all options but mark used ones as disabled const option = document.createElement('option'); option.value = isDisabled ? '' : question.type; option.textContent = optionText; option.disabled = isDisabled; option.style.color = isDisabled ? '#9CA3AF' : '#111827'; dropdown.appendChild(option); }); // Add change listener to dropdown dropdown.addEventListener('change', (e) => { if (e.target.value) { this.askQuestion(e.target.value); e.target.value = ''; // Reset dropdown } }); } // Ask a specific question async askQuestion(questionType) { if (this.activeChatBlank === null) return; // Get current user input for the blank const currentInput = this.getCurrentBlankInput(); // Get the actual question text from the button that was clicked const questions = this.game.getSuggestedQuestionsForBlank(this.activeChatBlank); const selectedQuestion = questions.find(q => q.type === questionType); const questionText = selectedQuestion ? selectedQuestion.text : this.getQuestionText(questionType); // Show question and loading this.addMessageToChat('You', questionText, true); this.showTypingIndicator(); try { // Send to chat service with question type const response = await this.game.askQuestionAboutBlank( this.activeChatBlank, questionType, currentInput ); this.hideTypingIndicator(); if (response.success) { // Make sure we're displaying the response string, not the object const responseText = typeof response.response === 'string' ? response.response : response.response.response || 'Sorry, I had trouble with that question.'; this.addMessageToChat('Cluemaster', responseText, false); // Refresh question buttons to show the used question as disabled this.loadQuestionButtons(); } else { this.addMessageToChat('Cluemaster', response.message || 'Sorry, I had trouble with that question.', false); } } catch (error) { this.hideTypingIndicator(); console.error('Chat error:', error); this.addMessageToChat('Cluemaster', 'Sorry, I encountered an error. Please try again.', false); } } // Get question text for display getQuestionText(questionType) { const questions = { 'grammar': 'What type of word is this?', 'meaning': 'What does this word mean?', 'context': 'Why does this word fit here?', 'clue': 'Give me a clue' }; return questions[questionType] || questions['clue']; } // Get current input for the active blank getCurrentBlankInput() { const input = document.querySelector(`input[data-blank-index="${this.activeChatBlank}"]`); return input ? input.value.trim() : ''; } // Add message to chat display and store in history addMessageToChat(sender, content, isUser) { // Store message in history for current blank if (this.activeChatBlank !== null) { const blankId = `blank_${this.activeChatBlank}`; if (!this.messageHistory.has(blankId)) { this.messageHistory.set(blankId, []); } // Change "Tutor" to "Cluemaster" for display and storage const displaySender = sender === 'Tutor' ? 'Cluemaster' : sender; this.messageHistory.get(blankId).push({ sender: displaySender, content: content, isUser: isUser, timestamp: Date.now() }); } // Display the message this.displayMessage(sender === 'Tutor' ? 'Cluemaster' : sender, content, isUser); } // Show typing indicator showTypingIndicator() { const messagesContainer = document.getElementById('chat-messages'); const typingHTML = `
Cluemaster
...
`; messagesContainer.insertAdjacentHTML('beforeend', typingHTML); messagesContainer.scrollTop = messagesContainer.scrollHeight; } // Hide typing indicator hideTypingIndicator() { const indicator = document.getElementById('typing-indicator'); if (indicator) indicator.remove(); } // Escape HTML to prevent XSS escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Setup chat buttons for blanks setupChatButtons() { // Remove existing listeners document.querySelectorAll('.chat-button').forEach(btn => { btn.replaceWith(btn.cloneNode(true)); }); // Add new listeners document.querySelectorAll('.chat-button').forEach(btn => { btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const blankIndex = parseInt(btn.dataset.blankIndex); this.openChat(blankIndex); }); }); } } export default ChatUI;