Spaces:
Sleeping
Sleeping
// 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 = ` | |
<div id="chat-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center"> | |
<div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4 max-h-[80vh] flex flex-col"> | |
<!-- Header --> | |
<div class="flex items-center justify-between p-4 border-b"> | |
<h3 id="chat-title" class="text-lg font-semibold text-gray-900"> | |
Chat about Word #1 | |
</h3> | |
<button id="chat-close" class="text-gray-400 hover:text-gray-600"> | |
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> | |
</svg> | |
</button> | |
</div> | |
<!-- Chat messages area --> | |
<div id="chat-messages" class="flex-1 overflow-y-auto p-4 min-h-[200px] max-h-[400px]"> | |
<div class="text-center text-gray-500 text-sm"> | |
Ask me anything about this word! I can help with meaning, context, grammar, or give you hints. | |
</div> | |
</div> | |
<!-- Suggested questions --> | |
<div id="suggested-questions" class="px-4 py-2 border-t border-gray-100"> | |
<div id="suggestion-buttons" class="flex flex-wrap gap-1"> | |
<!-- Suggestion buttons will be inserted here --> | |
</div> | |
</div> | |
<!-- Question dropdown area --> | |
<div class="p-4 border-t"> | |
<!-- Dropdown for all devices --> | |
<select id="question-dropdown" class="w-full p-2 border rounded mb-4"> | |
<option value="">Select a question...</option> | |
</select> | |
</div> | |
</div> | |
</div> | |
`; | |
// 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 = ` | |
<div class="text-center text-gray-500 text-sm mb-4"> | |
Choose a question below to get help with this word. | |
</div> | |
`; | |
} | |
// 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 = ` | |
<div class="mb-3 ${alignment}"> | |
<div class="${messageClass} rounded-lg px-3 py-2 max-w-[80%]"> | |
<div class="text-xs font-medium mb-1">${displaySender}</div> | |
<div class="text-sm">${this.escapeHtml(content)}</div> | |
</div> | |
</div> | |
`; | |
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 = '<option value="">Select a question...</option>'; | |
// 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 = ` | |
<div id="typing-indicator" class="mb-3 mr-auto max-w-[80%]"> | |
<div class="bg-gray-100 text-gray-900 rounded-lg px-3 py-2"> | |
<div class="text-xs font-medium mb-1">Cluemaster</div> | |
<div class="text-sm"> | |
<span class="typing-dots"> | |
<span>.</span><span>.</span><span>.</span> | |
</span> | |
</div> | |
</div> | |
</div> | |
`; | |
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; |