cloze-reader / src /chatInterface.js
milwright's picture
increase margin after bank of dropdown questions
83e8132
// 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;