// AI Chat functionality
class AIChat {
constructor() {
this.modal = document.getElementById('aiChatModal');
this.button = document.getElementById('aiChatButton');
this.closeBtn = document.getElementById('aiChatClose');
this.messagesContainer = document.getElementById('aiChatMessages');
this.input = document.getElementById('aiChatInput');
this.sendBtn = document.getElementById('aiChatSend');
this.emptyState = document.getElementById('aiChatEmptyState');
this.isOpen = false;
this.isTyping = false;
this.hasMessages = false;
// Track conversation history for tool calling
this.conversation = [];
// Initialize translations
this.translations = window.aiChatTranslations || {
typing_indicator: 'AI is thinking',
error: {
network: 'Unable to connect to the AI service. Please check your internet connection and try again.',
generic: 'Sorry, I\'m having trouble connecting right now. Please try again later.'
}
};
this.initializeEventListeners();
this.adjustInputHeight();
this.configureMarkdown();
}
configureMarkdown() {
// Configure marked.js options for security and rendering
if (typeof marked !== 'undefined') {
marked.setOptions({
breaks: true, // Convert \n to
gfm: true, // GitHub Flavored Markdown
sanitize: false, // We'll handle sanitization manually if needed
smartypants: true, // Use smart quotes
xhtml: false,
headerIds: false, // Disable header IDs for security
mangle: false // Don't mangle email addresses
});
// Custom renderer for better styling
const renderer = new marked.Renderer();
// Custom code rendering
renderer.code = function(code, language) {
const validLang = language && language.match(/^[a-zA-Z0-9_+-]*$/);
const langClass = validLang ? ` class="language-${language}"` : '';
const escapedCode = code.replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
return `
${escapedCode}
`;
};
// Custom link rendering with security
renderer.link = function(href, title, text) {
const escapedHref = href.replace(/"/g, '"');
const titleAttr = title ? ` title="${title.replace(/"/g, '"')}"` : '';
return `${text}`;
};
// Custom list item rendering for task lists
renderer.listitem = function(text) {
// Check if this is a task list item
const taskListMatch = text.match(/^\s*\[([ xX])\]\s*(.*)<\/p>$/);
if (taskListMatch) {
const checked = taskListMatch[1] !== ' ' ? 'checked' : '';
const content = taskListMatch[2];
return `
${content}`;
}
// Regular task list without wrapper
const simpleTaskMatch = text.match(/^\[([ xX])\]\s*(.*)$/);
if (simpleTaskMatch) {
const checked = simpleTaskMatch[1] !== ' ' ? 'checked' : '';
const content = simpleTaskMatch[2];
return `
${content}`;
}
return `${text}`;
};
marked.use({ renderer });
}
}
initializeEventListeners() {
// Toggle modal
this.button.addEventListener('click', () => this.toggleModal());
this.closeBtn.addEventListener('click', () => this.closeModal());
// Send message
this.sendBtn.addEventListener('click', () => this.sendMessage());
// Handle input events
this.input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
this.input.addEventListener('input', () => this.adjustInputHeight());
// Handle suggestion pills
this.initializeSuggestionPills();
// Close modal when clicking outside
document.addEventListener('click', (e) => {
if (!this.modal.contains(e.target) && !this.button.contains(e.target)) {
if (this.isOpen) {
this.closeModal();
}
}
});
}
toggleModal() {
if (this.isOpen) {
this.closeModal();
} else {
this.openModal();
}
}
openModal() {
this.modal.classList.add('show');
this.isOpen = true;
// Focus input when modal opens
setTimeout(() => this.input.focus(), 100);
}
closeModal() {
this.modal.classList.remove('show');
this.isOpen = false;
}
adjustInputHeight() {
this.input.style.height = 'auto';
this.input.style.height = Math.min(this.input.scrollHeight, 80) + 'px';
}
initializeSuggestionPills() {
const suggestionPills = document.querySelectorAll('.ai-suggestion-pill');
suggestionPills.forEach(pill => {
pill.addEventListener('click', () => {
const suggestion = pill.getAttribute('data-suggestion');
if (suggestion) {
this.input.value = suggestion;
this.adjustInputHeight();
this.sendMessage();
}
});
});
}
hideEmptyState() {
if (this.emptyState) {
this.emptyState.style.display = 'none';
this.hasMessages = true;
}
}
async sendMessage() {
const message = this.input.value.trim();
if (!message || this.isTyping) return;
// Hide empty state on first message
if (!this.hasMessages) {
this.hideEmptyState();
}
// Add user message
this.addMessage(message, 'user');
this.input.value = '';
this.adjustInputHeight();
// Disable send button and show typing indicator
this.setSending(true);
this.showTypingIndicator();
try {
// Add user message to conversation history
this.conversation.push({ role: 'user', content: message });
// Send to backend with conversation history
const response = await fetch('/api/ai-chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message,
conversation: this.conversation.slice(-10) // Send last 10 messages for context
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Update conversation history
if (data.conversation) {
this.conversation = data.conversation;
}
// Remove typing indicator and add AI response
this.hideTypingIndicator();
// Show tool usage indicator if tools were used
if (data.tool_calls_made && data.tool_calls_made > 0) {
this.addToolUsageIndicator(data.tool_calls_made, data.tools_used || []);
}
this.addMessage(data.response, 'ai');
// Add AI response to conversation history
this.conversation.push({ role: 'assistant', content: data.response });
} catch (error) {
console.error('AI Chat Error:', error);
this.hideTypingIndicator();
let errorMessage = error.message;
// Handle different types of errors
if (error.message.includes('Failed to fetch')) {
errorMessage = this.translations.error.network;
} else if (!error.message.includes('HTTP error')) {
// Use backend error message if available, otherwise use generic message
errorMessage = error.message || this.translations.error.generic;
}
this.addMessage(errorMessage, 'ai');
} finally {
this.setSending(false);
}
}
addMessage(content, sender) {
const messageDiv = document.createElement('div');
messageDiv.className = `ai-chat-message ${sender}`;
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
messageDiv.innerHTML = `
${this.formatMessage(content)}
${time}
`;
this.messagesContainer.appendChild(messageDiv);
this.scrollToBottom();
}
addToolUsageIndicator(toolCount, toolsUsed = []) {
const indicatorDiv = document.createElement('div');
indicatorDiv.className = 'ai-tool-usage-indicator';
// Create user-friendly tool names
const friendlyToolNames = {
'search_games': 'game search',
'get_search_suggestions': 'search suggestions'
};
let toolText;
if (toolsUsed.length > 0) {
const friendlyNames = toolsUsed.map(tool => friendlyToolNames[tool] || tool);
if (friendlyNames.length === 1) {
toolText = `Used ${friendlyNames[0]}`;
} else {
toolText = `Used ${friendlyNames.join(' & ')}`;
}
} else {
// Fallback to generic text
const genericToolText = toolCount === 1 ? 'tool' : 'tools';
toolText = `Used ${toolCount} ${genericToolText}`;
}
indicatorDiv.innerHTML = `
🔧
${toolText}
`;
this.messagesContainer.appendChild(indicatorDiv);
this.scrollToBottom();
}
formatMessage(message) {
// Use marked.js for full markdown parsing if available
if (typeof marked !== 'undefined') {
try {
return marked.parse(message);
} catch (error) {
console.warn('Markdown parsing error:', error);
// Fallback to basic formatting
return this.formatMessageBasic(message);
}
} else {
// Fallback to basic formatting if marked.js not available
return this.formatMessageBasic(message);
}
}
formatMessageBasic(message) {
// Basic formatting fallback
return message
.replace(/\n/g, '
')
.replace(/\*\*(.*?)\*\*/g, '$1')
.replace(/\*(.*?)\*/g, '$1')
.replace(/`(.*?)`/g, '$1')
.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '$1');
}
showTypingIndicator() {
if (this.isTyping) return;
this.isTyping = true;
const typingDiv = document.createElement('div');
typingDiv.className = 'ai-chat-message ai';
typingDiv.id = 'typingIndicator';
typingDiv.innerHTML = `
${this.translations.typing_indicator}
`;
this.messagesContainer.appendChild(typingDiv);
}
hideTypingIndicator() {
this.isTyping = false;
const typingIndicator = document.getElementById('typingIndicator');
if (typingIndicator) {
typingIndicator.remove();
}
}
setSending(sending) {
this.sendBtn.disabled = sending;
this.input.disabled = sending;
}
scrollToBottom() {
setTimeout(() => {
this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
}, 100);
}
}
// Initialize AI Chat when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
// Only initialize if AI chat elements exist
if (document.getElementById('aiChatModal')) {
new AIChat();
}
});